Skip to content

Commit

Permalink
Add scheduled deletion handling for servers
Browse files Browse the repository at this point in the history
  • Loading branch information
SupertigerDev committed Jan 14, 2025
1 parent 5f362b5 commit e9b0743
Show file tree
Hide file tree
Showing 15 changed files with 123 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- CreateTable
CREATE TABLE "ScheduleServerDelete" (
"serverId" TEXT NOT NULL,
"scheduledAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- CreateIndex
CREATE UNIQUE INDEX "ScheduleServerDelete_serverId_key" ON "ScheduleServerDelete"("serverId");

-- AddForeignKey
ALTER TABLE "ScheduleServerDelete" ADD CONSTRAINT "ScheduleServerDelete_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
5 changes: 5 additions & 0 deletions prisma/migrations/20250114114134_update/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "ScheduleServerDelete" DROP CONSTRAINT "ScheduleServerDelete_serverId_fkey";

-- AddForeignKey
ALTER TABLE "ScheduleServerDelete" ADD CONSTRAINT "ScheduleServerDelete_serverId_fkey" FOREIGN KEY ("serverId") REFERENCES "Server"("id") ON DELETE CASCADE ON UPDATE CASCADE;
11 changes: 11 additions & 0 deletions prisma/migrations/20250114155528_update/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:
- Added the required column `scheduledByUserId` to the `ScheduleServerDelete` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "ScheduleServerDelete" ADD COLUMN "scheduledByUserId" TEXT NOT NULL;

-- AddForeignKey
ALTER TABLE "ScheduleServerDelete" ADD CONSTRAINT "ScheduleServerDelete_scheduledByUserId_fkey" FOREIGN KEY ("scheduledByUserId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
15 changes: 15 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ model User {
pinnedPosts PinnedPost[]
scheduledServerDeletions ScheduleServerDelete[]
}

model UserConnection {
Expand Down Expand Up @@ -304,6 +307,9 @@ model Server {
channelPermissions ServerChannelPermissions[]
scheduledForDeletion ScheduleServerDelete?
}

// Personal settings for joined servers
Expand Down Expand Up @@ -667,6 +673,15 @@ model ScheduleAccountContentDelete {
scheduledAt DateTime @default(now())
}

// Only used when Nerimity mod deletes a server.
model ScheduleServerDelete {
serverId String @unique
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
scheduledByUserId String
scheduledBy User @relation(fields: [scheduledByUserId], references: [id])
scheduledAt DateTime @default(now())
}

model MessageReaction {
id String @id
name String
Expand Down
13 changes: 6 additions & 7 deletions src/cache/ServerCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ export interface ServerCache {
hexColor: string;
defaultRoleId: string;
public: boolean;
scheduledForDeletion?: { scheduledAt: Date } | null;
}

export const getServerCache = async (serverId: string) => {
const key = SERVER_KEY_STRING(serverId);
const serverString = await redisClient.get(key);
if (serverString) return JSON.parse(serverString);
if (serverString) return JSON.parse(serverString) as ServerCache;

const server = await prisma.server.findFirst({
where: { id: serverId },
include: { PublicServer: true },
include: { PublicServer: true, scheduledForDeletion: { select: { scheduledAt: true } } },
});
if (!server) return null;

Expand All @@ -31,21 +32,19 @@ export const getServerCache = async (serverId: string) => {
hexColor: server.hexColor,
defaultRoleId: server.defaultRoleId,
public: server.PublicServer ? true : false,
scheduledForDeletion: server.scheduledForDeletion,
};
const serverCacheString = JSON.stringify(serverCache);
await redisClient.set(key, serverCacheString);
return JSON.parse(serverCacheString);
return JSON.parse(serverCacheString) as ServerCache;
};

export const deleteServerCache = async (serverId: string) => {
const key = SERVER_KEY_STRING(serverId);
await redisClient.del(key);
};

export const updateServerCache = async (
serverId: string,
update: Partial<ServerCache>
) => {
export const updateServerCache = async (serverId: string, update: Partial<ServerCache>) => {
const key = SERVER_KEY_STRING(serverId);
const cache = await getServerCache(serverId);
if (!cache) return;
Expand Down
31 changes: 31 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { deleteAccount, deleteAllApplications, deleteOrLeaveAllServers } from '.
import { createHash } from 'node:crypto';
import { addToObjectIfExists } from './common/addToObjectIfExists';
import { createQueueProcessor } from '@nerimity/mimiqueue';
import { deleteServer } from './services/Server';

(Date.prototype.toJSON as unknown as (this: Date) => number) = function () {
return this.getTime();
Expand Down Expand Up @@ -47,6 +48,7 @@ if (cluster.isPrimary) {
removeIPAddressSchedule();
schedulePostViews();
scheduleSuspendedAccountDeletion();
scheduleServerDeletion();
removeExpiredBannedIpsSchedule();
});

Expand Down Expand Up @@ -329,3 +331,32 @@ function scheduleSuspendedAccountDeletion() {
scheduleSuspendedAccountDeletion();
}, oneMinuteToMilliseconds);
}
function scheduleServerDeletion() {
const oneMinuteToMilliseconds = 1 * 60 * 1000;
const fiveDaysInThePast = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000);
setTimeout(async () => {
const scheduleItem = await prisma.scheduleServerDelete.findFirst({
where: {
scheduledAt: {
lte: fiveDaysInThePast,
},
},
select: {
server: {
select: { name: true },
},
serverId: true,
scheduledByUserId: true,
},
});
if (scheduleItem) {
try {
await deleteServer(scheduleItem.serverId, scheduleItem.scheduledByUserId);
} catch (err) {
console.error(err);
}
}

scheduleServerDeletion();
}, oneMinuteToMilliseconds);
}
7 changes: 4 additions & 3 deletions src/middleware/channelVerification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import { ChannelType } from '../types/Channel';

interface Options {
allowBot?: boolean;
ignoreScheduledDeletion?: boolean;
}

interface Options {}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function channelVerification(opts?: Options) {
return async (req: Request, res: Response, next: NextFunction) => {
const { channelId } = req.params;
Expand All @@ -28,6 +26,9 @@ export function channelVerification(opts?: Options) {
}

const isServerChannel = channel.type === ChannelType.CATEGORY || channel.type === ChannelType.SERVER_TEXT;
if (!opts?.ignoreScheduledDeletion && isServerChannel && channel.server.scheduledForDeletion) {
return res.status(403).json(generateError('This server is scheduled for deletion.'));
}

if (isServerChannel) {
const [memberCache, error] = await getServerMemberCache(channel.server.id, req.userCache.id);
Expand Down
14 changes: 6 additions & 8 deletions src/middleware/serverMemberVerification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,14 @@ import { generateError } from '../common/errorHandler';

interface Options {
allowBot?: boolean;
ignoreScheduledDeletion?: boolean;
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Options {}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function serverMemberVerification(opts?: Options) {
return async (req: Request, res: Response, next: NextFunction) => {
const { serverId } = req.params;

const [memberCache, error] = await getServerMemberCache(
serverId,
req.userCache.id
);
const [memberCache, error] = await getServerMemberCache(serverId, req.userCache.id);
if (error !== null) {
return res.status(403).json(generateError(error));
}
Expand All @@ -28,6 +22,10 @@ export function serverMemberVerification(opts?: Options) {
return res.status(403).json(generateError('Server does not exist.'));
}

if (!opts?.ignoreScheduledDeletion && server.scheduledForDeletion) {
return res.status(403).json(generateError('This server is scheduled for deletion.'));
}

req.serverMemberCache = memberCache;
req.serverCache = server;

Expand Down
13 changes: 3 additions & 10 deletions src/routes/moderation/getServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import { isModMiddleware } from './isModMiddleware';
import { isExpired } from '../../services/User/User';

export function getServer(Router: Router) {
Router.get(
'/moderation/servers/:serverId',
authenticate(),
isModMiddleware,
route
);
Router.get('/moderation/servers/:serverId', authenticate(), isModMiddleware, route);
}

async function route(req: Request, res: Response) {
Expand All @@ -25,6 +20,7 @@ async function route(req: Request, res: Response) {
verified: true,
hexColor: true,
id: true,
scheduledForDeletion: true,
createdAt: true,
createdBy: {
select: {
Expand All @@ -42,10 +38,7 @@ async function route(req: Request, res: Response) {
},
});

if (
server?.createdBy.suspension?.expireAt &&
isExpired(server.createdBy.suspension.expireAt)
) {
if (server?.createdBy.suspension?.expireAt && isExpired(server.createdBy.suspension.expireAt)) {
server.createdBy.suspension = null;
}

Expand Down
1 change: 1 addition & 0 deletions src/routes/moderation/getServers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ async function route(req: Request, res: Response) {
take: limit,
...(after ? { cursor: { id: after } } : undefined),
select: {
scheduledForDeletion: true,
name: true,
hexColor: true,
id: true,
Expand Down
1 change: 1 addition & 0 deletions src/routes/moderation/getUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async function route(req: Request, res: Response) {
devices: { orderBy: { createdAt: 'desc' } },
servers: {
select: {
scheduledForDeletion: true,
verified: true,
name: true,
hexColor: true,
Expand Down
1 change: 1 addition & 0 deletions src/routes/moderation/searchServers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ async function route(req: Request, res: Response) {
take: limit,
...(after ? { cursor: { id: after } } : undefined),
select: {
scheduledForDeletion: true,
name: true,
hexColor: true,
id: true,
Expand Down
29 changes: 22 additions & 7 deletions src/routes/moderation/serverDelete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { customExpressValidatorResult, generateError } from '../../common/errorH

import { authenticate } from '../../middleware/authenticate';
import { isModMiddleware } from './isModMiddleware';
import { deleteServer } from '../../services/Server';
// import { deleteServer } from '../../services/Server';
import { generateId } from '../../common/flakeId';
import { ModAuditLogType } from '../../common/ModAuditLog';
import { checkUserPassword } from '../../services/UserAuthentication';
Expand All @@ -31,7 +31,7 @@ async function route(req: Request<Params, unknown, Body>, res: Response) {
}

const account = await prisma.account.findFirst({
where: { id: req.userCache.account.id },
where: { id: req.userCache.account!.id },
select: { password: true },
});
if (!account) return res.status(404).json(generateError('Something went wrong. Try again later.'));
Expand All @@ -45,14 +45,29 @@ async function route(req: Request<Params, unknown, Body>, res: Response) {

if (!server) return res.status(404).json(generateError('Server does not exist.'));

const [, error] = await deleteServer(req.params.serverId, req.userCache.id);
if (error) {
return res.status(403).json(error);
// const [, error] = await deleteServer(req.params.serverId, req.userCache.id);
// if (error) {
// return res.status(403).json(error);
// }

const scheduledDeletion = await prisma.scheduleServerDelete
.create({
data: {
serverId: server.id,
scheduledByUserId: req.userCache.id,
},
})
.catch((e) => {
console.error(e);
return null;
});
if (!scheduledDeletion) {
return res.status(500).json(generateError('Failed to schedule server deletion.'));
}

await warnUsersBatch({
userIds: [server.createdById],
reason: `${server.name} deleted: ` + req.body.reason,
reason: `${server.name} scheduled deletion: ${req.body.reason}`,
modUserId: req.userCache.id,
skipAuditLog: true,
});
Expand All @@ -68,5 +83,5 @@ async function route(req: Request<Params, unknown, Body>, res: Response) {
},
});

res.status(200).json({ success: true });
res.status(200).json({ success: true, scheduledDeletion });
}
2 changes: 1 addition & 1 deletion src/routes/servers/serverDelete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { deleteServer } from '../../services/Server';
import { Log } from '../../common/Log';

export function serverDelete(Router: Router) {
Router.delete('/servers/:serverId', authenticate(), serverMemberVerification(), route);
Router.delete('/servers/:serverId', authenticate(), serverMemberVerification({ ignoreScheduledDeletion: true }), route);
}

async function route(req: Request, res: Response) {
Expand Down
5 changes: 5 additions & 0 deletions src/services/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export const joinServer = async (
const server = await prisma.server.findFirst({
where: { id: serverId },
include: {
scheduledForDeletion: true,
_count: { select: { welcomeQuestions: true } },
customEmojis: {
select: { gif: true, id: true, name: true },
Expand All @@ -231,6 +232,10 @@ export const joinServer = async (
return [null, generateError('Server does not exist.')] as const;
}

if (server.scheduledForDeletion) {
return [null, generateError('Server is scheduled for deletion.')] as const;
}

// check if user is already in server
const isInServer = await exists(prisma.serverMember, {
where: { serverId, userId },
Expand Down

0 comments on commit e9b0743

Please sign in to comment.