1
0
mirror of https://github.com/spacebarchat/server.git synced 2024-11-22 10:22:39 +01:00

Merge pull request #1149 from DEVTomatoCake/feat/webhooks-3

Support webhooks
This commit is contained in:
Puyodead1 2024-08-08 14:18:29 -04:00 committed by GitHub
commit 9d2f97a92a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 9143 additions and 1082 deletions

View File

@ -2413,7 +2413,7 @@
"attachments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Attachment_1"
"$ref": "#/components/schemas/Attachment"
}
},
"embeds": {
@ -2510,6 +2510,12 @@
"poll": {
"$ref": "#/components/schemas/Poll"
},
"username": {
"type": "string"
},
"avatar": {
"type": "string"
},
"id": {
"type": "string"
}
@ -2817,6 +2823,15 @@
"source_guild": {
"$ref": "#/components/schemas/Guild"
},
"source_channel_id": {
"type": "string"
},
"source_channel": {
"$ref": "#/components/schemas/Channel"
},
"url": {
"type": "string"
},
"id": {
"type": "string"
}
@ -2824,14 +2839,15 @@
"required": [
"application",
"application_id",
"avatar",
"channel",
"channel_id",
"guild",
"guild_id",
"id",
"source_guild",
"source_guild_id",
"name",
"source_channel",
"source_channel_id",
"type",
"url",
"user",
"user_id"
]
@ -3167,7 +3183,7 @@
],
"type": "number"
},
"Attachment_1": {
"Attachment": {
"type": "object",
"properties": {
"filename": {
@ -3686,7 +3702,7 @@
"attachments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Attachment_1"
"$ref": "#/components/schemas/Attachment"
}
},
"embeds": {
@ -6956,6 +6972,107 @@
"name"
]
},
"WebhookExecuteSchema": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"username": {
"type": "string"
},
"avatar_url": {
"type": "string"
},
"tts": {
"type": "boolean"
},
"embeds": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Embed"
}
},
"allowed_mentions": {
"type": "object",
"properties": {
"parse": {
"type": "array",
"items": {
"type": "string"
}
},
"roles": {
"type": "array",
"items": {
"type": "string"
}
},
"users": {
"type": "array",
"items": {
"type": "string"
}
},
"replied_user": {
"type": "boolean"
}
},
"additionalProperties": false
},
"components": {
"type": "array",
"items": {}
},
"file": {
"type": "object",
"properties": {
"filename": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"filename"
]
},
"payload_json": {
"type": "string"
},
"attachments": {
"description": "TODO: we should create an interface for attachments\nTODO: OpenWAAO<-->attachment-style metadata conversion",
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"filename": {
"type": "string"
}
},
"additionalProperties": false,
"required": [
"filename",
"id"
]
}
},
"flags": {
"type": "integer"
},
"thread_name": {
"type": "string"
},
"applied_tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"WidgetModifySchema": {
"type": "object",
"properties": {
@ -8864,9 +8981,175 @@
},
{
"name": "voice"
},
{
"name": "webhooks"
}
],
"paths": {
"/webhooks/{webhook_id}/": {
"get": {
"security": [
{
"bearer": []
}
],
"description": "Returns a webhook object for the given id. Requires the MANAGE_WEBHOOKS permission or to be the owner of the webhook.",
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIWebhook"
}
}
}
},
"404": {
"description": "No description available"
}
},
"parameters": [
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "webhook_id"
}
],
"tags": [
"webhooks"
]
}
},
"/webhooks/{webhook_id}/{token}/": {
"get": {
"security": [
{
"bearer": []
}
],
"description": "Returns a webhook object for the given id and token.",
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIWebhook"
}
}
}
},
"404": {
"description": "No description available"
}
},
"parameters": [
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "webhook_id"
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "token"
}
],
"tags": [
"webhooks"
]
},
"post": {
"security": [
{
"bearer": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WebhookExecuteSchema"
}
}
}
},
"responses": {
"204": {
"description": "No description available"
},
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIErrorResponse"
}
}
}
},
"404": {
"description": "No description available"
}
},
"parameters": [
{
"name": "webhook_id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "webhook_id"
},
{
"name": "token",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "token"
},
{
"name": "wait",
"in": "query",
"required": false,
"schema": {
"type": "boolean"
},
"description": "waits for server confirmation of message send before response, and returns the created message body"
},
{
"name": "thread_id",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Send a message to the specified thread within a webhook's channel."
}
],
"tags": [
"webhooks"
]
}
},
"/voice/regions/": {
"get": {
"security": [
@ -11739,14 +12022,23 @@
},
"/guilds/{guild_id}/webhooks/": {
"get": {
"x-permission-required": "MANAGE_WEBHOOKS",
"security": [
{
"bearer": []
}
],
"description": "Returns a list of guild webhook objects. Requires the MANAGE_WEBHOOKS permission.",
"responses": {
"default": {
"description": "No description available"
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/APIWebhookArray"
}
}
}
}
},
"parameters": [
@ -15249,11 +15541,13 @@
},
"/channels/{channel_id}/webhooks/": {
"get": {
"x-permission-required": "MANAGE_WEBHOOKS",
"security": [
{
"bearer": []
}
],
"description": "Returns a list of channel webhook objects. Requires the MANAGE_WEBHOOKS permission.",
"responses": {
"200": {
"description": "",

File diff suppressed because it is too large Load Diff

View File

@ -16,8 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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",

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
@ -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,32 @@ router.get(
},
}),
async (req: Request, res: Response) => {
res.json([]);
const { channel_id } = req.params;
const webhooks = await Webhook.find({
where: { channel_id },
relations: [
"user",
"channel",
"source_channel",
"guild",
"source_guild",
"application",
],
});
const instanceUrl =
Config.get().api.endpointPublic || "http://localhost:3001";
return res.json(
webhooks.map((webhook) => ({
...webhook,
url:
instanceUrl +
"/webhooks/" +
webhook.id +
"/" +
webhook.token,
})),
);
},
);
@ -89,15 +116,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);

View File

@ -1,27 +1,66 @@
/*
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 { Router, Response, Request } from "express";
import { route } from "@spacebar/api";
import { Config, 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",
"channel",
"source_channel",
"guild",
"source_guild",
"application",
],
});
const instanceUrl =
Config.get().api.endpointPublic || "http://localhost:3001";
return res.json(
webhooks.map((webhook) => ({
...webhook,
url:
instanceUrl +
"/webhooks/" +
webhook.id +
"/" +
webhook.token,
})),
);
},
);
export default router;

View File

@ -0,0 +1,251 @@
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";
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 and token.",
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: [
"user",
"channel",
"source_channel",
"guild",
"source_guild",
"application",
],
});
if (!webhook) {
throw DiscordApiErrors.UNKNOWN_WEBHOOK;
}
if (webhook.token !== token) {
throw DiscordApiErrors.INVALID_WEBHOOK_TOKEN_PROVIDED;
}
const instanceUrl =
Config.get().api.endpointPublic || "http://localhost:3001";
return res.json({
...webhook,
url: instanceUrl + "/webhooks/" + webhook.id + "/" + webhook.token,
});
},
);
// 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
// TODO: GitHub/Slack compatible hooks
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 { wait } = req.query;
if (!wait) return res.status(204).send();
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,
// TODO: Support thread_id/thread_name once threads are implemented
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;
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);
},
);
export default router;

View File

@ -0,0 +1,57 @@
import { route } from "@spacebar/api";
import {
Config,
DiscordApiErrors,
getPermission,
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. Requires the MANAGE_WEBHOOKS permission or to be the owner of the webhook.",
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",
"channel",
"source_channel",
"guild",
"source_guild",
"application",
],
});
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,
});
},
);
export default router;

View File

@ -43,9 +43,12 @@ import {
//CHANNEL_MENTION,
USER_MENTION,
Webhook,
handleFile,
Permissions,
} from "@spacebar/util";
import { HTTPError } from "lambert-server";
import { In } from "typeorm";
import fetch from "node-fetch";
const allow_empty = false;
// TODO: check webhook, application, system author, stickers
// TODO: embed gifs/videos/images
@ -93,52 +96,102 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
where: { id: opts.application_id },
});
}
let permission: undefined | Permissions;
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 (!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;
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,
);
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 (!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
@ -183,14 +236,18 @@ export async function handleMessage(opts: MessageOptions): Promise<Message> {
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);
@ -316,4 +373,6 @@ interface MessageOptions extends MessageCreateSchema {
attachments?: Attachment[];
edited_timestamp?: Date;
timestamp?: Date;
username?: string;
avatar_url?: string;
}

View File

@ -221,6 +221,12 @@ export class Message extends BaseClass {
@Column({ type: "simple-json", nullable: true })
poll?: Poll;
@Column({ nullable: true })
username?: string;
@Column({ nullable: true })
avatar?: string;
toJSON(): Message {
return {
...this,
@ -237,7 +243,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,

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
@ -35,23 +35,23 @@ export class Webhook extends BaseClass {
type: WebhookType;
@Column({ nullable: true })
name?: string;
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)
@ -85,11 +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;
@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;
}

View File

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class WebhookMessageProperties1721298824927
implements MigrationInterface
{
name = "WebhookMessageProperties1721298824927";
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(
"ALTER TABLE `messages` DROP COLUMN `username`",
);
await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `avatar`");
}
}

View File

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class WebhookMessageProperties1721298824927
implements MigrationInterface
{
name = "WebhookMessageProperties1721298824927";
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(
"ALTER TABLE `messages` DROP COLUMN `username`",
);
await queryRunner.query("ALTER TABLE `messages` DROP COLUMN `avatar`");
}
}

View File

@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class WebhookMessageProperties1721298824927
implements MigrationInterface
{
name = "WebhookMessageProperties1721298824927";
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query("ALTER TABLE messages DROP COLUMN username");
await queryRunner.query("ALTER TABLE messages DROP COLUMN avatar");
}
}

View File

@ -23,7 +23,7 @@ import {
PollMedia,
} from "@spacebar/util";
type Attachment = {
export type MessageCreateAttachment = {
id: string;
filename: string;
};
@ -57,7 +57,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[];
components?: ActionRowComponent[];
// TODO: Fix TypeScript errors in src\api\util\handlers\Message.ts once this is enabled

View File

@ -16,7 +16,6 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// TODO: webhooks
export interface WebhookCreateSchema {
/**
* @maxLength 80

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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[];
}

View File

@ -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";

View File

@ -578,7 +578,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),