import {SavedWebhook} from "./SavedWebhook.js"; import {FastifyReply, FastifyRequest} from "fastify"; import {AccessToken, AuthorizationCode, ModuleOptions} from "simple-oauth2"; import pug from "pug"; import {renderError} from "./PugRenderer.js"; import {APIWebhook} from "discord-api-types"; import {getBaseUrl, getFirstValue} from "./FastifyHelpers.js"; import {join, relative} from "path"; import {RouteGenericInterface} from "fastify/types/route"; import {checkAndClearXSRFCookie, generateXSRFCookie} from "./CookieHelpers.js"; interface DiscordWebhookToken extends AccessToken { token: { webhook?: APIWebhook } } const AuthConfig: ModuleOptions["auth"] = { tokenHost: "https://discord.com", authorizePath: "/api/oauth2/authorize", tokenPath: "/api/oauth2/token", revokePath: "/api/oauth2/token/revoke", } export interface OAuthRoute extends RouteGenericInterface { Querystring: { code: string | string[] | undefined, state: string | string[] | undefined, error: string | string[] | undefined, error_description: string | string[] | undefined, error_uri: string | string[] | undefined, } } export class DiscordWebhookHandler { readonly webhook: SavedWebhook readonly templateFilename: string readonly config: ModuleOptions readonly destinationFunc: () => string constructor({ webhook, templateFilename, appId, secret, destinationFunc }: { webhook: SavedWebhook, templateFilename: string, appId: string, secret: string, destinationFunc: () => string }) { this.templateFilename = templateFilename this.webhook = webhook this.config = { client: { id: appId, secret, }, auth: AuthConfig, } this.destinationFunc = destinationFunc } async handleRequest(req: FastifyRequest, res: FastifyReply): Promise { const baseUrl = getBaseUrl(req) const withoutParams = new URL(req.url) withoutParams.search = "" const code = getFirstValue(req.query.code) ?? null const errorCode = getFirstValue(req.query.error) ?? null const client = new AuthorizationCode(this.config) if (code === null && errorCode === null) { const state = generateXSRFCookie(res) const authUrl = client.authorizeURL({ scope: ["applications.commands", "webhook.incoming"], redirect_uri: withoutParams.toString(), state, }) res.code(200) res.type("text/html") res.send(pug.renderFile(join("static/pages", this.templateFilename), { authUrl, baseUrl, isReset: this.webhook.isPresent })) return } const validXSRF = checkAndClearXSRFCookie(req, res) if (errorCode !== null || code === null) { const errorDescription = getFirstValue(req.query.error_description) ?? null const errorUrl = getFirstValue(req.query.error_uri) return renderError({ baseUrl, res, code: 400, error: errorDescription ?? errorCode ?? "There was no code or error present.", context: "authorizing the application", errorUrl, buttonText: "Try Again", buttonUrl: relative("/", new URL(req.url).pathname), }) } if (!validXSRF) { return renderError({ baseUrl, res, code: 409, error: "The state was incorrectly set - try restarting the authentication process.", context: "processing the new access code", buttonText: "Try Again", buttonUrl: relative("/", new URL(req.url).pathname), }) } let token: DiscordWebhookToken try { token = await client.getToken({ code, scope: ["applications.commands", "webhook.incoming"], redirect_uri: getBaseUrl(req) + "setup/gameChannel" }) as DiscordWebhookToken } catch (e) { return renderError({ baseUrl, res: res, error: e, code: 400, context: "exchanging the code you gave me for a token", buttonText: "Try Again", buttonUrl: relative("/", new URL(req.url).pathname), }) } if (!token.token.webhook) { return renderError({ baseUrl, res: res, code: 400, error: "The token did not contain a webhook.", context: "processing the token I received", buttonText: "Try Again", buttonUrl: relative("/", new URL(req.url).pathname), }) } const wasSet = this.webhook.isPresent try { await this.webhook.replaceHook(token.token.webhook) } catch (e) { return renderError({ baseUrl, res: res, code: 500, error: e, context: `saving the new webhook${wasSet ? " and deleting the old one" : ""}`, buttonText: "Try Again", buttonUrl: relative("/", new URL(req.url).pathname), }) } res.redirect(this.destinationFunc()) } }