forked from administration/panel
1163 lines
26 KiB
TypeScript
1163 lines
26 KiB
TypeScript
"use server";
|
|
|
|
import { readFile, readdir, writeFile } from "fs/promises";
|
|
import { PLATFORM_MOD_ID, RESTRICT_ACCESS_LIST } from "./constants";
|
|
import mongo, {
|
|
Account,
|
|
CaseDocument,
|
|
ChannelInvite,
|
|
EmailClassification,
|
|
ReportDocument,
|
|
createDM,
|
|
fetchAccountById,
|
|
findDM,
|
|
} from "./db";
|
|
import { publishMessage, sendChatMessage } from "./redis";
|
|
import { ulid } from "ulid";
|
|
import {
|
|
AccountInfo,
|
|
AccountStrike,
|
|
Bot,
|
|
Channel,
|
|
File,
|
|
Invite,
|
|
Member,
|
|
Message,
|
|
Report,
|
|
Server,
|
|
SessionInfo,
|
|
User,
|
|
} from "revolt-api";
|
|
import { checkPermission } from "./accessPermissions";
|
|
import { Long } from "mongodb";
|
|
import { nanoid } from "nanoid";
|
|
|
|
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 updateCaseNotes(caseId: string, notes: string) {
|
|
await checkPermission("cases/update/notes", caseId, { notes });
|
|
|
|
return await mongo()
|
|
.db("revolt")
|
|
.collection<CaseDocument>("safety_cases")
|
|
.updateOne(
|
|
{ _id: caseId },
|
|
{
|
|
$set: {
|
|
notes,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function assignReportToCase(reportId: string, caseId?: string) {
|
|
await checkPermission("reports/update/case", reportId);
|
|
|
|
const $set = {
|
|
case_id: (caseId ?? null)!,
|
|
} as ReportDocument;
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<ReportDocument>("safety_reports")
|
|
.updateOne(
|
|
{ _id: reportId },
|
|
(caseId
|
|
? {
|
|
$set,
|
|
}
|
|
: {
|
|
$unset: {
|
|
case_id: 1,
|
|
},
|
|
}) as never // fuck you
|
|
);
|
|
|
|
return $set;
|
|
}
|
|
|
|
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 closeCase(caseId: string) {
|
|
await checkPermission("cases/update/close", caseId);
|
|
|
|
const $set = {
|
|
status: "Closed",
|
|
closed_at: new Date().toISOString(),
|
|
} as CaseDocument;
|
|
|
|
await mongo().db("revolt").collection<CaseDocument>("safety_cases").updateOne(
|
|
{ _id: caseId },
|
|
{
|
|
$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 reopenCase(caseId: string) {
|
|
await checkPermission("cases/update/reopen", caseId);
|
|
|
|
const $set = {
|
|
status: "Open",
|
|
} as CaseDocument;
|
|
|
|
await mongo().db("revolt").collection<CaseDocument>("safety_cases").updateOne(
|
|
{ _id: caseId },
|
|
{
|
|
$set,
|
|
}
|
|
);
|
|
|
|
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 deleteMFARecoveryCodes(userId: string) {
|
|
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
|
|
await checkPermission("accounts/update/mfa", userId);
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Account>("accounts")
|
|
.updateOne(
|
|
{ _id: userId },
|
|
{
|
|
$unset: {
|
|
"mfa.recovery_codes": 1,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function disableMFA(userId: string) {
|
|
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
|
|
await checkPermission("accounts/update/mfa", userId);
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Account>("accounts")
|
|
.updateOne(
|
|
{ _id: userId },
|
|
{
|
|
$unset: {
|
|
"mfa.totp_token": 1,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function changeAccountEmail(userId: string, email: string) {
|
|
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
|
|
await checkPermission("accounts/update/email", userId);
|
|
|
|
const SPLIT_RE = /([^@]+)(@.+)/;
|
|
const SYMBOL_RE = /\+.+|\./g;
|
|
|
|
const segments = SPLIT_RE.exec(email);
|
|
if (!segments) throw "invalid email";
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Account>("accounts")
|
|
.updateOne(
|
|
{ _id: userId },
|
|
{
|
|
$set: {
|
|
email: email,
|
|
email_normalised: segments[1].replace(SYMBOL_RE, "") + segments[2],
|
|
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<Account>("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 lookupEmail(email: string): Promise<string | false> {
|
|
await checkPermission("accounts/fetch/by-email", email);
|
|
|
|
const accounts = mongo().db("revolt").collection<Account>("accounts");
|
|
|
|
let result = await accounts.findOne({ email: email });
|
|
if (result) return result._id;
|
|
|
|
result = await accounts.findOne({ email_normalised: email });
|
|
if (result) return result._id;
|
|
|
|
return false;
|
|
}
|
|
|
|
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 mongo()
|
|
.db("revolt")
|
|
.collection<{ _id: { user: string; server: string } }>("server_members")
|
|
.find({ "_id.user": userId })
|
|
.toArray();
|
|
|
|
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) {
|
|
await checkPermission("users/update/badges", userId, { badges });
|
|
await mongo().db("revolt").collection<User>("users").updateOne(
|
|
{
|
|
_id: userId,
|
|
},
|
|
{
|
|
$set: {
|
|
badges,
|
|
},
|
|
}
|
|
);
|
|
|
|
const memberships = await mongo()
|
|
.db("revolt")
|
|
.collection<{ _id: { user: string; server: string } }>("server_members")
|
|
.find({ "_id.user": userId })
|
|
.toArray();
|
|
|
|
for (const topic of [userId, ...memberships.map((x) => x._id.server)]) {
|
|
await publishMessage(topic, {
|
|
type: "UserUpdate",
|
|
id: userId,
|
|
data: {
|
|
badges,
|
|
},
|
|
clear: [],
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function wipeUser(
|
|
userId: string,
|
|
flags = 4,
|
|
onlyMessages = false
|
|
) {
|
|
if (RESTRICT_ACCESS_LIST.includes(userId)) throw "restricted access";
|
|
|
|
await checkPermission("users/action/wipe", userId, { flags });
|
|
|
|
const user = onlyMessages
|
|
? null
|
|
: await mongo()
|
|
.db("revolt")
|
|
.collection<User>("users")
|
|
.findOne({ _id: userId });
|
|
|
|
const messages = await mongo()
|
|
.db("revolt")
|
|
.collection<Message>("messages")
|
|
.find({ author: userId }, { sort: { _id: -1 } })
|
|
.toArray();
|
|
|
|
const dms = onlyMessages
|
|
? null
|
|
: await mongo()
|
|
.db("revolt")
|
|
.collection<Channel>("channels")
|
|
.find({
|
|
channel_type: "DirectMessage",
|
|
recipients: userId,
|
|
})
|
|
.toArray();
|
|
|
|
const memberships = onlyMessages
|
|
? null
|
|
: await mongo()
|
|
.db("revolt")
|
|
.collection<{ _id: { user: string; server: string } }>("server_members")
|
|
.find({ "_id.user": userId })
|
|
.toArray();
|
|
|
|
// retrieve messages, dm channels, relationships, server memberships
|
|
const backup = {
|
|
_event: onlyMessages ? "messages" : "wipe",
|
|
user,
|
|
messages,
|
|
dms,
|
|
memberships,
|
|
};
|
|
|
|
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 (!onlyMessages) {
|
|
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,
|
|
});
|
|
|
|
if (!onlyMessages) {
|
|
// 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 wipeUserProfile(
|
|
userId: string,
|
|
fields: {
|
|
banner: boolean;
|
|
avatar: boolean;
|
|
bio: boolean;
|
|
displayName: boolean;
|
|
status: boolean;
|
|
}
|
|
) {
|
|
await checkPermission("users/action/wipe-profile", userId);
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<User>("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 ? { display_name: 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 updateServerOwner(serverId: string, userId: string) {
|
|
await checkPermission("servers/update/owner", { serverId, userId });
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Server>("servers")
|
|
.updateOne({ _id: serverId }, { $set: { owner: userId } });
|
|
|
|
await publishMessage(serverId, {
|
|
type: "ServerUpdate",
|
|
id: serverId,
|
|
data: {
|
|
owner: userId,
|
|
},
|
|
clear: [],
|
|
});
|
|
}
|
|
|
|
export async function addServerMember(
|
|
serverId: string,
|
|
userId: string,
|
|
withEvent: boolean
|
|
) {
|
|
await checkPermission("servers/update/add-member", {
|
|
serverId,
|
|
userId,
|
|
withEvent,
|
|
});
|
|
|
|
const server = await mongo()
|
|
.db("revolt")
|
|
.collection<Server>("servers")
|
|
.findOne({ _id: serverId });
|
|
|
|
const channels = await mongo()
|
|
.db("revolt")
|
|
.collection<Channel>("channels")
|
|
.find({ server: serverId })
|
|
.toArray();
|
|
|
|
const member = await mongo()
|
|
.db("revolt")
|
|
.collection<Member>("server_members")
|
|
.findOne({ _id: { server: serverId, user: userId } });
|
|
|
|
if (!server) throw new Error("server doesn't exist");
|
|
if (member) throw new Error("already a member");
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Member>("server_members")
|
|
.insertOne({
|
|
_id: { server: serverId, user: userId },
|
|
joined_at: Long.fromNumber(Date.now()) as unknown as string,
|
|
});
|
|
|
|
await publishMessage(userId + "!", {
|
|
type: "ServerCreate",
|
|
id: serverId,
|
|
channels: channels,
|
|
server: server,
|
|
});
|
|
|
|
if (withEvent) {
|
|
await publishMessage(serverId, {
|
|
type: "ServerMemberJoin",
|
|
id: serverId,
|
|
user: userId,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function quarantineServer(serverId: string, message: string) {
|
|
await checkPermission("servers/update/quarantine", { serverId, message });
|
|
|
|
const server = await mongo()
|
|
.db("revolt")
|
|
.collection<Server>("servers")
|
|
.findOne({ _id: serverId });
|
|
|
|
const members = await mongo()
|
|
.db("revolt")
|
|
.collection<Member>("server_members")
|
|
.find({ "_id.server": serverId })
|
|
.toArray();
|
|
|
|
const invites = await mongo()
|
|
.db("revolt")
|
|
.collection<Invite>("channel_invites")
|
|
.find({ type: "Server", server: serverId })
|
|
.toArray();
|
|
|
|
if (!server) throw new Error("server doesn't exist");
|
|
|
|
const backup = {
|
|
_event: "quarantine",
|
|
server,
|
|
members,
|
|
invites,
|
|
};
|
|
|
|
await writeFile(
|
|
`./exports/${new Date().toISOString()} - ${serverId}.json`,
|
|
JSON.stringify(backup)
|
|
);
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Server>("servers")
|
|
.updateOne(
|
|
{ _id: serverId },
|
|
{
|
|
$set: {
|
|
owner: "0".repeat(26),
|
|
analytics: false,
|
|
discoverable: false,
|
|
},
|
|
}
|
|
);
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Member>("server_members")
|
|
.deleteMany({ "_id.server": serverId });
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Invite>("channel_invites")
|
|
.deleteMany({ type: "Server", server: serverId });
|
|
|
|
await publishMessage(serverId, {
|
|
type: "ServerDelete",
|
|
id: serverId,
|
|
});
|
|
|
|
while (members.length) {
|
|
const m = members.splice(0, 50);
|
|
|
|
await Promise.allSettled(
|
|
m.map(async (member) => {
|
|
const messageId = ulid();
|
|
|
|
let dm = await findDM(PLATFORM_MOD_ID, member._id.user);
|
|
if (!dm)
|
|
dm = await createDM(PLATFORM_MOD_ID, member._id.user, messageId);
|
|
|
|
await sendChatMessage({
|
|
_id: messageId,
|
|
author: PLATFORM_MOD_ID,
|
|
channel: dm._id,
|
|
content: message,
|
|
});
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function deleteInvite(invite: string) {
|
|
await checkPermission("channels/update/invites", invite);
|
|
|
|
if (!invite) throw new Error("invite is empty");
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<ChannelInvite>("channel_invites")
|
|
.deleteOne({ _id: invite });
|
|
}
|
|
|
|
export async function editInvite(invite: string, newInvite: string) {
|
|
await checkPermission("channels/update/invites", { invite, newInvite });
|
|
|
|
if (!invite) throw new Error("invite is empty");
|
|
if (!newInvite) throw new Error("new invite is empty");
|
|
|
|
const { value } = await mongo()
|
|
.db("revolt")
|
|
.collection<ChannelInvite>("channel_invites")
|
|
.findOneAndDelete({ _id: invite });
|
|
|
|
if (!value) throw new Error("invite doesn't exist");
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<ChannelInvite>("channel_invites")
|
|
.insertOne({ ...value, _id: newInvite });
|
|
}
|
|
|
|
export async function editInviteChannel(invite: string, newChannel: string) {
|
|
await checkPermission("channels/update/invites", { invite, newChannel });
|
|
|
|
if (!invite) throw new Error("invite is empty");
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<ChannelInvite>("channel_invites")
|
|
.updateOne({ _id: invite }, { $set: { channel: newChannel } });
|
|
}
|
|
|
|
export async function createInvite(invite: ChannelInvite) {
|
|
await checkPermission("channels/update/invites", invite);
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<ChannelInvite>("channel_invites")
|
|
.insertOne(invite);
|
|
}
|
|
|
|
export async function bulkDeleteInvites(invites: string[]) {
|
|
await checkPermission("channels/update/invites", invites);
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<ChannelInvite>("channel_invites")
|
|
.deleteMany({ _id: { $in: invites } });
|
|
}
|
|
|
|
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 resetBotToken(botId: string) {
|
|
await checkPermission("bots/update/reset-token", { botId });
|
|
|
|
// Should generate tokens the exact same as the backend generates them:
|
|
// https://github.com/revoltchat/backend/blob/41f20c2239ed6307ad821b321d13240dc6ff3327/crates/core/database/src/models/bots/model.rs#L106
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Bot>("bots")
|
|
.updateOne(
|
|
{
|
|
_id: botId,
|
|
},
|
|
{
|
|
$set: {
|
|
token: nanoid(64),
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function transferBot(
|
|
botId: string,
|
|
ownerId: string,
|
|
resetToken: boolean
|
|
) {
|
|
await checkPermission("bots/update/owner", { botId, ownerId, resetToken });
|
|
|
|
if (resetToken) {
|
|
await checkPermission("bots/update/reset-token", { botId });
|
|
}
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<Bot>("bots")
|
|
.updateOne(
|
|
{
|
|
_id: botId,
|
|
},
|
|
{
|
|
$set: {
|
|
owner: ownerId,
|
|
...(resetToken
|
|
? {
|
|
token: nanoid(64),
|
|
}
|
|
: {}),
|
|
},
|
|
}
|
|
);
|
|
|
|
await mongo()
|
|
.db("revolt")
|
|
.collection<User>("users")
|
|
.updateOne(
|
|
{
|
|
_id: botId,
|
|
},
|
|
{
|
|
$set: {
|
|
"bot.owner": ownerId,
|
|
},
|
|
}
|
|
);
|
|
|
|
// This doesn't appear to work, maybe Revite can't handle it. I'll leave it in regardless.
|
|
await publishMessage(botId, {
|
|
type: "UserUpdate",
|
|
id: botId,
|
|
data: {
|
|
bot: {
|
|
owner: ownerId,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
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,
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function fetchBackups() {
|
|
await checkPermission("backup/fetch", null);
|
|
|
|
return await Promise.all(
|
|
(
|
|
await readdir("./exports", { withFileTypes: true })
|
|
)
|
|
.filter((file) => file.isFile() && file.name.endsWith(".json"))
|
|
.map(async (file) => {
|
|
let type: string | null = null;
|
|
try {
|
|
type = JSON.parse(
|
|
(await readFile(`./exports/${file.name}`)).toString("utf-8")
|
|
)._event;
|
|
} catch (e) {}
|
|
|
|
return { name: file.name, type: type };
|
|
})
|
|
);
|
|
}
|
|
|
|
export async function fetchBackup(name: string) {
|
|
await checkPermission("backup/fetch/by-name", null);
|
|
|
|
return JSON.parse((await readFile(`./exports/${name}`)).toString("utf-8"));
|
|
}
|
|
|
|
export async function fetchEmailClassifications(): Promise<
|
|
EmailClassification[]
|
|
> {
|
|
await checkPermission("authifier/classification/fetch", null);
|
|
|
|
return await mongo()
|
|
.db("authifier")
|
|
.collection<EmailClassification>("email_classification")
|
|
.find({})
|
|
.toArray();
|
|
}
|
|
|
|
export async function createEmailClassification(
|
|
domain: string,
|
|
classification: string
|
|
) {
|
|
await checkPermission("authifier/classification/create", {
|
|
domain,
|
|
classification,
|
|
});
|
|
|
|
await mongo()
|
|
.db("authifier")
|
|
.collection<EmailClassification>("email_classification")
|
|
.insertOne({ _id: domain, classification });
|
|
}
|
|
|
|
export async function updateEmailClassification(
|
|
domain: string,
|
|
classification: string
|
|
) {
|
|
await checkPermission("authifier/classification/update", {
|
|
domain,
|
|
classification,
|
|
});
|
|
|
|
await mongo()
|
|
.db("authifier")
|
|
.collection<EmailClassification>("email_classification")
|
|
.updateOne({ _id: domain }, { $set: { classification } });
|
|
}
|
|
|
|
export async function deleteEmailClassification(domain: string) {
|
|
await checkPermission("authifier/classification/delete", domain);
|
|
|
|
await mongo()
|
|
.db("authifier")
|
|
.collection<EmailClassification>("email_classification")
|
|
.deleteOne({ _id: domain });
|
|
}
|
|
|
|
export async function searchUserByTag(
|
|
username: string,
|
|
discriminator: string
|
|
): Promise<string | false> {
|
|
await checkPermission("users/fetch/by-tag", { username, discriminator });
|
|
|
|
const result = await mongo().db("revolt").collection<User>("users").findOne({
|
|
username,
|
|
discriminator,
|
|
});
|
|
|
|
return result?._id || false;
|
|
}
|
|
|
|
export async function fetchUsersByUsername(username: string) {
|
|
await checkPermission("users/fetch/bulk-by-username", { username });
|
|
|
|
return await mongo()
|
|
.db("revolt")
|
|
.collection<User>("users")
|
|
.find({
|
|
username,
|
|
})
|
|
.toArray();
|
|
}
|