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),