/* 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 { Request, Response, Router } from "express"; import { Not } from "typeorm"; import { route } from "@spacebar/api/middlewares"; import { Channel, Emoji, Guild, InstanceBan, Member, Recipient, Sticker, User, UserSettingsProtos } from "@spacebar/database"; import { ChannelDeleteEvent, ChannelRecipientRemoveEvent, emitEvent, UserDeleteEvent } from "@spacebar/util"; import { ChannelType, InstanceUserDeleteSchema, PrivateUserProjection } from "@spacebar/schemas"; import { Stopwatch } from "@spacebar/extensions"; const router = Router({ mergeParams: true }); router.post( "/", route({ right: "MANAGE_USERS", requestBody: "InstanceUserDeleteSchema", responses: { 204: {}, 403: { body: "APIErrorResponse", }, 404: { body: "APIErrorResponse", }, }, }), async (req: Request, res: Response) => { const sw = Stopwatch.startNew(); const body = req.body as InstanceUserDeleteSchema | undefined; const user = await User.findOneOrFail({ where: { id: req.params.user_id as string }, select: Object.fromEntries([...PrivateUserProjection, "data"].map((i) => [i, true])), // TODO: clean up }); if ((body?.persistInstanceBan ?? true) && !(await InstanceBan.findOne({ where: { user_id: user.id } }))) await InstanceBan.create({ user_id: user.id, reason: body?.reason ?? "" }).save(); // prevent bugginess with clients - delete all DMs, only having half of the conversation is quite useless anyhow const dmChannels = await user.getDmChannels(); for (const channel of dmChannels) { console.log(`[Instance ban] Deleting DM channel ${channel.id} for user ${user.id}`); await emitEvent({ event: "CHANNEL_DELETE", data: channel.toJSON(), channel_id: channel.id, } satisfies ChannelDeleteEvent); await Recipient.delete({ channel_id: channel.id }); await Channel.deleteChannel(channel); } //leave all group channels const groupChannels = await Channel.find({ where: { type: ChannelType.GROUP_DM }, relations: { recipients: true }, select: { id: true, owner_id: true, recipients: { id: true, user_id: true, }, }, }); await Promise.all( groupChannels.map(async (channel) => { const recipient = channel.recipients!.find((r) => r.user_id === user.id); if (recipient) { await Recipient.delete({ id: recipient.id }); await emitEvent({ event: "CHANNEL_RECIPIENT_REMOVE", data: { user: user.toPublicUser(), channel_id: channel.id, }, channel_id: channel.id, } satisfies ChannelRecipientRemoveEvent); console.log(`[Instance ban] Removed user ${user.id} from group channel ${channel.id}`); } // if no recipients remain, delete the channel const remainingRecipients = await Recipient.find({ where: { channel_id: channel.id } }); if (remainingRecipients.length === 0) { await emitEvent({ event: "CHANNEL_DELETE", data: channel.toJSON(), channel_id: channel.id, } satisfies ChannelDeleteEvent); await Channel.deleteChannel(channel); console.log(`[Instance ban] Deleted empty group channel ${channel.id}`); } else { // otherwise, if the banned user was the owner, reassign ownership if (channel.owner_id === user.id) { channel.owner_id = remainingRecipients[0].user_id; await channel.save(); console.log(`[Instance ban] Reassigned ownership of group channel ${channel.id} to user ${channel.owner_id}`); } } }), ); // change ownership on guilds const guilds = await Guild.find({ where: { owner_id: req.params.user_id as string } }); await Promise.all( guilds.map(async (guild) => { const members = await Member.find({ where: { guild_id: guild.id, id: Not(req.params.user_id as string) }, relations: { roles: true }, select: { id: true, roles: { id: true, position: true } }, }); const sortedMembers = members .filter((m) => m.id !== req.params.user_id) .sort((a, b) => { const aHighestRole = a.roles.reduce((prev, curr) => (curr.position > prev.position ? curr : prev), { position: -1 } as { position: number }); const bHighestRole = b.roles.reduce((prev, curr) => (curr.position > prev.position ? curr : prev), { position: -1 } as { position: number }); return bHighestRole.position - aHighestRole.position; }); if (sortedMembers.length === 0) { // no members left, delete guild await guild.remove(); console.log(`[Instance ban] Deleted guild ${guild.id} as user ${user.id} was the last member`); } else { // assign new owner guild.owner_id = sortedMembers[0].id; await guild.save(); console.log(`[Instance ban] Transferred ownership of guild ${guild.id} to user ${guild.owner_id}`); // safety - reassign emojis/stickers owned by the old owner const stickers = await Sticker.find({ where: { guild_id: guild.id, user_id: req.params.user_id as string } }); await Promise.all( stickers.map(async (sticker) => { sticker.user_id = guild.owner_id; await sticker.save(); console.log(`[Instance ban] Reassigned sticker ${sticker.id} ownership to user ${guild.owner_id}`); }), ); const emojis = await Emoji.find({ where: { guild_id: guild.id, user_id: req.params.user_id as string } }); await Promise.all( emojis.map(async (emoji) => { emoji.user_id = guild.owner_id!; await emoji.save(); console.log(`[Instance ban] Reassigned emoji ${emoji.id} ownership to user ${guild.owner_id}`); }), ); } }), ); const members = await Member.find({ where: { id: req.params.user_id as string } }); await Promise.all([...members.map((member) => Member.removeFromGuild(member.id, member.guild_id))]); await UserSettingsProtos.delete({ user_id: req.params.user_id as string }); await User.delete({ id: req.params.user_id as string }); // TODO: respect intents as USER_DELETE has potential to cause privacy issues await emitEvent({ event: "USER_DELETE", user_id: req.user_id, data: { user_id: req.params.user_id as string }, } satisfies UserDeleteEvent); console.log(`[Instance ban] Deleted user ${user.id} from instance in ${sw.elapsed().toString()}`); res.sendStatus(204); }, ); export default router;