"use server"; import { Filter, MongoClient, WithId } from "mongodb"; import type { AccountStrike, Bot, Channel, Invite, Message, Report, Server, SessionInfo, SnapshotContent, User, } from "revolt-api"; import { ulid } from "ulid"; import { publishMessage } from "./redis"; import { checkPermission, hasPermissionFromSession } from "./accessPermissions"; import { PLATFORM_MOD_ID } from "./constants"; import { getServerSession } from "next-auth"; let client: MongoClient; function mongo() { if (!client) { client = new MongoClient(process.env.MONGODB!); } return client; } export default mongo; export async function insertAuditLog( permission: string, context: any, args?: any ) { const session = await getServerSession(); if (!session!.user!.email!) throw "invalid procedure call"; await mongo() .db("revolt") .collection<{ _id: string; moderator: string; permission: string; context: any; args: any; }>("safety_audit") .insertOne({ _id: ulid(), moderator: session!.user!.email!, permission, context, args, }); } export async function fetchBotById(id: string) { await checkPermission("bots/fetch/by-id", id); return await mongo() .db("revolt") .collection("bots") .findOne({ _id: id }); } export type Account = { _id: string; email: string; email_normalised: string; verification: | { status: "Verified"; } | { status: "Pending"; token: string; expiry: string; } | { status: "Moving"; new_email: string; token: string; expiry: string; }; password_reset: null | { token: string; expiry: string; }; deletion: | null | { status: "WaitingForVerification"; token: string; expiry: string; } | { status: "Scheduled"; after: string; }; disabled: boolean; lockout: null | { attempts: number; expiry: string; }; mfa?: { totp_token?: { status: "Pending" | "Enabled"; }; recovery_codes?: number; }; }; export async function fetchAccountById(id: string) { await checkPermission("accounts/fetch/by-id", id); return await mongo() .db("revolt") .collection("accounts") .aggregate( [ { $match: { _id: id }, }, { $project: { password: 0, "mfa.totp_token.secret": 0, } }, { $set: { // Replace recovery code array with amount of codes "mfa.recovery_codes": { $cond: { if: { $isArray: "$mfa.recovery_codes" }, then: { $size: "$mfa.recovery_codes", }, else: undefined, } } } } ] ).next() as WithId; } export async function fetchSessionsByAccount(accountId: string) { await checkPermission("sessions/fetch/by-account-id", accountId); return await mongo() .db("revolt") .collection("sessions") .find( { user_id: accountId }, { projection: { token: 0, }, sort: { _id: -1, }, } ) .toArray(); } export async function fetchUserById(id: string) { await checkPermission("users/fetch/by-id", id); return await mongo() .db("revolt") .collection("users") .findOne( { _id: id }, { projection: (await hasPermissionFromSession("users/fetch/relations")) ? undefined : { relations: 0, }, } ); } export async function fetchUsersById(ids: string[]) { await checkPermission("users/fetch/by-id", ids); return await mongo() .db("revolt") .collection("users") .find( { _id: { $in: ids } }, { projection: (await hasPermissionFromSession("users/fetch/relations")) ? undefined : { relations: 0, }, } ) .toArray(); } export async function fetchChannelById(id: string) { await checkPermission("channels/fetch/by-id", id); return await mongo() .db("revolt") .collection("channels") .findOne({ _id: id }); } export async function fetchChannels(query: Filter) { await checkPermission("channels/fetch", query); return await mongo() .db("revolt") .collection("channels") .find(query) .toArray(); } export async function findDM(user_a: string, user_b: string) { await checkPermission("channels/fetch/dm", { user_a, user_b }); return await mongo() .db("revolt") .collection("channels") .findOne({ channel_type: "DirectMessage", recipients: { $all: [user_a, user_b], }, }); } export async function createDM( userA: string, userB: string, lastMessageId?: string ) { await checkPermission("channels/create/dm", { userA, userB }, lastMessageId); const newChannel: Channel & { channel_type: "DirectMessage" } = { _id: ulid(), channel_type: "DirectMessage", active: typeof lastMessageId !== "undefined", recipients: [userA, userB], }; await mongo() .db("revolt") .collection("channels") .insertOne(newChannel); await publishMessage(userA + "!", { type: "ChannelCreate", ...newChannel, }); await publishMessage(userB + "!", { type: "ChannelCreate", ...newChannel, }); return newChannel; } export async function fetchServerById(id: string) { await checkPermission("servers/fetch/by-id", id); return await mongo() .db("revolt") .collection("servers") .findOne({ _id: id }); } export async function fetchServers(query: Filter) { await checkPermission("servers/fetch", query); return await mongo() .db("revolt") .collection("servers") .find(query) .toArray(); } // `vanity` should eventually be added to the backend as well export type ChannelInvite = Invite & { vanity?: boolean } export async function fetchInvites(query: Filter) { await checkPermission("channels/fetch/invites", query); const invites = await mongo() .db("revolt") .collection("channel_invites") .find(query) .toArray(); const channels = await mongo() .db("revolt") .collection("channels") .find({ _id: { $in: invites.map((invite) => invite.channel) } }) .toArray(); const users = await mongo() .db("revolt") .collection("users") .find({ _id: { $in: invites.map((invite) => invite.creator) } }) .toArray(); return { invites, channels, users } } export async function fetchMessageById(id: string) { await checkPermission("messages/fetch/by-id", id); return await mongo() .db("revolt") .collection("messages") .findOne({ _id: id }); } export async function fetchMessagesByUser(userId: string) { await checkPermission("messages/fetch/by-user", userId); return await mongo() .db("revolt") .collection("messages") .find({ author: userId }, { sort: { _id: -1 }, limit: 50 }) .toArray(); } export async function fetchMessagesByChannel(channelId: string) { await checkPermission("messages/fetch/by-user", channelId); return await mongo() .db("revolt") .collection("messages") .find({ channel: channelId }, { sort: { _id: -1 }, limit: 50 }) .toArray(); } export async function fetchMessages(query: Filter, limit = 50) { await checkPermission("messages/fetch", query, { limit }); return await mongo() .db("revolt") .collection("messages") .find(query, { sort: { _id: -1 }, limit }) .toArray(); } export async function fetchOpenReports() { await checkPermission("reports/fetch/open", "all"); return await mongo() .db("revolt") .collection("safety_reports") .find( { status: "Created" }, { sort: { _id: -1, }, } ) .toArray(); } export async function fetchRelatedReportsByContent(contentId: string) { await checkPermission("reports/fetch/related/by-content", contentId); return await mongo() .db("revolt") .collection("safety_reports") .find( { status: "Created", "content.id": contentId }, { sort: { _id: -1, }, } ) .toArray(); } export async function fetchReportsByUser(userId: string) { await checkPermission("reports/fetch/related/by-user", userId); return await mongo() .db("revolt") .collection("safety_reports") .find( { author_id: userId }, { sort: { _id: -1, }, } ) .toArray(); } export async function fetchReportsAgainstUser(userId: string) { await checkPermission("reports/fetch/related/against-user", userId); const reportIdsInvolvingUser = await mongo() .db("revolt") .collection<{ _id: string; report_id: string; content: SnapshotContent }>( "safety_snapshots" ) .find({ $or: [{ "content._id": userId }, { "content.author": userId }], }) .toArray() .then((snapshots) => [ ...new Set(snapshots.map((snapshot) => snapshot.report_id)), ]); return await mongo() .db("revolt") .collection("safety_reports") .find( { _id: { $in: reportIdsInvolvingUser, }, }, { sort: { _id: -1, }, } ) .toArray(); } export async function fetchReports( query: Filter = { status: "Created" } ) { await checkPermission("reports/fetch", query); return await mongo() .db("revolt") .collection("safety_reports") .find(query as never, { sort: { _id: -1, }, }) .toArray(); } export async function fetchReportById(id: string) { await checkPermission("reports/fetch/by-id", id); return await mongo() .db("revolt") .collection("safety_reports") .findOne({ _id: id }); } export async function fetchMembershipsByUser(userId: string) { await checkPermission("users/fetch/memberships", userId); return await mongo() .db("revolt") .collection<{ _id: { user: string; server: string } }>("server_members") .find({ "_id.user": userId }) .toArray(); } export async function fetchSnapshots( query: Filter<{ _id: string; report_id: string; content: SnapshotContent }> ) { await checkPermission("reports/fetch/snapshots", query); return await mongo() .db("revolt") .collection<{ _id: string; report_id: string; content: SnapshotContent }>( "safety_snapshots" ) .find(query) .toArray(); } export async function fetchSnapshotsByReport(reportId: string) { await checkPermission("reports/fetch/snapshots/by-report", reportId); return await mongo() .db("revolt") .collection<{ content: SnapshotContent }>("safety_snapshots") .find({ report_id: reportId }) .toArray() .then((snapshots) => snapshots.map((snapshot) => snapshot!.content)); } export async function fetchStrikesByUser(userId: string) { await checkPermission("users/fetch/strikes", userId); return await mongo() .db("revolt") .collection("safety_strikes") .find( { user_id: userId }, { sort: { _id: -1, }, } ) .toArray(); } export async function fetchNoticesByUser(userId: string) { await checkPermission("users/fetch/notices", userId); const dm = await mongo() .db("revolt") .collection("channels") .findOne({ channel_type: "DirectMessage", recipients: { $all: [userId, PLATFORM_MOD_ID], }, }); if (!dm) return []; return await mongo() .db("revolt") .collection("messages") .find({ channel: dm!._id }, { sort: { _id: -1 } }) .toArray(); } export async function fetchBotsByUser(userId: string) { await checkPermission("bots/fetch/by-user", userId); return await mongo() .db("revolt") .collection("bots") .find({ owner: userId }) .toArray(); } export type EmailClassification = { _id: string; classification: string; }; export async function fetchAuthifierEmailClassification(provider: string) { await checkPermission("authifier", provider); return await mongo() .db("authifier") .collection("email_classification") .findOne({ _id: provider }); } export type SafetyNotes = { _id: { id: string, type: "message" | "channel" | "server" | "user" | "account" | "global" }; text: string; edited_by: string; edited_at: Date; } export async function fetchSafetyNote(objectId: string, type: SafetyNotes["_id"]["type"]) { await checkPermission(`safety_notes/fetch/${type}`, objectId); return mongo() .db("revolt") .collection("safety_notes") .findOne({ _id: { id: objectId, type: type } }); } export async function updateSafetyNote(objectId: string, type: SafetyNotes["_id"]["type"], note: string) { await checkPermission(`safety_notes/update/${type}`, objectId); const session = await getServerSession(); return mongo() .db("revolt") .collection("safety_notes") .updateOne( { _id: { id: objectId, type: type } }, { $set: { text: note, edited_at: new Date(Date.now()), edited_by: session?.user?.email ?? "", }, }, { upsert: true }, ); }