Skip to content

Commit

Permalink
reformats, ups, etc.
Browse files Browse the repository at this point in the history
Signed-off-by: CyberFlame <[email protected]>
  • Loading branch information
CyberFlameGO committed Aug 17, 2023
1 parent 4d2ee10 commit adadc14
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 105 deletions.
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 28 additions & 7 deletions src/discord.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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 {
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -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);
Expand Down
35 changes: 23 additions & 12 deletions src/nzqa_lookup.ts
Original file line number Diff line number Diff line change
@@ -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}`,
{
Expand All @@ -14,7 +22,6 @@ async function follow_up(body: RESTPostAPIInteractionFollowupJSONBody, applicati
);
}


export async function lookup(
input: number,
applicationId: string,
Expand All @@ -23,15 +30,18 @@ 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);

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;
}

Expand All @@ -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();
Expand Down Expand Up @@ -166,21 +176,22 @@ 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);
} catch (error) {
const followupData: RESTPostAPIInteractionFollowupJSONBody = {
content:
'Please enter a valid standard! (</lookup:1137896912020840599>)\nIf you believe this is a mistake, message `cyberflameu`.',
}
await follow_up(followupData, applicationId, token)
};
await follow_up(followupData, applicationId, token);
}
return;
}
156 changes: 88 additions & 68 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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: {
Expand All @@ -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,
Expand Down Expand Up @@ -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(): {
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand Down
Loading

0 comments on commit adadc14

Please sign in to comment.