diff --git a/assets/openapi.json b/assets/openapi.json index 2788cdb0..50d4fca1 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index 00f86b36..a3db68f8 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ diff --git a/flake.lock b/flake.lock index 77bf2b86..29505fc3 100644 Binary files a/flake.lock and b/flake.lock differ diff --git a/hashes.json b/hashes.json index bc319094..bc1807d4 100644 --- a/hashes.json +++ b/hashes.json @@ -1,3 +1,3 @@ { - "npmDepsHash": "sha256-RxGkjCU9qqqDMjhJ5aEq1w7c7lS4nAp0/3F0zASJQms=" + "npmDepsHash": "sha256-kdS1SwcBu6Dor92iO1ickLgz0T5UL16nyA49xXGajf4=" } diff --git a/package-lock.json b/package-lock.json index a350da90..9f77c385 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 7f466e44..e95d6253 100644 --- a/package.json +++ b/package.json @@ -59,14 +59,14 @@ "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "eslint": "^8.56.0", - "express": "^4.18.2", + "express": "^4.19.2", "husky": "^8.0.3", "prettier": "^2.8.8", "pretty-quick": "^3.1.3", "typescript": "^4.9.5" }, "dependencies": { - "@aws-sdk/client-s3": "^3.385.0", + "@aws-sdk/client-s3": "^3.629.0", "@sentry/integrations": "^7.66.0", "@sentry/node": "^7.66.0", "ajv": "8.6.2", @@ -97,7 +97,7 @@ "node-2fa": "^2.0.3", "node-fetch": "^2.6.12", "node-os-utils": "^1.3.7", - "nodemailer": "^6.9.4", + "nodemailer": "^6.9.14", "picocolors": "^1.0.0", "probe-image-size": "^7.2.3", "proxy-agent": "^6.3.0", @@ -107,7 +107,7 @@ "typeorm": "^0.3.17", "typescript-json-schema": "^0.50.1", "wretch": "^2.6.0", - "ws": "^8.13.0" + "ws": "^8.17.1" }, "_moduleAliases": { "@spacebar/api": "dist/api", diff --git a/patches/express+4.18.2.patch b/patches/express+4.19.2.patch similarity index 78% rename from patches/express+4.18.2.patch rename to patches/express+4.19.2.patch index de52db60..c69be207 100644 --- a/patches/express+4.18.2.patch +++ b/patches/express+4.19.2.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/express/lib/response.js b/node_modules/express/lib/response.js -index fede486..e3d868e 100644 +index dd7b3c8..a339896 100644 --- a/node_modules/express/lib/response.js +++ b/node_modules/express/lib/response.js @@ -27,7 +27,6 @@ var merge = require('utils-merge'); @@ -10,21 +10,15 @@ index fede486..e3d868e 100644 var cookie = require('cookie'); var send = require('send'); var extname = path.extname; -@@ -49,13 +48,6 @@ var res = Object.create(http.ServerResponse.prototype) +@@ -54,7 +53,6 @@ module.exports = res + * @private + */ - module.exports = res - --/** -- * Module variables. -- * @private -- */ -- -var charsetRegExp = /;\s*charset\s*=/; -- + var schemaAndHostRegExp = /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?\/\/[^\\\/\?]+/; + /** - * Set status `code`. - * -@@ -164,17 +156,6 @@ res.send = function send(body) { +@@ -165,16 +163,6 @@ res.send = function send(body) { break; } @@ -38,11 +32,10 @@ index fede486..e3d868e 100644 - this.set('Content-Type', setCharset(type, 'utf-8')); - } - } -- + // determine if ETag should be generated var etagFn = app.get('etag fn') - var generateETag = !this.get('ETag') && typeof etagFn === 'function' -@@ -780,17 +761,6 @@ res.header = function header(field, val) { +@@ -781,17 +769,6 @@ res.header = function header(field, val) { ? val.map(String) : String(val); diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts index 9e41b453..a6cad51c 100644 --- a/src/api/middlewares/Authentication.ts +++ b/src/api/middlewares/Authentication.ts @@ -16,8 +16,8 @@ along with this program. If not, see . */ -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/applications/#id/bot/index.ts b/src/api/routes/applications/#id/bot/index.ts index 3c431e3d..af754106 100644 --- a/src/api/routes/applications/#id/bot/index.ts +++ b/src/api/routes/applications/#id/bot/index.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 . */ @@ -57,7 +57,7 @@ router.post( res.send({ token: await generateToken(user.id), - }).status(204); + }); }, ); diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts index c2222b0a..4792c534 100644 --- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts @@ -331,4 +331,74 @@ router.delete( }, ); +router.delete( + "/:emoji/:burst/:user_id", + route({ + responses: { + 204: {}, + 400: { + body: "APIErrorResponse", + }, + 404: {}, + 403: {}, + }, + }), + async (req: Request, res: Response) => { + let { user_id } = req.params; + const { message_id, channel_id } = req.params; + + const emoji = getEmoji(req.params.emoji); + + const channel = await Channel.findOneOrFail({ + where: { id: channel_id }, + }); + const message = await Message.findOneOrFail({ + where: { id: message_id, channel_id }, + }); + + if (user_id === "@me") user_id = req.user_id; + else { + const permissions = await getPermission( + req.user_id, + undefined, + channel_id, + ); + permissions.hasThrow("MANAGE_MESSAGES"); + } + + const already_added = message.reactions.find( + (x) => + (x.emoji.id === emoji.id && emoji.id) || + x.emoji.name === emoji.name, + ); + if (!already_added || !already_added.user_ids.includes(user_id)) + throw new HTTPError("Reaction not found", 404); + + already_added.count--; + + if (already_added.count <= 0) message.reactions.remove(already_added); + else + already_added.user_ids.splice( + already_added.user_ids.indexOf(user_id), + 1, + ); + + await message.save(); + + await emitEvent({ + event: "MESSAGE_REACTION_REMOVE", + channel_id, + data: { + user_id: req.user_id, + channel_id, + message_id, + guild_id: channel.guild_id, + emoji, + }, + } as MessageReactionRemoveEvent); + + res.sendStatus(204); + }, +); + export default router; diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts index 645c6db2..521ab7a1 100644 --- a/src/api/routes/channels/#channel_id/messages/index.ts +++ b/src/api/routes/channels/#channel_id/messages/index.ts @@ -130,30 +130,45 @@ router.get( query.take = Math.floor(limit / 2); if (query.take != 0) { const [right, left] = await Promise.all([ - Message.find({ ...query, where: { id: LessThan(around) } }), Message.find({ ...query, - where: { id: MoreThanOrEqual(around) }, + where: { channel_id, id: LessThan(around) }, + }), + Message.find({ + ...query, + where: { channel_id, id: MoreThanOrEqual(around) }, + order: { timestamp: "ASC" }, }), ]); left.push(...right); - messages = left; + messages = left.sort( + (a, b) => a.timestamp.getTime() - b.timestamp.getTime(), + ); } else { query.take = 1; const message = await Message.findOne({ ...query, - where: { id: around }, + where: { channel_id, id: around }, }); messages = message ? [message] : []; } } else { if (after) { if (BigInt(after) > BigInt(Snowflake.generate())) - return res.status(422); + throw new HTTPError( + "after parameter must not be greater than current time", + 422, + ); + query.where.id = MoreThan(after); + query.order = { timestamp: "ASC" }; } else if (before) { if (BigInt(before) > BigInt(Snowflake.generate())) - return res.status(422); + throw new HTTPError( + "before parameter must not be greater than current time", + 422, + ); + query.where.id = LessThan(before); } diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts index d54756a1..2060760d 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 . */ @@ -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); diff --git a/src/api/routes/discovery.ts b/src/api/routes/discovery.ts index a045c191..dd3cc15d 100644 --- a/src/api/routes/discovery.ts +++ b/src/api/routes/discovery.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 . */ @@ -39,8 +39,8 @@ router.get( const { primary_only } = req.query; const out = primary_only - ? await Categories.find() - : await Categories.find({ where: { is_primary: true } }); + ? await Categories.find({ where: { is_primary: true } }) + : await Categories.find(); res.send(out); }, diff --git a/src/api/routes/guilds/#guild_id/bulk-ban.ts b/src/api/routes/guilds/#guild_id/bulk-ban.ts index f544103a..e5f7a0d8 100644 --- a/src/api/routes/guilds/#guild_id/bulk-ban.ts +++ b/src/api/routes/guilds/#guild_id/bulk-ban.ts @@ -27,6 +27,7 @@ import { } from "@spacebar/util"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; +import { Config } from "@spacebar/util"; const router: Router = Router(); @@ -52,7 +53,8 @@ router.post( const userIds: Array = req.body.user_ids; if (!userIds) throw new HTTPError("The user_ids array is missing", 400); - if (userIds.length > 200) + + if (userIds.length > Config.get().limits.guild.maxBulkBanUsers) throw new HTTPError( "The user_ids array must be between 1 and 200 in length", 400, diff --git a/src/api/routes/guilds/#guild_id/webhooks.ts b/src/api/routes/guilds/#guild_id/webhooks.ts index d58659a4..47e19947 100644 --- a/src/api/routes/guilds/#guild_id/webhooks.ts +++ b/src/api/routes/guilds/#guild_id/webhooks.ts @@ -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 . */ -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; diff --git a/src/api/routes/read-states/ack-bulk.ts b/src/api/routes/read-states/ack-bulk.ts index 3ee25d1a..2f41937b 100644 --- a/src/api/routes/read-states/ack-bulk.ts +++ b/src/api/routes/read-states/ack-bulk.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 . */ @@ -60,7 +60,7 @@ router.post( }), ]); - return res.status(204); + return res.sendStatus(204); }, ); diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts index db0922d6..44271cad 100644 --- a/src/api/routes/users/#id/profile.ts +++ b/src/api/routes/users/#id/profile.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 . */ @@ -19,6 +19,8 @@ import { route } from "@spacebar/api"; import { Badge, + Config, + FieldErrors, Member, PrivateUserProjection, User, @@ -136,6 +138,18 @@ router.patch( select: [...PrivateUserProjection, "data"], }); + if (body.bio) { + const { maxBio } = Config.get().limits.user; + if (body.bio.length > maxBio) { + throw FieldErrors({ + bio: { + code: "BIO_INVALID", + message: `Bio must be less than ${maxBio} in length`, + }, + }); + } + } + user.assign(body); await user.save(); diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts index ad11a428..5caf0d11 100644 --- a/src/api/routes/users/@me/index.ts +++ b/src/api/routes/users/@me/index.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 . */ @@ -120,7 +120,7 @@ router.patch( if (!body.password) throw FieldErrors({ password: { - message: req.t("auth:register.INVALID_PASSWORD"), + message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD", }, }); @@ -160,6 +160,15 @@ router.patch( }, }); } + + if (!body.password) { + throw FieldErrors({ + password: { + message: req.t("auth:login.INVALID_PASSWORD"), + code: "INVALID_PASSWORD", + }, + }); + } } if (body.discriminator) { @@ -180,6 +189,18 @@ router.patch( } } + if (body.bio) { + const { maxBio } = Config.get().limits.user; + if (body.bio.length > maxBio) { + throw FieldErrors({ + bio: { + code: "BIO_INVALID", + message: `Bio must be less than ${maxBio} in length`, + }, + }); + } + } + user.assign(body); user.validate(); await user.save(); diff --git a/src/api/routes/users/@me/notes.ts b/src/api/routes/users/@me/notes.ts index 248e61f9..365e73b4 100644 --- a/src/api/routes/users/@me/notes.ts +++ b/src/api/routes/users/@me/notes.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 . */ @@ -107,7 +107,7 @@ router.put( user_id: owner.id, }); - return res.status(204); + return res.sendStatus(204); }, ); 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..8e0ad0dd --- /dev/null +++ b/src/api/routes/webhooks/#webhook_id/#token/index.ts @@ -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; 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..59fdb76d --- /dev/null +++ b/src/api/routes/webhooks/#webhook_id/index.ts @@ -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; diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index c3658668..f037417a 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -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 { 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 { 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; } diff --git a/src/gateway/events/Close.ts b/src/gateway/events/Close.ts index 16f6b188..311ed32a 100644 --- a/src/gateway/events/Close.ts +++ b/src/gateway/events/Close.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 . */ @@ -50,7 +50,7 @@ export async function Close(this: WebSocket, code: number, reason: Buffer) { } as SessionsReplace); const session = sessions.first() || { activities: [], - client_info: {}, + client_status: {}, status: "offline", }; @@ -68,7 +68,7 @@ export async function Close(this: WebSocket, code: number, reason: Buffer) { data: { user: userOrId, activities: session.activities, - client_status: session?.client_info, + client_status: session?.client_status, status: session.status, }, } as PresenceUpdateEvent); diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index 41f9f83d..e30a1ee0 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.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 . */ @@ -122,8 +122,8 @@ export async function onIdentify(this: WebSocket, data: Payload) { session_id: this.session_id, status: identify.presence?.status || "online", client_info: { - client: identify.properties?.$device, - os: identify.properties?.os, + client: identify.properties?.device || identify.properties?.$device, + os: identify.properties?.os || identify.properties?.$os, version: 0, }, activities: identify.presence?.activities, // TODO: validation @@ -372,7 +372,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { data: { user: user.toPublicUser(), activities: session.activities, - client_status: session.client_info, + client_status: session.client_status, status: session.status, }, } as PresenceUpdateEvent), diff --git a/src/gateway/opcodes/LazyRequest.ts b/src/gateway/opcodes/LazyRequest.ts index 3c21b708..27e9b00a 100644 --- a/src/gateway/opcodes/LazyRequest.ts +++ b/src/gateway/opcodes/LazyRequest.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 . */ @@ -248,7 +248,7 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) { d: { user: user, activities: session?.activities || [], - client_status: session?.client_info, + client_status: session?.client_status, status: session?.status || "offline", } as Presence, }); diff --git a/src/gateway/opcodes/PresenceUpdate.ts b/src/gateway/opcodes/PresenceUpdate.ts index 03736263..f84da120 100644 --- a/src/gateway/opcodes/PresenceUpdate.ts +++ b/src/gateway/opcodes/PresenceUpdate.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,14 +35,19 @@ export async function onPresenceUpdate(this: WebSocket, { d }: Payload) { { status: presence.status, activities: presence.activities }, ); + const session = await Session.findOneOrFail({ + select: ["client_status"], + where: { session_id: this.session_id }, + }); + await emitEvent({ event: "PRESENCE_UPDATE", user_id: this.user_id, data: { user: await User.getPublicUser(this.user_id), - activities: presence.activities, - client_status: {}, // TODO: status: presence.status, + activities: presence.activities, + client_status: session.client_status, }, } as PresenceUpdateEvent); } diff --git a/src/gateway/opcodes/RequestGuildMembers.ts b/src/gateway/opcodes/RequestGuildMembers.ts index 304d4b39..d294f4d3 100644 --- a/src/gateway/opcodes/RequestGuildMembers.ts +++ b/src/gateway/opcodes/RequestGuildMembers.ts @@ -1,23 +1,124 @@ /* 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 { WebSocket } from "@spacebar/gateway"; +import { + getPermission, + GuildMembersChunkEvent, + Member, + Presence, + RequestGuildMembersSchema, + Session, +} from "@spacebar/util"; +import { WebSocket, Payload, OPCODES, Send } from "@spacebar/gateway"; +import { check } from "./instanceOf"; +import { FindManyOptions, In, Like } from "typeorm"; -export function onRequestGuildMembers(this: WebSocket) { - // return this.close(CLOSECODES.Unknown_error); +export async function onRequestGuildMembers(this: WebSocket, { d }: Payload) { + // TODO: check data + check.call(this, RequestGuildMembersSchema, d); + + const { guild_id, query, presences, nonce } = + d as RequestGuildMembersSchema; + let { limit, user_ids } = d as RequestGuildMembersSchema; + + if ("query" in d && (!limit || Number.isNaN(limit))) + throw new Error('"query" requires "limit" to be set'); + if ("query" in d && user_ids) + throw new Error('"query" and "user_ids" are mutually exclusive'); + if (user_ids && !Array.isArray(user_ids)) user_ids = [user_ids]; + user_ids = user_ids as string[] | undefined; + + // TODO: Configurable limit? + if ((query || (user_ids && user_ids.length > 0)) && (!limit || limit > 100)) + limit = 100; + + const permissions = await getPermission(this.user_id, guild_id); + permissions.hasThrow("VIEW_CHANNEL"); + + const whereQuery: FindManyOptions["where"] = {}; + if (query) { + whereQuery.user = { + username: Like(query + "%"), + }; + } else if (user_ids && user_ids.length > 0) { + whereQuery.id = In(user_ids); + } + + const memberFind: FindManyOptions = { + where: { + ...whereQuery, + guild_id, + }, + relations: ["user", "roles"], + }; + if (limit) memberFind.take = Math.abs(Number(limit || 100)); + const members = await Member.find(memberFind); + + const baseData = { + guild_id, + nonce, + }; + + const chunkCount = Math.ceil(members.length / 1000); + + let notFound: string[] = []; + if (user_ids && user_ids.length > 0) + notFound = user_ids.filter( + (id) => !members.some((member) => member.id == id), + ); + + const chunks: GuildMembersChunkEvent["data"][] = []; + while (members.length > 0) { + const chunk: Member[] = members.splice(0, 1000); + + const presenceList: Presence[] = []; + if (presences) { + for await (const member of chunk) { + const session = await Session.findOne({ + where: { user_id: member.id }, + }); + if (session) + presenceList.push({ + user: member.user.toPublicUser(), + status: session.status, + activities: session.activities, + client_status: session.client_status, + }); + } + } + + chunks.push({ + ...baseData, + members: chunk.map((member) => member.toPublicMember()), + presences: presences ? presenceList : undefined, + chunk_index: chunks.length, + chunk_count: chunkCount, + }); + } + + if (notFound.length > 0) chunks[0].not_found = notFound; + + chunks.forEach((chunk) => { + Send(this, { + op: OPCODES.Dispatch, + s: this.sequence++, + t: "GUILD_MEMBERS_CHUNK", + d: chunk, + }); + }); } diff --git a/src/util/config/types/subconfigurations/limits/GuildLimits.ts b/src/util/config/types/subconfigurations/limits/GuildLimits.ts index b64d9485..e77cf424 100644 --- a/src/util/config/types/subconfigurations/limits/GuildLimits.ts +++ b/src/util/config/types/subconfigurations/limits/GuildLimits.ts @@ -21,5 +21,6 @@ export class GuildLimits { maxEmojis: number = 2000; maxMembers: number = 25000000; maxChannels: number = 65535; + maxBulkBanUsers: number = 200; maxChannelsInCategory: number = 65535; } diff --git a/src/util/config/types/subconfigurations/limits/UserLimits.ts b/src/util/config/types/subconfigurations/limits/UserLimits.ts index 8f9b1a97..afe9afbe 100644 --- a/src/util/config/types/subconfigurations/limits/UserLimits.ts +++ b/src/util/config/types/subconfigurations/limits/UserLimits.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 . */ @@ -20,4 +20,5 @@ export class UserLimits { maxGuilds: number = 1048576; maxUsername: number = 32; maxFriends: number = 5000; + maxBio: number = 190; } diff --git a/src/util/entities/Categories.ts b/src/util/entities/Categories.ts index bba1bfa7..13d969de 100644 --- a/src/util/entities/Categories.ts +++ b/src/util/entities/Categories.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 . */ @@ -46,6 +46,10 @@ export class Categories extends BaseClassWithoutId { @Column({ type: "simple-json" }) localizations: string; + // Whether to show the category prominently (e.g. in a sidebar) instead of only secondary (e.g. in search results) @Column({ nullable: true }) is_primary: boolean; + + @Column({ nullable: true }) + icon?: string; } diff --git a/src/util/entities/Message.ts b/src/util/entities/Message.ts index d28c8c29..1dd89dc1 100644 --- a/src/util/entities/Message.ts +++ b/src/util/entities/Message.ts @@ -216,17 +216,23 @@ export class Message extends BaseClass { }; @Column({ type: "simple-json", nullable: true }) - components?: MessageComponent[]; + components?: ActionRowComponent[]; @Column({ type: "simple-json", nullable: true }) poll?: Poll; + @Column({ nullable: true }) + username?: string; + + @Column({ nullable: true }) + avatar?: string; + toJSON(): Message { return { ...this, author_id: undefined, member_id: undefined, - webhook_id: undefined, + webhook_id: this.webhook_id ?? undefined, application_id: undefined, nonce: this.nonce ?? undefined, @@ -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, @@ -248,21 +259,100 @@ export class Message extends BaseClass { } export interface MessageComponent { - type: number; - style?: number; + type: MessageComponentType; +} + +export interface ActionRowComponent extends MessageComponent { + type: MessageComponentType.ActionRow; + components: ( + | ButtonComponent + | StringSelectMenuComponent + | SelectMenuComponent + | TextInputComponent + )[]; +} + +export interface ButtonComponent extends MessageComponent { + type: MessageComponentType.Button; + style: ButtonStyle; label?: string; emoji?: PartialEmoji; custom_id?: string; sku_id?: string; url?: string; disabled?: boolean; - components: MessageComponent[]; +} + +export enum ButtonStyle { + Primary = 1, + Secondary = 2, + Success = 3, + Danger = 4, + Link = 5, + Premium = 6, +} + +export interface SelectMenuComponent extends MessageComponent { + type: + | MessageComponentType.StringSelect + | MessageComponentType.UserSelect + | MessageComponentType.RoleSelect + | MessageComponentType.MentionableSelect + | MessageComponentType.ChannelSelect; + custom_id: string; + channel_types?: number[]; + placeholder?: string; + default_values?: SelectMenuDefaultOption[]; // only for non-string selects + min_values?: number; + max_values?: number; + disabled?: boolean; +} + +export interface SelectMenuOption { + label: string; + value: string; + description?: string; + emoji?: PartialEmoji; + default?: boolean; +} + +export interface SelectMenuDefaultOption { + id: string; + type: "user" | "role" | "channel"; +} + +export interface StringSelectMenuComponent extends SelectMenuComponent { + type: MessageComponentType.StringSelect; + options: SelectMenuOption[]; +} + +export interface TextInputComponent extends MessageComponent { + type: MessageComponentType.TextInput; + custom_id: string; + style: TextInputStyle; + label: string; + min_length?: number; + max_length?: number; + required?: boolean; + value?: string; + placeholder?: string; +} + +export enum TextInputStyle { + Short = 1, + Paragraph = 2, } export enum MessageComponentType { Script = 0, // self command script ActionRow = 1, Button = 2, + StringSelect = 3, + TextInput = 4, + UserSelect = 5, + RoleSelect = 6, + MentionableSelect = 7, + ChannelSelect = 8, } export interface Embed { diff --git a/src/util/entities/Session.ts b/src/util/entities/Session.ts index 6c6f7caa..15f8faa2 100644 --- a/src/util/entities/Session.ts +++ b/src/util/entities/Session.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 . */ @@ -19,7 +19,7 @@ import { User } from "./User"; import { BaseClass } from "./BaseClass"; import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm"; -import { Status } from "../interfaces/Status"; +import { ClientStatus, Status } from "../interfaces/Status"; import { Activity } from "../interfaces/Activity"; //TODO we need to remove all sessions on server start because if the server crashes without closing websockets it won't delete them @@ -43,7 +43,6 @@ export class Session extends BaseClass { @Column({ type: "simple-json", nullable: true }) activities: Activity[]; - // TODO client_status @Column({ type: "simple-json", select: false }) client_info: { client: string; @@ -51,6 +50,9 @@ export class Session extends BaseClass { version: number; }; + @Column({ type: "simple-json" }) + client_status: ClientStatus; + @Column({ nullable: false, type: "varchar" }) status: Status; //TODO enum } diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index c929039e..b299bcfc 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.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 . */ @@ -130,7 +130,7 @@ export class User extends BaseClass { bot: boolean = false; // if user is bot @Column() - bio: string = ""; // short description of the user (max 190 chars -> should be configurable) + bio: string = ""; // short description of the user @Column() system: boolean = false; // shouldn't be used, the api sends this field type true, if the generated message comes from a system generated author diff --git a/src/util/entities/Webhook.ts b/src/util/entities/Webhook.ts index 91498a22..9539d6e8 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,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; } diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts index 98a64e94..a31e2263 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.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 . */ @@ -280,8 +280,8 @@ export interface GuildMembersChunkEvent extends Event { members: PublicMember[]; chunk_index: number; chunk_count: number; - not_found: string[]; - presences: Presence[]; + not_found?: string[]; + presences?: Presence[]; nonce?: string; }; } diff --git a/src/util/interfaces/Status.ts b/src/util/interfaces/Status.ts index 407a813e..0f2f4e13 100644 --- a/src/util/interfaces/Status.ts +++ b/src/util/interfaces/Status.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 . */ @@ -21,5 +21,6 @@ export type Status = "idle" | "dnd" | "online" | "offline" | "invisible"; export interface ClientStatus { desktop?: string; // e.g. Windows/Linux/Mac mobile?: string; // e.g. iOS/Android - web?: string; // e.g. browser, bot account + web?: string; // e.g. browser, bot account, unknown + embedded?: string; // e.g. embedded } diff --git a/src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts b/src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts new file mode 100644 index 00000000..775847e0 --- /dev/null +++ b/src/util/migration/mariadb/1721298824927-webhookMessageProperties.ts @@ -0,0 +1,23 @@ +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/mariadb/1723347738541-client_status.ts b/src/util/migration/mariadb/1723347738541-client_status.ts new file mode 100644 index 00000000..0e02c45e --- /dev/null +++ b/src/util/migration/mariadb/1723347738541-client_status.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class client_status1723347738541 implements MigrationInterface { + name = "client_status1723347738541"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE `sessions` ADD `client_status` text NULL AFTER `client_info`", + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE `sessions` DROP COLUMN `client_status`", + ); + } +} diff --git a/src/util/migration/mariadb/1723577874393-discoveryCategoryIcon.ts b/src/util/migration/mariadb/1723577874393-discoveryCategoryIcon.ts new file mode 100644 index 00000000..18bc0a77 --- /dev/null +++ b/src/util/migration/mariadb/1723577874393-discoveryCategoryIcon.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class DiscoveryCategoryIcon1723577874393 implements MigrationInterface { + name = "DiscoveryCategoryIcon1723577874393"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE `categories` ADD `icon` text NULL", + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query("ALTER TABLE `categories` DROP COLUMN `icon`"); + } +} diff --git a/src/util/migration/mariadb/1723644478176-webhookSourceChannel.ts b/src/util/migration/mariadb/1723644478176-webhookSourceChannel.ts new file mode 100644 index 00000000..ac2b1799 --- /dev/null +++ b/src/util/migration/mariadb/1723644478176-webhookSourceChannel.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class WebhookSourceChannel1723644478176 implements MigrationInterface { + name = "WebhookSourceChannel1723644478176"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE `webhooks` ADD COLUMN `source_channel_id` VARCHAR(255) NULL DEFAULT NULL AFTER `source_guild_id`", + ); + await queryRunner.query( + "ALTER TABLE `webhooks` ADD CONSTRAINT `FK_d64f38834fa676f6caa4786ddd6` FOREIGN KEY (`source_channel_id`) REFERENCES `channels` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE", + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE `webhooks` DROP FOREIGN KEY `FK_d64f38834fa676f6caa4786ddd6`", + ); + await queryRunner.query( + "ALTER TABLE `webhooks` DROP COLUMN `source_channel_id`", + ); + } +} diff --git a/src/util/migration/mysql/1721298824927-webhookMessageProperties.ts b/src/util/migration/mysql/1721298824927-webhookMessageProperties.ts new file mode 100644 index 00000000..775847e0 --- /dev/null +++ b/src/util/migration/mysql/1721298824927-webhookMessageProperties.ts @@ -0,0 +1,23 @@ +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/1723347738541-client_status.ts b/src/util/migration/mysql/1723347738541-client_status.ts new file mode 100644 index 00000000..0e02c45e --- /dev/null +++ b/src/util/migration/mysql/1723347738541-client_status.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class client_status1723347738541 implements MigrationInterface { + name = "client_status1723347738541"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE `sessions` ADD `client_status` text NULL AFTER `client_info`", + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE `sessions` DROP COLUMN `client_status`", + ); + } +} diff --git a/src/util/migration/mysql/1723577874393-discoveryCategoryIcon.ts b/src/util/migration/mysql/1723577874393-discoveryCategoryIcon.ts new file mode 100644 index 00000000..18bc0a77 --- /dev/null +++ b/src/util/migration/mysql/1723577874393-discoveryCategoryIcon.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class DiscoveryCategoryIcon1723577874393 implements MigrationInterface { + name = "DiscoveryCategoryIcon1723577874393"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE `categories` ADD `icon` text NULL", + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query("ALTER TABLE `categories` DROP COLUMN `icon`"); + } +} diff --git a/src/util/migration/mysql/1723644478176-webhookSourceChannel.ts b/src/util/migration/mysql/1723644478176-webhookSourceChannel.ts new file mode 100644 index 00000000..ac2b1799 --- /dev/null +++ b/src/util/migration/mysql/1723644478176-webhookSourceChannel.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class WebhookSourceChannel1723644478176 implements MigrationInterface { + name = "WebhookSourceChannel1723644478176"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE `webhooks` ADD COLUMN `source_channel_id` VARCHAR(255) NULL DEFAULT NULL AFTER `source_guild_id`", + ); + await queryRunner.query( + "ALTER TABLE `webhooks` ADD CONSTRAINT `FK_d64f38834fa676f6caa4786ddd6` FOREIGN KEY (`source_channel_id`) REFERENCES `channels` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE", + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE `webhooks` DROP FOREIGN KEY `FK_d64f38834fa676f6caa4786ddd6`", + ); + await queryRunner.query( + "ALTER TABLE `webhooks` DROP COLUMN `source_channel_id`", + ); + } +} diff --git a/src/util/migration/postgres/1721298824927-webhookMessageProperties.ts b/src/util/migration/postgres/1721298824927-webhookMessageProperties.ts new file mode 100644 index 00000000..bd603f10 --- /dev/null +++ b/src/util/migration/postgres/1721298824927-webhookMessageProperties.ts @@ -0,0 +1,17 @@ +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/1723347738541-client_status.ts b/src/util/migration/postgres/1723347738541-client_status.ts new file mode 100644 index 00000000..35d9391f --- /dev/null +++ b/src/util/migration/postgres/1723347738541-client_status.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class client_status1723347738541 implements MigrationInterface { + name = "client_status1723347738541"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE sessions ADD client_status text NULL", + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE sessions DROP COLUMN client_status", + ); + } +} diff --git a/src/util/migration/postgres/1723577874393-discoveryCategoryIcon.ts b/src/util/migration/postgres/1723577874393-discoveryCategoryIcon.ts new file mode 100644 index 00000000..29b4138a --- /dev/null +++ b/src/util/migration/postgres/1723577874393-discoveryCategoryIcon.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class DiscoveryCategoryIcon1723577874393 implements MigrationInterface { + name = "DiscoveryCategoryIcon1723577874393"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query("ALTER TABLE categories ADD icon text NULL"); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query("ALTER TABLE categories DROP COLUMN icon"); + } +} diff --git a/src/util/migration/postgres/1723644478176-webhookSourceChannel.ts b/src/util/migration/postgres/1723644478176-webhookSourceChannel.ts new file mode 100644 index 00000000..63ce961f --- /dev/null +++ b/src/util/migration/postgres/1723644478176-webhookSourceChannel.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class WebhookSourceChannel1723644478176 implements MigrationInterface { + name = "WebhookSourceChannel1723644478176"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE webhooks ADD COLUMN source_channel_id VARCHAR(255) NULL DEFAULT NULL", + ); + await queryRunner.query( + "ALTER TABLE webhooks ADD CONSTRAINT FK_d64f38834fa676f6caa4786ddd6 FOREIGN KEY (source_channel_id) REFERENCES channels (id) ON UPDATE NO ACTION ON DELETE CASCADE", + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + "ALTER TABLE webhooks DROP CONSTRAINT FK_d64f38834fa676f6caa4786ddd6", + ); + await queryRunner.query( + "ALTER TABLE webhooks DROP COLUMN source_channel_id", + ); + } +} diff --git a/src/util/schemas/MessageCreateSchema.ts b/src/util/schemas/MessageCreateSchema.ts index 014f6c87..15537ca8 100644 --- a/src/util/schemas/MessageCreateSchema.ts +++ b/src/util/schemas/MessageCreateSchema.ts @@ -16,9 +16,14 @@ along with this program. If not, see . */ -import { Embed, MessageComponent, PollAnswer, PollMedia } from "@spacebar/util"; +import { + ActionRowComponent, + Embed, + PollAnswer, + PollMedia, +} from "@spacebar/util"; -type Attachment = { +export type MessageCreateAttachment = { id: string; filename: string; }; @@ -52,9 +57,9 @@ 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?: MessageComponent[]; + components?: ActionRowComponent[]; // TODO: Fix TypeScript errors in src\api\util\handlers\Message.ts once this is enabled poll?: PollCreationSchema; enforce_nonce?: boolean; // For Discord compatibility, it's the default behavior here diff --git a/src/util/schemas/RequestGuildMembersSchema.ts b/src/util/schemas/RequestGuildMembersSchema.ts new file mode 100644 index 00000000..01ba4f2e --- /dev/null +++ b/src/util/schemas/RequestGuildMembersSchema.ts @@ -0,0 +1,35 @@ +/* + 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 . +*/ + +export interface RequestGuildMembersSchema { + guild_id: string; + query?: string; + limit?: number; + presences?: boolean; + user_ids?: string | string[]; + nonce?: string; +} + +export const RequestGuildMembersSchema = { + guild_id: String, + $query: String, + $limit: Number, + $presences: Boolean, + $user_ids: [] as string | string[], + $nonce: String, +}; diff --git a/src/util/schemas/UserModifySchema.ts b/src/util/schemas/UserModifySchema.ts index e155b9af..4be6ad43 100644 --- a/src/util/schemas/UserModifySchema.ts +++ b/src/util/schemas/UserModifySchema.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 . */ @@ -23,9 +23,6 @@ export interface UserModifySchema { */ username?: string; avatar?: string | null; - /** - * @maxLength 1024 - */ bio?: string; accent_color?: number; banner?: string | null; 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..62199dfb 100644 --- a/src/util/schemas/index.ts +++ b/src/util/schemas/index.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 . */ @@ -58,6 +58,7 @@ export * from "./PurgeSchema"; export * from "./RegisterSchema"; export * from "./RelationshipPostSchema"; export * from "./RelationshipPutSchema"; +export * from "./RequestGuildMembersSchema"; export * from "./RoleModifySchema"; export * from "./RolePositionUpdateSchema"; export * from "./SelectProtocolSchema"; @@ -79,5 +80,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/schemas/responses/GuildMessagesSearchResponse.ts b/src/util/schemas/responses/GuildMessagesSearchResponse.ts index 6121983e..ec41965b 100644 --- a/src/util/schemas/responses/GuildMessagesSearchResponse.ts +++ b/src/util/schemas/responses/GuildMessagesSearchResponse.ts @@ -17,9 +17,9 @@ */ import { + ActionRowComponent, Attachment, Embed, - MessageComponent, MessageType, Poll, PublicUser, @@ -42,7 +42,7 @@ export interface GuildMessagesSearchMessage { timestamp: string; edited_timestamp: string | null; flags: number; - components: MessageComponent[]; + components: ActionRowComponent[]; poll: Poll; hit: true; } diff --git a/src/util/util/Constants.ts b/src/util/util/Constants.ts index 98ae2d31..a6caae00 100644 --- a/src/util/util/Constants.ts +++ b/src/util/util/Constants.ts @@ -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),