1
0
Fork 0
panel/lib/actions.ts

509 lines
11 KiB
TypeScript

"use server";
import { writeFile } from "fs/promises";
import { PLATFORM_MOD_ID, RESTRICT_ACCESS_LIST } 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", userId, { content });
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
) {
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
await checkPermission("users/create/strike", userId, {
givenReason,
additionalContext,
});
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", reportId, { 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", reportId);
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", reportId, { reason });
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", reportId);
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) {
await checkPermission("reports/update/bulk-close/by-user", userId);
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) {
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
await checkPermission("accounts/disable", userId);
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) {
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
await checkPermission("users/action/suspend", userId);
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): Promise<{ updatePublished: boolean }> {
await checkPermission("users/update/badges", userId, { badges });
await mongo().db("revolt").collection<User>("users").updateOne(
{
_id: userId,
},
{
$set: {
badges,
},
}
);
try {
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: [],
});
}
} catch(e) {
return { updatePublished: false };
}
return { updatePublished: true };
}
export async function wipeUser(userId: string, flags = 4) {
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
await checkPermission("users/action/wipe", userId, { flags });
// 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) {
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
await checkPermission("users/action/ban", userId);
return await wipeUser(userId, 4);
}
export async function unsuspendUser(userId: string) {
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
await checkPermission("users/action/unsuspend", userId);
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", serverId, { 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", serverId, { state });
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", botId, { state });
await mongo()
.db("revolt")
.collection<Bot>("bots")
.updateOne(
{
_id: botId,
},
{
$set: {
analytics: state,
discoverable: state,
},
}
);
}
export async function restoreAccount(accountId: string) {
if (RESTRICT_ACCESS_LIST.includes(accountId)) throw "restricted access";
await checkPermission("accounts/restore", accountId);
await mongo()
.db("revolt")
.collection<Account>("accounts")
.updateOne(
{
_id: accountId,
},
{
$unset: {
disabled: 1,
},
}
);
}
export async function queueAccountDeletion(accountId: string) {
if (RESTRICT_ACCESS_LIST.includes(accountId)) throw "restricted access";
await checkPermission("accounts/deletion/queue", accountId);
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) {
if (RESTRICT_ACCESS_LIST.includes(accountId)) throw "restricted access";
await checkPermission("accounts/deletion/cancel", accountId);
await mongo()
.db("revolt")
.collection<Account>("accounts")
.updateOne(
{
_id: accountId,
},
{
$unset: {
deletion: 1,
},
}
);
}