1
0
Fork 0

feat: features yeah

fix-1
Paul Makles 2023-07-27 16:27:03 +01:00
parent bebe115db2
commit efafed4931
No known key found for this signature in database
GPG Key ID: 5059F398521BB0F6
8 changed files with 376 additions and 16 deletions

4
.gitignore vendored
View File

@ -33,3 +33,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# data
exports/**
!exports/.gitkeep

View File

@ -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<Message>;
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<string, any>);
return (
<Card>
<CardHeader>
<CardTitle>Recent Messages</CardTitle>
<CardDescription>Overview of recent messages</CardDescription>
</CardHeader>
<CardContent>
{/* enter reason for fetching */}
{recentMessages.map((message) => (
<CompactMessage
key={message._id}
message={message}
hideUser={Object.keys(userList).length === 0}
users={userList}
/>
))}
</CardContent>
</Card>
);
}

View File

@ -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 (
<div className="flex gap-2">
@ -34,12 +42,73 @@ export function UserActions({ id }: { id: string }) {
>
Account
</Link>
<Button className="flex-1 bg-orange-400 hover:bg-orange-300">
Suspend
</Button>
<Button className="flex-1" variant="destructive">
Ban
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="flex-1 bg-orange-400 hover:bg-orange-300">
Suspend
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to suspend this user?
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() =>
suspendUser(id)
.then(() => toast({ title: "Suspended user" }))
.catch((err) =>
toast({
title: "Failed to suspend user!",
description: err,
variant: "destructive",
})
)
}
>
Suspend
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="flex-1" variant="destructive">
Ban
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to ban this user?
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() =>
banUser(id)
.then(() => toast({ title: "Banned user" }))
.catch((err) =>
toast({
title: "Failed to ban user!",
description: err,
variant: "destructive",
})
)
}
>
Ban
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="flex-1">
@ -79,8 +148,17 @@ export function UserActions({ id }: { id: string }) {
<AlertDialogAction
onClick={() => {
if (!alertMessage.current) return;
sendAlert(id, alertMessage.current);
alertMessage.current = "";
sendAlert(id, alertMessage.current)
.then(() => toast({ title: "Sent Alert" }))
.catch((err) =>
toast({
title: "Failed to send alert!",
description: err,
variant: "destructive",
})
);
}}
>
Send
@ -89,9 +167,13 @@ export function UserActions({ id }: { id: string }) {
</AlertDialogContent>
</AlertDialog>
<DropdownMenuItem>Inspect Messages</DropdownMenuItem>
<DropdownMenuItem>Wipe Messages</DropdownMenuItem>
<DropdownMenuItem>Clear Friends</DropdownMenuItem>
{/* <DropdownMenuItem>
Clear ({counts.pending}) Friend Requests
</DropdownMenuItem>
<DropdownMenuItem>
Clear All ({counts.all}) Relations
</DropdownMenuItem> */}
</DropdownMenuContent>
</DropdownMenu>
</div>

0
exports/.gitkeep Normal file
View File

View File

@ -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<AccountInfo>("accounts")
.updateOne({ _id: userId }, { $set: { disabled: true } });
await mongo().db("revolt").collection<SessionInfo>("sessions").deleteMany({
user_id: userId,
});
}
export async function suspendUser(userId: string) {
await disableAccount(userId);
await mongo()
.db("revolt")
.collection<User>("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<File>("attachments")
.updateMany(
{ _id: { $in: attachmentIds } },
{
$set: {
reported: true,
deleted: true,
},
}
);
}
// delete messages
await mongo().db("revolt").collection<Message>("messages").deleteMany({
author: userId,
});
// 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) {
return await wipeUser(userId, 4);
}

View File

@ -48,6 +48,14 @@ export async function fetchChannelById(id: string) {
.findOne({ _id: id });
}
export async function fetchChannels(query: Filter<Channel>) {
return await mongo()
.db("revolt")
.collection<Channel>("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<Message>) {
export async function fetchMessages(query: Filter<Message>, limit = 50) {
return await mongo()
.db("revolt")
.collection<Message>("messages")
.find(query, { sort: { _id: -1 } })
.find(query, { sort: { _id: -1 }, limit })
.toArray();
}

View File

@ -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",

View File

@ -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: