forked from administration/panel
487 lines
10 KiB
TypeScript
487 lines
10 KiB
TypeScript
"use server";
|
|
|
|
import { writeFile } from "fs/promises";
|
|
import { PLATFORM_MOD_ID } from "./constants";
|
|
import mongo, {
|
|
Account,
|
|
createDM,
|
|
fetchChannels,
|
|
fetchMembershipsByUser,
|
|
fetchMessages,
|
|
fetchUserById,
|
|
findDM,
|
|
} from "./db";
|
|
import { publishMessage, sendChatMessage } from "./redis";
|
|
import { ulid } from "ulid";
|
|
import {
|
|
AccountInfo,
|
|
AccountStrike,
|
|
Bot,
|
|
File,
|
|
Member,
|
|
Message,
|
|
Report,
|
|
Server,
|
|
SessionInfo,
|
|
User,
|
|
} from "revolt-api";
|
|
import { checkPermission } from "./accessPermissions";
|
|
|
|
export async function sendAlert(userId: string, content: string) {
|
|
await checkPermission("users/create/alert");
|
|
|
|
const messageId = ulid();
|
|
|
|
let dm = await findDM(PLATFORM_MOD_ID, userId);
|
|
if (!dm) dm = await createDM(PLATFORM_MOD_ID, userId, messageId);
|
|
|
|
await sendChatMessage({
|
|
_id: messageId,
|
|
author: PLATFORM_MOD_ID,
|
|
channel: dm._id,
|
|
content,
|
|
});
|
|
}
|
|
|
|
export async function createStrike(
|
|
userId: string,
|
|
givenReason: string,
|
|
additionalContext: string
|
|
) {
|
|
await checkPermission("users/create/strike");
|
|
|
|
const strike: AccountStrike & { moderator_id: string } = {
|
|
_id: ulid(),
|
|
user_id: userId,
|
|
moderator_id: "01EX2NCWQ0CHS3QJF0FEQS1GR4", // TODO
|
|
reason: additionalContext
|
|
? givenReason + " - " + additionalContext
|
|
: givenReason,
|
|
};
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<{ _id: string }>("safety_strikes")
|
|
.insertOne(strike);
|
|
await sendAlert(
|
|
userId,
|
|
`You have received an account strike, for one or more reasons:
|
|
- ${givenReason}
|
|
|
|
Further violations will result in suspension or a permanent ban depending on severity, please abide by the [Acceptable Usage Policy](https://revolt.chat/aup).`
|
|
);
|
|
|
|
return strike;
|
|
}
|
|
|
|
export async function updateReportNotes(reportId: string, notes: string) {
|
|
await checkPermission("reports/update/notes");
|
|
|
|
return await mongo()
|
|
.db("revolt")
|
|
.collection<Report>("safety_reports")
|
|
.updateOne(
|
|
{ _id: reportId },
|
|
{
|
|
$set: {
|
|
notes,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function resolveReport(reportId: string) {
|
|
await checkPermission("reports/update/resolve");
|
|
|
|
const $set = {
|
|
status: "Resolved",
|
|
closed_at: new Date().toISOString(),
|
|
} as Report;
|
|
|
|
await mongo().db("revolt").collection<Report>("safety_reports").updateOne(
|
|
{ _id: reportId },
|
|
{
|
|
$set,
|
|
}
|
|
);
|
|
|
|
return $set;
|
|
}
|
|
|
|
export async function rejectReport(reportId: string, reason: string) {
|
|
await checkPermission("reports/update/reject");
|
|
|
|
const $set = {
|
|
status: "Rejected",
|
|
rejection_reason: reason,
|
|
closed_at: new Date().toISOString(),
|
|
} as Report;
|
|
|
|
await mongo().db("revolt").collection<Report>("safety_reports").updateOne(
|
|
{ _id: reportId },
|
|
{
|
|
$set,
|
|
}
|
|
);
|
|
|
|
return $set;
|
|
}
|
|
|
|
export async function reopenReport(reportId: string) {
|
|
await checkPermission("reports/update/reopen");
|
|
|
|
const $set = {
|
|
status: "Created",
|
|
} as Report;
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Report>("safety_reports")
|
|
.updateOne(
|
|
{ _id: reportId },
|
|
{
|
|
$set,
|
|
$unset: {
|
|
closed_at: 1,
|
|
} as never,
|
|
}
|
|
);
|
|
|
|
return $set;
|
|
}
|
|
|
|
export async function closeReportsByUser(userId: string) {
|
|
if (Math.random() > -1) throw "TODO: ACL";
|
|
|
|
return await mongo()
|
|
.db("revolt")
|
|
.collection<Report>("safety_reports")
|
|
.updateMany(
|
|
{
|
|
status: "Created",
|
|
author_id: userId,
|
|
},
|
|
{
|
|
$set: {
|
|
status: "Rejected",
|
|
rejection_reason: "bulk close",
|
|
closed_at: new Date().toISOString(),
|
|
},
|
|
}
|
|
)
|
|
.then((res) => res.modifiedCount);
|
|
}
|
|
|
|
export async function disableAccount(userId: string) {
|
|
await checkPermission("accounts/disable");
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<AccountInfo>("accounts")
|
|
.updateOne({ _id: userId }, { $set: { disabled: true } });
|
|
|
|
await mongo().db("revolt").collection<SessionInfo>("sessions").deleteMany({
|
|
user_id: userId,
|
|
});
|
|
}
|
|
|
|
export async function suspendUser(userId: string) {
|
|
await checkPermission("users/action/suspend");
|
|
await disableAccount(userId);
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<User>("users")
|
|
.updateOne(
|
|
{
|
|
_id: userId,
|
|
},
|
|
{
|
|
$set: {
|
|
flags: 1,
|
|
},
|
|
}
|
|
);
|
|
|
|
const memberships = await fetchMembershipsByUser(userId);
|
|
for (const topic of memberships.map((x) => x._id.server)) {
|
|
await publishMessage(topic, {
|
|
type: "UserUpdate",
|
|
id: userId,
|
|
data: {
|
|
flags: 1,
|
|
},
|
|
clear: [],
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function updateUserBadges(userId: string, badges: number) {
|
|
await checkPermission("users/update/badges");
|
|
await mongo().db("revolt").collection<User>("users").updateOne(
|
|
{
|
|
_id: userId,
|
|
},
|
|
{
|
|
$set: {
|
|
badges,
|
|
},
|
|
}
|
|
);
|
|
|
|
const memberships = await fetchMembershipsByUser(userId);
|
|
for (const topic of [userId, ...memberships.map((x) => x._id.server)]) {
|
|
await publishMessage(topic, {
|
|
type: "UserUpdate",
|
|
id: userId,
|
|
data: {
|
|
badges,
|
|
},
|
|
clear: [],
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function wipeUser(userId: string, flags = 4) {
|
|
await checkPermission("users/action/wipe");
|
|
|
|
// retrieve messages, dm channels, relationships, server memberships
|
|
const backup = {
|
|
_event: "wipe",
|
|
user: await fetchUserById(userId),
|
|
messages: await fetchMessages({ author: userId }, undefined),
|
|
dms: await fetchChannels({
|
|
channel_type: "DirectMessage",
|
|
recipients: userId,
|
|
}),
|
|
memberships: await fetchMembershipsByUser(userId),
|
|
};
|
|
|
|
await writeFile(
|
|
`./exports/${new Date().toISOString()} - ${userId}.json`,
|
|
JSON.stringify(backup)
|
|
);
|
|
|
|
// mark all attachments as deleted + reported
|
|
const attachmentIds = backup.messages
|
|
.filter((message) => message.attachments)
|
|
.map((message) => message.attachments)
|
|
.flat()
|
|
.filter((attachment) => attachment)
|
|
.map((attachment) => attachment!._id);
|
|
|
|
if (backup.user?.avatar) {
|
|
attachmentIds.push(backup.user.avatar._id);
|
|
}
|
|
|
|
if (backup.user?.profile?.background) {
|
|
attachmentIds.push(backup.user.profile.background._id);
|
|
}
|
|
|
|
if (attachmentIds.length) {
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<File>("attachments")
|
|
.updateMany(
|
|
{ _id: { $in: attachmentIds } },
|
|
{
|
|
$set: {
|
|
reported: true,
|
|
deleted: true,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
// delete messages
|
|
await mongo().db("revolt").collection<Message>("messages").deleteMany({
|
|
author: userId,
|
|
});
|
|
|
|
// delete server memberships
|
|
await mongo().db("revolt").collection<Member>("server_members").deleteMany({
|
|
"_id.user": userId,
|
|
});
|
|
|
|
// disable account
|
|
await disableAccount(userId);
|
|
|
|
// clear user profile
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<User>("users")
|
|
.updateOne(
|
|
{
|
|
_id: userId,
|
|
},
|
|
{
|
|
$set: {
|
|
flags,
|
|
},
|
|
$unset: {
|
|
avatar: 1,
|
|
profile: 1,
|
|
status: 1,
|
|
},
|
|
}
|
|
);
|
|
|
|
// broadcast wipe event
|
|
for (const topic of [
|
|
...backup.dms.map((x) => x._id),
|
|
...backup.memberships.map((x) => x._id.server),
|
|
]) {
|
|
await publishMessage(topic, {
|
|
type: "UserPlatformWipe",
|
|
user_id: userId,
|
|
flags,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function banUser(userId: string) {
|
|
await checkPermission("users/action/ban");
|
|
return await wipeUser(userId, 4);
|
|
}
|
|
|
|
export async function unsuspendUser(userId: string) {
|
|
await checkPermission("users/action/unsuspend");
|
|
await restoreAccount(userId);
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<User>("users")
|
|
.updateOne(
|
|
{
|
|
_id: userId,
|
|
},
|
|
{
|
|
$unset: {
|
|
flags: 1,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function updateServerFlags(serverId: string, flags: number) {
|
|
await checkPermission("servers/update/flags");
|
|
await mongo().db("revolt").collection<Server>("servers").updateOne(
|
|
{
|
|
_id: serverId,
|
|
},
|
|
{
|
|
$set: {
|
|
flags,
|
|
},
|
|
}
|
|
);
|
|
|
|
await publishMessage(serverId, {
|
|
type: "ServerUpdate",
|
|
id: serverId,
|
|
data: {
|
|
flags,
|
|
},
|
|
clear: [],
|
|
});
|
|
}
|
|
|
|
export async function updateServerDiscoverability(
|
|
serverId: string,
|
|
state: boolean
|
|
) {
|
|
await checkPermission("servers/update/discoverability");
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Server>("servers")
|
|
.updateOne(
|
|
{
|
|
_id: serverId,
|
|
},
|
|
{
|
|
$set: {
|
|
analytics: state,
|
|
discoverable: state,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function updateBotDiscoverability(botId: string, state: boolean) {
|
|
await checkPermission("bots/update/discoverability");
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Bot>("bots")
|
|
.updateOne(
|
|
{
|
|
_id: botId,
|
|
},
|
|
{
|
|
$set: {
|
|
analytics: state,
|
|
discoverable: state,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function restoreAccount(accountId: string) {
|
|
await checkPermission("accounts/restore");
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Account>("accounts")
|
|
.updateOne(
|
|
{
|
|
_id: accountId,
|
|
},
|
|
{
|
|
$unset: {
|
|
disabled: 1,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function queueAccountDeletion(accountId: string) {
|
|
await checkPermission("accounts/deletion/queue");
|
|
const twoWeeksFuture = new Date();
|
|
twoWeeksFuture.setDate(twoWeeksFuture.getDate() + 14);
|
|
|
|
const $set: Partial<Account> = {
|
|
disabled: true,
|
|
deletion: {
|
|
status: "Scheduled",
|
|
after: twoWeeksFuture.toISOString(),
|
|
},
|
|
};
|
|
|
|
await disableAccount(accountId);
|
|
await mongo().db("revolt").collection<Account>("accounts").updateOne(
|
|
{
|
|
_id: accountId,
|
|
},
|
|
{
|
|
$set,
|
|
}
|
|
);
|
|
|
|
return $set;
|
|
}
|
|
|
|
export async function cancelAccountDeletion(accountId: string) {
|
|
await checkPermission("accounts/deletion/cancel");
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Account>("accounts")
|
|
.updateOne(
|
|
{
|
|
_id: accountId,
|
|
},
|
|
{
|
|
$unset: {
|
|
deletion: 1,
|
|
},
|
|
}
|
|
);
|
|
}
|