diff --git a/apps/golinks-v2/src/api/golinks.ts b/apps/golinks-v2/src/api/golinks.ts index 30e4c08..eb44870 100644 --- a/apps/golinks-v2/src/api/golinks.ts +++ b/apps/golinks-v2/src/api/golinks.ts @@ -131,7 +131,7 @@ export class GoLinkList extends OpenAPIRoute { export class GoLinkUpdate extends OpenAPIRoute { schema = { summary: "Update a golink", - description: `\ + description: `\ Updating a golink's slug and/or its target URL requires admin level permissions. If you generated a random slug or requested a new golink as a regular user, please contact Andrei Jiroh to get it sorted.`, @@ -140,7 +140,7 @@ If you generated a random slug or requested a new golink as a regular user, plea { name: "slug", in: "path", - description: "Slug name of the golink to be changed" + description: "Slug name of the golink to be changed. Forward-slashes must be URL-encoded before sending the request.", }, ], request: { @@ -149,13 +149,13 @@ If you generated a random slug or requested a new golink as a regular user, plea "application/json": { schema: z.object({ slug: Str({ - required: false, - description: "The desired slug for the golinks." - }), + required: false, + description: "The desired slug for the golinks.", + }), targetUrl: Str({ - example: "https://example.com", - description: "The target URL of the golink to redirect into" - }), + example: "https://example.com", + description: "The target URL of the golink to redirect into", + }), }), }, }, @@ -172,9 +172,9 @@ If you generated a random slug or requested a new golink as a regular user, plea content: { "application/json": { schema: z.object({ - ok: Bool({default: true}), - result: GoLinks - }), + ok: Bool({ default: true }), + result: GoLinks, + }), }, }, }, @@ -183,7 +183,7 @@ If you generated a random slug or requested a new golink as a regular user, plea async handle(c: Context) { const data = await this.getValidatedData(); const { newSlug, targetUrl } = data.body; - const { slug } = c.req.param() + const { slug } = c.req.param(); try { const result = await updateGoLink(c.env.golinks, slug, targetUrl, "golinks", newSlug); return c.json({ @@ -207,22 +207,22 @@ export class GoLinkInfo extends OpenAPIRoute { { name: "slug", in: "path", - description: "Slug name of the golink", + description: "Slug name of the golink. Forward-slashes must be URL-encoded before sending the request.", }, ], - responses: { - "200": { - description: "Shows a information about a golink", + responses: { + "200": { + description: "Shows a information about a golink", content: { "application/json": { schema: z.object({ - ok: Bool({default: true}), - result: GoLinks - }), + ok: Bool({ default: true }), + result: GoLinks, + }), }, - } - } - } + }, + }, + }, }; async handle(context: Context) { const { slug } = context.req.param(); @@ -246,7 +246,7 @@ If you are requesting to delete a golink for legal reasons, please contact Andre { name: "slug", in: "path", - description: "Slug name of the golink to be deleted.", + description: "Slug name of the golink to be deleted. Forward-slashes must be URL-encoded before sending the request.", }, ], security: [ diff --git a/apps/golinks-v2/src/api/slack.ts b/apps/golinks-v2/src/api/slack.ts index c7acce8..18827b7 100644 --- a/apps/golinks-v2/src/api/slack.ts +++ b/apps/golinks-v2/src/api/slack.ts @@ -13,6 +13,7 @@ import { Bool, OpenAPIRoute, Str } from "chanfana"; import { adminApiKey } from "lib/constants"; import { z } from "zod"; import { EnvBindings } from "types"; +import { addGoLink } from "lib/db"; type SlackSlashCommand = { token?: string; @@ -77,6 +78,13 @@ type SlackInteractivityActions = { async function helpMessage(context: Context, params: SlackSlashCommand) { const challenge = generateSlug(24); + const availableCommands = `*Available commands (add \`-stg\` to slash command for staging or \`-dev\` for localdev)*: + +* \`/go lookup \` - look up a golink, Discord/Slack invite, or wikilink \ +(just paste the full link and it handles the rest) +* \`/go help\` - this command (*you're here btw*) +* \`/go shorten \` - shorten a link for you +* \`/ping\` or \`/go ping\` - check platform and API availability`; const templateJson = { blocks: [ { @@ -98,34 +106,7 @@ async function helpMessage(context: Context, params: SlackSlashCommand) { type: "section", text: { type: "mrkdwn", - text: "Need to shorten a link, add a Discord or wikilink? Submit a request and you'll be notified via DMs if it's added.", - }, - accessory: { - type: "button", - text: { - type: "plain_text", - text: "Request a link", - emoji: true, - }, - value: "request-link", - action_id: "golinks-bot-action", - }, - }, - { - type: "section", - text: { - type: "mrkdwn", - text: "Want to use me programmatically? You can request a API token using your GitHub account through the OAuth prompt.", - }, - accessory: { - type: "button", - text: { - type: "plain_text", - text: "Sign in to get token", - emoji: true, - }, - value: challenge, - action_id: "github-auth-challenge", + text: availableCommands, }, }, { @@ -317,6 +298,49 @@ export async function slackOAuthCallback(context: Context) { } } +export function generateQRCodeImgUrl(context: Context, url?: string) { + let base = "https://go.andreijiroh.xyz" + if (context.env.DEPLOY_ENV == "staging") { + base = context.env.BASE_URL || "https://staging.go-next.andreijiroh.xyz" + } else if (context.env.DEPLOY_ENV == "development") { + base = context.env.BASE_URL || "https://staging.go-next.andreijiroh.xyz"; + } + return `https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${base}/${url}`; +} + +type sendMesgParams = { + teamId: string, + enterpriseId?: string, + channel: string, + blocks: Array + text?: string +} + +async function sendMessage(context: Context, params: sendMesgParams) { + try { + const { bot } = await slackAppInstaller(context.env).installationStore.fetchInstallation({ + teamId: params.teamId, + isEnterpriseInstall: params.enterpriseId !== undefined ? true : false, + enterpriseId: params.enterpriseId !== undefined ? params.enterpriseId : null + }) + const result = fetch("https://slack.com/api/chat.postMessage", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `bearer ${bot.token}` + }, + body: { + channel: params.channel, + blocks: params.blocks, + text: params.text + } + }); + return [result, null] + } catch (error) { + return [null, error] + } +} + /** * Function handler for `/api/slack/slash-commands/:command` POST requests * on Hono. @@ -359,14 +383,148 @@ export async function handleSlackCommand(context: Context) { } */ + const args = data.text.toString().split(" ") + console.log(args) if (command === "go") { - if (data.text == "" || data.text == "help") { + if (args[0] == "help" || data.text == "") { return await helpMessage(context, data); - } + } else if (args[0] == "ping") { + return context.json({ + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ":tw_white_check_mark: *golinks API is reachable at `https://go.andreijiroh.xyz/api` at the moment*\n\nIf the bot is up but still not reachable on your side, check your network or ", + }, + }, + { + type: "context", + elements: [ + { + text: "Check and for updates.", + type: "mrkdwn", + }, + ], + }, + ], + }); + } else if (args[0] == "shorten") { + if (args[1]) { + const linkToShorten = args[1].replace(/^<|>$/gm, ''); + const randomSlug = generateSlug(12) + const result = await addGoLink(context.env.golinks, randomSlug, args[1], "golinks") + return context.json({ + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ":link: *Here's your short link*: https://go.andreijiroh.xyz/tbd\n\n:point_down: *Your QR code is also provided below, powered by `api.qrserver.com`.* Save the image to your device or share as you do.", + }, + }, + { + type: "image", + image_url: generateQRCodeImgUrl(context, randomSlug), + alt_text: "QR code of the shortened link", + }, + { + type: "divider", + }, + { + type: "rich_text", + elements: [ + { + type: "rich_text_section", + elements: [ + { + type: "emoji", + name: "information_source", + unicode: "2139-fe0f", + }, + { + type: "text", + text: " ", + }, + { + type: "text", + text: "Here's the resulting data from the backend", + style: { + bold: true, + }, + }, + { + type: "text", + text: ":\n\n", + }, + ], + }, + { + type: "rich_text_preformatted", + border: 0, + elements: [ + { + type: "text", + text: JSON.stringify(result), + }, + ], + }, + ], + }, + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: "@ajhalili2006 is being notified about this for abuse monitoring via Worker logs and in Slack.", + }, + ], + }, + ], + }); + } + } + return context.json({ + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ":warning: Sorry, I don't understand that command. Have you tried `/go help`?", + }, + }, + { + type: "context", + elements: [ + { + text: "If something go wrong, or ping @ajhalili2006 on the fediverse (or here in the Slack if he's here).", + type: "mrkdwn", + }, + ], + }, + ], + }); } else if (command == "ping") { - const end = Date.now() - start; - await console.log(`Pong with ${end}ms`); - return context.newResponse(`Pong with ${end}ms response time in backend`); + return context.json({ + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: ":tw_white_check_mark: *golinks API is reachable at `https://go.andreijiroh.xyz/api` at the moment*\n\nIf the bot is up but still not reachable on your side, check your network or ", + }, + }, + { + type: "context", + elements: [ + { + text: "Check and for updates.", + type: "mrkdwn", + }, + ], + }, + ], + }); } return context.newResponse("Unsupported command"); } diff --git a/apps/golinks-v2/src/api/wikilinks.ts b/apps/golinks-v2/src/api/wikilinks.ts index 3a9e2f1..275e62f 100644 --- a/apps/golinks-v2/src/api/wikilinks.ts +++ b/apps/golinks-v2/src/api/wikilinks.ts @@ -22,7 +22,7 @@ export class WikiLinkCreate extends OpenAPIRoute { }, security: [ { - adminApiKey: [], + userApiKey: [], }, ], }; @@ -78,9 +78,6 @@ export class WikiLinkList extends OpenAPIRoute { }), }, security: [ - { - adminApiKey: [], - }, { userApiKey: [], }, @@ -114,3 +111,18 @@ export class WikiLinkList extends OpenAPIRoute { }; } } + +export class WikiLinkUpdate extends OpenAPIRoute { + schema = { + tags: ["wikilinks"], + summary: "Update a wikilink", + parameters: [ + { + name: "slug", + in: "path", + description: "Slug name of the wikilink to be changed", + }, + ], + security: [{ userApiKey: [] }], + }; +} diff --git a/apps/golinks-v2/src/index.tsx b/apps/golinks-v2/src/index.tsx index 4578ae2..b106372 100644 --- a/apps/golinks-v2/src/index.tsx +++ b/apps/golinks-v2/src/index.tsx @@ -16,7 +16,6 @@ import { wikilinkNotAvailable, } from "lib/constants"; import { DiscordInviteLinkCreate, DiscordInviteLinkList } from "api/discord"; -import { adminApiKeyAuth, slackAppInstaller } from "lib/auth"; import { DeprecatedGoLinkPage } from "pages/deprecated-link"; import { CommitHash, PingPong } from "api/meta"; import { prettyJSON } from "hono/pretty-json"; @@ -31,10 +30,7 @@ import { slackOAuthCallback, } from "api/slack"; import { githubAuth } from "api/github"; -import * as jose from "jose"; -import { IncomingMessage, ServerResponse } from "node:http"; -import { InstallationQuery } from "@slack/oauth"; -import { WikiLinkCreate } from "api/wikilinks"; +import { WikiLinkCreate, WikiLinkList } from "api/wikilinks"; import { debugApiGetBindings } from "api/debug"; import { bearerAuth } from 'hono/bearer-auth' @@ -75,9 +71,6 @@ app.on(debugApiMethods, "/api/debug/*", async (c, next) => { }) return bearer(c, next) }) -app.on("POST", "/api/slack/*", async (c, next) => { - return await next() -}) app.on(privilegedMethods, "/api/*", async (c, next) => { const bearer = bearerAuth({ verifyToken: async (token: string, context: Context) => { @@ -87,6 +80,10 @@ app.on(privilegedMethods, "/api/*", async (c, next) => { } } }) + + if (c.req.path.startsWith("/api/slack")) { + return await next() + } return bearer(c, next) }) app.use("/*", async (c, next) => await handleOldUrls(c, next)); @@ -122,6 +119,7 @@ openapi.get("/api/links/:slug", GoLinkInfo) openapi.put("/api/links/:slug", GoLinkUpdate); openapi.delete("/api/links/:slug", GoLinkDelete) // category:wikilinks +openapi.get("/api/wikilinks", WikiLinkList) openapi.post("/api/wikilinks", WikiLinkCreate) // category:discord-invites openapi.get("/api/discord-invites", DiscordInviteLinkList); @@ -185,21 +183,29 @@ app.get("/feedback/:type", (c) => { return c.redirect(generateNewIssueUrl(type, "golinks", url)); }); -app.get("/:link", async (c) => { - try { - const { link } = c.req.param(); - console.log(`[redirector]: incoming request with path - ${link}`); - const result = await getLink(c.env.golinks, link); - console.log(`[redirector]: resulting data - ${JSON.stringify(result)}`); - if (!result) { - return c.newResponse(golinkNotFound(c.req.url), 404); - } - return c.redirect(result.targetUrl); - } catch (error) { - console.error(`[redirector]: error`, error); - return c.newResponse(golinkNotFound(c.req.url), 500); - } -}); +app.on("GET", "/*", async(c, next) => { + const path = c.req.path.replace(/^\/|\/$/g, '');; + const params = c.req.query() + console.log(`[redirector]: incoming request with path - ${path}`); + try { + if (path.startsWith("/api") || path.startsWith("/go") || path.startsWith("/discord")) { + return await next() + } + const result = await getLink(c.env.golinks, path); + console.log(`[redirector]: resulting data - ${JSON.stringify(result)}`); + if (!result) { + return c.newResponse(golinkNotFound(c.req.url), 404); + } + + if (result.is_active == false && !params.force_redirect) { + return c.redirect(`/landing/deprecated?golink=${path}&reason=${result.deactivation_reason}`) + } + return c.redirect(result.targetUrl) + } catch (error) { + console.error(`[redirector]: error`, error); + return c.newResponse(golinkNotFound(c.req.url), 500); + } +}) app.get("/discord/:inviteCode", async (c) => { try { const { inviteCode } = c.req.param(); @@ -214,6 +220,7 @@ app.get("/discord/:inviteCode", async (c) => { return c.newResponse(discordServerNotFound(c.req.url), 500); } }); + app.get("/go/:link", async (c) => { const url = new URL(c.req.url) const { hostname } = url diff --git a/apps/golinks-v2/src/lib/auth.ts b/apps/golinks-v2/src/lib/auth.ts index 1a6bd6b..276e302 100644 --- a/apps/golinks-v2/src/lib/auth.ts +++ b/apps/golinks-v2/src/lib/auth.ts @@ -22,7 +22,6 @@ export const slackAppInstaller = (env: EnvBindings) => // single team app installation return await env.slackBotTokens.put(installation.team.id, JSON.stringify(installation)); } - throw new Error("Failed saving installation data to installationStore"); }, fetchInstallation: async (query) => { if (query.isEnterpriseInstall && query.enterpriseId !== undefined) {