1
0
Fork 0
panel/lib/db.ts

679 lines
15 KiB
TypeScript

"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;
export type CaseDocument = {
_id: string;
title: string;
notes?: string;
author: string;
status: "Open" | "Closed";
closed_at?: string;
};
export type ReportDocument = Report & {
case_id?: string;
};
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,
});
return session!.user!.email!;
}
export async function fetchBotById(id: string) {
await checkPermission("bots/fetch/by-id", id);
return await mongo()
.db("revolt")
.collection<Bot>("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<Account>("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<Account>;
}
export async function fetchSessionsByAccount(accountId: string) {
await checkPermission("sessions/fetch/by-account-id", accountId);
return await mongo()
.db("revolt")
.collection<SessionInfo>("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<User>("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<User>("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<Channel>("channels")
.findOne({ _id: id });
}
export async function fetchChannels(query: Filter<Channel>) {
await checkPermission("channels/fetch", query);
return await mongo()
.db("revolt")
.collection<Channel>("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<Channel & { channel_type: "DirectMessage" }>("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<Channel & { channel_type: "DirectMessage" }>("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<Server>("servers")
.findOne({ _id: id });
}
export async function fetchServers(query: Filter<Server>) {
await checkPermission("servers/fetch", query);
return await mongo()
.db("revolt")
.collection<Server>("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<ChannelInvite>) {
await checkPermission("channels/fetch/invites", query);
const invites = await mongo()
.db("revolt")
.collection<ChannelInvite>("channel_invites")
.find(query)
.toArray();
const channels = await mongo()
.db("revolt")
.collection<Channel>("channels")
.find({ _id: { $in: invites.map((invite) => invite.channel) } })
.toArray();
const users = await mongo()
.db("revolt")
.collection<User>("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<Message>("messages")
.findOne({ _id: id });
}
export async function fetchMessagesByUser(userId: string) {
await checkPermission("messages/fetch/by-user", userId);
return await mongo()
.db("revolt")
.collection<Message>("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<Message>("messages")
.find({ channel: channelId }, { sort: { _id: -1 }, limit: 50 })
.toArray();
}
export async function fetchMessages(query: Filter<Message>, limit = 50) {
await checkPermission("messages/fetch", query, { limit });
return await mongo()
.db("revolt")
.collection<Message>("messages")
.find(query, { sort: { _id: -1 }, limit })
.toArray();
}
export async function fetchOpenReports() {
await checkPermission("reports/fetch/open", "all");
return await mongo()
.db("revolt")
.collection<ReportDocument>("safety_reports")
.find(
{ status: "Created" },
{
sort: {
_id: -1,
},
}
)
.toArray();
}
export async function fetchOpenCases() {
await checkPermission("cases/fetch/open", "all");
return await mongo()
.db("revolt")
.collection<CaseDocument>("safety_cases")
.find(
{ status: "Open" },
{
sort: {
_id: -1,
},
}
)
.toArray();
}
export async function fetchRelatedReportsByContent(contentId: string) {
await checkPermission("reports/fetch/related/by-content", contentId);
return await mongo()
.db("revolt")
.collection<Report>("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<Report>("safety_reports")
.find(
{ author_id: userId },
{
sort: {
_id: -1,
},
}
)
.toArray();
}
export async function fetchReportsByCase(caseId: string) {
await checkPermission("reports/fetch/related/by-case", caseId);
return await mongo()
.db("revolt")
.collection<ReportDocument>("safety_reports")
.find(
{ case_id: caseId },
{
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<Report>("safety_reports")
.find(
{
_id: {
$in: reportIdsInvolvingUser,
},
},
{
sort: {
_id: -1,
},
}
)
.toArray();
}
export async function fetchReports(
query: Filter<Report> = { status: "Created" }
) {
await checkPermission("reports/fetch", query);
return await mongo()
.db("revolt")
.collection<Report>("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<Report>("safety_reports")
.findOne({ _id: id });
}
export async function createCase(title: string) {
const id = ulid();
const author = await checkPermission("cases/create", { title });
await mongo()
.db("revolt")
.collection<CaseDocument>("safety_cases")
.insertOne({ _id: id, author, status: "Open", title });
return id;
}
export async function fetchCaseById(id: string) {
await checkPermission("cases/fetch/by-id", id);
return await mongo()
.db("revolt")
.collection<CaseDocument>("safety_cases")
.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<AccountStrike>("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<Channel & { channel_type: "DirectMessage" }>("channels")
.findOne({
channel_type: "DirectMessage",
recipients: {
$all: [userId, PLATFORM_MOD_ID],
},
});
if (!dm) return [];
return await mongo()
.db("revolt")
.collection<Message>("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<Bot>("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<EmailClassification>("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<SafetyNotes>("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<SafetyNotes>("safety_notes")
.updateOne(
{ _id: { id: objectId, type: type } },
{
$set: {
text: note,
edited_at: new Date(Date.now()),
edited_by: session?.user?.email ?? "",
},
},
{ upsert: true }
);
}