From efafed49318622c8d0148c245db44b217c3d764c Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Thu, 27 Jul 2023 16:27:03 +0100 Subject: [PATCH] feat: features yeah --- .gitignore | 4 + components/inspector/RecentMessages.tsx | 52 ++++++++ components/inspector/UserActions.tsx | 106 ++++++++++++++-- exports/.gitkeep | 0 lib/actions.ts | 161 +++++++++++++++++++++++- lib/db.ts | 12 +- package.json | 1 + pnpm-lock.yaml | 56 +++++++++ 8 files changed, 376 insertions(+), 16 deletions(-) create mode 100644 components/inspector/RecentMessages.tsx create mode 100644 exports/.gitkeep diff --git a/.gitignore b/.gitignore index 8f322f0..388b93b 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# data +exports/** +!exports/.gitkeep diff --git a/components/inspector/RecentMessages.tsx b/components/inspector/RecentMessages.tsx new file mode 100644 index 0000000..3d44ebd --- /dev/null +++ b/components/inspector/RecentMessages.tsx @@ -0,0 +1,52 @@ +import type { Filter } from "mongodb"; +import { Message, User } from "revolt-api"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "../ui/card"; +import { fetchMessages, fetchUsersById } from "@/lib/db"; +import { CompactMessage } from "../cards/CompactMessage"; + +export async function RecentMessages({ + query, + users, +}: { + query: Filter; + users?: boolean | User[]; +}) { + const recentMessages = await fetchMessages(query); + + const userList = ( + users === true + ? await fetchUsersById([...new Set(recentMessages.map((x) => x.author))]) + : Array.isArray(users) + ? users + : [] + ).reduce((prev, next) => { + prev[next._id] = next; + return prev; + }, {} as Record); + + return ( + + + Recent Messages + Overview of recent messages + + + {/* enter reason for fetching */} + {recentMessages.map((message) => ( + + ))} + + + ); +} diff --git a/components/inspector/UserActions.tsx b/components/inspector/UserActions.tsx index 0a243b0..fb2946c 100644 --- a/components/inspector/UserActions.tsx +++ b/components/inspector/UserActions.tsx @@ -20,11 +20,19 @@ import { AlertDialogTrigger, } from "../ui/alert-dialog"; import { Input } from "../ui/input"; -import { sendAlert } from "@/lib/actions"; +import { banUser, sendAlert, suspendUser } from "@/lib/actions"; import { useRef } from "react"; +import { useToast } from "../ui/use-toast"; -export function UserActions({ id }: { id: string }) { +export function UserActions({ + id, + counts, +}: { + id: string; + counts: { pending: number; all: number }; +}) { const alertMessage = useRef(""); + const { toast } = useToast(); return (
@@ -34,12 +42,73 @@ export function UserActions({ id }: { id: string }) { > Account - - + + + + + + + + + Are you sure you want to suspend this user? + + + + Cancel + + suspendUser(id) + .then(() => toast({ title: "Suspended user" })) + .catch((err) => + toast({ + title: "Failed to suspend user!", + description: err, + variant: "destructive", + }) + ) + } + > + Suspend + + + + + + + + + + + + + Are you sure you want to ban this user? + + + + Cancel + + banUser(id) + .then(() => toast({ title: "Banned user" })) + .catch((err) => + toast({ + title: "Failed to ban user!", + description: err, + variant: "destructive", + }) + ) + } + > + Ban + + + + +
diff --git a/exports/.gitkeep b/exports/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/actions.ts b/lib/actions.ts index b3abede..7ddaf91 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -1,9 +1,26 @@ "use server"; +import { writeFile } from "fs/promises"; import { PLATFORM_MOD_ID } from "./constants"; -import { createDM, findDM, updateLastMessageId } from "./db"; -import { sendChatMessage } from "./redis"; +import mongo, { + createDM, + fetchChannels, + fetchMembershipsByUser, + fetchMessages, + fetchUserById, + findDM, + updateLastMessageId, +} from "./db"; +import { publishMessage, sendChatMessage } from "./redis"; import { ulid } from "ulid"; +import { + AccountInfo, + File, + Member, + Message, + SessionInfo, + User, +} from "revolt-api"; export async function sendAlert(userId: string, content: string) { const messageId = ulid(); @@ -19,3 +36,143 @@ export async function sendAlert(userId: string, content: string) { content, }); } + +export async function disableAccount(userId: string) { + 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 suspendUser(userId: string) { + 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, + }, + }); + } +} + +export async function wipeUser(userId: string, flags = 4) { + // 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) { + return await wipeUser(userId, 4); +} diff --git a/lib/db.ts b/lib/db.ts index 401e4a2..6607f4e 100644 --- a/lib/db.ts +++ b/lib/db.ts @@ -48,6 +48,14 @@ export async function fetchChannelById(id: string) { .findOne({ _id: id }); } +export async function fetchChannels(query: Filter) { + return await mongo() + .db("revolt") + .collection("channels") + .find(query) + .toArray(); +} + export async function updateLastMessageId( channelId: string, messageId: string @@ -122,11 +130,11 @@ export async function fetchMessageById(id: string) { .findOne({ _id: id }); } -export async function fetchMessages(query: Filter) { +export async function fetchMessages(query: Filter, limit = 50) { return await mongo() .db("revolt") .collection("messages") - .find(query, { sort: { _id: -1 } }) + .find(query, { sort: { _id: -1 }, limit }) .toArray(); } diff --git a/package.json b/package.json index 4f08c1a..46df035 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.4", "@types/node": "20.4.4", "@types/react": "18.2.15", "@types/react-dom": "18.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52032c3..cbc5415 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,9 @@ dependencies: '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-toast': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) '@types/node': specifier: 20.4.4 version: 20.4.4 @@ -913,6 +916,38 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-toast@1.1.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-wf+fc8DOywrpRK3jlPlWVe+ELYGHdKDaaARJZNuUTWyWYq7+ANCFLp4rTjZ/mcGkJJQ/vZ949Zis9xxEpfq9OA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.6 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.15)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.15 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.15)(react@18.2.0): resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} peerDependencies: @@ -1001,6 +1036,27 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.6 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.15 + '@types/react-dom': 18.2.7 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/rect@1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: