mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-25 03:49:11 +00:00
remove old-src, same some historic pieces
This commit is contained in:
parent
8e21d2e5cc
commit
33df1c7edc
31 changed files with 221 additions and 1414 deletions
16
packages/api/historic/interactions/hello-world.ts~
Normal file
16
packages/api/historic/interactions/hello-world.ts~
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {
|
||||||
|
InteractionCallbackType,
|
||||||
|
InteractionRequestCommand,
|
||||||
|
InteractionResponse,
|
||||||
|
} from '@roleypoly/types';
|
||||||
|
|
||||||
|
export const helloWorld = async (
|
||||||
|
interaction: InteractionRequestCommand
|
||||||
|
): Promise<InteractionResponse> => {
|
||||||
|
return {
|
||||||
|
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||||
|
data: {
|
||||||
|
content: `Hey there, ${interaction.member?.nick || interaction.user?.username}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
86
packages/api/historic/interactions/pick-role.ts~
Normal file
86
packages/api/historic/interactions/pick-role.ts~
Normal file
|
@ -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<InteractionResponse> => {
|
||||||
|
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
|
||||||
|
);
|
63
packages/api/historic/interactions/pickable-roles.ts~
Normal file
63
packages/api/historic/interactions/pickable-roles.ts~
Normal file
|
@ -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<InteractionResponse> => {
|
||||||
|
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
|
||||||
|
);
|
56
packages/api/historic/interactions/roleypoly.ts~
Normal file
56
packages/api/historic/interactions/roleypoly.ts~
Normal file
|
@ -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<InteractionResponse> => {
|
||||||
|
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}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -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'],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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<Response> => {
|
|
||||||
const result = await getGuild(guildID, { skipCachePull: true });
|
|
||||||
if (!result) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok();
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -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<Response> => {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -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,
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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<Response> => {
|
|
||||||
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);
|
|
||||||
};
|
|
|
@ -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<Response> => {
|
|
||||||
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 }));
|
|
||||||
};
|
|
|
@ -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<Response> => {
|
|
||||||
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<StateSession>(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<DiscordUser | null> => {
|
|
||||||
const user = await discordFetch<DiscordUser>(
|
|
||||||
'/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<UserGuildsPayload>(
|
|
||||||
'/users/@me/guilds',
|
|
||||||
accessToken,
|
|
||||||
AuthType.Bearer
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!guilds) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const guildSlugs = guilds.map<GuildSlug>((guild) => ({
|
|
||||||
id: guild.id,
|
|
||||||
name: guild.name,
|
|
||||||
icon: guild.icon,
|
|
||||||
permissionLevel: parsePermissions(BigInt(guild.permissions), guild.owner),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return guildSlugs;
|
|
||||||
};
|
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -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<Response> => {
|
|
||||||
const legacyGuild = await fetchLegacyServer(guildID);
|
|
||||||
if (!legacyGuild) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const newGuildData = transformLegacyGuild(legacyGuild);
|
|
||||||
await GuildData.put(guildID, newGuildData);
|
|
||||||
|
|
||||||
return ok();
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -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<Response> => {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -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<Member>(
|
|
||||||
`/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<string[]>(
|
|
||||||
(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;
|
|
||||||
};
|
|
|
@ -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));
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -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, any>): string => {
|
|
||||||
return Object.keys(obj)
|
|
||||||
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
|
|
||||||
.join('&');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const respond = (obj: Record<string, any>, init: ResponseInit = {}) =>
|
|
||||||
new Response(JSON.stringify(obj), init);
|
|
||||||
|
|
||||||
export const resolveFailures =
|
|
||||||
(
|
|
||||||
handleWith: () => Response,
|
|
||||||
handler: (request: Request) => Promise<Response> | Response
|
|
||||||
) =>
|
|
||||||
async (request: Request): Promise<Response> => {
|
|
||||||
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 =
|
|
||||||
<Identity, Data>(
|
|
||||||
kv: WrappedKVNamespace,
|
|
||||||
keyFactory: (identity: Identity) => string,
|
|
||||||
missHandler: (identity: Identity) => Promise<Data | null>,
|
|
||||||
ttlSeconds?: number
|
|
||||||
) =>
|
|
||||||
async (identity: Identity, options: CacheLayerOptions = {}): Promise<Data | null> => {
|
|
||||||
const key = keyFactory(identity);
|
|
||||||
|
|
||||||
if (!options.skipCachePull) {
|
|
||||||
const value = await kv.get<Data>(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<Response> => {
|
|
||||||
const sessionID = getSessionID(request);
|
|
||||||
if (!sessionID) {
|
|
||||||
return NotAuthenticated('missing authentication');
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await Sessions.get<SessionData>(sessionID.id);
|
|
||||||
if (!session) {
|
|
||||||
return NotAuthenticated('authentication expired or not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await wrappedHandler(session)(request, tools);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setupStateSession = async <T>(data: T): Promise<string> => {
|
|
||||||
const stateID = (await KSUID.random()).string;
|
|
||||||
|
|
||||||
await Sessions.put(`state_${stateID}`, { data }, 60 * 5);
|
|
||||||
|
|
||||||
return stateID;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStateSession = async <T>(stateID: string): Promise<T | undefined> => {
|
|
||||||
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<Response> => {
|
|
||||||
const authHeader = request.headers.get('authorization') || '';
|
|
||||||
if (authHeader !== `Shared ${interactionsSharedKey}`) {
|
|
||||||
return notAuthenticated();
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler(request, tools);
|
|
||||||
};
|
|
|
@ -1,7 +0,0 @@
|
||||||
export const Bounce = (url: string): Response =>
|
|
||||||
new Response(null, {
|
|
||||||
status: 303,
|
|
||||||
headers: {
|
|
||||||
location: url,
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,16 +0,0 @@
|
||||||
const self = global as any as Record<string, string>;
|
|
||||||
|
|
||||||
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');
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { hasFeature } from '@roleypoly/misc-utils/hasFeature';
|
|
||||||
import { Features, Guild, GuildData } from '@roleypoly/types';
|
|
||||||
|
|
||||||
const flagPercents: Record<Features, { percent: number; rotation: number }> = {
|
|
||||||
[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<Features, boolean> => {
|
|
||||||
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);
|
|
||||||
};
|
|
|
@ -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<APIGuild>(
|
|
||||||
`/guilds/${id}`,
|
|
||||||
botToken,
|
|
||||||
AuthType.Bot
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!guildRaw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const botMemberRoles =
|
|
||||||
(await getGuildMemberRoles({
|
|
||||||
serverID: id,
|
|
||||||
userID: botClientID,
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
const highestRolePosition = botMemberRoles.reduce<number>((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>((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<GuildMemberIdentity, Member>(
|
|
||||||
Guilds,
|
|
||||||
guildMemberIdentity,
|
|
||||||
async ({ serverID, userID }) => {
|
|
||||||
const discordMember = await discordFetch<APIMember>(
|
|
||||||
`/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<GuildDataT> => {
|
|
||||||
const guildData = await GuildData.get<GuildDataT>(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<Response> => {
|
|
||||||
const { rateLimitKey, rateLimitTimeoutSeconds } = options;
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const [, , guildID] = url.pathname.split('/');
|
|
||||||
if (!guildID) {
|
|
||||||
return missingParameters();
|
|
||||||
}
|
|
||||||
|
|
||||||
let rateLimit: null | ReturnType<typeof useGuildRateLimiter> = 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);
|
|
||||||
}
|
|
||||||
);
|
|
|
@ -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<LegacyGuildData | null> => {
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { kvOrLocal, WrappedKVNamespace } from '@roleypoly/worker-utils';
|
|
||||||
|
|
||||||
const self = global as any as Record<string, any>;
|
|
||||||
|
|
||||||
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));
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { WrappedKVNamespace } from '@roleypoly/worker-utils';
|
|
||||||
|
|
||||||
export const useRateLimiter =
|
|
||||||
(kv: WrappedKVNamespace, key: string, timeoutSeconds: number) =>
|
|
||||||
async (): Promise<boolean> => {
|
|
||||||
const value = await kv.get<boolean>(key);
|
|
||||||
if (value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await kv.put(key, true, timeoutSeconds);
|
|
||||||
return false;
|
|
||||||
};
|
|
|
@ -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 });
|
|
|
@ -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'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -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'],
|
|
||||||
};
|
|
Loading…
Add table
Reference in a new issue