diff --git a/app/panel/inspect/server/[id]/page.tsx b/app/panel/inspect/server/[id]/page.tsx
index ec414cf..7d38f89 100644
--- a/app/panel/inspect/server/[id]/page.tsx
+++ b/app/panel/inspect/server/[id]/page.tsx
@@ -36,9 +36,11 @@ export default async function Server({ params }: { params: { id: string } }) {
)}
-
+ {
+ owner &&
+ }
diff --git a/components/pages/inspector/ServerActions.tsx b/components/pages/inspector/ServerActions.tsx
index 422e1f9..32d1a0d 100644
--- a/components/pages/inspector/ServerActions.tsx
+++ b/components/pages/inspector/ServerActions.tsx
@@ -14,19 +14,21 @@ import {
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { useState } from "react";
-import { addServerMember, updateServerDiscoverability, updateServerFlags, updateServerOwner } from "@/lib/actions";
+import { addServerMember, quarantineServer, updateServerDiscoverability, updateServerFlags, updateServerOwner } from "@/lib/actions";
import { useToast } from "../../ui/use-toast";
import Link from "next/link";
import { DropdownMenu, DropdownMenuContent } from "@/components/ui/dropdown-menu";
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
-import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import UserSelector from "@/components/ui/user-selector";
+import { Textarea } from "@/components/ui/textarea";
+import { SEVRER_REMOVAL_MESSAGE } from "@/lib/constants";
export function ServerActions({ server }: { server: Server }) {
const [selectBadges, setSelectBadges] = useState(false);
const [serverDraft, setDraft] = useState(server);
+ const [quarantineMessage, setQuarantineMessage] = useState(SEVRER_REMOVAL_MESSAGE(server));
const [newOwner, setNewOwner] = useState(null);
const [newMember, setNewMember] = useState(null);
const [newMemberEvent, setNewMemberEvent] = useState(true);
@@ -144,9 +146,64 @@ export function ServerActions({ server }: { server: Server }) {
Invites
-
+
+
+
+
+
+
+
+ Quarantine server
+
+
+ This will remove all members from this server and revoke all invites.
+ This action is irreversible!
+
+
+
+
+ Cancel
+ {
+ if (serverDraft.flags) {
+ // Intentionally not clearing the quarantine message draft
+ toast({
+ title: "Refusing to quarantine",
+ description: "This server is marked as verified or official",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ try {
+ await quarantineServer(server._id, quarantineMessage);
+ toast({
+ title: "Quarantined server",
+ });
+ setQuarantineMessage(SEVRER_REMOVAL_MESSAGE(server));
+ } catch(e) {
+ toast({
+ title: "Failed to quarantine",
+ description: String(e),
+ variant: "destructive",
+ });
+ }
+ }}
+ >
+ Quarantine
+
+
+
+
diff --git a/lib/accessPermissions.ts b/lib/accessPermissions.ts
index 5922c5b..ed6d08c 100644
--- a/lib/accessPermissions.ts
+++ b/lib/accessPermissions.ts
@@ -37,7 +37,7 @@ type Permission =
| `servers${
| ""
| `/fetch${"" | "/by-id"}`
- | `/update${"" | "/flags" | "/discoverability" | "/owner" | "/add-member"}`}`
+ | `/update${"" | "/flags" | "/discoverability" | "/owner" | "/add-member" | "/quarantine"}`}`
| `users${
| ""
| `/fetch${
@@ -148,6 +148,8 @@ const PermissionSets = {
"channels/fetch/invites",
"channels/create/dm",
+ "servers/update/quarantine",
+
"reports/fetch/related/by-user",
"reports/fetch/related/by-content",
"reports/fetch/related/against-user",
diff --git a/lib/actions.ts b/lib/actions.ts
index 9a30a58..b8c66c2 100644
--- a/lib/actions.ts
+++ b/lib/actions.ts
@@ -17,6 +17,7 @@ import {
Bot,
Channel,
File,
+ Invite,
Member,
Message,
Report,
@@ -622,7 +623,13 @@ export async function addServerMember(serverId: string, userId: string, withEven
.find({ server: serverId })
.toArray();
+ const member = await mongo()
+ .db("revolt")
+ .collection("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")
@@ -648,6 +655,74 @@ export async function addServerMember(serverId: string, userId: string, withEven
}
}
+export async function quarantineServer(serverId: string, message: string) {
+ await checkPermission("servers/update/quarantine", { serverId, message });
+
+ const server = await mongo()
+ .db("revolt")
+ .collection("servers")
+ .findOne({ _id: serverId });
+
+ const members = await mongo()
+ .db("revolt")
+ .collection("server_members")
+ .find({ "_id.server": serverId })
+ .toArray();
+
+ const invites = await mongo()
+ .db("revolt")
+ .collection("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("servers")
+ .updateOne(
+ { _id: serverId },
+ {
+ $set: {
+ owner: "0".repeat(26),
+ analytics: false,
+ discoverable: false,
+ }
+ }
+ );
+
+ await mongo()
+ .db("revolt")
+ .collection("server_members")
+ .deleteMany({ "_id.server": serverId });
+
+ await mongo()
+ .db("revolt")
+ .collection("channel_invites")
+ .deleteMany({ type: "Server", server: serverId });
+
+ await publishMessage(serverId, {
+ type: "ServerDelete",
+ id: serverId,
+ });
+
+ for (const member of members) {
+ await sendAlert(member._id.user, message);
+ }
+}
+
export async function deleteInvite(invite: string) {
await checkPermission("channels/update/invites", invite);
diff --git a/lib/constants.ts b/lib/constants.ts
index 369a646..f428e99 100644
--- a/lib/constants.ts
+++ b/lib/constants.ts
@@ -1,3 +1,5 @@
+import { Server } from "revolt-api";
+
export const PLATFORM_MOD_ID =
process.env.PLATFORM_MOD_ID || "01FC17E1WTM2BGE4F3ARN3FDAF";
export const AUTUMN_URL =
@@ -14,3 +16,5 @@ export const RESTRICT_ACCESS_LIST = [
"01EXAHMSGNDCAZTJXDJZ0BK8N3", //- wait what
"01FEEFJCKY5C4DMMJYZ20ACWWC", //- rexo
];
+
+export const SEVRER_REMOVAL_MESSAGE = (server: Server) => `A server you were in ("${server.name}") has been removed from Revolt for the following reason:\n- CHANGEME`;