"use server"; import { writeFile } from "fs/promises"; import { PLATFORM_MOD_ID, RESTRICT_ACCESS_LIST } from "./constants"; import mongo, { Account, createDM, fetchAccountById, 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("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("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("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("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("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("accounts") .updateOne({ _id: userId }, { $set: { disabled: true } }); await mongo().db("revolt").collection("sessions").deleteMany({ user_id: userId, }); } export async function changeAccountEmail(userId: string, email: string) { if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access"; await checkPermission("accounts/update/email", userId); await mongo() .db("revolt") .collection("accounts") .updateOne( { _id: userId }, { $set: { email: email, email_normalised: email, verification: { status: "Verified" }, } } ) } export async function verifyAccountEmail(userId: string) { if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access"; await checkPermission("accounts/update/email", userId); const account = await fetchAccountById(userId); if (!account) throw new Error("couldn't find account"); if (account.verification.status == "Verified") throw new Error("already verified"); let email = account.email; if (account.verification.status == "Moving") { email = account.verification.new_email; } await mongo() .db("revolt") .collection("accounts") .updateOne( { _id: userId }, { $set: { email: email, email_normalised: email, // <-- should be fine but someone should fix this in the future verification: { status: "Verified" }, } } ) } 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("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("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("attachments") .updateMany( { _id: { $in: attachmentIds } }, { $set: { reported: true, deleted: true, }, } ); } // delete messages await mongo().db("revolt").collection("messages").deleteMany({ author: userId, }); // delete server memberships await mongo().db("revolt").collection("server_members").deleteMany({ "_id.user": userId, }); // disable account await disableAccount(userId); // clear user profile await mongo() .db("revolt") .collection("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("users") .updateOne( { _id: userId, }, { $unset: { flags: 1, }, } ); } export async function wipeUserProfile( userId: string, fields: { banner: boolean, avatar: boolean, bio: boolean, displayName: boolean, status: boolean, } ) { await checkPermission("users/action/wipe-profile", userId); const newDisplayName = (await fetchUserById(userId))?.username || "--"; await mongo() .db("revolt") .collection("users") .updateOne( { _id: userId, }, { $unset: { ...(fields.banner ? { "profile.background": 1 } : {}), ...(fields.bio ? { "profile.content": 1 } : {}), ...(fields.status ? { "status.text": 1 } : {}), ...(fields.avatar ? { "avatar": 1 } : {}), }, ...(fields.displayName ? { $set: { display_name: newDisplayName, }, } : {}), } ) } export async function updateServerFlags(serverId: string, flags: number) { await checkPermission("servers/update/flags", serverId, { flags }); await mongo().db("revolt").collection("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("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("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("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 = { disabled: true, deletion: { status: "Scheduled", after: twoWeeksFuture.toISOString(), }, }; await disableAccount(accountId); await mongo().db("revolt").collection("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("accounts") .updateOne( { _id: accountId, }, { $unset: { deletion: 1, }, } ); }