diff --git a/assets/openapi.json b/assets/openapi.json
index 19686c59..6860c21b 100644
Binary files a/assets/openapi.json and b/assets/openapi.json differ
diff --git a/assets/schemas.json b/assets/schemas.json
index 018ddaca..39961d30 100644
Binary files a/assets/schemas.json and b/assets/schemas.json differ
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");
+ }
+}