summary refs log tree commit diff
path: root/src/api
diff options
context:
space:
mode:
authorCyberL1 <cyber.hf18@gmail.com>2025-10-17 13:01:36 +0200
committerRory& <root@rory.gay>2025-10-17 14:32:22 +0200
commit81d4f1f3107e518b8a346cd0a0501999916697fa (patch)
tree43a26397fbc43373d51fa97922ed39bae233e3f3 /src/api
parentfix: do the same for applicaiton_commands (diff)
downloadserver-ts-81d4f1f3107e518b8a346cd0a0501999916697fa.tar.xz
feat: interactions (incomplete)
Diffstat (limited to 'src/api')
-rw-r--r--src/api/middlewares/Authentication.ts10
-rw-r--r--src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts90
-rw-r--r--src/api/routes/interactions/index.ts82
3 files changed, 175 insertions, 7 deletions
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts

index 987493f9..71c2a168 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts
@@ -32,6 +32,7 @@ export const NO_AUTHORIZATION_ROUTES = [ "GET /invites/", // Routes with a seperate auth system /^(POST|HEAD|GET|PATCH|DELETE) \/webhooks\/\d+\/\w+\/?/, // no token requires auth + /^POST \/interactions\/\d+\/\w+\/callback/, // Public information endpoints "GET /ping", "GET /gateway", @@ -72,11 +73,7 @@ declare global { } } -export async function Authentication( - req: Request, - res: Response, - next: NextFunction, -) { +export async function Authentication(req: Request, res: Response, next: NextFunction) { if (req.method === "OPTIONS") return res.sendStatus(204); const url = req.url.replace(API_PREFIX, ""); if ( @@ -104,8 +101,7 @@ export async function Authentication( }) ) return next(); - if (!req.headers.authorization) - return next(new HTTPError("Missing Authorization Header", 401)); + if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401)); try { const { decoded, user } = await checkToken(req.headers.authorization); diff --git a/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts b/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts new file mode 100644
index 00000000..4ce69446 --- /dev/null +++ b/src/api/routes/interactions/#interaction_id/#interaction_token/callback.ts
@@ -0,0 +1,90 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { InteractionCallbackSchema, InteractionCallbackType, MessageType } from "@spacebar/schemas"; +import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; +import { ApplicationCommand, emitEvent, InteractionSuccessEvent, Message, MessageCreateEvent, pendingInteractions } from "@spacebar/util"; + +const router = Router({ mergeParams: true }); + +router.post("/", route({}), async (req: Request, res: Response) => { + const body = req.body as InteractionCallbackSchema; + + const interactionId = req.params.interaction_id; + const interaction = pendingInteractions.get(req.params.interaction_id); + + if (!interaction) { + return; + } + + clearTimeout(interaction.timeout); + + emitEvent({ + event: "INTERACTION_SUCCESS", + user_id: interaction?.userId, + data: { + id: interactionId, + nonce: interaction?.nonce, + }, + } as InteractionSuccessEvent); + + switch (body.type) { + case InteractionCallbackType.PONG: + // TODO + break; + case InteractionCallbackType.ACKNOWLEDGE: + // Deprected + break; + case InteractionCallbackType.CHANNEL_MESSAGE: + // TODO + break; + case InteractionCallbackType.CHANNEL_MESSAGE_WITH_SOURCE: + // TODO + break; + case InteractionCallbackType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: + // TODO + break; + case InteractionCallbackType.DEFERRED_UPDATE_MESSAGE: + // TODO + break; + case InteractionCallbackType.UPDATE_MESSAGE: + // TODO + break; + case InteractionCallbackType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT: + // TODO + break; + case InteractionCallbackType.MODAL: + // TODO + break; + case InteractionCallbackType.PREMIUM_REQUIRED: + // Deprecated + break; + case InteractionCallbackType.IFRAME_MODAL: + // TODO + break; + case InteractionCallbackType.LAUNCH_ACTIVITY: + // TODO + break; + } + + pendingInteractions.delete(interactionId); + res.sendStatus(204); +}); + +export default router; diff --git a/src/api/routes/interactions/index.ts b/src/api/routes/interactions/index.ts new file mode 100644
index 00000000..815d2b86 --- /dev/null +++ b/src/api/routes/interactions/index.ts
@@ -0,0 +1,82 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2023 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { randomBytes } from "crypto"; +import { InteractionSchema } from "@spacebar/schemas"; +import { route } from "@spacebar/api"; +import { Request, Response, Router } from "express"; +import { Application, ApplicationCommand, emitEvent, InteractionCreateEvent, InteractionFailureEvent, Snowflake } from "@spacebar/util"; +import { pendingInteractions } from "../../../util/imports/Interactions"; + +const router = Router({ mergeParams: true }); + +router.post("/", route({}), async (req: Request, res: Response) => { + const body = req.body as InteractionSchema; + + const interactionId = Snowflake.generate(); + const interactionToken = randomBytes(24).toString("base64url"); + + emitEvent({ + event: "INTERACTION_CREATE", + user_id: req.user_id, + data: { + id: interactionId, + nonce: body.nonce, + }, + } as InteractionCreateEvent); + + emitEvent({ + event: "INTERACTION_CREATE", + user_id: body.application_id, + data: { + channel_id: body.channel_id, + guild_id: body.guild_id, + id: interactionId, + member_id: req.user_id, + token: interactionToken, + type: body.type, + nonce: body.nonce, + }, + } as InteractionCreateEvent); + + const interactionTimeout = setTimeout(() => { + emitEvent({ + event: "INTERACTION_FAILURE", + user_id: req.user_id, + data: { + id: interactionId, + nonce: body.nonce, + reason_code: 2, // when types are done: InteractionFailureReason.TIMEOUT, + }, + } as InteractionFailureEvent); + }, 3000); + + pendingInteractions.set(interactionId, { + timeout: interactionTimeout, + nonce: body.nonce, + userId: req.user_id, + guildId: body.guild_id, + channelId: body.channel_id, + type: body.type, + commandName: body.data.name, + }); + + res.sendStatus(204); +}); + +export default router;