forked from administration/panel
679 lines
15 KiB
TypeScript
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 }
|
|
);
|
|
}
|