From 99d9bf563fb5e157600824776b149ca03cbea47c Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Sun, 10 Dec 2023 17:02:27 -0500 Subject: [PATCH 1/8] Start implementing webhooks --- assets/schemas.json | Bin 18300690 -> 18445000 bytes src/api/middlewares/Authentication.ts | 4 +- .../routes/channels/#channel_id/webhooks.ts | 25 +- src/api/routes/guilds/#guild_id/webhooks.ts | 35 ++- .../webhooks/#webhook_id/#token/index.ts | 215 ++++++++++++++++++ src/api/routes/webhooks/#webhook_id/index.ts | 32 +++ src/util/schemas/MessageCreateSchema.ts | 4 +- src/util/schemas/WebhookCreateSchema.ts | 1 - src/util/schemas/WebhookExecuteSchema.ts | 46 ++++ src/util/schemas/index.ts | 1 + src/util/util/Constants.ts | 2 +- 11 files changed, 348 insertions(+), 17 deletions(-) create mode 100644 src/api/routes/webhooks/#webhook_id/#token/index.ts create mode 100644 src/api/routes/webhooks/#webhook_id/index.ts create mode 100644 src/util/schemas/WebhookExecuteSchema.ts diff --git a/assets/schemas.json b/assets/schemas.json index a0ab1697c99e4bad5533af3b5d5feaa87e003ecc..018ddacace716f96c6f3733a521e7a5daaa385ef 100644 GIT binary patch delta 6760 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 a#WB1*bvZHo?I$P3jXR1}%F. */ -import { checkToken, Rights } from "@spacebar/util"; import * as Sentry from "@sentry/node"; +import { checkToken, Rights } from "@spacebar/util"; import { NextFunction, Request, Response } from "express"; import { HTTPError } from "lambert-server"; @@ -32,7 +32,7 @@ export const NO_AUTHORIZATION_ROUTES = [ "/auth/forgot", "/auth/reset", // Routes with a seperate auth system - "/webhooks/", + /\/webhooks\/\d+\/\w+\/?/, // no token requires auth // Public information endpoints "/ping", "/gateway", diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts index d54756a1..4c1ccbdf 100644 --- a/src/api/routes/channels/#channel_id/webhooks.ts +++ b/src/api/routes/channels/#channel_id/webhooks.ts @@ -26,8 +26,8 @@ import { WebhookCreateSchema, WebhookType, handleFile, - trimSpecial, isTextChannel, + trimSpecial, } from "@spacebar/util"; import crypto from "crypto"; import { Request, Response, Router } from "express"; @@ -35,10 +35,12 @@ import { HTTPError } from "lambert-server"; const router: Router = Router(); -//TODO: implement webhooks router.get( "/", route({ + description: + "Returns a list of channel webhook objects. Requires the MANAGE_WEBHOOKS permission.", + permission: "MANAGE_WEBHOOKS", responses: { 200: { body: "APIWebhookArray", @@ -46,7 +48,18 @@ router.get( }, }), async (req: Request, res: Response) => { - res.json([]); + const { channel_id } = req.params; + const webhooks = await Webhook.find({ + where: { channel_id }, + relations: [ + "user", + "guild", + "source_guild", + "application" /*"source_channel"*/, + ], + }); + + return res.json(webhooks); }, ); @@ -89,15 +102,15 @@ router.post( if (avatar) avatar = await handleFile(`/avatars/${channel_id}`, avatar); - const hook = Webhook.create({ + const hook = await Webhook.create({ type: WebhookType.Incoming, name, avatar, guild_id: channel.guild_id, channel_id: channel.id, user_id: req.user_id, - token: crypto.randomBytes(24).toString("base64"), - }); + token: crypto.randomBytes(24).toString("base64url"), + }).save(); const user = await User.getPublicUser(req.user_id); diff --git a/src/api/routes/guilds/#guild_id/webhooks.ts b/src/api/routes/guilds/#guild_id/webhooks.ts index d58659a4..a2ef7d69 100644 --- a/src/api/routes/guilds/#guild_id/webhooks.ts +++ b/src/api/routes/guilds/#guild_id/webhooks.ts @@ -16,12 +16,37 @@ along with this program. If not, see . */ -import { Router, Response, Request } from "express"; import { route } from "@spacebar/api"; +import { Webhook } from "@spacebar/util"; +import { Request, Response, Router } from "express"; const router = Router(); -//TODO: implement webhooks -router.get("/", route({}), async (req: Request, res: Response) => { - res.json([]); -}); +router.get( + "/", + route({ + description: + "Returns a list of guild webhook objects. Requires the MANAGE_WEBHOOKS permission.", + permission: "MANAGE_WEBHOOKS", + responses: { + 200: { + body: "APIWebhookArray", + }, + }, + }), + async (req: Request, res: Response) => { + const { guild_id } = req.params; + const webhooks = await Webhook.find({ + where: { guild_id }, + relations: [ + "user", + "guild", + "source_guild", + "application" /*"source_channel"*/, + ], + }); + + return res.json(webhooks); + }, +); + export default router; diff --git a/src/api/routes/webhooks/#webhook_id/#token/index.ts b/src/api/routes/webhooks/#webhook_id/#token/index.ts new file mode 100644 index 00000000..b47502b4 --- /dev/null +++ b/src/api/routes/webhooks/#webhook_id/#token/index.ts @@ -0,0 +1,215 @@ +import { handleMessage, route } from "@spacebar/api"; +import { + Attachment, + Config, + DiscordApiErrors, + FieldErrors, + Message, + Webhook, + WebhookExecuteSchema, + uploadFile, +} from "@spacebar/util"; +import { Request, Response, Router } from "express"; +import { HTTPError } from "lambert-server"; +import multer from "multer"; +import { MoreThan } from "typeorm"; +const router = Router(); + +router.get( + "/", + route({ + description: "Returns a webhook object for the given id.", + responses: { + 200: { + body: "APIWebhook", + }, + 404: {}, + }, + }), + async (req: Request, res: Response) => { + const { webhook_id, token } = req.params; + const webhook = await Webhook.findOne({ + where: { + id: webhook_id, + }, + relations: ["channel", "guild", "application"], + }); + + if (!webhook) { + throw DiscordApiErrors.UNKNOWN_WEBHOOK; + } + + if (webhook.token !== token) { + throw DiscordApiErrors.INVALID_WEBHOOK_TOKEN_PROVIDED; + } + + return res.json(webhook); + }, +); + +// TODO: config max upload size +const messageUpload = multer({ + limits: { + fileSize: Config.get().limits.message.maxAttachmentSize, + fields: 10, + // files: 1 + }, + storage: multer.memoryStorage(), +}); // max upload 50 mb + +// https://discord.com/developers/docs/resources/webhook#execute-webhook +router.post( + "/", + messageUpload.any(), + (req, res, next) => { + if (req.body.payload_json) { + req.body = JSON.parse(req.body.payload_json); + } + + next(); + }, + route({ + requestBody: "WebhookExecuteSchema", + query: { + wait: { + type: "boolean", + required: false, + description: + "waits for server confirmation of message send before response, and returns the created message body", + }, + thread_id: { + type: "string", + required: false, + description: + "Send a message to the specified thread within a webhook's channel.", + }, + }, + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: {}, + }, + }), + async (req: Request, res: Response) => { + const { webhook_id, token } = req.params; + const body = req.body as WebhookExecuteSchema; + const attachments: Attachment[] = []; + + // ensure one of content, embeds, components, or file is present + if ( + !body.content && + !body.embeds && + !body.components && + !body.file && + !body.attachments + ) { + throw DiscordApiErrors.CANNOT_SEND_EMPTY_MESSAGE; + } + + // block username from containing certain words + // TODO: configurable additions + const blockedContains = ["discord", "clyde", "spacebar"]; + for (const word of blockedContains) { + if (body.username?.toLowerCase().includes(word)) { + return res.status(400).json({ + username: [`Username cannot contain "${word}"`], + }); + } + } + + // block username from being certain words + // TODO: configurable additions + const blockedEquals = ["everyone", "here"]; + for (const word of blockedEquals) { + if (body.username?.toLowerCase() === word) { + return res.status(400).json({ + username: [`Username cannot be "${word}"`], + }); + } + } + + const webhook = await Webhook.findOne({ + where: { + id: webhook_id, + }, + relations: ["channel", "guild", "application"], + }); + + if (!webhook) { + throw DiscordApiErrors.UNKNOWN_WEBHOOK; + } + + if (!webhook.channel.isWritable()) { + throw new HTTPError( + `Cannot send messages to channel of type ${webhook.channel.type}`, + 400, + ); + } + + if (webhook.token !== token) { + throw DiscordApiErrors.INVALID_WEBHOOK_TOKEN_PROVIDED; + } + + // TODO: creating messages by users checks if the user can bypass rate limits, we cant do that on webhooks, but maybe we could check the application if there is one? + const limits = Config.get().limits; + if (limits.absoluteRate.register.enabled) { + const count = await Message.count({ + where: { + channel_id: webhook.channel_id, + timestamp: MoreThan( + new Date( + Date.now() - limits.absoluteRate.sendMessage.window, + ), + ), + }, + }); + + if (count >= limits.absoluteRate.sendMessage.limit) + throw FieldErrors({ + channel_id: { + code: "TOO_MANY_MESSAGES", + message: req.t("common:toomany.MESSAGE"), + }, + }); + } + + const files = (req.files as Express.Multer.File[]) ?? []; + for (const currFile of files) { + try { + const file = await uploadFile( + `/attachments/${webhook.channel.id}`, + currFile, + ); + attachments.push( + Attachment.create({ ...file, proxy_url: file.url }), + ); + } catch (error) { + return res.status(400).json({ message: error?.toString() }); + } + } + + // TODO: set username and avatar based on body + + const embeds = body.embeds || []; + const message = await handleMessage({ + ...body, + type: 0, + pinned: false, + webhook_id: webhook.id, + application_id: webhook.application?.id, + embeds, + channel_id: webhook.channel_id, + attachments, + timestamp: new Date(), + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore dont care2 + message.edited_timestamp = null; + + webhook.channel.last_message_id = message.id; + }, +); + +export default router; diff --git a/src/api/routes/webhooks/#webhook_id/index.ts b/src/api/routes/webhooks/#webhook_id/index.ts new file mode 100644 index 00000000..cc8c0386 --- /dev/null +++ b/src/api/routes/webhooks/#webhook_id/index.ts @@ -0,0 +1,32 @@ +import { route } from "@spacebar/api"; +import { Webhook } from "@spacebar/util"; +import { Request, Response, Router } from "express"; +const router = Router(); + +router.get( + "/", + route({ + description: "Returns a webhook object for the given id.", + responses: { + 200: { + body: "APIWebhook", + }, + 404: {}, + }, + }), + async (req: Request, res: Response) => { + const { webhook_id } = req.params; + const webhook = await Webhook.findOneOrFail({ + where: { id: webhook_id }, + relations: [ + "user", + "guild", + "source_guild", + "application" /*"source_channel"*/, + ], + }); + return res.json(webhook); + }, +); + +export default router; diff --git a/src/util/schemas/MessageCreateSchema.ts b/src/util/schemas/MessageCreateSchema.ts index 57abf62f..8093a10a 100644 --- a/src/util/schemas/MessageCreateSchema.ts +++ b/src/util/schemas/MessageCreateSchema.ts @@ -18,7 +18,7 @@ import { Embed } from "@spacebar/util"; -type Attachment = { +export type MessageCreateAttachment = { id: string; filename: string; }; @@ -52,7 +52,7 @@ export interface MessageCreateSchema { TODO: we should create an interface for attachments TODO: OpenWAAO<-->attachment-style metadata conversion **/ - attachments?: Attachment[]; + attachments?: MessageCreateAttachment[]; sticker_ids?: string[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any components?: any[]; diff --git a/src/util/schemas/WebhookCreateSchema.ts b/src/util/schemas/WebhookCreateSchema.ts index f92cb63e..7bd0afa8 100644 --- a/src/util/schemas/WebhookCreateSchema.ts +++ b/src/util/schemas/WebhookCreateSchema.ts @@ -16,7 +16,6 @@ along with this program. If not, see . */ -// TODO: webhooks export interface WebhookCreateSchema { /** * @maxLength 80 diff --git a/src/util/schemas/WebhookExecuteSchema.ts b/src/util/schemas/WebhookExecuteSchema.ts new file mode 100644 index 00000000..943cbe9e --- /dev/null +++ b/src/util/schemas/WebhookExecuteSchema.ts @@ -0,0 +1,46 @@ +/* + 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 . +*/ + +import { Embed } from "../entities"; +import { MessageCreateAttachment } from "./MessageCreateSchema"; + +export interface WebhookExecuteSchema { + content?: string; + username?: string; + avatar_url?: string; + tts?: boolean; + embeds?: Embed[]; + allowed_mentions?: { + parse?: string[]; + roles?: string[]; + users?: string[]; + replied_user?: boolean; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + components?: any[]; + file?: { filename: string }; + payload_json?: string; + /** + TODO: we should create an interface for attachments + TODO: OpenWAAO<-->attachment-style metadata conversion + **/ + attachments?: MessageCreateAttachment[]; + flags?: number; + thread_name?: string; + applied_tags?: string[]; +} diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts index 44a504cd..4812b535 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.ts @@ -79,5 +79,6 @@ export * from "./VoiceStateUpdateSchema"; export * from "./VoiceVideoSchema"; export * from "./WebAuthnSchema"; export * from "./WebhookCreateSchema"; +export * from "./WebhookExecuteSchema"; export * from "./WidgetModifySchema"; export * from "./responses"; diff --git a/src/util/util/Constants.ts b/src/util/util/Constants.ts index e68bb0b7..112b0cc4 100644 --- a/src/util/util/Constants.ts +++ b/src/util/util/Constants.ts @@ -576,7 +576,7 @@ export const DiscordApiErrors = { UNKNOWN_TOKEN: new ApiError("Unknown token", 10012), UNKNOWN_USER: new ApiError("Unknown user", 10013), UNKNOWN_EMOJI: new ApiError("Unknown emoji", 10014), - UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015), + UNKNOWN_WEBHOOK: new ApiError("Unknown webhook", 10015, 404), UNKNOWN_WEBHOOK_SERVICE: new ApiError("Unknown webhook service", 10016), UNKNOWN_CONNECTION: new ApiError("Unknown connection", 10017, 400), UNKNOWN_SESSION: new ApiError("Unknown session", 10020), From e7a98b6c46e3d4fdaa68bd8a04ffcf53a9dac184 Mon Sep 17 00:00:00 2001 From: TomatoCake <60300461+DEVTomatoCake@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:42:07 +0200 Subject: [PATCH 2/8] webhook fixes & username/avatar property for msg --- assets/openapi.json | Bin 571764 -> 584567 bytes assets/schemas.json | Bin 18445000 -> 18473638 bytes .../webhooks/#webhook_id/#token/index.ts | 25 +++- src/api/util/handlers/Message.ts | 119 ++++++++++++------ src/util/entities/Message.ts | 19 ++- src/util/entities/Webhook.ts | 8 +- .../1721298824927-webhookMessageProperties.ts | 15 +++ .../1721298824927-webhookMessageProperties.ts | 15 +++ .../1721298824927-webhookMessageProperties.ts | 15 +++ 9 files changed, 172 insertions(+), 44 deletions(-) create mode 100644 src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts create mode 100644 src/util/migration/mysql/1721298824927-webhookMessageProperties.ts create mode 100644 src/util/migration/postgres/1721298824927-webhookMessageProperties.ts diff --git a/assets/openapi.json b/assets/openapi.json index 19686c591fa5d8c8f8ab92715324cd5a311d8f48..6860c21b7c9f20565abb06947b07217aaac64dd3 100644 GIT binary patch delta 2189 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 CUkZ}| delta 6850 zcmbW5`BPI@6vyX50s$2RLQEh$zy(kvCL-d}3Py=mHpSKz1+k*msUo6)1r)bSt7S3h z)kI4yNG*a;D;`@VQNazWh$9j~iQ3|VHj3!j1+fqK2TuA!CYj89=KDS8o_pRoulUjv z!PT}l!PZkuNFaT+Ue>(-Y!$oTeX$gBtp}?|&{x-LLus(p5I6ej?FAcqhF zU5Q_m75GJ2zj4B1q+<&YI=1iw2cd~~Wj64x%yu(gBe+EX!7X;vL|6^V5w>Ju|9-Hr ze}5~qwLVNSX#glDIZVgX+vE>8|oHqp;}#v6zWpktHqY7#g(!USL!jt1&fiC zd!CST&&$gPO{`*v!YX!HI64tzNBD5q5$>IacU7P61NG^?%aL_pG83!P5nxq1(hKQ| zBRZQ$L1(kf8n1C#;0KolV_rMra>Z$61;`;QK;h+yCQ=i|LTZA)(F09*9Uli?#{*bI zg%Pf$ai8&AYYS{;a0;zva-TmD?(--4BNY0YCNYhj45qOk`QVAhn2(_`W=bczMrdi( z)B&U@HwcPygD2vJI1Eawl*93au6-Kl+NT#H3x#`Ta0HKtBq|U|W*o)~ZBxyJZK_Yg zr0A15oIVQ3)- zsrWPuooBKn<}+9lGw&AORrl`s(7k(sd@wr2@2eKV`>I9D(1l4xn-{}qbM#HLD_TI_ z_ajZNOQFd%CJx!bxD*R!#e!hgvb`)inAO|kVDIaU2^u$e`yLv01RSO-Hh_2iQbmg`ULTqNJ zYMpo#sYv<~Dw5W2z}M{SBkSPxk%YVWglcZA2hEK{8Cs*DJj{j}K=Y;I1me`S0i3#$ z&Z6adVOvt@y9o+?llADl5_xP2$YWC{qixdIsNZ=s_eBPevbT9I)YFhQfmZP5c+ucT z&wQx7JJ^*w9zWudy%{{Rzj8nq6zS5ZL6<)Lf@M|+X9aR!Y^g!#N}VipCV`Sn2$XD_ zfEFWDRNG;SYDW~lZN((*gqWmVv(YVua)d3}SwvuGQPxFtKt$B=HHaF%8HWz0Z`5VG zcLUqKM~C;6I7yJ_sn1&&Ta7*+ zYV`YVSm-bQ`F@ChPA$a*;)5U%Yfe~ZlGZu!?1(x<3+j+Vi~bK=n|K&%6ThpqFv?X0 z1+c0>Hy(WoeTSFWcmxs~zvrWG0F7L6Oh6*6iy*@KsHNCQMKO$2{4h^~E)`A1jL8?ocz1@vi(n|Mp6QC!=ygY0jWy6}yfhOp^p?Gk~&Z>`E5Tw7@fYpA33@AI-J~l!vinGVW89Bl%WaQD^%Rx0LAUM zBxsCei*7@kNjK>yaS5xu?G$l`~k^P+8t+?wC zvX*w;NbHp)TF)1t_53pw9mhWQojoyxnZXeDat4|rjVZ67F{NWJn&@j5ABtYXhoa7T zXp9VNcmu;4-i|^)(sr`%D%rm*xF#yu-=LEHGaNrSaJ#jDC>Q+;%0*q-=w?p11se)T zcO~*Y9C!Xht3R9_TFneZTgzcO7{si%XMEAIA7%OxQ3{cHdL diff --git a/src/api/routes/webhooks/#webhook_id/#token/index.ts b/src/api/routes/webhooks/#webhook_id/#token/index.ts index b47502b4..538ee181 100644 --- a/src/api/routes/webhooks/#webhook_id/#token/index.ts +++ b/src/api/routes/webhooks/#webhook_id/#token/index.ts @@ -1,12 +1,14 @@ -import { handleMessage, route } from "@spacebar/api"; +import { handleMessage, postHandleMessage, route } from "@spacebar/api"; import { Attachment, Config, DiscordApiErrors, FieldErrors, Message, + MessageCreateEvent, Webhook, WebhookExecuteSchema, + emitEvent, uploadFile, } from "@spacebar/util"; import { Request, Response, Router } from "express"; @@ -93,7 +95,11 @@ router.post( }, }), async (req: Request, res: Response) => { + const { wait, thread_id } = req.query; + if (!wait) return res.status(204).send(); + const { webhook_id, token } = req.params; + const body = req.body as WebhookExecuteSchema; const attachments: Attachment[] = []; @@ -200,6 +206,7 @@ router.post( webhook_id: webhook.id, application_id: webhook.application?.id, embeds, + // TODO: Support thread_id/thread_name once threads are implemented channel_id: webhook.channel_id, attachments, timestamp: new Date(), @@ -209,6 +216,22 @@ router.post( message.edited_timestamp = null; webhook.channel.last_message_id = message.id; + + await Promise.all([ + message.save(), + emitEvent({ + event: "MESSAGE_CREATE", + channel_id: webhook.channel_id, + data: message, + } as MessageCreateEvent), + ]); + + // no await as it shouldnt block the message send function and silently catch error + postHandleMessage(message).catch((e) => + console.error("[Message] post-message handler failed", e), + ); + + return res.json(message); }, ); diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 6172a3d0..18616506 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -1,17 +1,17 @@ /* 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 . */ @@ -41,11 +41,13 @@ import { Sticker, MessageCreateSchema, EmbedCache, + handleFile, } from "@spacebar/util"; import { HTTPError } from "lambert-server"; import { In } from "typeorm"; import { EmbedHandlers } from "@spacebar/api"; import * as Sentry from "@sentry/node"; +import fetch from "node-fetch"; const allow_empty = false; // TODO: check webhook, application, system author, stickers // TODO: embed gifs/videos/images @@ -92,44 +94,89 @@ export async function handleMessage(opts: MessageOptions): Promise { where: { id: opts.application_id }, }); } + + let permission: any; if (opts.webhook_id) { message.webhook = await Webhook.findOneOrFail({ where: { id: opts.webhook_id }, }); - } - const permission = await getPermission( - opts.author_id, - channel.guild_id, - opts.channel_id, - ); - permission.hasThrow("SEND_MESSAGES"); - if (permission.cache.member) { - message.member = permission.cache.member; - } + message.author = (await User.findOne({ + where: { id: opts.webhook_id }, + })) || undefined; - if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES"); - if (opts.message_reference) { - permission.hasThrow("READ_MESSAGE_HISTORY"); - // code below has to be redone when we add custom message routing - if (message.guild_id !== null) { - const guild = await Guild.findOneOrFail({ - where: { id: channel.guild_id }, + if (!message.author) { + message.author = User.create({ + id: opts.webhook_id, + username: message.webhook.name, + discriminator: "0000", + avatar: message.webhook.avatar, + public_flags: 0, + premium: false, + premium_type: 0, + bot: true, + created_at: new Date(), + verified: true, + rights: "0", + data: { + valid_tokens_since: new Date(), + }, }); - if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) { - if (opts.message_reference.guild_id !== channel.guild_id) - throw new HTTPError( - "You can only reference messages from this guild", - ); - if (opts.message_reference.channel_id !== opts.channel_id) - throw new HTTPError( - "You can only reference messages from this channel", - ); - } + + await message.author.save(); + } + + if (opts.username) { + message.username = opts.username; + message.author.username = message.username; + } + if (opts.avatar_url) { + const avatarData = await fetch(opts.avatar_url); + const base64 = await avatarData.buffer().then((x) => x.toString("base64")); + + const dataUri = "data:" + avatarData.headers.get("content-type") + ";base64," + base64; + + message.avatar = await handleFile( + `/avatars/${opts.webhook_id}`, + dataUri as string, + ); + console.log(message.avatar); + message.author.avatar = message.avatar; + } + } else { + permission = await getPermission( + opts.author_id, + channel.guild_id, + opts.channel_id, + ); + permission.hasThrow("SEND_MESSAGES"); + if (permission.cache.member) { + message.member = permission.cache.member; + } + + if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES"); + if (opts.message_reference) { + permission.hasThrow("READ_MESSAGE_HISTORY"); + // code below has to be redone when we add custom message routing + if (message.guild_id !== null) { + const guild = await Guild.findOneOrFail({ + where: { id: channel.guild_id }, + }); + if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) { + if (opts.message_reference.guild_id !== channel.guild_id) + throw new HTTPError( + "You can only reference messages from this guild", + ); + if (opts.message_reference.channel_id !== opts.channel_id) + throw new HTTPError( + "You can only reference messages from this channel", + ); + } + } + /** Q: should be checked if the referenced message exists? ANSWER: NO + otherwise backfilling won't work **/ + message.type = MessageType.REPLY; } - /** Q: should be checked if the referenced message exists? ANSWER: NO - otherwise backfilling won't work **/ - message.type = MessageType.REPLY; } // TODO: stickers/activity @@ -172,14 +219,14 @@ export async function handleMessage(opts: MessageOptions): Promise { const role = await Role.findOneOrFail({ where: { id: mention, guild_id: channel.guild_id }, }); - if (role.mentionable || permission.has("MANAGE_ROLES")) { + if (role.mentionable || (opts.webhook_id || permission.has("MANAGE_ROLES"))) { mention_role_ids.push(mention); } }, ), ); - if (permission.has("MENTION_EVERYONE")) { + if (opts.webhook_id || permission.has("MENTION_EVERYONE")) { mention_everyone = !!content.match(EVERYONE_MENTION) || !!content.match(HERE_MENTION); @@ -302,4 +349,6 @@ interface MessageOptions extends MessageCreateSchema { attachments?: Attachment[]; edited_timestamp?: Date; timestamp?: Date; + username?: string; + avatar_url?: string; } diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index b519099a..86238e53 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -1,17 +1,17 @@ /* 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 . */ @@ -218,6 +218,12 @@ export class Message extends BaseClass { @Column({ type: "simple-json", nullable: true }) components?: MessageComponent[]; + @Column({ nullable: true }) + username?: string; + + @Column({ nullable: true }) + avatar?: string; + toJSON(): Message { return { ...this, @@ -234,7 +240,12 @@ export class Message extends BaseClass { reactions: this.reactions ?? undefined, sticker_items: this.sticker_items ?? undefined, message_reference: this.message_reference ?? undefined, - author: this.author?.toPublicUser() ?? undefined, + author: { + ...this.author?.toPublicUser() ?? undefined, + // Webhooks + username: this.username ?? this.author?.username, + avatar: this.avatar ?? this.author?.avatar, + }, activity: this.activity ?? undefined, application: this.application ?? undefined, components: this.components ?? undefined, diff --git a/src/util/entities/Webhook.ts b/src/util/entities/Webhook.ts index 91498a22..b7fba53a 100644 --- a/src/util/entities/Webhook.ts +++ b/src/util/entities/Webhook.ts @@ -1,17 +1,17 @@ /* 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 . */ @@ -35,7 +35,7 @@ export class Webhook extends BaseClass { type: WebhookType; @Column({ nullable: true }) - name?: string; + name: string; @Column({ nullable: true }) avatar?: string; diff --git a/src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts b/src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts new file mode 100644 index 00000000..ccbe689a --- /dev/null +++ b/src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class WebhookMessageProperties1721298824927 implements MigrationInterface { + name = "WebhookMessageProperties1721298824927"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query("ALTER TABLE `messages` ADD `username` text NULL"); + await queryRunner.query("ALTER TABLE `messages` ADD `avatar` text NULL"); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `username`"); + await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `avatar`"); + } +} diff --git a/src/util/migration/mysql/1721298824927-webhookMessageProperties.ts b/src/util/migration/mysql/1721298824927-webhookMessageProperties.ts new file mode 100644 index 00000000..ccbe689a --- /dev/null +++ b/src/util/migration/mysql/1721298824927-webhookMessageProperties.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class WebhookMessageProperties1721298824927 implements MigrationInterface { + name = "WebhookMessageProperties1721298824927"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query("ALTER TABLE `messages` ADD `username` text NULL"); + await queryRunner.query("ALTER TABLE `messages` ADD `avatar` text NULL"); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `username`"); + await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `avatar`"); + } +} diff --git a/src/util/migration/postgres/1721298824927-webhookMessageProperties.ts b/src/util/migration/postgres/1721298824927-webhookMessageProperties.ts new file mode 100644 index 00000000..46c507d4 --- /dev/null +++ b/src/util/migration/postgres/1721298824927-webhookMessageProperties.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class WebhookMessageProperties1721298824927 implements MigrationInterface { + name = "WebhookMessageProperties1721298824927"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query("ALTER TABLE messages ADD username text NULL"); + await queryRunner.query("ALTER TABLE messages ADD avatar text NULL"); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query("ALTER TABLE messages DROP COLUMN username"); + await queryRunner.query("ALTER TABLE messages DROP COLUMN avatar"); + } +} From 6be3714593d282b4ac9881005a358355f729aa0d Mon Sep 17 00:00:00 2001 From: TomatoCake <60300461+DEVTomatoCake@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:52:00 +0200 Subject: [PATCH 3/8] Fix ESLint & prettier --- .../webhooks/#webhook_id/#token/index.ts | 2 +- src/api/routes/webhooks/#webhook_id/index.ts | 1 + src/api/util/handlers/Message.ts | 28 +++++++++++++------ src/util/entities/Message.ts | 2 +- .../1721298824927-webhookMessageProperties.ts | 16 ++++++++--- .../1721298824927-webhookMessageProperties.ts | 16 ++++++++--- .../1721298824927-webhookMessageProperties.ts | 4 ++- 7 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/api/routes/webhooks/#webhook_id/#token/index.ts b/src/api/routes/webhooks/#webhook_id/#token/index.ts index 538ee181..49c47cca 100644 --- a/src/api/routes/webhooks/#webhook_id/#token/index.ts +++ b/src/api/routes/webhooks/#webhook_id/#token/index.ts @@ -95,7 +95,7 @@ router.post( }, }), async (req: Request, res: Response) => { - const { wait, thread_id } = req.query; + const { wait } = req.query; if (!wait) return res.status(204).send(); const { webhook_id, token } = req.params; diff --git a/src/api/routes/webhooks/#webhook_id/index.ts b/src/api/routes/webhooks/#webhook_id/index.ts index cc8c0386..7d528dbf 100644 --- a/src/api/routes/webhooks/#webhook_id/index.ts +++ b/src/api/routes/webhooks/#webhook_id/index.ts @@ -15,6 +15,7 @@ router.get( }, }), async (req: Request, res: Response) => { + // TODO: Permission check const { webhook_id } = req.params; const webhook = await Webhook.findOneOrFail({ where: { id: webhook_id }, diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 18616506..0a20fbc8 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -42,6 +42,7 @@ import { MessageCreateSchema, EmbedCache, handleFile, + Permissions, } from "@spacebar/util"; import { HTTPError } from "lambert-server"; import { In } from "typeorm"; @@ -95,15 +96,16 @@ export async function handleMessage(opts: MessageOptions): Promise { }); } - let permission: any; + let permission: undefined | Permissions; if (opts.webhook_id) { message.webhook = await Webhook.findOneOrFail({ where: { id: opts.webhook_id }, }); - message.author = (await User.findOne({ - where: { id: opts.webhook_id }, - })) || undefined; + message.author = + (await User.findOne({ + where: { id: opts.webhook_id }, + })) || undefined; if (!message.author) { message.author = User.create({ @@ -132,9 +134,15 @@ export async function handleMessage(opts: MessageOptions): Promise { } if (opts.avatar_url) { const avatarData = await fetch(opts.avatar_url); - const base64 = await avatarData.buffer().then((x) => x.toString("base64")); + const base64 = await avatarData + .buffer() + .then((x) => x.toString("base64")); - const dataUri = "data:" + avatarData.headers.get("content-type") + ";base64," + base64; + const dataUri = + "data:" + + avatarData.headers.get("content-type") + + ";base64," + + base64; message.avatar = await handleFile( `/avatars/${opts.webhook_id}`, @@ -219,14 +227,18 @@ export async function handleMessage(opts: MessageOptions): Promise { const role = await Role.findOneOrFail({ where: { id: mention, guild_id: channel.guild_id }, }); - if (role.mentionable || (opts.webhook_id || permission.has("MANAGE_ROLES"))) { + if ( + role.mentionable || + opts.webhook_id || + permission?.has("MANAGE_ROLES") + ) { mention_role_ids.push(mention); } }, ), ); - if (opts.webhook_id || permission.has("MENTION_EVERYONE")) { + if (opts.webhook_id || permission?.has("MENTION_EVERYONE")) { mention_everyone = !!content.match(EVERYONE_MENTION) || !!content.match(HERE_MENTION); diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index 86238e53..6f712023 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -241,7 +241,7 @@ export class Message extends BaseClass { sticker_items: this.sticker_items ?? undefined, message_reference: this.message_reference ?? undefined, author: { - ...this.author?.toPublicUser() ?? undefined, + ...(this.author?.toPublicUser() ?? undefined), // Webhooks username: this.username ?? this.author?.username, avatar: this.avatar ?? this.author?.avatar, diff --git a/src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts b/src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts index ccbe689a..775847e0 100644 --- a/src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts +++ b/src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts @@ -1,15 +1,23 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class WebhookMessageProperties1721298824927 implements MigrationInterface { +export class WebhookMessageProperties1721298824927 + implements MigrationInterface +{ name = "WebhookMessageProperties1721298824927"; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query("ALTER TABLE `messages` ADD `username` text NULL"); - await queryRunner.query("ALTER TABLE `messages` ADD `avatar` text NULL"); + await queryRunner.query( + "ALTER TABLE `messages` ADD `username` text NULL", + ); + await queryRunner.query( + "ALTER TABLE `messages` ADD `avatar` text NULL", + ); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `username`"); + await queryRunner.query( + "ALTER TABLE `messages` DROP COLUMN `username`", + ); await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `avatar`"); } } diff --git a/src/util/migration/mysql/1721298824927-webhookMessageProperties.ts b/src/util/migration/mysql/1721298824927-webhookMessageProperties.ts index ccbe689a..775847e0 100644 --- a/src/util/migration/mysql/1721298824927-webhookMessageProperties.ts +++ b/src/util/migration/mysql/1721298824927-webhookMessageProperties.ts @@ -1,15 +1,23 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class WebhookMessageProperties1721298824927 implements MigrationInterface { +export class WebhookMessageProperties1721298824927 + implements MigrationInterface +{ name = "WebhookMessageProperties1721298824927"; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query("ALTER TABLE `messages` ADD `username` text NULL"); - await queryRunner.query("ALTER TABLE `messages` ADD `avatar` text NULL"); + await queryRunner.query( + "ALTER TABLE `messages` ADD `username` text NULL", + ); + await queryRunner.query( + "ALTER TABLE `messages` ADD `avatar` text NULL", + ); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `username`"); + await queryRunner.query( + "ALTER TABLE `messages` DROP COLUMN `username`", + ); await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `avatar`"); } } diff --git a/src/util/migration/postgres/1721298824927-webhookMessageProperties.ts b/src/util/migration/postgres/1721298824927-webhookMessageProperties.ts index 46c507d4..bd603f10 100644 --- a/src/util/migration/postgres/1721298824927-webhookMessageProperties.ts +++ b/src/util/migration/postgres/1721298824927-webhookMessageProperties.ts @@ -1,6 +1,8 @@ import { MigrationInterface, QueryRunner } from "typeorm"; -export class WebhookMessageProperties1721298824927 implements MigrationInterface { +export class WebhookMessageProperties1721298824927 + implements MigrationInterface +{ name = "WebhookMessageProperties1721298824927"; public async up(queryRunner: QueryRunner): Promise { From 59c42c950b76ed99b3349ff64d98f979c75b2701 Mon Sep 17 00:00:00 2001 From: TomatoCake <60300461+DEVTomatoCake@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:31:04 +0200 Subject: [PATCH 4/8] try manual conflict resolving --- src/util/entities/Message.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index 6f712023..4a1ed5b4 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -218,6 +218,9 @@ export class Message extends BaseClass { @Column({ type: "simple-json", nullable: true }) components?: MessageComponent[]; + @Column({ type: "simple-json", nullable: true }) + poll?: Poll; + @Column({ nullable: true }) username?: string; From 15a2e57b05223f93361fe01bb3bf42e90cea3483 Mon Sep 17 00:00:00 2001 From: TomatoCake <60300461+DEVTomatoCake@users.noreply.github.com> Date: Thu, 18 Jul 2024 14:42:22 +0200 Subject: [PATCH 5/8] Prettier & fix merge util/handlers/Message --- src/api/util/handlers/Message.ts | 58 +++++++++++++++++--------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 9b267928..461cddb4 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -43,16 +43,12 @@ import { //CHANNEL_MENTION, USER_MENTION, Webhook, - Attachment, - Config, - Sticker, - MessageCreateSchema, - EmbedCache, + handleFile, + Permissions, } from "@spacebar/util"; import { HTTPError } from "lambert-server"; import { In } from "typeorm"; -import { EmbedHandlers } from "@spacebar/api"; -import * as Sentry from "@sentry/node"; +import fetch from "node-fetch"; const allow_empty = false; // TODO: check webhook, application, system author, stickers // TODO: embed gifs/videos/images @@ -167,28 +163,36 @@ export async function handleMessage(opts: MessageOptions): Promise { message.member = permission.cache.member; } - if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES"); - if (opts.message_reference) { - permission.hasThrow("READ_MESSAGE_HISTORY"); - // code below has to be redone when we add custom message routing - if (message.guild_id !== null) { - const guild = await Guild.findOneOrFail({ - where: { id: channel.guild_id }, - }); - if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) { - if (opts.message_reference.guild_id !== channel.guild_id) - throw new HTTPError( - "You can only reference messages from this guild", - ); - if (opts.message_reference.channel_id !== opts.channel_id) - throw new HTTPError( - "You can only reference messages from this channel", - ); + if (opts.tts) permission.hasThrow("SEND_TTS_MESSAGES"); + if (opts.message_reference) { + permission.hasThrow("READ_MESSAGE_HISTORY"); + // code below has to be redone when we add custom message routing + if (message.guild_id !== null) { + const guild = await Guild.findOneOrFail({ + where: { id: channel.guild_id }, + }); + if (!opts.message_reference.guild_id) + opts.message_reference.guild_id = channel.guild_id; + if (!opts.message_reference.channel_id) + opts.message_reference.channel_id = opts.channel_id; + + if (!guild.features.includes("CROSS_CHANNEL_REPLIES")) { + if (opts.message_reference.guild_id !== channel.guild_id) + throw new HTTPError( + "You can only reference messages from this guild", + ); + if (opts.message_reference.channel_id !== opts.channel_id) + throw new HTTPError( + "You can only reference messages from this channel", + ); + } + + message.message_reference = opts.message_reference; } + /** Q: should be checked if the referenced message exists? ANSWER: NO + otherwise backfilling won't work **/ + message.type = MessageType.REPLY; } - /** Q: should be checked if the referenced message exists? ANSWER: NO - otherwise backfilling won't work **/ - message.type = MessageType.REPLY; } // TODO: stickers/activity From 873107f90d44f907f5964b8c0a95261224a74759 Mon Sep 17 00:00:00 2001 From: TomatoCake <60300461+DEVTomatoCake@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:16:50 +0200 Subject: [PATCH 6/8] Perms for GET webhook, url property --- assets/openapi.json | Bin 604064 -> 604136 bytes assets/schemas.json | Bin 22032984 -> 22041912 bytes .../webhooks/#webhook_id/#token/index.ts | 9 +++-- src/api/routes/webhooks/#webhook_id/index.ts | 34 +++++++++++++----- src/api/util/handlers/Message.ts | 1 - src/util/entities/Webhook.ts | 8 +++-- 6 files changed, 37 insertions(+), 15 deletions(-) diff --git a/assets/openapi.json b/assets/openapi.json index 9bcb54a53686b0b09ffc6b43f05d4ade613597f3..5cf313273185976af200242369f0c2d3c2ff14c9 100644 GIT binary patch delta 132 zcmZ4RUggDmm4+>hA7ZEP4`Yd){vnp}>U8-u7WU~APBM#4UoXYTG5wt>qr~)v<%~*` z9bTJF|G%4&XZrKqjP}zN%2*VqZ#c~8*KVA^2*gZ4%)H$=fhE#_TOlzoMWG}=J2g*l hav`(Gc7cB^a*XZ!C$a!BD-g2*G5hxY6FD9(1po;0G`#=- delta 154 zcmaFyUS+|1m4+>hA7ZCZ5MpwjY#`!3yAOW%ok_#HS z&>#mziWZsixdgP`*s)yOQEX?l1p%2U3`G=TwTs4~8nm5u4&Wcq-5;`667DD8@80|D z^WtCkuiCVo`P8Q95=q-$^VQ)Lsynd!3Y7~sE2wUFoP>US@AFx-dEmN?9={a0o(|<3 zl^~D!J8$7mr_F%VX;#uS6h?J+izTI3tUtOUT9bq#IVzU`l}ndyLSgh6*xGW+3>hdh zY&1wo#=7 z1o~2a-bP}z7k#1jqF)@+q?a@RyrhA_NVTSEwnxOBQfWCAqZSA1Z$EJ zrKq-W4b~yhU>&MQO6ps~#WY`SNTB}uC9%{kSLA9;lu4;McSR<*bCVW!ZVGEk#aUs$ z3HsKEmB_hgTp9)AQk~~6gc@53^f}R>&snw>lP`un>v#p8b;K0mOg8wB#1awLp^SwN zWn4OPG-c$^C^<`oDDprB7VzmV~=~0GBxZS(f!0ug% z>K(X(m~XEI^X+x2-y*r)a|3MmeC<&tPO`3SBh;0>j_Nm-9Pz*#ARgFcjV$9j|0G!F zpR7TaqN^kYx=J>CBe9siREX)*TW|xFEAdT-65kB$lI&8v1zd`^s*om2d*6i8-fe$H zW-@PaCgd&N(S%e>FMJEq3$qfDOY-ynKfv?;o%4`t8GhN2;g^HOZWial&Eh|H;o5*3 z>D>(@y?JjUO&V4CU{vi%L>9oi@NMue+$0);kxRbt;nIA zTT}$OMep_?O;&UlLq&H<4sN#P;#B(~PF1>W8*U2fD(9f9ylenxk{x&|MBMX%a(F&) zz!SUsnq;%o2FOw$G9aU5x}ZD2H(^?+k}W3>D#7T=Xv-}ujme+rQ$tvrMy{7kJuc;vl zr$$$qnxypS0(%$gUU5c4+ppQpp_O0HaWm?aa$5MVLcw-&KGdu`A)!V20nSt&>AQfM zPy6`L=CQL2seHpy4|;sIr-)wlbXvromCv5yU3!+?Ke0<-*ZxWkH zi!Ss}{ho;W|4HmR^<9HrA|@*8E@)>>{^}GbqQ`%6U=QvN45XbC&hw|ubVkM~C!cdP zHrv=!_ru2`XmjLk1s(FvcjQ=)D|*@a)jpjhG3ZHEiwhIj9{zzaBH?GE5zM0o0+*r~oT1luZv3F452e ziH25xOitMW=GHeC;np{oPGX!gjGdXFJsAFhDFwrN%iEy0{BmdpVwPuE(&)#KH2OCI z8QV7PC$LT1ej91B^pF6hhpu9=rlC)vY3Q@oUu%+MS8D8aja{u>|8XrMA@vq6t*ism z%5Hp#jI5ylb13NlLW@klRmeZVD&(dN(*(SQo*(*3$|W0aL9*dXB+D@t&JT5_BQF}K zBvZp~Luy!eGIDLUs_Fr&s$LDI6Ihh&m?-ATOn0En)HfF?`7r$7@G$)AK};Fh!X>Zl z2YKbd2AmpYn22i``46;=4A$cIkdv&GI}$krMm+JLVYg;t))5_TSY@+T`*Zr%~z})=-7{B&eW2=#$F+& zy0c=JsZWI-GUe2pL3>4<5T%<<#51@~{#)qeA9`gW+L;wJ>d7OBdh(qD8ERwP7&OLx z|7|)>t*6lpJ&lhyAl0&;{ui>Jjwd5A=lCb!9RD;4i9wwOpl(8gnW|VfPF?&*PdtO@ ziRTNDlIIeBf^!M~^T%P+l796+gJ1nGN*sHz`g%$3W@1k%;ye{BT+kvJm3fm3^Get$?j r5%z~9kWOU!>kqCZLjA#43F;ipTlkj2RYYhRTuruM{FYf}*zEg1s5&i2 delta 7267 zcmb7JYgAO%6JlEjcE(UwqmV_rh1}F zxe}I~nj!;=1yKPBxKW7LR9lTz!6wmI!n0CpQ^6u()FSrI(6!oMVfV+ZdE9UB^X=!^ z=S*|W=QiIwzGhRnUIm^9f6V3{D`7dYf-$W%d&*WG)NXT*HjdWm> z1q^JOV!df{^f4m~*Lx}Hf!3*TpjEOeb#f7LfkZniNVKzFxDkV9p7yU~)KM=5N4-r5 zYJmxn&Sn{OHp|me@T=UG+rn+RvR;c3IjH9@>FxSZM$oNb4_m=u4T=m^%N#*1o4yQX zEgjdr1>-uW4{>O#CTFl}av4DIE-|u`DwQ+4));Ik)+r^-_E#3ufDcMoZNc*8Q(TG@ zr2cGaU-nEx!UZ{rE#@}68LocqLS`_|!PC8XnHxvzE(ojwXq1@~}2>**{F{dG1KWA0?rC20=%xm+g8#*sk~Ag+u2y2S9FfU=)H0Z)74`LL(LPAyTnmK5l`R z84KZMMvw}HF424@p;e8+P}R67JZ*A+@jpnkLCz;Cw3h-fHcE}z{Q4`+8Kgl$8)aa%_s zZ0lI-gVXIC@Gdw9Bn`iZ(p^-v9*T;RacN8!_iup3{Trv^d~zc+6>fy4g`m=x?mMc5 zeMi%Map(oTI#|$~VZ^-vC2gf9!CmRBKwp8JKk{bLTqy}$0 zgVTNR(ED)k(2o-Is92^A{20;(elioKn~7*^Dhta6S=e@GlnAXU`~YeScihJ%BI)}n zkiMT~<8(g_-U&~G^Ad1wync8WTtEDa2RJ4kUAw`f>z6?~lp7x{%7;gb_HIU{mZ5E< zKe48Fjuyb3qxD>=Ly&q=d+L2?(0dNdC7`X*yLnouq$v*lP#fL_G zcL*ZBJ3J4u(~He(DDnslMIJ3e@n)iK36-5F1lb9`9F@nczU-nk>++V&G0j{dOxdS% zXZ10~JLr&}gCV`$^9_^xK%7XD@;8Af8G8CeDV#oW@(Yx|>9Da3hK+^@ly0{7#TjcV#8iL~b7}~YF=8Zk zKH^lYeXKK-T?-6!HxJA`XHHAmvLJ~m0{%?=#8!J(4%o3-fFTts3xQ$|DmN1q!gR%5- zXx8L)`kzU+<>eJ^P|T?2_i{Lo6RlnpGh{R>d{kzGb#R82KE8k>53-_?RTv zVCf&ER1$pyB+gDR@N0!7u=A*eiM;;?Q>+h0F`_SX-Q z^L_5RN07Vj&t0fAVt*}FxS3j52s2bZS;4rP4ol{#u{CcbO!K|c$=t_k@8nE;D0F>) zhWVE+%~Y5FUIg5$?tpvM-=?7QgVv69LhV@BN!-|SKdA@qC-r8a?4eNI2T=W&l{hB* z#~#D}vHmY{x;x?rpdU?&-@^w|Tyx188j^K^5Fkf-y;R!3^XTq2}4_>c&cO}wYXmk6E`KjMSh&3V&F zR%qK8Kt4e=Zr*f?6^1UnO@5BLYBFfEgNTBE!7U)d3vMCl#<4O<3EJ#n;()`CURgwh z(JLXu4`+qxcqLTZE+)9kAwDlNOpIFQ5;7AP1&^k1B6u_{B?UM>UeaYm@RBYk^H6w} zEt&7c%Eun7i#>Kv{1-8}K}_Rv?PV1cDx~hr)6&+F6142DQieOE)1kLh6toKmf%i?{5ql`!a5R9^axTP87*w~{2O1| BY*7FJ diff --git a/src/api/routes/webhooks/#webhook_id/#token/index.ts b/src/api/routes/webhooks/#webhook_id/#token/index.ts index 49c47cca..6d1449eb 100644 --- a/src/api/routes/webhooks/#webhook_id/#token/index.ts +++ b/src/api/routes/webhooks/#webhook_id/#token/index.ts @@ -20,7 +20,7 @@ const router = Router(); router.get( "/", route({ - description: "Returns a webhook object for the given id.", + description: "Returns a webhook object for the given id and token.", responses: { 200: { body: "APIWebhook", @@ -45,7 +45,12 @@ router.get( throw DiscordApiErrors.INVALID_WEBHOOK_TOKEN_PROVIDED; } - return res.json(webhook); + const instanceUrl = + Config.get().api.endpointPublic || "http://localhost:3001"; + return res.json({ + ...webhook, + url: instanceUrl + "/webhooks/" + webhook.id + "/" + webhook.token, + }); }, ); diff --git a/src/api/routes/webhooks/#webhook_id/index.ts b/src/api/routes/webhooks/#webhook_id/index.ts index 7d528dbf..98faaac1 100644 --- a/src/api/routes/webhooks/#webhook_id/index.ts +++ b/src/api/routes/webhooks/#webhook_id/index.ts @@ -1,5 +1,10 @@ import { route } from "@spacebar/api"; -import { Webhook } from "@spacebar/util"; +import { + Config, + DiscordApiErrors, + getPermission, + Webhook, +} from "@spacebar/util"; import { Request, Response, Router } from "express"; const router = Router(); @@ -15,18 +20,29 @@ router.get( }, }), async (req: Request, res: Response) => { - // TODO: Permission check const { webhook_id } = req.params; const webhook = await Webhook.findOneOrFail({ where: { id: webhook_id }, - relations: [ - "user", - "guild", - "source_guild", - "application" /*"source_channel"*/, - ], + relations: ["channel", "guild", "application", "user"], + }); + + if (webhook.guild_id) { + const permission = await getPermission( + req.user_id, + webhook.guild_id, + ); + + if (!permission.has("MANAGE_WEBHOOKS")) + throw DiscordApiErrors.UNKNOWN_WEBHOOK; + } else if (webhook.user_id != req.user_id) + throw DiscordApiErrors.UNKNOWN_WEBHOOK; + + const instanceUrl = + Config.get().api.endpointPublic || "http://localhost:3001"; + return res.json({ + ...webhook, + url: instanceUrl + "/webhooks/" + webhook.id + "/" + webhook.token, }); - return res.json(webhook); }, ); diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index 461cddb4..f037417a 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -149,7 +149,6 @@ export async function handleMessage(opts: MessageOptions): Promise { `/avatars/${opts.webhook_id}`, dataUri as string, ); - console.log(message.avatar); message.author.avatar = message.avatar; } } else { diff --git a/src/util/entities/Webhook.ts b/src/util/entities/Webhook.ts index b7fba53a..8b1585ad 100644 --- a/src/util/entities/Webhook.ts +++ b/src/util/entities/Webhook.ts @@ -38,20 +38,20 @@ export class Webhook extends BaseClass { name: string; @Column({ nullable: true }) - avatar?: string; + avatar: string; @Column({ nullable: true }) token?: string; @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.guild) - guild_id: string; + guild_id?: string; @JoinColumn({ name: "guild_id" }) @ManyToOne(() => Guild, { onDelete: "CASCADE", }) - guild: Guild; + guild?: Guild; @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.channel) @@ -92,4 +92,6 @@ export class Webhook extends BaseClass { onDelete: "CASCADE", }) source_guild: Guild; + + url?: string; } From adc3474216af90df8ff54089eb6d2ad67a95b533 Mon Sep 17 00:00:00 2001 From: TomatoCake <60300461+DEVTomatoCake@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:35:10 +0200 Subject: [PATCH 7/8] Consistent relations, url property, src channel --- assets/openapi.json | Bin 604136 -> 604475 bytes assets/schemas.json | Bin 22041912 -> 22078920 bytes .../routes/channels/#channel_id/webhooks.ts | 17 +++++++++++----- src/api/routes/guilds/#guild_id/webhooks.ts | 19 ++++++++++++------ .../webhooks/#webhook_id/#token/index.ts | 10 ++++++++- src/api/routes/webhooks/#webhook_id/index.ts | 11 ++++++++-- src/util/entities/Webhook.ts | 16 ++++++++++++--- 7 files changed, 56 insertions(+), 17 deletions(-) diff --git a/assets/openapi.json b/assets/openapi.json index 5cf313273185976af200242369f0c2d3c2ff14c9..be317720b104de104adb5083e0e2c4368e1b094d 100644 GIT binary patch delta 197 zcmaFyUS;hA7ZET#xk-`H=4_o0%2WpWhvkOA(rtR<8+63MtO+f#EHyGlM`52 zr`M%0Nl#z;f>C^WLkN)j?l7a@bp1wV_V$VdMj&PaV&?4?2`mQM(>q_YYbYt?7b%qF zDhA7ZEb&u4avY|{jP{cg zSXifjsALwK-oKogZMwrMM!)F^l`QP-4T+3E%ml>D+Zz&D474W~GK*{v*vBfv*sk5j T0>rF9%m&2l+qL^R1Xcq8H2o|I diff --git a/assets/schemas.json b/assets/schemas.json index cd3a018f0b34691e45472a154bb8f5e97b1bfe77..aecc76a210318ca1858f9baaa6ab1145bd4a0c5e 100644 GIT binary patch delta 12256 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 diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts index 4c1ccbdf..7533fceb 100644 --- a/src/api/routes/channels/#channel_id/webhooks.ts +++ b/src/api/routes/channels/#channel_id/webhooks.ts @@ -1,17 +1,17 @@ /* 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 . */ @@ -53,13 +53,20 @@ router.get( where: { channel_id }, relations: [ "user", + "channel", + "source_channel", "guild", "source_guild", - "application" /*"source_channel"*/, + "application", ], }); - return res.json(webhooks); + const instanceUrl = + Config.get().api.endpointPublic || "http://localhost:3001"; + return res.json(webhooks.map(webhook => ({ + ...webhook, + url: instanceUrl + "/webhooks/" + webhook.id + "/" + webhook.token, + }))); }, ); diff --git a/src/api/routes/guilds/#guild_id/webhooks.ts b/src/api/routes/guilds/#guild_id/webhooks.ts index a2ef7d69..60c9818a 100644 --- a/src/api/routes/guilds/#guild_id/webhooks.ts +++ b/src/api/routes/guilds/#guild_id/webhooks.ts @@ -1,23 +1,23 @@ /* 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 . */ import { route } from "@spacebar/api"; -import { Webhook } from "@spacebar/util"; +import { Config, Webhook } from "@spacebar/util"; import { Request, Response, Router } from "express"; const router = Router(); @@ -39,13 +39,20 @@ router.get( where: { guild_id }, relations: [ "user", + "channel", + "source_channel", "guild", "source_guild", - "application" /*"source_channel"*/, + "application", ], }); - return res.json(webhooks); + const instanceUrl = + Config.get().api.endpointPublic || "http://localhost:3001"; + return res.json(webhooks.map(webhook => ({ + ...webhook, + url: instanceUrl + "/webhooks/" + webhook.id + "/" + webhook.token, + }))); }, ); diff --git a/src/api/routes/webhooks/#webhook_id/#token/index.ts b/src/api/routes/webhooks/#webhook_id/#token/index.ts index 6d1449eb..8e0ad0dd 100644 --- a/src/api/routes/webhooks/#webhook_id/#token/index.ts +++ b/src/api/routes/webhooks/#webhook_id/#token/index.ts @@ -34,7 +34,14 @@ router.get( where: { id: webhook_id, }, - relations: ["channel", "guild", "application"], + relations: [ + "user", + "channel", + "source_channel", + "guild", + "source_guild", + "application", + ], }); if (!webhook) { @@ -65,6 +72,7 @@ const messageUpload = multer({ }); // max upload 50 mb // https://discord.com/developers/docs/resources/webhook#execute-webhook +// TODO: GitHub/Slack compatible hooks router.post( "/", messageUpload.any(), diff --git a/src/api/routes/webhooks/#webhook_id/index.ts b/src/api/routes/webhooks/#webhook_id/index.ts index 98faaac1..c10bf224 100644 --- a/src/api/routes/webhooks/#webhook_id/index.ts +++ b/src/api/routes/webhooks/#webhook_id/index.ts @@ -11,7 +11,7 @@ const router = Router(); router.get( "/", route({ - description: "Returns a webhook object for the given id.", + description: "Returns a webhook object for the given id. Requires the MANAGE_WEBHOOKS permission or to be the owner of the webhook.", responses: { 200: { body: "APIWebhook", @@ -23,7 +23,14 @@ router.get( const { webhook_id } = req.params; const webhook = await Webhook.findOneOrFail({ where: { id: webhook_id }, - relations: ["channel", "guild", "application", "user"], + relations: [ + "user", + "channel", + "source_channel", + "guild", + "source_guild", + "application", + ], }); if (webhook.guild_id) { diff --git a/src/util/entities/Webhook.ts b/src/util/entities/Webhook.ts index 8b1585ad..9539d6e8 100644 --- a/src/util/entities/Webhook.ts +++ b/src/util/entities/Webhook.ts @@ -85,13 +85,23 @@ export class Webhook extends BaseClass { @Column({ nullable: true }) @RelationId((webhook: Webhook) => webhook.guild) - source_guild_id: string; + source_guild_id?: string; @JoinColumn({ name: "source_guild_id" }) @ManyToOne(() => Guild, { onDelete: "CASCADE", }) - source_guild: Guild; + source_guild?: Guild; - url?: string; + @Column({ nullable: true }) + @RelationId((webhook: Webhook) => webhook.channel) + source_channel_id: string; + + @JoinColumn({ name: "source_channel_id" }) + @ManyToOne(() => Channel, { + onDelete: "CASCADE", + }) + source_channel: Channel; + + url: string; } From 4b1e9ba851a38cd36f6c5b362b51ecf39bad6ef2 Mon Sep 17 00:00:00 2001 From: TomatoCake <60300461+DEVTomatoCake@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:36:00 +0200 Subject: [PATCH 8/8] Prettier --- src/api/routes/channels/#channel_id/webhooks.ts | 15 +++++++++++---- src/api/routes/guilds/#guild_id/webhooks.ts | 15 +++++++++++---- src/api/routes/webhooks/#webhook_id/index.ts | 3 ++- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts index 7533fceb..2060760d 100644 --- a/src/api/routes/channels/#channel_id/webhooks.ts +++ b/src/api/routes/channels/#channel_id/webhooks.ts @@ -63,10 +63,17 @@ router.get( const instanceUrl = Config.get().api.endpointPublic || "http://localhost:3001"; - return res.json(webhooks.map(webhook => ({ - ...webhook, - url: instanceUrl + "/webhooks/" + webhook.id + "/" + webhook.token, - }))); + return res.json( + webhooks.map((webhook) => ({ + ...webhook, + url: + instanceUrl + + "/webhooks/" + + webhook.id + + "/" + + webhook.token, + })), + ); }, ); diff --git a/src/api/routes/guilds/#guild_id/webhooks.ts b/src/api/routes/guilds/#guild_id/webhooks.ts index 60c9818a..47e19947 100644 --- a/src/api/routes/guilds/#guild_id/webhooks.ts +++ b/src/api/routes/guilds/#guild_id/webhooks.ts @@ -49,10 +49,17 @@ router.get( const instanceUrl = Config.get().api.endpointPublic || "http://localhost:3001"; - return res.json(webhooks.map(webhook => ({ - ...webhook, - url: instanceUrl + "/webhooks/" + webhook.id + "/" + webhook.token, - }))); + return res.json( + webhooks.map((webhook) => ({ + ...webhook, + url: + instanceUrl + + "/webhooks/" + + webhook.id + + "/" + + webhook.token, + })), + ); }, ); diff --git a/src/api/routes/webhooks/#webhook_id/index.ts b/src/api/routes/webhooks/#webhook_id/index.ts index c10bf224..59fdb76d 100644 --- a/src/api/routes/webhooks/#webhook_id/index.ts +++ b/src/api/routes/webhooks/#webhook_id/index.ts @@ -11,7 +11,8 @@ const router = Router(); router.get( "/", route({ - description: "Returns a webhook object for the given id. Requires the MANAGE_WEBHOOKS permission or to be the owner of the webhook.", + description: + "Returns a webhook object for the given id. Requires the MANAGE_WEBHOOKS permission or to be the owner of the webhook.", responses: { 200: { body: "APIWebhook",