From adadc141f237d92d681913a8247f550a104499e6 Mon Sep 17 00:00:00 2001 From: CyberFlame Date: Fri, 18 Aug 2023 10:52:54 +1200 Subject: [PATCH] reformats, ups, etc. Signed-off-by: CyberFlame --- pnpm-lock.yaml | 8 +-- src/discord.ts | 35 ++++++++-- src/nzqa_lookup.ts | 35 ++++++---- src/server.ts | 156 +++++++++++++++++++++++++-------------------- src/storage.ts | 30 +++++---- wrangler.toml | 2 +- 6 files changed, 161 insertions(+), 105 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bb2dca..0fba9ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -690,8 +690,8 @@ packages: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: true - /better-sqlite3@8.5.0: - resolution: {integrity: sha512-vbPcv/Hx5WYdyNg/NbcfyaBZyv9s/NVbxb7yCeC5Bq1pVocNxeL2tZmSu3Rlm4IEOTjYdGyzWQgyx0OSdORBzw==} + /better-sqlite3@8.5.1: + resolution: {integrity: sha512-aDfC67xfll6bugnOqRJhdUWioQZnkhLkrwZ+oo6yZbNMtyktbwkDO4SfBcCVWbm4BlsCjCNTJchlHaBt+vB4Iw==} requiresBuild: true dependencies: bindings: 1.5.0 @@ -949,7 +949,7 @@ packages: dev: true /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} dev: true /convert-source-map@1.9.0: @@ -1924,7 +1924,7 @@ packages: dependencies: acorn: 8.10.0 acorn-walk: 8.2.0 - better-sqlite3: 8.5.0 + better-sqlite3: 8.5.1 capnp-ts: 0.7.0 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 diff --git a/src/discord.ts b/src/discord.ts index 3362f67..e54aaa2 100644 --- a/src/discord.ts +++ b/src/discord.ts @@ -1,8 +1,12 @@ -import { RESTGetAPIOAuth2CurrentAuthorizationResult, RESTPostOAuth2AccessTokenResult, RESTPostOAuth2RefreshTokenResult, Snowflake } from 'discord-api-types/v10'; +import { + RESTGetAPIOAuth2CurrentAuthorizationResult, + RESTPostOAuth2AccessTokenResult, + RESTPostOAuth2RefreshTokenResult, + Snowflake, +} from 'discord-api-types/v10'; import { Bindings } from './server'; import * as storage from './storage'; - export function getOAuthUrl(env: any) { const state = crypto.randomUUID(); const url = new URL('https://discord.com/api/oauth2/authorize'); @@ -11,7 +15,10 @@ export function getOAuthUrl(env: any) { url.searchParams.set('redirect_uri', env.WORKER_URL + '/oauth-callback'); url.searchParams.set('response_type', 'code'); url.searchParams.set('state', state); - url.searchParams.set('scope', 'role_connections.write identify guilds connections guilds.members.read email guilds.join'); + url.searchParams.set( + 'scope', + 'role_connections.write identify guilds connections guilds.members.read email guilds.join' + ); url.searchParams.set('prompt', 'consent'); return { @@ -52,7 +59,11 @@ export async function getOAuthTokens(code: string | undefined, env: Bindings) { } } -export async function getAccessToken(userId: Snowflake, tokens: storage.Tokens, env: Bindings) { +export async function getAccessToken( + userId: Snowflake, + tokens: storage.Tokens, + env: Bindings +) { if (Date.now() > tokens.expires_in) { const url = 'https://discord.com/api/v10/oauth2/token'; @@ -98,7 +109,8 @@ export async function getUserData(tokens: RESTPostOAuth2AccessTokenResult) { }); if (response.ok) { - const data: RESTGetAPIOAuth2CurrentAuthorizationResult = await response.json(); + const data: RESTGetAPIOAuth2CurrentAuthorizationResult = + await response.json(); return data; } else { @@ -107,7 +119,11 @@ export async function getUserData(tokens: RESTPostOAuth2AccessTokenResult) { } } -export async function getMetadata(userId: Snowflake, tokens: any, env: Bindings) { +export async function getMetadata( + userId: Snowflake, + tokens: any, + env: Bindings +) { const url = `https://discord.com/api/v10/users/@me/applications/${env.DISCORD_APPLICATION_ID}/role-connection`; const accessToken = await getAccessToken(userId, tokens, env); @@ -130,7 +146,12 @@ export async function getMetadata(userId: Snowflake, tokens: any, env: Bindings) } } -export async function pushMetadata(userId: Snowflake, tokens: storage.Tokens, body: Object, env: Bindings) { +export async function pushMetadata( + userId: Snowflake, + tokens: storage.Tokens, + body: Object, + env: Bindings +) { const url = `https://discord.com/api/v10/users/@me/applications/${env.DISCORD_APPLICATION_ID}/role-connection`; const accessToken = await getAccessToken(userId, tokens, env); diff --git a/src/nzqa_lookup.ts b/src/nzqa_lookup.ts index ca6ce9e..8f23724 100644 --- a/src/nzqa_lookup.ts +++ b/src/nzqa_lookup.ts @@ -1,7 +1,15 @@ import { CheerioAPI, Cheerio, Element, load, AnyNode } from 'cheerio'; -import { APIEmbed, APIEmbedField, RESTPostAPIInteractionFollowupJSONBody } from 'discord-api-types/v10'; - -async function follow_up(body: RESTPostAPIInteractionFollowupJSONBody, applicationId: string, token: string) { +import { + APIEmbed, + APIEmbedField, + RESTPostAPIInteractionFollowupJSONBody, +} from 'discord-api-types/v10'; + +async function follow_up( + body: RESTPostAPIInteractionFollowupJSONBody, + applicationId: string, + token: string +) { await fetch( `https://discord.com/api/v10/webhooks/${applicationId}/${token}`, { @@ -14,7 +22,6 @@ async function follow_up(body: RESTPostAPIInteractionFollowupJSONBody, applicati ); } - export async function lookup( input: number, applicationId: string, @@ -23,7 +30,6 @@ export async function lookup( var standard: number = input; const standardUri: string = `https://www.nzqa.govt.nz/ncea/assessment/view-detailed.do?standardNumber=${standard}`; - const cacheKey: string = new URL(standardUri).toString(); // Use a valid URL for cacheKey const cache: Cache = caches.default; const cachedResponse: Response | undefined = await cache.match(cacheKey); @@ -31,7 +37,11 @@ export async function lookup( if (cachedResponse) { console.log('Cache hit'); // Use cached response if it exists - await follow_up(await cachedResponse.json() as RESTPostAPIInteractionFollowupJSONBody, applicationId, token) + await follow_up( + (await cachedResponse.json()) as RESTPostAPIInteractionFollowupJSONBody, + applicationId, + token + ); return; } @@ -44,8 +54,8 @@ export async function lookup( if (!response.ok) { const followupData: RESTPostAPIInteractionFollowupJSONBody = { content: `An error occurred! Response code: ${response.status}\nIf this repetitively occurs and NZQA is not having an outage, message \`cyberflameu\`.`, - } - await follow_up(followupData, applicationId, token) + }; + await follow_up(followupData, applicationId, token); return; } const data: string = await response.text(); @@ -166,12 +176,13 @@ export async function lookup( // perhaps look at using the discord-api-methods interactionskit package for these if i end up needing to use them for something else - await follow_up(followupData, applicationId, token) + await follow_up(followupData, applicationId, token); const cacheResponse: Response = new Response(JSON.stringify(followupData), { headers: { 'content-type': 'application/json', - 'Cache-Control': 'public, max-age=7200, stale-while-revalidate=7200, stale-if-error=86400', + 'Cache-Control': + 'public, max-age=7200, stale-while-revalidate=7200, stale-if-error=86400', }, }); await cache.put(cacheKey, cacheResponse); @@ -179,8 +190,8 @@ export async function lookup( const followupData: RESTPostAPIInteractionFollowupJSONBody = { content: 'Please enter a valid standard! ()\nIf you believe this is a mistake, message `cyberflameu`.', - } - await follow_up(followupData, applicationId, token) + }; + await follow_up(followupData, applicationId, token); } return; } diff --git a/src/server.ts b/src/server.ts index 428c964..7d9e499 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,44 +16,17 @@ import * as storage from './storage.js'; import * as discord from './discord.js'; export type Bindings = { - DISCORD_PUBLIC_KEY: string - DISCORD_APPLICATION_ID: string - DISCORD_CLIENT_SECRET: string - TOKEN: string - WORKER_URL: string - COOKIE_SECRET: string - TOKEN_STORE: KVNamespace -} + DISCORD_PUBLIC_KEY: string; + DISCORD_APPLICATION_ID: string; + DISCORD_CLIENT_SECRET: string; + TOKEN: string; + WORKER_URL: string; + COOKIE_SECRET: string; + TOKEN_STORE: KVNamespace; +}; const router = new Hono<{ Bindings: Bindings }>(); -async function updateMetadata(userId: discordJs.Snowflake, env: Bindings) { - // Fetch the Discord tokens from storage - const tokens = await storage.getDiscordTokens(userId, env.TOKEN_STORE) as storage.Tokens; - - let metadata = {}; - try { - // Fetch the new metadata you want to use from an external source. - // This data could be POST-ed to this endpoint, but every service - // is going to be different. To keep the example simple, we'll - // just generate some random data. - metadata = { - cookieseaten: 1483, - allergictonuts: 0, // 0 for false, 1 for true - firstcookiebaked: '2003-12-20', - }; - } catch (e) { - console.error(e); - // If fetching the profile data for the external service fails for any reason, - // ensure metadata on the Discord side is nulled out. This prevents cases - // where the user revokes an external app permissions, and is left with - // stale linked role data. - } - - // Push the data to Discord. - await discord.pushMetadata(userId, tokens, metadata, env); -} - /** * A simple :wave: hello page to verify the worker is working. */ @@ -75,9 +48,13 @@ router.post('/interactions', async (c) => { // console.error('Invalid Request'); // return c.text('Bad request signature.', 401); // } - const isValid: boolean | void = await isValidRequest(c.req.raw, c.env.DISCORD_PUBLIC_KEY).catch(console.error) - if (!isValid) return new Response('Invalid request', { status: 401 }) - const interaction: discordJs.APIInteraction = await c.req.json() as discordJs.APIInteraction; + const isValid: boolean | void = await isValidRequest( + c.req.raw, + c.env.DISCORD_PUBLIC_KEY + ).catch(console.error); + if (!isValid) return new Response('Invalid request', { status: 401 }); + const interaction: discordJs.APIInteraction = + (await c.req.json()) as discordJs.APIInteraction; switch (interaction.type) { case discordJs.InteractionType.Ping: { @@ -101,7 +78,10 @@ router.post('/interactions', async (c) => { switch (interaction.data.name.toLowerCase()) { // Revive ping command - checks if a user has a role and pings a role if they do case commands.REVIVE_COMMAND.name.toLowerCase(): { - if (interaction.member && interaction.member.roles.includes('909724765026148402')) { + if ( + interaction.member && + interaction.member.roles.includes('909724765026148402') + ) { console.log('handling revive request'); return c.json({ type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, @@ -150,28 +130,33 @@ router.post('/interactions', async (c) => { }, }); } -case commands.LOOKUP_COMMAND.name.toLowerCase(): { - if (interaction.data && 'options' in interaction.data && interaction.data.options) { - const standardNumber = interaction.data.options[0] as discordJs.APIApplicationCommandInteractionDataNumberOption; - c.executionCtx.waitUntil( - lookup( - standardNumber.value, - interaction.application_id, - interaction.token - ) - ); - return c.json({ - type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, - }); - } else { - return c.json({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: "I'm sorry, I don't recognize that command.", - }, - }); - } -} + case commands.LOOKUP_COMMAND.name.toLowerCase(): { + if ( + interaction.data && + 'options' in interaction.data && + interaction.data.options + ) { + const standardNumber = interaction.data + .options[0] as discordJs.APIApplicationCommandInteractionDataNumberOption; + c.executionCtx.waitUntil( + lookup( + standardNumber.value, + interaction.application_id, + interaction.token + ) + ); + return c.json({ + type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, + }); + } else { + return c.json({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "I'm sorry, I don't recognize that command.", + }, + }); + } + } // Ping command - for checking latency of the bot, returned as a non-ephemeral message case commands.PING_COMMAND.name.toLowerCase(): { @@ -205,12 +190,44 @@ router.get('/linked-roles', async (c) => { return c.redirect(url); }); +async function updateMetadata(userId: discordJs.Snowflake, env: Bindings) { + // Fetch the Discord tokens from storage + const tokens = (await storage.getDiscordTokens( + userId, + env.TOKEN_STORE + )) as storage.Tokens; + + let metadata = {}; + try { + // Fetch the new metadata you want to use from an external source. + // This data could be POST-ed to this endpoint, but every service + // is going to be different. To keep the example simple, we'll + // just generate some random data. + metadata = { + cookieseaten: 1483, + allergictonuts: 0, // 0 for false, 1 for true + firstcookiebaked: '2003-12-20', + }; + } catch (e) { + console.error(e); + // If fetching the profile data for the external service fails for any reason, + // ensure metadata on the Discord side is nulled out. This prevents cases + // where the user revokes an external app permissions, and is left with + // stale linked role data. + } + + // Push the data to Discord. + await discord.pushMetadata(userId, tokens, metadata, env); +} + router.get('/oauth-callback', async (c) => { try { const code = c.req.query('code'); const state = c.req.query('state'); - if (await getSignedCookie(c, c.env.COOKIE_SECRET, 'client_state') !== state) + if ( + (await getSignedCookie(c, c.env.COOKIE_SECRET, 'client_state')) !== state + ) return c.text('state verification failed', 403); const tokens = await discord.getOAuthTokens(code, c.env); @@ -219,12 +236,15 @@ router.get('/oauth-callback', async (c) => { const data = await discord.getUserData(tokens); if (!data || !data.user) return c.text('failed to fetch user data', 500); - - await storage.storeDiscordTokens(data.user.id, { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - expires_in: Date.now() + tokens.expires_in * 1000, - }, c.env.TOKEN_STORE); + await storage.storeDiscordTokens( + data.user.id, + { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + expires_in: Date.now() + tokens.expires_in * 1000, + }, + c.env.TOKEN_STORE + ); await updateMetadata(data.user.id, c.env); diff --git a/src/storage.ts b/src/storage.ts index 971f66b..93bb17f 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,15 +1,19 @@ -import { Snowflake } from "discord-api-types/globals"; +import { Snowflake } from 'discord-api-types/globals'; -export async function storeDiscordTokens(userId: Snowflake, tokens: Object, kv: KVNamespace) { - await kv.put(`discord-${userId}`, JSON.stringify(tokens)); - } - - export async function getDiscordTokens(userId: Snowflake, kv: KVNamespace) { - return kv.get(`discord-${userId}`, { type: "json" }); - } +export async function storeDiscordTokens( + userId: Snowflake, + tokens: Object, + kv: KVNamespace +) { + await kv.put(`discord-${userId}`, JSON.stringify(tokens)); +} - export type Tokens = { - access_token: string; - refresh_token: string; - expires_in: number; - } +export async function getDiscordTokens(userId: Snowflake, kv: KVNamespace) { + return kv.get(`discord-${userId}`, { type: 'json' }); +} + +export type Tokens = { + access_token: string; + refresh_token: string; + expires_in: number; +}; diff --git a/wrangler.toml b/wrangler.toml index b4946ea..932c27b 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,6 +1,6 @@ name = "nceahelpworker" main = "./src/server.ts" -compatibility_date="2023-08-10" +compatibility_date="2023-08-17" account_id= "4ed1f8e12cda519236361f09dd8956cf" usage_model = "bundled"