diff --git a/assets/email_templates/new_login_location.html b/assets/email_templates/new_login_location.html
index 8c188def..f1c5f8c5 100644
--- a/assets/email_templates/new_login_location.html
+++ b/assets/email_templates/new_login_location.html
@@ -73,7 +73,6 @@
.
*/
@@ -19,7 +19,6 @@
import { getIpAdress, route } from "@spacebar/api";
import {
Ban,
- BanModeratorSchema,
BanRegistrySchema,
DiscordApiErrors,
GuildBanAddEvent,
@@ -82,7 +81,7 @@ router.get(
);
router.get(
- "/:user",
+ "/:user_id",
route({
permission: "BAN_MEMBERS",
responses: {
@@ -98,23 +97,21 @@ router.get(
},
}),
async (req: Request, res: Response) => {
- const { guild_id } = req.params;
- const user_id = req.params.ban;
+ const { guild_id, user_id } = req.params;
- let ban = (await Ban.findOneOrFail({
+ const ban = (await Ban.findOneOrFail({
where: { guild_id: guild_id, user_id: user_id },
})) as BanRegistrySchema;
if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN;
// pretend self-bans don't exist to prevent victim chasing
- /* Filter secret from registry. */
+ const banInfo = {
+ user: await User.getPublicUser(ban.user_id),
+ reason: ban.reason,
+ };
- ban = ban as BanModeratorSchema;
-
- delete ban.ip;
-
- return res.json(ban);
+ return res.json(banInfo);
},
);
@@ -151,6 +148,12 @@ router.put(
if (req.permission?.cache.guild?.owner_id === banned_user_id)
throw new HTTPError("You can't ban the owner", 400);
+ const existingBan = await Ban.findOne({
+ where: { guild_id: guild_id, user_id: banned_user_id },
+ });
+ // Bans on already banned users are silently ignored
+ if (existingBan) return res.status(204).send();
+
const banned_user = await User.getPublicUser(banned_user_id);
const ban = Ban.create({
@@ -174,7 +177,7 @@ router.put(
} as GuildBanAddEvent),
]);
- return res.json(ban);
+ return res.status(204).send();
},
);
diff --git a/src/api/routes/guilds/#guild_id/bulk-ban.ts b/src/api/routes/guilds/#guild_id/bulk-ban.ts
new file mode 100644
index 00000000..f544103a
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/bulk-ban.ts
@@ -0,0 +1,130 @@
+/*
+ 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 { getIpAdress, route } from "@spacebar/api";
+import {
+ Ban,
+ DiscordApiErrors,
+ GuildBanAddEvent,
+ Member,
+ User,
+ emitEvent,
+} from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+
+const router: Router = Router();
+
+router.post(
+ "/",
+ route({
+ requestBody: "BulkBanSchema",
+ permission: ["BAN_MEMBERS", "MANAGE_GUILD"],
+ responses: {
+ 200: {
+ body: "Ban",
+ },
+ 400: {
+ body: "APIErrorResponse",
+ },
+ 403: {
+ body: "APIErrorResponse",
+ },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const { guild_id } = req.params;
+
+ const userIds: Array = req.body.user_ids;
+ if (!userIds) throw new HTTPError("The user_ids array is missing", 400);
+ if (userIds.length > 200)
+ throw new HTTPError(
+ "The user_ids array must be between 1 and 200 in length",
+ 400,
+ );
+
+ const banned_users = [];
+ const failed_users = [];
+ for await (const banned_user_id of userIds) {
+ if (
+ req.user_id === banned_user_id &&
+ banned_user_id === req.permission?.cache.guild?.owner_id
+ ) {
+ failed_users.push(banned_user_id);
+ continue;
+ }
+
+ if (req.permission?.cache.guild?.owner_id === banned_user_id) {
+ failed_users.push(banned_user_id);
+ continue;
+ }
+
+ const existingBan = await Ban.findOne({
+ where: { guild_id: guild_id, user_id: banned_user_id },
+ });
+ if (existingBan) {
+ failed_users.push(banned_user_id);
+ continue;
+ }
+
+ let banned_user;
+ try {
+ banned_user = await User.getPublicUser(banned_user_id);
+ } catch {
+ failed_users.push(banned_user_id);
+ continue;
+ }
+
+ const ban = Ban.create({
+ user_id: banned_user_id,
+ guild_id: guild_id,
+ ip: getIpAdress(req),
+ executor_id: req.user_id,
+ reason: req.body.reason, // || otherwise empty
+ });
+
+ try {
+ await Promise.all([
+ Member.removeFromGuild(banned_user_id, guild_id),
+ ban.save(),
+ emitEvent({
+ event: "GUILD_BAN_ADD",
+ data: {
+ guild_id: guild_id,
+ user: banned_user,
+ },
+ guild_id: guild_id,
+ } as GuildBanAddEvent),
+ ]);
+ banned_users.push(banned_user_id);
+ } catch {
+ failed_users.push(banned_user_id);
+ continue;
+ }
+ }
+
+ if (banned_users.length === 0 && failed_users.length > 0)
+ throw DiscordApiErrors.BULK_BAN_FAILED;
+ return res.json({
+ banned_users: banned_users,
+ failed_users: failed_users,
+ });
+ },
+);
+
+export default router;
diff --git a/src/api/util/handlers/route.ts b/src/api/util/handlers/route.ts
index 5a0b48e6..2c98783a 100644
--- a/src/api/util/handlers/route.ts
+++ b/src/api/util/handlers/route.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 .
*/
@@ -90,19 +90,23 @@ export function route(opts: RouteOptions) {
return async (req: Request, res: Response, next: NextFunction) => {
if (opts.permission) {
- const required = new Permissions(opts.permission);
req.permission = await getPermission(
req.user_id,
req.params.guild_id,
req.params.channel_id,
);
- // bitfield comparison: check if user lacks certain permission
- if (!req.permission.has(required)) {
- throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(
- opts.permission as string,
- );
- }
+ const requiredPerms = Array.isArray(opts.permission)
+ ? opts.permission
+ : [opts.permission];
+ requiredPerms.forEach((perm) => {
+ // bitfield comparison: check if user lacks certain permission
+ if (!req.permission!.has(new Permissions(perm))) {
+ throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(
+ perm as string,
+ );
+ }
+ });
}
if (opts.right) {
diff --git a/src/util/schemas/BulkBanSchema.ts b/src/util/schemas/BulkBanSchema.ts
new file mode 100644
index 00000000..48a7bac8
--- /dev/null
+++ b/src/util/schemas/BulkBanSchema.ts
@@ -0,0 +1,22 @@
+/*
+ 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 BulkBanSchema {
+ user_ids: string[];
+ delete_message_seconds?: number;
+}
diff --git a/src/util/util/Constants.ts b/src/util/util/Constants.ts
index e68bb0b7..98ae2d31 100644
--- a/src/util/util/Constants.ts
+++ b/src/util/util/Constants.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 .
*/
@@ -553,6 +553,8 @@ export const VerificationLevels = [
* * LOTTIE_ANIMATION_MAXIMUM_DIMENSIONS
* * STICKER_FRAME_RATE_TOO_SMALL_OR_TOO_LARGE
* * STICKER_ANIMATION_DURATION_MAXIMUM
+ * * AUTOMODERATOR_BLOCK
+ * * BULK_BAN_FAILED
* * UNKNOWN_VOICE_STATE
* @typedef {string} APIError
*/
@@ -1001,6 +1003,7 @@ export const DiscordApiErrors = {
"Message was blocked by automatic moderation",
200000,
),
+ BULK_BAN_FAILED: new ApiError("Failed to ban users", 500000),
//Other errors
UNKNOWN_VOICE_STATE: new ApiError("Unknown Voice State", 10065, 404),