mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-15 09:09:10 +00:00
fix(interactions): add async responses
This commit is contained in:
parent
3601d435b2
commit
26bc74bcbc
11 changed files with 220 additions and 149 deletions
|
@ -66,5 +66,5 @@ router.addFallback('root', () => {
|
|||
});
|
||||
|
||||
addEventListener('fetch', (event: FetchEvent) => {
|
||||
event.respondWith(router.handle(event.request));
|
||||
event.respondWith(router.handle(event));
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
permissions as Permissions,
|
||||
} from '@roleypoly/misc-utils/hasPermission';
|
||||
import { SessionData, UserGuildPermissions } from '@roleypoly/types';
|
||||
import { Handler, WrappedKVNamespace } from '@roleypoly/worker-utils';
|
||||
import { Handler, HandlerTools, WrappedKVNamespace } from '@roleypoly/worker-utils';
|
||||
import KSUID from 'ksuid';
|
||||
import {
|
||||
allowedCallbackHosts,
|
||||
|
@ -107,7 +107,7 @@ const NotAuthenticated = (extra?: string) =>
|
|||
|
||||
export const withSession =
|
||||
(wrappedHandler: (session: SessionData) => Handler): Handler =>
|
||||
async (request: Request): Promise<Response> => {
|
||||
async (request: Request, tools: HandlerTools): Promise<Response> => {
|
||||
const sessionID = getSessionID(request);
|
||||
if (!sessionID) {
|
||||
return NotAuthenticated('missing authentication');
|
||||
|
@ -118,7 +118,7 @@ export const withSession =
|
|||
return NotAuthenticated('authentication expired or not found');
|
||||
}
|
||||
|
||||
return await wrappedHandler(session)(request);
|
||||
return await wrappedHandler(session)(request, tools);
|
||||
};
|
||||
|
||||
export const setupStateSession = async <T>(data: T): Promise<string> => {
|
||||
|
@ -138,9 +138,9 @@ export const getStateSession = async <T>(stateID: string): Promise<T | undefined
|
|||
export const isRoot = (userID: string): boolean => rootUsers.includes(userID);
|
||||
|
||||
export const onlyRootUsers = (handler: Handler): Handler =>
|
||||
withSession((session) => (request: Request) => {
|
||||
withSession((session) => (request: Request, tools: HandlerTools) => {
|
||||
if (isRoot(session.user.id)) {
|
||||
return handler(request);
|
||||
return handler(request, tools);
|
||||
}
|
||||
|
||||
return respond(
|
||||
|
@ -166,11 +166,11 @@ export const isAllowedCallbackHost = (host: string): boolean => {
|
|||
|
||||
export const interactionsEndpoint =
|
||||
(handler: Handler): Handler =>
|
||||
async (request: Request): Promise<Response> => {
|
||||
async (request: Request, tools: HandlerTools): Promise<Response> => {
|
||||
const authHeader = request.headers.get('authorization') || '';
|
||||
if (authHeader !== `Shared ${interactionsSharedKey}`) {
|
||||
return notAuthenticated();
|
||||
}
|
||||
|
||||
return handler(request);
|
||||
return handler(request, tools);
|
||||
};
|
||||
|
|
|
@ -101,6 +101,9 @@ const server = http.createServer((req, res) => {
|
|||
);
|
||||
isResponseConstructorAllowed = false;
|
||||
},
|
||||
waitUntil: async (promise) => {
|
||||
await promise;
|
||||
},
|
||||
request: new fetch.Request(
|
||||
new URL(`http://${req.headers.host || 'localhost'}${req.url}`),
|
||||
{
|
||||
|
|
|
@ -2,21 +2,17 @@ import {
|
|||
InteractionData,
|
||||
InteractionRequest,
|
||||
InteractionRequestCommand,
|
||||
InteractionResponse,
|
||||
InteractionType,
|
||||
} from '@roleypoly/types';
|
||||
import { respond } from '@roleypoly/worker-utils';
|
||||
import { verifyRequest } from '../utils/interactions';
|
||||
import { HandlerTools, respond } from '@roleypoly/worker-utils';
|
||||
import { CommandHandler, verifyRequest } from '../utils/interactions';
|
||||
import { somethingWentWrong } from '../utils/responses';
|
||||
import { helloWorld } from './interactions/hello-world';
|
||||
import { pickRole } from './interactions/pick-role';
|
||||
import { pickableRoles } from './interactions/pickable-roles';
|
||||
import { roleypoly } from './interactions/roleypoly';
|
||||
|
||||
const commands: Record<
|
||||
InteractionData['name'],
|
||||
(request: InteractionRequestCommand) => Promise<InteractionResponse>
|
||||
> = {
|
||||
const commands: Record<InteractionData['name'], CommandHandler> = {
|
||||
'hello-world': helloWorld,
|
||||
roleypoly: roleypoly,
|
||||
'pickable-roles': pickableRoles,
|
||||
|
@ -24,7 +20,10 @@ const commands: Record<
|
|||
'remove-role': pickRole('remove'),
|
||||
};
|
||||
|
||||
export const interactionHandler = async (request: Request): Promise<Response> => {
|
||||
export const interactionHandler = async (
|
||||
request: Request,
|
||||
{ waitUntil }: HandlerTools
|
||||
): Promise<Response> => {
|
||||
const interaction = (await request.json()) as InteractionRequest;
|
||||
|
||||
if (!verifyRequest(request, interaction)) {
|
||||
|
@ -49,7 +48,10 @@ export const interactionHandler = async (request: Request): Promise<Response> =>
|
|||
}
|
||||
|
||||
try {
|
||||
const response = await handler(interaction as InteractionRequestCommand);
|
||||
const response = await handler(interaction as InteractionRequestCommand, {
|
||||
request,
|
||||
waitUntil,
|
||||
});
|
||||
return respond(response);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { selectRole } from '@roleypoly/interactions/utils/api';
|
||||
import { asyncResponse } from '@roleypoly/interactions/utils/interactions';
|
||||
import { invalid, mustBeInGuild } from '@roleypoly/interactions/utils/responses';
|
||||
import {
|
||||
InteractionCallbackType,
|
||||
|
@ -7,74 +8,75 @@ import {
|
|||
InteractionResponse,
|
||||
} from '@roleypoly/types';
|
||||
|
||||
export const pickRole =
|
||||
(mode: 'add' | 'remove') =>
|
||||
async (interaction: InteractionRequestCommand): Promise<InteractionResponse> => {
|
||||
if (!interaction.guild_id) {
|
||||
return mustBeInGuild();
|
||||
}
|
||||
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 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 roleID = interaction.data.options?.find(
|
||||
(option) => option.name === 'role'
|
||||
)?.value;
|
||||
if (!roleID) {
|
||||
return invalid();
|
||||
}
|
||||
|
||||
const code = await selectRole(mode, interaction.guild_id, userID, roleID);
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (code === 409) {
|
||||
return {
|
||||
type: InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
content: `:x: You ${mode === 'add' ? 'already' : "don't"} have that role.`,
|
||||
content: `:white_check_mark: You ${
|
||||
mode === 'add' ? 'got' : 'removed'
|
||||
} the role: <@&${roleID}>`,
|
||||
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,
|
||||
},
|
||||
};
|
||||
};
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getPickableRoles } from '@roleypoly/interactions/utils/api';
|
||||
import { uiPublicURI } from '@roleypoly/interactions/utils/config';
|
||||
import { asyncResponse } from '@roleypoly/interactions/utils/interactions';
|
||||
import { mustBeInGuild } from '@roleypoly/interactions/utils/responses';
|
||||
import {
|
||||
CategoryType,
|
||||
|
@ -10,49 +11,49 @@ import {
|
|||
InteractionResponse,
|
||||
} from '@roleypoly/types';
|
||||
|
||||
export const pickableRoles = async (
|
||||
interaction: InteractionRequestCommand
|
||||
): Promise<InteractionResponse> => {
|
||||
if (!interaction.guild_id) {
|
||||
return mustBeInGuild();
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
);
|
||||
|
|
|
@ -25,5 +25,5 @@ router.addFallback('root', () => {
|
|||
});
|
||||
|
||||
addEventListener('fetch', (event: FetchEvent) => {
|
||||
event.respondWith(router.handle(event.request));
|
||||
event.respondWith(router.handle(event));
|
||||
});
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import { publicKey } from '@roleypoly/interactions/utils/config';
|
||||
import { InteractionRequest } from '@roleypoly/types';
|
||||
import {
|
||||
InteractionCallbackType,
|
||||
InteractionFlags,
|
||||
InteractionRequest,
|
||||
InteractionRequestCommand,
|
||||
InteractionResponse,
|
||||
} from '@roleypoly/types';
|
||||
import { AuthType, discordFetch, HandlerTools } from '@roleypoly/worker-utils';
|
||||
import nacl from 'tweetnacl';
|
||||
|
||||
export const verifyRequest = (
|
||||
|
@ -25,3 +32,47 @@ export const verifyRequest = (
|
|||
|
||||
return true;
|
||||
};
|
||||
|
||||
export type RequestInfo = HandlerTools & { request: Request };
|
||||
|
||||
export type CommandHandler = (
|
||||
request: InteractionRequestCommand,
|
||||
requestInfo: RequestInfo
|
||||
) => Promise<InteractionResponse>;
|
||||
|
||||
export const asyncResponse =
|
||||
(handler: CommandHandler): CommandHandler =>
|
||||
async (
|
||||
command: InteractionRequestCommand,
|
||||
requestInfo: RequestInfo
|
||||
): Promise<InteractionResponse> => {
|
||||
requestInfo.waitUntil(
|
||||
(async () => {
|
||||
const response = await handler(command, requestInfo);
|
||||
await updateOriginalMessage(command.application_id, command.token, response);
|
||||
})()
|
||||
);
|
||||
|
||||
return {
|
||||
type: InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
flags: InteractionFlags.EPHEMERAL,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const updateOriginalMessage = async (
|
||||
appID: string,
|
||||
token: string,
|
||||
response: InteractionResponse
|
||||
) => {
|
||||
const url = `/webhooks/${appID}/${token}/messages/@original`;
|
||||
|
||||
return await discordFetch(url, '', AuthType.None, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(response.data),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ export const discordAPIBase = 'https://discordapp.com/api/v9';
|
|||
export enum AuthType {
|
||||
Bearer = 'Bearer',
|
||||
Bot = 'Bot',
|
||||
None = 'None',
|
||||
}
|
||||
|
||||
export const discordFetch = async <T>(
|
||||
|
@ -17,7 +18,11 @@ export const discordFetch = async <T>(
|
|||
...(init || {}),
|
||||
headers: {
|
||||
...(init?.headers || {}),
|
||||
authorization: `${AuthType[authType]} ${auth}`,
|
||||
...(authType !== AuthType.None
|
||||
? {
|
||||
authorization: `${AuthType[authType]} ${auth}`,
|
||||
}
|
||||
: {}),
|
||||
'user-agent': userAgent,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
export type Handler = (request: Request) => Promise<Response> | Response;
|
||||
export type Handler = (
|
||||
request: Request,
|
||||
tools: HandlerTools
|
||||
) => Promise<Response> | Response;
|
||||
|
||||
export type HandlerTools = {
|
||||
waitUntil: FetchEvent['waitUntil'];
|
||||
};
|
||||
|
||||
type RoutingTree = {
|
||||
[method: string]: {
|
||||
|
@ -40,17 +47,17 @@ export class Router {
|
|||
this.routingTree[lowerMethod][rootPath] = handler;
|
||||
}
|
||||
|
||||
async handle(request: Request): Promise<Response> {
|
||||
const response = await this.processRequest(request);
|
||||
this.injectCORSHeaders(request, response.headers);
|
||||
async handle(event: FetchEvent): Promise<Response> {
|
||||
const response = await this.processRequest(event);
|
||||
this.injectCORSHeaders(event.request, response.headers);
|
||||
return response;
|
||||
}
|
||||
|
||||
private async processRequest(request: Request): Promise<Response> {
|
||||
private async processRequest({ request, waitUntil }: FetchEvent): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname === '/' || url.pathname === '') {
|
||||
return this.fallbacks.root(request);
|
||||
return this.fallbacks.root(request, { waitUntil });
|
||||
}
|
||||
const lowerMethod = request.method.toLowerCase();
|
||||
const rootPath = url.pathname.split('/')[1];
|
||||
|
@ -58,11 +65,11 @@ export class Router {
|
|||
|
||||
if (handler) {
|
||||
try {
|
||||
const response = await handler(request);
|
||||
const response = await handler(request, { waitUntil });
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return this.fallbacks[500](request);
|
||||
return this.fallbacks[500](request, { waitUntil });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +77,7 @@ export class Router {
|
|||
return new Response(null, {});
|
||||
}
|
||||
|
||||
return this.fallbacks[404](request);
|
||||
return this.fallbacks[404](request, { waitUntil });
|
||||
}
|
||||
|
||||
private respondToRoot(): Response {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue