From 33df1c7edc8392b1ae8378f7c229af5e9fd23f52 Mon Sep 17 00:00:00 2001 From: Katalina Okano Date: Sun, 30 Jan 2022 03:34:41 -0500 Subject: [PATCH] remove old-src, same some historic pieces --- .../audit-log.ts => historic/audit-log.ts~} | 0 .../interactions-pick-role.ts~} | 0 .../interactions-pickable-roles.ts~} | 0 .../api/historic/interactions/hello-world.ts~ | 16 + .../api/historic/interactions/pick-role.ts~ | 86 ++++++ .../historic/interactions/pickable-roles.ts~ | 63 ++++ .../api/historic/interactions/roleypoly.ts~ | 56 ++++ packages/api/old-src/handlers/bot-join.ts | 40 --- .../api/old-src/handlers/clear-guild-cache.ts | 18 -- .../api/old-src/handlers/get-picker-data.ts | 64 ---- packages/api/old-src/handlers/get-session.ts | 13 - packages/api/old-src/handlers/get-slug.ts | 40 --- packages/api/old-src/handlers/login-bounce.ts | 34 --- .../api/old-src/handlers/login-callback.ts | 160 ---------- .../api/old-src/handlers/revoke-session.ts | 28 -- .../api/old-src/handlers/sync-from-legacy.ts | 23 -- packages/api/old-src/handlers/update-guild.ts | 51 ---- packages/api/old-src/handlers/update-roles.ts | 142 --------- packages/api/old-src/index.ts | 70 ----- packages/api/old-src/utils/access-control.ts | 52 ---- packages/api/old-src/utils/api-tools.ts | 176 ----------- packages/api/old-src/utils/bounce.ts | 7 - packages/api/old-src/utils/config.ts | 16 - packages/api/old-src/utils/feature-flags.ts | 38 --- packages/api/old-src/utils/guild.ts | 289 ------------------ .../api/old-src/utils/import-from-legacy.ts | 62 ---- packages/api/old-src/utils/kv.ts | 7 - packages/api/old-src/utils/rate-limiting.ts | 13 - packages/api/old-src/utils/responses.ts | 25 -- packages/api/old-src/webpack.config.js | 28 -- packages/api/old-src/worker.config.js | 18 -- 31 files changed, 221 insertions(+), 1414 deletions(-) rename packages/api/{old-src/utils/audit-log.ts => historic/audit-log.ts~} (100%) rename packages/api/{old-src/handlers/interactions-pick-role.ts => historic/interactions-pick-role.ts~} (100%) rename packages/api/{old-src/handlers/interactions-pickable-roles.ts => historic/interactions-pickable-roles.ts~} (100%) create mode 100644 packages/api/historic/interactions/hello-world.ts~ create mode 100644 packages/api/historic/interactions/pick-role.ts~ create mode 100644 packages/api/historic/interactions/pickable-roles.ts~ create mode 100644 packages/api/historic/interactions/roleypoly.ts~ delete mode 100644 packages/api/old-src/handlers/bot-join.ts delete mode 100644 packages/api/old-src/handlers/clear-guild-cache.ts delete mode 100644 packages/api/old-src/handlers/get-picker-data.ts delete mode 100644 packages/api/old-src/handlers/get-session.ts delete mode 100644 packages/api/old-src/handlers/get-slug.ts delete mode 100644 packages/api/old-src/handlers/login-bounce.ts delete mode 100644 packages/api/old-src/handlers/login-callback.ts delete mode 100644 packages/api/old-src/handlers/revoke-session.ts delete mode 100644 packages/api/old-src/handlers/sync-from-legacy.ts delete mode 100644 packages/api/old-src/handlers/update-guild.ts delete mode 100644 packages/api/old-src/handlers/update-roles.ts delete mode 100644 packages/api/old-src/index.ts delete mode 100644 packages/api/old-src/utils/access-control.ts delete mode 100644 packages/api/old-src/utils/api-tools.ts delete mode 100644 packages/api/old-src/utils/bounce.ts delete mode 100644 packages/api/old-src/utils/config.ts delete mode 100644 packages/api/old-src/utils/feature-flags.ts delete mode 100644 packages/api/old-src/utils/guild.ts delete mode 100644 packages/api/old-src/utils/import-from-legacy.ts delete mode 100644 packages/api/old-src/utils/kv.ts delete mode 100644 packages/api/old-src/utils/rate-limiting.ts delete mode 100644 packages/api/old-src/utils/responses.ts delete mode 100644 packages/api/old-src/webpack.config.js delete mode 100644 packages/api/old-src/worker.config.js diff --git a/packages/api/old-src/utils/audit-log.ts b/packages/api/historic/audit-log.ts~ similarity index 100% rename from packages/api/old-src/utils/audit-log.ts rename to packages/api/historic/audit-log.ts~ diff --git a/packages/api/old-src/handlers/interactions-pick-role.ts b/packages/api/historic/interactions-pick-role.ts~ similarity index 100% rename from packages/api/old-src/handlers/interactions-pick-role.ts rename to packages/api/historic/interactions-pick-role.ts~ diff --git a/packages/api/old-src/handlers/interactions-pickable-roles.ts b/packages/api/historic/interactions-pickable-roles.ts~ similarity index 100% rename from packages/api/old-src/handlers/interactions-pickable-roles.ts rename to packages/api/historic/interactions-pickable-roles.ts~ diff --git a/packages/api/historic/interactions/hello-world.ts~ b/packages/api/historic/interactions/hello-world.ts~ new file mode 100644 index 0000000..990ffe7 --- /dev/null +++ b/packages/api/historic/interactions/hello-world.ts~ @@ -0,0 +1,16 @@ +import { + InteractionCallbackType, + InteractionRequestCommand, + InteractionResponse, +} from '@roleypoly/types'; + +export const helloWorld = async ( + interaction: InteractionRequestCommand +): Promise => { + return { + type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `Hey there, ${interaction.member?.nick || interaction.user?.username}`, + }, + }; +}; diff --git a/packages/api/historic/interactions/pick-role.ts~ b/packages/api/historic/interactions/pick-role.ts~ new file mode 100644 index 0000000..52c0864 --- /dev/null +++ b/packages/api/historic/interactions/pick-role.ts~ @@ -0,0 +1,86 @@ +import { selectRole } from '@roleypoly/interactions/utils/api'; +import { + asyncPreflightEphemeral, + asyncResponse, +} from '@roleypoly/interactions/utils/interactions'; +import { invalid, mustBeInGuild } from '@roleypoly/interactions/utils/responses'; +import { + InteractionCallbackType, + InteractionFlags, + InteractionRequestCommand, + InteractionResponse, +} from '@roleypoly/types'; + +export const pickRole = (mode: 'add' | 'remove') => + asyncResponse( + async (interaction: InteractionRequestCommand): Promise => { + if (!interaction.guild_id) { + return mustBeInGuild(); + } + + const userID = interaction.member?.user?.id; + if (!userID) { + return mustBeInGuild(); + } + + const roleID = interaction.data.options?.find( + (option) => option.name === 'role' + )?.value; + if (!roleID) { + return invalid(); + } + + const code = await selectRole(mode, interaction.guild_id, userID, roleID); + + if (code === 409) { + return { + type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `:x: You ${mode === 'add' ? 'already' : "don't"} have that role.`, + flags: InteractionFlags.EPHEMERAL, + }, + }; + } + + if (code === 404) { + return { + type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `:x: <@&${roleID}> isn't pickable.`, + flags: InteractionFlags.EPHEMERAL, + }, + }; + } + + if (code === 403) { + return { + type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `:x: <@&${roleID}> has unsafe permissions.`, + flags: InteractionFlags.EPHEMERAL, + }, + }; + } + + if (code !== 200) { + return { + type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `:x: Something went wrong, please try again later.`, + flags: InteractionFlags.EPHEMERAL, + }, + }; + } + + return { + type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `:white_check_mark: You ${ + mode === 'add' ? 'got' : 'removed' + } the role: <@&${roleID}>`, + flags: InteractionFlags.EPHEMERAL, + }, + }; + }, + asyncPreflightEphemeral + ); diff --git a/packages/api/historic/interactions/pickable-roles.ts~ b/packages/api/historic/interactions/pickable-roles.ts~ new file mode 100644 index 0000000..a1a253a --- /dev/null +++ b/packages/api/historic/interactions/pickable-roles.ts~ @@ -0,0 +1,63 @@ +import { getPickableRoles } from '@roleypoly/interactions/utils/api'; +import { uiPublicURI } from '@roleypoly/interactions/utils/config'; +import { + asyncPreflightEphemeral, + asyncResponse, +} from '@roleypoly/interactions/utils/interactions'; +import { mustBeInGuild } from '@roleypoly/interactions/utils/responses'; +import { + CategoryType, + Embed, + InteractionCallbackType, + InteractionFlags, + InteractionRequestCommand, + InteractionResponse, +} from '@roleypoly/types'; + +export const pickableRoles = asyncResponse( + async (interaction: InteractionRequestCommand): Promise => { + if (!interaction.guild_id) { + return mustBeInGuild(); + } + + const pickableRoles = await getPickableRoles(interaction.guild_id); + const embed: Embed = { + color: 0xab9b9a, + fields: [], + title: 'You can pick any of these roles with /pick-role', + }; + + for (let categoryName in pickableRoles) { + const { roles, type } = pickableRoles[categoryName]; + + embed.fields.push({ + name: `${categoryName}${type === CategoryType.Single ? ' *(pick one)*' : ''}`, + value: roles.map((role) => `<@&${role}>`).join('\n'), + inline: true, + }); + } + + return { + type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [embed], + flags: InteractionFlags.EPHEMERAL, + components: [ + { + type: 1, + components: [ + // Link to Roleypoly + { + type: 2, + label: 'Pick roles on your browser', + url: `${uiPublicURI}/s/${interaction.guild_id}`, + style: 5, + }, + ], + }, + ], + }, + }; + }, + asyncPreflightEphemeral +); diff --git a/packages/api/historic/interactions/roleypoly.ts~ b/packages/api/historic/interactions/roleypoly.ts~ new file mode 100644 index 0000000..f31ce2d --- /dev/null +++ b/packages/api/historic/interactions/roleypoly.ts~ @@ -0,0 +1,56 @@ +import { uiPublicURI } from '@roleypoly/interactions/utils/config'; +import { + Embed, + InteractionCallbackType, + InteractionFlags, + InteractionRequestCommand, + InteractionResponse, +} from '@roleypoly/types'; + +export const roleypoly = async ( + interaction: InteractionRequestCommand +): Promise => { + if (interaction.guild_id) { + return { + type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [ + { + color: 0x453e3d, + title: `:beginner: Hey there, ${ + interaction.member?.nick || interaction.member?.user?.username || 'friend' + }!`, + description: `Try these slash commands, or pick roles from your browser!`, + fields: [ + { name: 'See all the roles', value: '/pickable-roles' }, + { name: 'Pick a role', value: '/pick-role' }, + { name: 'Remove a role', value: '/remove-role' }, + ], + } as Embed, + ], + components: [ + { + type: 1, + components: [ + // Link to Roleypoly + { + type: 2, + label: `Pick roles on ${new URL(uiPublicURI).hostname}`, + url: `${uiPublicURI}/s/${interaction.guild_id}`, + style: 5, + }, + ], + }, + ], + flags: InteractionFlags.EPHEMERAL, + }, + }; + } + + return { + type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `:beginner: Hey! I don't know what server you're in, so check out ${uiPublicURI}`, + }, + }; +}; diff --git a/packages/api/old-src/handlers/bot-join.ts b/packages/api/old-src/handlers/bot-join.ts deleted file mode 100644 index 18a40ca..0000000 --- a/packages/api/old-src/handlers/bot-join.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Bounce } from '../utils/bounce'; -import { botClientID } from '../utils/config'; - -const validGuildID = /^[0-9]+$/; - -type URLParams = { - clientID: string; - permissions: number; - guildID?: string; - scopes: string[]; -}; - -const buildURL = (params: URLParams) => { - let url = `https://discord.com/api/oauth2/authorize?client_id=${ - params.clientID - }&scope=${params.scopes.join('%20')}&permissions=${params.permissions}`; - - if (params.guildID) { - url += `&guild_id=${params.guildID}&disable_guild_select=true`; - } - - return url; -}; - -export const BotJoin = (request: Request): Response => { - let guildID = new URL(request.url).searchParams.get('guild') || ''; - - if (guildID && !validGuildID.test(guildID)) { - guildID = ''; - } - - return Bounce( - buildURL({ - clientID: botClientID, - permissions: 268435456, - guildID, - scopes: ['bot', 'applications.commands'], - }) - ); -}; diff --git a/packages/api/old-src/handlers/clear-guild-cache.ts b/packages/api/old-src/handlers/clear-guild-cache.ts deleted file mode 100644 index b122ad2..0000000 --- a/packages/api/old-src/handlers/clear-guild-cache.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { asEditor, getGuild, GuildRateLimiterKey } from '../utils/guild'; -import { notFound, ok } from '../utils/responses'; - -export const ClearGuildCache = asEditor( - { - rateLimitKey: GuildRateLimiterKey.cacheClear, - rateLimitTimeoutSeconds: 60 * 5, - }, - (session, { guildID }) => - async (request: Request): Promise => { - const result = await getGuild(guildID, { skipCachePull: true }); - if (!result) { - return notFound(); - } - - return ok(); - } -); diff --git a/packages/api/old-src/handlers/get-picker-data.ts b/packages/api/old-src/handlers/get-picker-data.ts deleted file mode 100644 index 786f237..0000000 --- a/packages/api/old-src/handlers/get-picker-data.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { memberPassesAccessControl } from '@roleypoly/api/utils/access-control'; -import { accessControlViolation } from '@roleypoly/api/utils/responses'; -import { DiscordUser, GuildSlug, PresentableGuild, SessionData } from '@roleypoly/types'; -import { respond } from '@roleypoly/worker-utils'; -import { withSession } from '../utils/api-tools'; -import { getGuild, getGuildData, getGuildMember } from '../utils/guild'; - -const fail = () => respond({ error: 'guild not found' }, { status: 404 }); - -export const GetPickerData = withSession( - (session: SessionData) => - async (request: Request): Promise => { - const url = new URL(request.url); - const [, , guildID] = url.pathname.split('/'); - - if (!guildID) { - return respond({ error: 'missing guild id' }, { status: 400 }); - } - - const { id: userID } = session.user as DiscordUser; - const guilds = session.guilds as GuildSlug[]; - - // Save a Discord API request by checking if this user is a member by session first - const checkGuild = guilds.find((guild) => guild.id === guildID); - if (!checkGuild) { - return fail(); - } - - const guild = await getGuild(guildID, { - skipCachePull: url.searchParams.has('__no_cache'), // TODO: rate limit this - }); - if (!guild) { - return fail(); - } - - const memberP = getGuildMember({ - serverID: guildID, - userID, - }); - - const guildDataP = getGuildData(guildID); - - const [guildData, member] = await Promise.all([guildDataP, memberP]); - if (!member) { - return fail(); - } - - if (!memberPassesAccessControl(checkGuild, member, guildData.accessControl)) { - return accessControlViolation(); - } - - const presentableGuild: PresentableGuild = { - id: guildID, - guild: checkGuild, - roles: guild.roles, - member: { - roles: member.roles, - }, - data: guildData, - }; - - return respond(presentableGuild); - } -); diff --git a/packages/api/old-src/handlers/get-session.ts b/packages/api/old-src/handlers/get-session.ts deleted file mode 100644 index 3684ff7..0000000 --- a/packages/api/old-src/handlers/get-session.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { SessionData } from '@roleypoly/types'; -import { respond } from '@roleypoly/worker-utils'; -import { withSession } from '../utils/api-tools'; - -export const GetSession = withSession((session?: SessionData) => (): Response => { - const { user, guilds, sessionID } = session || {}; - - return respond({ - user, - guilds, - sessionID, - }); -}); diff --git a/packages/api/old-src/handlers/get-slug.ts b/packages/api/old-src/handlers/get-slug.ts deleted file mode 100644 index 860231c..0000000 --- a/packages/api/old-src/handlers/get-slug.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { GuildSlug } from '@roleypoly/types'; -import { respond } from '@roleypoly/worker-utils'; -import { getGuild } from '../utils/guild'; - -export const GetSlug = async (request: Request): Promise => { - const reqURL = new URL(request.url); - const [, , serverID] = reqURL.pathname.split('/'); - - if (!serverID) { - return respond( - { - error: 'missing server ID', - }, - { - status: 400, - } - ); - } - - const guild = await getGuild(serverID); - if (!guild) { - return respond( - { - error: 'guild not found', - }, - { - status: 404, - } - ); - } - - const { id, name, icon } = guild; - const guildSlug: GuildSlug = { - id, - name, - icon, - permissionLevel: 0, - }; - return respond(guildSlug); -}; diff --git a/packages/api/old-src/handlers/login-bounce.ts b/packages/api/old-src/handlers/login-bounce.ts deleted file mode 100644 index bbfb5e1..0000000 --- a/packages/api/old-src/handlers/login-bounce.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { StateSession } from '@roleypoly/types'; -import { getQuery } from '@roleypoly/worker-utils'; -import { isAllowedCallbackHost, setupStateSession } from '../utils/api-tools'; -import { Bounce } from '../utils/bounce'; -import { apiPublicURI, botClientID } from '../utils/config'; - -type URLParams = { - clientID: string; - redirectURI: string; - state: string; -}; - -const buildURL = (params: URLParams) => - `https://discord.com/api/oauth2/authorize?client_id=${ - params.clientID - }&response_type=code&scope=identify%20guilds&prompt=none&redirect_uri=${encodeURIComponent( - params.redirectURI - )}&state=${params.state}`; - -export const LoginBounce = async (request: Request): Promise => { - const stateSessionData: StateSession = {}; - - const { cbh: callbackHost } = getQuery(request); - if (callbackHost && isAllowedCallbackHost(callbackHost)) { - stateSessionData.callbackHost = callbackHost; - } - - const state = await setupStateSession(stateSessionData); - - const redirectURI = `${apiPublicURI}/login-callback`; - const clientID = botClientID; - - return Bounce(buildURL({ state, redirectURI, clientID })); -}; diff --git a/packages/api/old-src/handlers/login-callback.ts b/packages/api/old-src/handlers/login-callback.ts deleted file mode 100644 index 4258f1d..0000000 --- a/packages/api/old-src/handlers/login-callback.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { - AuthTokenResponse, - DiscordUser, - GuildSlug, - SessionData, - StateSession, -} from '@roleypoly/types'; -import { - AuthType, - discordAPIBase, - discordFetch, - userAgent, -} from '@roleypoly/worker-utils'; -import KSUID from 'ksuid'; -import { - formData, - getStateSession, - isAllowedCallbackHost, - parsePermissions, - resolveFailures, -} from '../utils/api-tools'; -import { Bounce } from '../utils/bounce'; -import { apiPublicURI, botClientID, botClientSecret, uiPublicURI } from '../utils/config'; -import { Sessions } from '../utils/kv'; - -const AuthErrorResponse = (extra?: string) => - Bounce( - uiPublicURI + - `/machinery/error?error_code=authFailure${extra ? `&extra=${extra}` : ''}` - ); - -export const LoginCallback = resolveFailures( - AuthErrorResponse, - async (request: Request): Promise => { - let bounceBaseUrl = uiPublicURI; - - const query = new URL(request.url).searchParams; - const stateValue = query.get('state'); - - if (stateValue === null) { - return AuthErrorResponse('state missing'); - } - - try { - const state = KSUID.parse(stateValue); - const stateExpiry = state.date.getTime() + 1000 * 60 * 5; - const currentTime = Date.now(); - - if (currentTime > stateExpiry) { - return AuthErrorResponse('state expired'); - } - - const stateSession = await getStateSession(state.string); - if ( - stateSession?.callbackHost && - isAllowedCallbackHost(stateSession.callbackHost) - ) { - bounceBaseUrl = stateSession.callbackHost; - } - } catch (e) { - return AuthErrorResponse('state invalid'); - } - - const code = query.get('code'); - if (!code) { - return AuthErrorResponse('code missing'); - } - - const tokenRequest = { - client_id: botClientID, - client_secret: botClientSecret, - grant_type: 'authorization_code', - redirect_uri: apiPublicURI + '/login-callback', - scope: 'identify guilds', - code, - }; - - const tokenFetch = await fetch(discordAPIBase + '/oauth2/token', { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - 'user-agent': userAgent, - }, - body: formData(tokenRequest), - }); - - const tokens = (await tokenFetch.json()) as AuthTokenResponse; - - if (!tokens.access_token) { - return AuthErrorResponse('token response invalid'); - } - - const [sessionID, user, guilds] = await Promise.all([ - KSUID.random(), - getUser(tokens.access_token), - getGuilds(tokens.access_token), - ]); - - if (!user) { - return AuthErrorResponse('failed to fetch user'); - } - - const sessionData: SessionData = { - tokens, - sessionID: sessionID.string, - user, - guilds, - }; - - await Sessions.put(sessionID.string, sessionData, 60 * 60 * 6); - - return Bounce(bounceBaseUrl + 'machinery/new-session/' + sessionID.string); - } -); - -const getUser = async (accessToken: string): Promise => { - const user = await discordFetch( - '/users/@me', - accessToken, - AuthType.Bearer - ); - - if (!user) { - return null; - } - - const { id, username, discriminator, bot, avatar } = user; - - return { id, username, discriminator, bot, avatar }; -}; - -type UserGuildsPayload = { - id: string; - name: string; - icon: string; - owner: boolean; - permissions: number; - features: string[]; -}[]; - -const getGuilds = async (accessToken: string) => { - const guilds = await discordFetch( - '/users/@me/guilds', - accessToken, - AuthType.Bearer - ); - - if (!guilds) { - return []; - } - - const guildSlugs = guilds.map((guild) => ({ - id: guild.id, - name: guild.name, - icon: guild.icon, - permissionLevel: parsePermissions(BigInt(guild.permissions), guild.owner), - })); - - return guildSlugs; -}; diff --git a/packages/api/old-src/handlers/revoke-session.ts b/packages/api/old-src/handlers/revoke-session.ts deleted file mode 100644 index c80acce..0000000 --- a/packages/api/old-src/handlers/revoke-session.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { SessionData } from '@roleypoly/types'; -import { discordAPIBase, respond, userAgent } from '@roleypoly/worker-utils'; -import { formData, withSession } from '../utils/api-tools'; -import { botClientID, botClientSecret } from '../utils/config'; -import { Sessions } from '../utils/kv'; - -export const RevokeSession = withSession( - (session: SessionData) => async (request: Request) => { - const tokenRequest = { - token: session.tokens.access_token, - client_id: botClientID, - client_secret: botClientSecret, - }; - - await fetch(discordAPIBase + '/oauth2/token/revoke', { - method: 'POST', - headers: { - 'content-type': 'application/x-www-form-urlencoded', - 'user-agent': userAgent, - }, - body: formData(tokenRequest), - }); - - await Sessions.delete(session.sessionID); - - return respond({ ok: true }); - } -); diff --git a/packages/api/old-src/handlers/sync-from-legacy.ts b/packages/api/old-src/handlers/sync-from-legacy.ts deleted file mode 100644 index 0fbd080..0000000 --- a/packages/api/old-src/handlers/sync-from-legacy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { asEditor, GuildRateLimiterKey } from '../utils/guild'; -import { fetchLegacyServer, transformLegacyGuild } from '../utils/import-from-legacy'; -import { GuildData } from '../utils/kv'; -import { notFound, ok } from '../utils/responses'; - -export const SyncFromLegacy = asEditor( - { - rateLimitKey: GuildRateLimiterKey.legacyImport, - rateLimitTimeoutSeconds: 60 * 20, - }, - (session, { guildID }) => - async (request: Request): Promise => { - const legacyGuild = await fetchLegacyServer(guildID); - if (!legacyGuild) { - return notFound(); - } - - const newGuildData = transformLegacyGuild(legacyGuild); - await GuildData.put(guildID, newGuildData); - - return ok(); - } -); diff --git a/packages/api/old-src/handlers/update-guild.ts b/packages/api/old-src/handlers/update-guild.ts deleted file mode 100644 index d04d56e..0000000 --- a/packages/api/old-src/handlers/update-guild.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { sendAuditLog, validateAuditLogWebhook } from '@roleypoly/api/utils/audit-log'; -import { GuildDataUpdate, WebhookValidationStatus } from '@roleypoly/types'; -import { asEditor, getGuildData } from '../utils/guild'; -import { GuildData } from '../utils/kv'; -import { invalid, ok } from '../utils/responses'; - -export const UpdateGuild = asEditor( - {}, - (session, { guildID, guild }) => - async (request: Request): Promise => { - const guildUpdate = (await request.json()) as GuildDataUpdate; - - const oldGuildData = await getGuildData(guildID); - const newGuildData = { - ...oldGuildData, - ...guildUpdate, - }; - - if (oldGuildData.auditLogWebhook !== newGuildData.auditLogWebhook) { - try { - const validationStatus = await validateAuditLogWebhook( - guild, - newGuildData.auditLogWebhook - ); - - if (validationStatus !== WebhookValidationStatus.Ok) { - if (validationStatus === WebhookValidationStatus.NoneSet) { - newGuildData.auditLogWebhook = null; - } else { - return invalid({ - what: 'webhookValidationStatus', - webhookValidationStatus: validationStatus, - }); - } - } - } catch (e) { - invalid(); - } - } - - await GuildData.put(guildID, newGuildData); - - try { - await sendAuditLog(oldGuildData, guildUpdate, session.user); - } catch (e) { - // Catching errors here because this isn't a critical task, and could simply fail due to operator error. - } - - return ok(); - } -); diff --git a/packages/api/old-src/handlers/update-roles.ts b/packages/api/old-src/handlers/update-roles.ts deleted file mode 100644 index 237b91b..0000000 --- a/packages/api/old-src/handlers/update-roles.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - GuildData, - Member, - Role, - RoleSafety, - RoleTransaction, - RoleUpdate, - SessionData, - TransactionType, -} from '@roleypoly/types'; -import { AuthType, discordFetch, respond } from '@roleypoly/worker-utils'; -import { difference, groupBy, keyBy, union } from 'lodash'; -import { withSession } from '../utils/api-tools'; -import { botToken, uiPublicURI } from '../utils/config'; -import { - getGuild, - getGuildData, - getGuildMember, - updateGuildMember, -} from '../utils/guild'; - -const notFound = () => respond({ error: 'guild not found' }, { status: 404 }); - -export const UpdateRoles = withSession( - ({ guilds, user: { id: userID, username, discriminator } }: SessionData) => - async (request: Request) => { - const updateRequest = (await request.json()) as RoleUpdate; - const url = new URL(request.url); - const [, , guildID] = url.pathname.split('/'); - - if (!guildID) { - return respond({ error: 'guild ID missing from URL' }, { status: 400 }); - } - - if (updateRequest.transactions.length === 0) { - return respond({ error: 'must have as least one transaction' }, { status: 400 }); - } - - const guildCheck = guilds.find((guild) => guild.id === guildID); - if (!guildCheck) { - return notFound(); - } - - const guild = await getGuild(guildID); - if (!guild) { - return notFound(); - } - - const guildMember = await getGuildMember( - { serverID: guildID, userID }, - { skipCachePull: true } - ); - if (!guildMember) { - return notFound(); - } - - const guildData = await getGuildData(guildID); - - const newRoles = calculateNewRoles({ - currentRoles: guildMember.roles, - guildRoles: guild.roles, - guildData, - updateRequest, - }); - - const patchMemberRoles = await discordFetch( - `/guilds/${guildID}/members/${userID}`, - botToken, - AuthType.Bot, - { - method: 'PATCH', - headers: { - 'content-type': 'application/json', - 'x-audit-log-reason': `Picked their roles via ${uiPublicURI}`, - }, - body: JSON.stringify({ - roles: newRoles, - }), - } - ); - - if (!patchMemberRoles) { - return respond({ error: 'discord rejected the request' }, { status: 500 }); - } - - const updatedMember: Member = { - roles: patchMemberRoles.roles, - }; - - // Delete the cache by re-pulling... might be dangerous :) - await updateGuildMember({ serverID: guildID, userID }); - - return respond(updatedMember); - } -); - -const calculateNewRoles = ({ - currentRoles, - guildData, - guildRoles, - updateRequest, -}: { - currentRoles: string[]; - guildRoles: Role[]; - guildData: GuildData; - updateRequest: RoleUpdate; -}): string[] => { - const roleMap = keyBy(guildRoles, 'id'); - - // These roles were ones changed between knownState (role picker page load/cache) and current (fresh from discord). - // We could cause issues, so we'll re-add them later. - // const diffRoles = difference(updateRequest.knownState, currentRoles); - - // Only these are safe - const allSafeRoles = guildData.categories.reduce( - (categorizedRoles, category) => - !category.hidden - ? [ - ...categorizedRoles, - ...category.roles.filter( - (roleID) => roleMap[roleID]?.safety === RoleSafety.Safe - ), - ] - : categorizedRoles, - [] - ); - - const safeTransactions = updateRequest.transactions.filter((tx: RoleTransaction) => - allSafeRoles.includes(tx.id) - ); - - const changesByAction = groupBy(safeTransactions, 'action'); - - const rolesToAdd = (changesByAction[TransactionType.Add] ?? []).map((tx) => tx.id); - const rolesToRemove = (changesByAction[TransactionType.Remove] ?? []).map( - (tx) => tx.id - ); - - const final = union(difference(currentRoles, rolesToRemove), rolesToAdd); - - return final; -}; diff --git a/packages/api/old-src/index.ts b/packages/api/old-src/index.ts deleted file mode 100644 index b7fd599..0000000 --- a/packages/api/old-src/index.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { InteractionsPickRole } from '@roleypoly/api/handlers/interactions-pick-role'; -import { InteractionsPickableRoles } from '@roleypoly/api/handlers/interactions-pickable-roles'; -import { Router } from '@roleypoly/worker-utils/router'; -import { BotJoin } from './handlers/bot-join'; -import { ClearGuildCache } from './handlers/clear-guild-cache'; -import { GetPickerData } from './handlers/get-picker-data'; -import { GetSession } from './handlers/get-session'; -import { GetSlug } from './handlers/get-slug'; -import { LoginBounce } from './handlers/login-bounce'; -import { LoginCallback } from './handlers/login-callback'; -import { RevokeSession } from './handlers/revoke-session'; -import { SyncFromLegacy } from './handlers/sync-from-legacy'; -import { UpdateGuild } from './handlers/update-guild'; -import { UpdateRoles } from './handlers/update-roles'; -import { respond } from './utils/api-tools'; -import { uiPublicURI } from './utils/config'; - -const router = new Router(); - -// OAuth -router.add('GET', 'bot-join', BotJoin); -router.add('GET', 'login-bounce', LoginBounce); -router.add('GET', 'login-callback', LoginCallback); - -// Session -router.add('GET', 'get-session', GetSession); -router.add('POST', 'revoke-session', RevokeSession); - -// Main biz logic -router.add('GET', 'get-slug', GetSlug); -router.add('GET', 'get-picker-data', GetPickerData); -router.add('PATCH', 'update-roles', UpdateRoles); -router.add('PATCH', 'update-guild', UpdateGuild); -router.add('POST', 'sync-from-legacy', SyncFromLegacy); -router.add('POST', 'clear-guild-cache', ClearGuildCache); - -// Interactions endpoints -router.add('GET', 'interactions-pickable-roles', InteractionsPickableRoles); -router.add('PUT', 'interactions-pick-role', InteractionsPickRole); -router.add('DELETE', 'interactions-pick-role', InteractionsPickRole); - -// Tester Routes -router.add('GET', 'x-headers', (request) => { - const headers: { [x: string]: string } = {}; - - for (let [key, value] of request.headers.entries()) { - headers[key] = value; - } - - return new Response(JSON.stringify(headers)); -}); - -// Root Zen <3 -router.addFallback('root', () => { - return respond({ - __warning: '🦊', - this: 'is', - a: 'fox-based', - web: 'application', - please: 'be', - mindful: 'of', - your: 'surroundings', - warning__: '🦊', - meta: uiPublicURI, - }); -}); - -addEventListener('fetch', (event: FetchEvent) => { - router.handle(event)); -}); diff --git a/packages/api/old-src/utils/access-control.ts b/packages/api/old-src/utils/access-control.ts deleted file mode 100644 index 211e6cd..0000000 --- a/packages/api/old-src/utils/access-control.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { isRoot } from '@roleypoly/api/utils/api-tools'; -import { - GuildAccessControl, - GuildSlug, - Member, - UserGuildPermissions, -} from '@roleypoly/types'; -import { xor } from 'lodash'; - -export const memberPassesAccessControl = ( - guildSlug: GuildSlug, - member: Member, - accessControl: GuildAccessControl -): boolean => { - return true; - - // Root has a bypass - if (isRoot(member.user?.id || '')) { - return true; - } - - // Admin and Manager has a bypass - if (guildSlug.permissionLevel !== UserGuildPermissions.User) { - return true; - } - - // Block pending members, "Welcome Screen" feature - if (accessControl.blockPending && member.pending) { - return false; - } - - // If member has roles in the blockList, block. - // Blocklist takes precedence over allowlist - // We use xor because xor([1, 3], [2, 3]) returns [3]), e.g. present in both lists - if ( - accessControl.blockList && - xor(member.roles, accessControl.blockList).length !== 0 - ) { - return false; - } - - // If there is an allowList, and the member is not in it, block. - // If thew allowList is empty, we bypass this. - if ( - accessControl.allowList && - xor(member.roles, accessControl.allowList).length === 0 - ) { - return false; - } - - return true; -}; diff --git a/packages/api/old-src/utils/api-tools.ts b/packages/api/old-src/utils/api-tools.ts deleted file mode 100644 index f876eae..0000000 --- a/packages/api/old-src/utils/api-tools.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { notAuthenticated } from '@roleypoly/api/utils/responses'; -import { - evaluatePermission, - permissions as Permissions, -} from '@roleypoly/misc-utils/hasPermission'; -import { SessionData, UserGuildPermissions } from '@roleypoly/types'; -import { Handler, HandlerTools, WrappedKVNamespace } from '@roleypoly/worker-utils'; -import KSUID from 'ksuid'; -import { - allowedCallbackHosts, - apiPublicURI, - interactionsSharedKey, - rootUsers, -} from './config'; -import { Sessions } from './kv'; - -export const formData = (obj: Record): string => { - return Object.keys(obj) - .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`) - .join('&'); -}; - -export const respond = (obj: Record, init: ResponseInit = {}) => - new Response(JSON.stringify(obj), init); - -export const resolveFailures = - ( - handleWith: () => Response, - handler: (request: Request) => Promise | Response - ) => - async (request: Request): Promise => { - try { - return handler(request); - } catch (e) { - console.error(e); - return handleWith() || respond({ error: 'internal server error' }, { status: 500 }); - } - }; - -export const parsePermissions = ( - permissions: bigint, - owner: boolean = false -): UserGuildPermissions => { - if (owner || evaluatePermission(permissions, Permissions.ADMINISTRATOR)) { - return UserGuildPermissions.Admin; - } - - if (evaluatePermission(permissions, Permissions.MANAGE_ROLES)) { - return UserGuildPermissions.Manager; - } - - return UserGuildPermissions.User; -}; - -export const getSessionID = (request: Request): { type: string; id: string } | null => { - const sessionID = request.headers.get('authorization'); - if (!sessionID) { - return null; - } - - const [type, id] = sessionID.split(' '); - if (type !== 'Bearer') { - return null; - } - - return { type, id }; -}; - -export type CacheLayerOptions = { - skipCachePull?: boolean; -}; - -export const cacheLayer = - ( - kv: WrappedKVNamespace, - keyFactory: (identity: Identity) => string, - missHandler: (identity: Identity) => Promise, - ttlSeconds?: number - ) => - async (identity: Identity, options: CacheLayerOptions = {}): Promise => { - const key = keyFactory(identity); - - if (!options.skipCachePull) { - const value = await kv.get(key); - if (value) { - return value; - } - } - - const fallbackValue = await missHandler(identity); - if (!fallbackValue) { - return null; - } - - await kv.put(key, fallbackValue, ttlSeconds); - - return fallbackValue; - }; - -const NotAuthenticated = (extra?: string) => - respond( - { - error: extra || 'not authenticated', - }, - { status: 403 } - ); - -export const withSession = - (wrappedHandler: (session: SessionData) => Handler): Handler => - async (request: Request, tools: HandlerTools): Promise => { - const sessionID = getSessionID(request); - if (!sessionID) { - return NotAuthenticated('missing authentication'); - } - - const session = await Sessions.get(sessionID.id); - if (!session) { - return NotAuthenticated('authentication expired or not found'); - } - - return await wrappedHandler(session)(request, tools); - }; - -export const setupStateSession = async (data: T): Promise => { - const stateID = (await KSUID.random()).string; - - await Sessions.put(`state_${stateID}`, { data }, 60 * 5); - - return stateID; -}; - -export const getStateSession = async (stateID: string): Promise => { - const stateSession = await Sessions.get<{ data: T }>(`state_${stateID}`); - - return stateSession?.data; -}; - -export const isRoot = (userID: string): boolean => rootUsers.includes(userID); - -export const onlyRootUsers = (handler: Handler): Handler => - withSession((session) => (request: Request, tools: HandlerTools) => { - if (isRoot(session.user.id)) { - return handler(request, tools); - } - - return respond( - { - error: 'not_found', - }, - { - status: 404, - } - ); - }); - -export const isAllowedCallbackHost = (host: string): boolean => { - return ( - host === apiPublicURI || - allowedCallbackHosts.includes(host) || - allowedCallbackHosts - .filter((callbackHost) => callbackHost.includes('*')) - .find((wildcard) => new RegExp(wildcard.replace('*', '[a-z0-9-]+')).test(host)) !== - null - ); -}; - -export const interactionsEndpoint = - (handler: Handler): Handler => - async (request: Request, tools: HandlerTools): Promise => { - const authHeader = request.headers.get('authorization') || ''; - if (authHeader !== `Shared ${interactionsSharedKey}`) { - return notAuthenticated(); - } - - return handler(request, tools); - }; diff --git a/packages/api/old-src/utils/bounce.ts b/packages/api/old-src/utils/bounce.ts deleted file mode 100644 index 9cccd74..0000000 --- a/packages/api/old-src/utils/bounce.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const Bounce = (url: string): Response => - new Response(null, { - status: 303, - headers: { - location: url, - }, - }); diff --git a/packages/api/old-src/utils/config.ts b/packages/api/old-src/utils/config.ts deleted file mode 100644 index 57ba3e5..0000000 --- a/packages/api/old-src/utils/config.ts +++ /dev/null @@ -1,16 +0,0 @@ -const self = global as any as Record; - -const env = (key: string) => self[key] ?? ''; - -const safeURI = (x: string) => x.replace(/\/$/, ''); -const list = (x: string) => x.split(','); - -export const botClientID = env('BOT_CLIENT_ID'); -export const botClientSecret = env('BOT_CLIENT_SECRET'); -export const botToken = env('BOT_TOKEN'); -export const uiPublicURI = safeURI(env('UI_PUBLIC_URI')); -export const apiPublicURI = safeURI(env('API_PUBLIC_URI')); -export const rootUsers = list(env('ROOT_USERS')); -export const allowedCallbackHosts = list(env('ALLOWED_CALLBACK_HOSTS')); -export const importSharedKey = env('BOT_IMPORT_TOKEN'); -export const interactionsSharedKey = env('INTERACTIONS_SHARED_KEY'); diff --git a/packages/api/old-src/utils/feature-flags.ts b/packages/api/old-src/utils/feature-flags.ts deleted file mode 100644 index 4764b34..0000000 --- a/packages/api/old-src/utils/feature-flags.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { hasFeature } from '@roleypoly/misc-utils/hasFeature'; -import { Features, Guild, GuildData } from '@roleypoly/types'; - -const flagPercents: Record = { - [Features.AuditLogging]: { percent: 0, rotation: 0 }, - [Features.AccessControl]: { percent: 0, rotation: 33 }, -}; - -const testingGroup: Guild['id'][] = [ - '386659935687147521', // Roleypoly -]; - -const ONE_HUNDRED = BigInt(100); - -export const getFeatureFlags = ( - feature: Features, - guildData: GuildData -): Record => { - const flags = Object.entries(flagPercents).map(([flag, value]) => { - const intFlag = Number(flag); - const intGuildID = BigInt(guildData.id); - const rotation = BigInt(value.rotation); - const percent = BigInt(value.percent); - - if (testingGroup.includes(guildData.id)) { - return [intFlag, true]; - } - - const percentValue = (intGuildID + rotation) % ONE_HUNDRED; - if (percentValue >= percent) { - return [intFlag, true]; - } - - return [intFlag, hasFeature(feature, intFlag)]; - }); - - return Object.fromEntries(flags); -}; diff --git a/packages/api/old-src/utils/guild.ts b/packages/api/old-src/utils/guild.ts deleted file mode 100644 index b04ee32..0000000 --- a/packages/api/old-src/utils/guild.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { - lowPermissions, - missingParameters, - notFound, - rateLimited, -} from '@roleypoly/api/utils/responses'; -import { evaluatePermission, permissions } from '@roleypoly/misc-utils/hasPermission'; -import { - Features, - Guild, - GuildData as GuildDataT, - GuildSlug, - Member, - OwnRoleInfo, - Role, - RoleSafety, - SessionData, - UserGuildPermissions, -} from '@roleypoly/types'; -import { AuthType, discordFetch, Handler, HandlerTools } from '@roleypoly/worker-utils'; -import { cacheLayer, CacheLayerOptions, isRoot, withSession } from './api-tools'; -import { botClientID, botToken } from './config'; -import { GuildData, Guilds } from './kv'; -import { useRateLimiter } from './rate-limiting'; - -type APIGuild = { - // Only relevant stuff - id: string; - name: string; - icon: string; - roles: APIRole[]; -}; - -type APIRole = { - id: string; - name: string; - color: number; - position: number; - permissions: string; - managed: boolean; -}; - -export const getGuild = cacheLayer( - Guilds, - (id: string) => `guilds/${id}`, - async (id: string) => { - const guildRaw = await discordFetch( - `/guilds/${id}`, - botToken, - AuthType.Bot - ); - - if (!guildRaw) { - return null; - } - - const botMemberRoles = - (await getGuildMemberRoles({ - serverID: id, - userID: botClientID, - })) || []; - - const highestRolePosition = botMemberRoles.reduce((highest, roleID) => { - const role = guildRaw.roles.find((guildRole) => guildRole.id === roleID); - if (!role) { - return highest; - } - - // If highest is a bigger number, it stays the highest. - if (highest > role.position) { - return highest; - } - - return role.position; - }, 0); - - const guildData = await getGuildData(id); - - const roles = guildRaw.roles.map((role) => ({ - id: role.id, - name: role.name, - color: role.color, - managed: role.managed, - position: role.position, - permissions: role.permissions, - safety: calculateRoleSafety(role, highestRolePosition, guildData), - })); - - // Filters the raw guild data into data we actually want - const guild: Guild & OwnRoleInfo = { - id: guildRaw.id, - name: guildRaw.name, - icon: guildRaw.icon, - roles, - highestRolePosition, - }; - - return guild; - }, - 60 * 60 * 2 // 2 hour TTL -); - -type GuildMemberIdentity = { - serverID: string; - userID: string; -}; - -type APIMember = { - // Only relevant stuff, again. - roles: string[]; - pending: boolean; -}; - -export const getGuildMemberRoles = async ( - { serverID, userID }: GuildMemberIdentity, - opts?: CacheLayerOptions -) => (await getGuildMember({ serverID, userID }, opts))?.roles; - -const guildMemberIdentity = ({ serverID, userID }: GuildMemberIdentity) => - `guilds/${serverID}/members/${userID}`; - -export const getGuildMember = cacheLayer( - Guilds, - guildMemberIdentity, - async ({ serverID, userID }) => { - const discordMember = await discordFetch( - `/guilds/${serverID}/members/${userID}`, - botToken, - AuthType.Bot - ); - - if (!discordMember) { - return null; - } - - return { - roles: discordMember.roles, - pending: discordMember.pending, - }; - }, - 60 * 5 // 5 minute TTL -); - -export const updateGuildMember = async (identity: GuildMemberIdentity) => { - await getGuildMember(identity, { skipCachePull: true }); -}; - -export const getGuildData = async (id: string): Promise => { - const guildData = await GuildData.get(id); - const empty = { - id, - message: '', - categories: [], - features: Features.None, - auditLogWebhook: null, - accessControl: { - allowList: [], - blockList: [], - blockPending: true, - }, - }; - - if (!guildData) { - return empty; - } - - return { - ...empty, - ...guildData, - }; -}; - -const calculateRoleSafety = ( - role: Role | APIRole, - highestBotRolePosition: number, - guildData: GuildDataT -) => { - let safety = RoleSafety.Safe; - - if (role.managed) { - safety |= RoleSafety.ManagedRole; - } - - if (role.position > highestBotRolePosition) { - safety |= RoleSafety.HigherThanBot; - } - - const permBigInt = BigInt(role.permissions); - if ( - evaluatePermission(permBigInt, permissions.ADMINISTRATOR) || - evaluatePermission(permBigInt, permissions.MANAGE_ROLES) - ) { - safety |= RoleSafety.DangerousPermissions; - } - - if ( - guildData.accessControl.allowList.includes(role.id) || - guildData.accessControl.blockList.includes(role.id) - ) { - safety |= RoleSafety.AccessControl; - } - - return safety; -}; - -export enum GuildRateLimiterKey { - legacyImport = 'legacyImport', - cacheClear = 'cacheClear', -} - -export const useGuildRateLimiter = ( - guildID: string, - key: GuildRateLimiterKey, - timeoutSeconds: number -) => useRateLimiter(Guilds, `guilds/${guildID}/rate-limit/${key}`, timeoutSeconds); - -type AsEditorOptions = { - rateLimitKey?: GuildRateLimiterKey; - rateLimitTimeoutSeconds?: number; -}; - -type UserGuildContext = { - guildID: string; - guild: GuildSlug; - url: URL; -}; - -export const asEditor = ( - options: AsEditorOptions = {}, - wrappedHandler: (session: SessionData, userGuildContext: UserGuildContext) => Handler -): Handler => - withSession( - (session: SessionData) => - async (request: Request, tools: HandlerTools): Promise => { - const { rateLimitKey, rateLimitTimeoutSeconds } = options; - const url = new URL(request.url); - const [, , guildID] = url.pathname.split('/'); - if (!guildID) { - return missingParameters(); - } - - let rateLimit: null | ReturnType = null; - if (rateLimitKey) { - rateLimit = await useGuildRateLimiter( - guildID, - rateLimitKey, - rateLimitTimeoutSeconds || 60 - ); - } - - const userIsRoot = isRoot(session.user.id); - - let guild = session.guilds.find((guild) => guild.id === guildID); - if (!guild) { - if (!userIsRoot) { - return notFound(); - } - - const fullGuild = await getGuild(guildID); - if (!fullGuild) { - return notFound(); - } - - guild = { - id: fullGuild.id, - name: fullGuild.name, - icon: fullGuild.icon, - permissionLevel: UserGuildPermissions.Admin, // root will always be considered admin - }; - } - - const userIsManager = guild.permissionLevel === UserGuildPermissions.Manager; - const userIsAdmin = guild.permissionLevel === UserGuildPermissions.Admin; - - if (!userIsAdmin && !userIsManager) { - return lowPermissions(); - } - - if (!userIsRoot && rateLimit && (await rateLimit())) { - return rateLimited(); - } - - return await wrappedHandler(session, { - guildID, - guild, - url, - })(request, tools); - } - ); diff --git a/packages/api/old-src/utils/import-from-legacy.ts b/packages/api/old-src/utils/import-from-legacy.ts deleted file mode 100644 index 1fc1009..0000000 --- a/packages/api/old-src/utils/import-from-legacy.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { sortBy } from '@roleypoly/misc-utils/sortBy'; -import { CategoryType, Features, GuildData } from '@roleypoly/types'; -import KSUID from 'ksuid'; -import { importSharedKey } from './config'; - -export type LegacyCategory = { - id: string; - name: string; - roles: string[]; - hidden: boolean; - type: 'single' | 'multi'; - position: number; -}; - -export type LegacyGuildData = { - id: string; - categories: LegacyCategory[]; - message: string; -}; - -export const fetchLegacyServer = async (id: string): Promise => { - const guildDataResponse = await fetch( - `https://beta.roleypoly.com/x/import-to-next/${id}`, - { - headers: { - authorization: `Shared ${importSharedKey}`, - }, - } - ); - - if (guildDataResponse.status === 404) { - return null; - } - - if (guildDataResponse.status !== 200) { - throw new Error('Guild data fetch failed'); - } - - return await guildDataResponse.json(); -}; - -export const transformLegacyGuild = (guild: LegacyGuildData): GuildData => { - return { - id: guild.id, - message: guild.message, - features: Features.LegacyGuild, - auditLogWebhook: null, - accessControl: { - allowList: [], - blockList: [], - blockPending: true, - }, - categories: sortBy(Object.values(guild.categories), 'position').map( - (category, idx) => ({ - ...category, - id: KSUID.randomSync().string, - position: idx, // Reset positions by index. May have side-effects but oh well. - type: category.type === 'multi' ? CategoryType.Multi : CategoryType.Single, - }) - ), - }; -}; diff --git a/packages/api/old-src/utils/kv.ts b/packages/api/old-src/utils/kv.ts deleted file mode 100644 index 33c75c9..0000000 --- a/packages/api/old-src/utils/kv.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { kvOrLocal, WrappedKVNamespace } from '@roleypoly/worker-utils'; - -const self = global as any as Record; - -export const Sessions = new WrappedKVNamespace(kvOrLocal(self.KV_SESSIONS ?? null)); -export const GuildData = new WrappedKVNamespace(kvOrLocal(self.KV_GUILD_DATA ?? null)); -export const Guilds = new WrappedKVNamespace(kvOrLocal(self.KV_GUILDS ?? null)); diff --git a/packages/api/old-src/utils/rate-limiting.ts b/packages/api/old-src/utils/rate-limiting.ts deleted file mode 100644 index fa7f7c7..0000000 --- a/packages/api/old-src/utils/rate-limiting.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { WrappedKVNamespace } from '@roleypoly/worker-utils'; - -export const useRateLimiter = - (kv: WrappedKVNamespace, key: string, timeoutSeconds: number) => - async (): Promise => { - const value = await kv.get(key); - if (value) { - return true; - } - - await kv.put(key, true, timeoutSeconds); - return false; - }; diff --git a/packages/api/old-src/utils/responses.ts b/packages/api/old-src/utils/responses.ts deleted file mode 100644 index 6ad2fd5..0000000 --- a/packages/api/old-src/utils/responses.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { respond } from '@roleypoly/worker-utils'; - -export const ok = () => respond({ ok: true }); - -export const missingParameters = () => - respond({ error: 'missing parameters' }, { status: 400 }); - -export const lowPermissions = () => - respond({ error: 'no permissions for this action' }, { status: 403 }); - -export const accessControlViolation = () => - respond({ error: 'member fails access control requirements' }, { status: 403 }); - -export const notFound = () => respond({ error: 'not found' }, { status: 404 }); - -export const conflict = () => respond({ error: 'conflict' }, { status: 409 }); - -export const rateLimited = () => - respond({ error: 'rate limit hit, enhance your calm' }, { status: 429 }); - -export const invalid = (obj: any = {}) => - respond({ err: 'client sent something invalid', data: obj }, { status: 400 }); - -export const notAuthenticated = () => - respond({ err: 'not authenticated' }, { status: 403 }); diff --git a/packages/api/old-src/webpack.config.js b/packages/api/old-src/webpack.config.js deleted file mode 100644 index bc319c2..0000000 --- a/packages/api/old-src/webpack.config.js +++ /dev/null @@ -1,28 +0,0 @@ -const path = require('path'); - -const mode = process.env.NODE_ENV || 'production'; - -module.exports = { - target: 'webworker', - entry: path.join(__dirname, 'index.ts'), - output: { - filename: `worker.${mode}.js`, - path: path.join(__dirname, 'dist'), - }, - mode, - resolve: { - extensions: ['.ts', '.tsx', '.js'], - }, - module: { - rules: [ - { - test: /\.tsx?$/, - loader: 'ts-loader', - options: { - transpileOnly: true, - configFile: path.join(__dirname, 'tsconfig.json'), - }, - }, - ], - }, -}; diff --git a/packages/api/old-src/worker.config.js b/packages/api/old-src/worker.config.js deleted file mode 100644 index ec8e41f..0000000 --- a/packages/api/old-src/worker.config.js +++ /dev/null @@ -1,18 +0,0 @@ -const reexportEnv = (keys = []) => { - return keys.reduce((acc, key) => ({ ...acc, [key]: process.env[key] }), {}); -}; - -module.exports = { - environment: reexportEnv([ - 'BOT_CLIENT_ID', - 'BOT_CLIENT_SECRET', - 'BOT_TOKEN', - 'BOT_IMPORT_TOKEN', - 'UI_PUBLIC_URI', - 'API_PUBLIC_URI', - 'ROOT_USERS', - 'ALLOWED_CALLBACK_HOSTS', - 'INTERACTIONS_SHARED_KEY', - ]), - kv: ['KV_SESSIONS', 'KV_GUILDS', 'KV_GUILD_DATA'], -};