diff --git a/app/panel/inspect/server/[id]/invites/page.tsx b/app/panel/inspect/server/[id]/invites/page.tsx
new file mode 100644
index 0000000..67724b2
--- /dev/null
+++ b/app/panel/inspect/server/[id]/invites/page.tsx
@@ -0,0 +1,23 @@
+import { ServerCard } from "@/components/cards/ServerCard";
+import { NavigationToolbar } from "@/components/common/NavigationToolbar";
+import ServerInviteList from "@/components/pages/inspector/InviteList";
+import { fetchInvites, fetchServerById } from "@/lib/db";
+import { notFound } from "next/navigation";
+
+export default async function ServerInvites({ params }: { params: { id: string } }) {
+ const server = await fetchServerById(params.id);
+ if (!server) return notFound();
+
+ const { invites, channels, users } = await fetchInvites({
+ type: "Server",
+ server: params.id,
+ });
+
+ return (
+
+ Inspecting Server Invites
+
+
+
+ )
+}
diff --git a/components/cards/InviteCard.tsx b/components/cards/InviteCard.tsx
new file mode 100644
index 0000000..d4b38ac
--- /dev/null
+++ b/components/cards/InviteCard.tsx
@@ -0,0 +1,51 @@
+"use client"
+
+import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card";
+import { ChannelInvite } from "@/lib/db";
+import Link from "next/link";
+import { Channel, User } from "revolt-api";
+
+export function InviteCard({
+ invite,
+ channel,
+ user,
+}: {
+ invite: ChannelInvite;
+ channel?: Channel;
+ user?: User;
+}) {
+ return (
+
+
+
+ rvlt.gg/{invite._id}
+ {" "} {/* looks better like this when for some reason the css doesnt load */}
+ {invite.vanity
+ ?
+ Vanity
+
+ : <>>}
+
+
+ {invite.type}
+ {" • "}
+
+ {(
+ channel &&
+ channel.channel_type != "DirectMessage" &&
+ channel.channel_type != "SavedMessages"
+ )
+ ? `#${channel.name}`
+ : Unknown Channel}
+
+ {" • "}
+
+ {user ? `${user.username}#${user.discriminator}` : Unknown Creator}
+
+
+
+
+ );
+}
diff --git a/components/pages/inspector/InviteList.tsx b/components/pages/inspector/InviteList.tsx
new file mode 100644
index 0000000..80d57de
--- /dev/null
+++ b/components/pages/inspector/InviteList.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import { InviteCard } from "@/components/cards/InviteCard";
+import { Button } from "@/components/ui/button";
+import { Command, CommandItem } from "@/components/ui/command";
+import { Input } from "@/components/ui/input";
+import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
+import { ChannelInvite } from "@/lib/db";
+import { ChevronsUpDown } from "lucide-react";
+import { useMemo, useState } from "react";
+import { Channel, Server, User } from "revolt-api";
+
+export default function ServerInviteList({ server, invites, channels, users }: {
+ server: Server,
+ invites: ChannelInvite[],
+ channels?: Channel[],
+ users?: User[],
+}) {
+ const [selectVanityOnly, setSelectVanityOnly] = useState(false);
+ const [selectChannel, setSelectChannel] = useState(false);
+ const [selectUser, setSelectUser] = useState(false);
+ const [vanityOnly, setVanityOnly] = useState(false);
+ const [channelFilter, setChannelFilter] = useState("");
+ const [userFilter, setUserFilter] = useState("");
+
+ const filteredInvites = useMemo(() => {
+ return invites
+ ?.filter(invite => vanityOnly ? invite.vanity : true)
+ ?.filter(invite => channelFilter ? invite.channel == channelFilter : true)
+ ?.filter(invite => userFilter ? invite.creator == userFilter : true);
+ }, [vanityOnly, channelFilter, userFilter, invites]);
+
+ return (
+
+
+
+
+
+
+
+
+ {[
+ { value: false, label: "All" },
+ { value: true, label: "Vanity" },
+ ].map((option) => (
+ {
+ setSelectVanityOnly(false);
+ setVanityOnly(option.value);
+ }}
+ >{option.label}
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {
+ setSelectChannel(false);
+ setChannelFilter("");
+ }}
+ >All channels
+ {channels?.map((channel) => (
+ {
+ setSelectChannel(false);
+ setChannelFilter(channel._id);
+ }}
+ >{'#' + (channel as any).name}
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {
+ setSelectUser(false);
+ setUserFilter("");
+ }}
+ >All users
+ {users?.map((user) => (
+ {
+ setSelectUser(false);
+ setUserFilter(user._id);
+ }}
+ >{user.username}#{user.discriminator}
+ ))}
+
+
+
+
+
+ {filteredInvites.map(invite => (
c._id == invite.channel)}
+ user={users?.find(c => c._id == invite.creator)}
+ key={invite._id}
+ />))}
+
+ )
+}
diff --git a/components/pages/inspector/ServerActions.tsx b/components/pages/inspector/ServerActions.tsx
index 36e299d..6d9b6c3 100644
--- a/components/pages/inspector/ServerActions.tsx
+++ b/components/pages/inspector/ServerActions.tsx
@@ -1,12 +1,9 @@
"use client";
import { Server } from "revolt-api";
-import { Button } from "../../ui/button";
+import { Button, buttonVariants } from "../../ui/button";
import {
Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
@@ -19,6 +16,7 @@ import { cn } from "@/lib/utils";
import { useState } from "react";
import { updateServerDiscoverability, updateServerFlags } from "@/lib/actions";
import { useToast } from "../../ui/use-toast";
+import Link from "next/link";
export function ServerActions({ server }: { server: Server }) {
const [selectBadges, setSelectBadges] = useState(false);
@@ -130,6 +128,13 @@ export function ServerActions({ server }: { server: Server }) {
+
+ Invites
+
+
diff --git a/lib/accessPermissions.ts b/lib/accessPermissions.ts
index 3790b3e..be8e432 100644
--- a/lib/accessPermissions.ts
+++ b/lib/accessPermissions.ts
@@ -16,7 +16,7 @@ type Permission =
| ""
| `/fetch${"" | "/by-id" | "/by-user"}`
| `/update${"" | "/discoverability"}`}`
- | `channels${"" | `/fetch${"" | "/by-id" | "/dm"}` | `/create${"" | "/dm"}`}`
+ | `channels${"" | `/fetch${"" | "/by-id" | "/by-server" | "/dm" | "/invites"}` | `/create${"" | "/dm"}`}`
| `messages${"" | `/fetch${"" | "/by-id" | "/by-user"}`}`
| `reports${
| ""
@@ -125,6 +125,7 @@ const PermissionSets = {
"messages/fetch/by-id",
"channels/fetch/by-id",
"channels/fetch/dm",
+ "channels/fetch/invites",
"channels/create/dm",
"reports/fetch/related/by-user",
diff --git a/lib/db.ts b/lib/db.ts
index 14d810c..de6652a 100644
--- a/lib/db.ts
+++ b/lib/db.ts
@@ -5,6 +5,7 @@ import type {
AccountStrike,
Bot,
Channel,
+ Invite,
Message,
Report,
Server,
@@ -286,6 +287,33 @@ export async function fetchServers(query: Filter) {
.toArray();
}
+// `vanity` should eventually be added to the backend as well
+export type ChannelInvite = Invite & { vanity?: boolean }
+
+export async function fetchInvites(query: Filter) {
+ await checkPermission("channels/fetch/invites", query);
+
+ const invites = await mongo()
+ .db("revolt")
+ .collection("channel_invites")
+ .find(query)
+ .toArray();
+
+ const channels = await mongo()
+ .db("revolt")
+ .collection("channels")
+ .find({ _id: { $in: invites.map((invite) => invite.channel) } })
+ .toArray();
+
+ const users = await mongo()
+ .db("revolt")
+ .collection("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);