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] 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"); + } +}