forked from administration/panel
feat: server invite list
parent
9249b4e58d
commit
a04a10f492
|
@ -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 (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<NavigationToolbar>Inspecting Server Invites</NavigationToolbar>
|
||||||
|
<ServerCard server={server} subtitle={`${invites.length} invite${invites.length == 1 ? '' : 's'}`} />
|
||||||
|
<ServerInviteList invites={invites} server={server} channels={channels} users={users} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<Card className="my-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center">
|
||||||
|
<span className="font-extralight mr-0.5 select-none">rvlt.gg/</span>{invite._id}
|
||||||
|
<span className="select-none">{" "}</span> {/* looks better like this when for some reason the css doesnt load */}
|
||||||
|
{invite.vanity
|
||||||
|
? <span
|
||||||
|
className="select-none ml-2 p-1.5 bg-gray-400 text-white rounded-md font-normal text-base"
|
||||||
|
>
|
||||||
|
Vanity
|
||||||
|
</span>
|
||||||
|
: <></>}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{invite.type}
|
||||||
|
{" • "}
|
||||||
|
<Link href={`/panel/inspect/channel/${invite.channel}`}>
|
||||||
|
{(
|
||||||
|
channel &&
|
||||||
|
channel.channel_type != "DirectMessage" &&
|
||||||
|
channel.channel_type != "SavedMessages"
|
||||||
|
)
|
||||||
|
? `#${channel.name}`
|
||||||
|
: <i>Unknown Channel</i>}
|
||||||
|
</Link>
|
||||||
|
{" • "}
|
||||||
|
<Link href={`/panel/inspect/user/${invite.creator}`}>
|
||||||
|
{user ? `${user.username}#${user.discriminator}` : <i>Unknown Creator</i>}
|
||||||
|
</Link>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Popover open={selectVanityOnly} onOpenChange={setSelectVanityOnly}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={selectVanityOnly}
|
||||||
|
className="flex-1 justify-between"
|
||||||
|
>
|
||||||
|
{vanityOnly ? "Vanity" : "All"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0">
|
||||||
|
<Command>
|
||||||
|
{[
|
||||||
|
{ value: false, label: "All" },
|
||||||
|
{ value: true, label: "Vanity" },
|
||||||
|
].map((option) => (
|
||||||
|
<CommandItem
|
||||||
|
key={String(option.value)}
|
||||||
|
onSelect={async () => {
|
||||||
|
setSelectVanityOnly(false);
|
||||||
|
setVanityOnly(option.value);
|
||||||
|
}}
|
||||||
|
>{option.label}</CommandItem>
|
||||||
|
))}
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover open={selectChannel} onOpenChange={setSelectChannel}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={selectChannel}
|
||||||
|
className="flex-1 justify-between"
|
||||||
|
>
|
||||||
|
{channelFilter
|
||||||
|
? (channels?.find(c => c._id == channelFilter) as any)?.name
|
||||||
|
: "Select channel"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandItem onSelect={async () => {
|
||||||
|
setSelectChannel(false);
|
||||||
|
setChannelFilter("");
|
||||||
|
}}
|
||||||
|
>All channels</CommandItem>
|
||||||
|
{channels?.map((channel) => (
|
||||||
|
<CommandItem
|
||||||
|
key={String(channel._id)}
|
||||||
|
onSelect={async () => {
|
||||||
|
setSelectChannel(false);
|
||||||
|
setChannelFilter(channel._id);
|
||||||
|
}}
|
||||||
|
>{'#' + (channel as any).name}</CommandItem>
|
||||||
|
))}
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Popover open={selectUser} onOpenChange={setSelectUser}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={selectUser}
|
||||||
|
className="flex-1 justify-between"
|
||||||
|
>
|
||||||
|
{userFilter
|
||||||
|
? `${users?.find(c => c._id == userFilter)?.username}#${users?.find(c => c._id == userFilter)?.discriminator}`
|
||||||
|
: "Select user"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandItem onSelect={async () => {
|
||||||
|
setSelectUser(false);
|
||||||
|
setUserFilter("");
|
||||||
|
}}
|
||||||
|
>All users</CommandItem>
|
||||||
|
{users?.map((user) => (
|
||||||
|
<CommandItem
|
||||||
|
key={String(user._id)}
|
||||||
|
onSelect={async () => {
|
||||||
|
setSelectUser(false);
|
||||||
|
setUserFilter(user._id);
|
||||||
|
}}
|
||||||
|
>{user.username}#{user.discriminator}</CommandItem>
|
||||||
|
))}
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredInvites.map(invite => (<InviteCard
|
||||||
|
invite={invite}
|
||||||
|
channel={channels?.find(c => c._id == invite.channel)}
|
||||||
|
user={users?.find(c => c._id == invite.creator)}
|
||||||
|
key={invite._id}
|
||||||
|
/>))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,12 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Server } from "revolt-api";
|
import { Server } from "revolt-api";
|
||||||
import { Button } from "../../ui/button";
|
import { Button, buttonVariants } from "../../ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
CommandItem,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import {
|
import {
|
||||||
|
@ -19,6 +16,7 @@ import { cn } from "@/lib/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { updateServerDiscoverability, updateServerFlags } from "@/lib/actions";
|
import { updateServerDiscoverability, updateServerFlags } from "@/lib/actions";
|
||||||
import { useToast } from "../../ui/use-toast";
|
import { useToast } from "../../ui/use-toast";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export function ServerActions({ server }: { server: Server }) {
|
export function ServerActions({ server }: { server: Server }) {
|
||||||
const [selectBadges, setSelectBadges] = useState(false);
|
const [selectBadges, setSelectBadges] = useState(false);
|
||||||
|
@ -130,6 +128,13 @@ export function ServerActions({ server }: { server: Server }) {
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={`flex-1 ${buttonVariants()}`}
|
||||||
|
href={`/panel/inspect/server/${server._id}/invites`}
|
||||||
|
>
|
||||||
|
Invites
|
||||||
|
</Link>
|
||||||
|
|
||||||
<Button className="flex-1" variant="destructive">
|
<Button className="flex-1" variant="destructive">
|
||||||
Quarantine
|
Quarantine
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -16,7 +16,7 @@ type Permission =
|
||||||
| ""
|
| ""
|
||||||
| `/fetch${"" | "/by-id" | "/by-user"}`
|
| `/fetch${"" | "/by-id" | "/by-user"}`
|
||||||
| `/update${"" | "/discoverability"}`}`
|
| `/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"}`}`
|
| `messages${"" | `/fetch${"" | "/by-id" | "/by-user"}`}`
|
||||||
| `reports${
|
| `reports${
|
||||||
| ""
|
| ""
|
||||||
|
@ -125,6 +125,7 @@ const PermissionSets = {
|
||||||
"messages/fetch/by-id",
|
"messages/fetch/by-id",
|
||||||
"channels/fetch/by-id",
|
"channels/fetch/by-id",
|
||||||
"channels/fetch/dm",
|
"channels/fetch/dm",
|
||||||
|
"channels/fetch/invites",
|
||||||
"channels/create/dm",
|
"channels/create/dm",
|
||||||
|
|
||||||
"reports/fetch/related/by-user",
|
"reports/fetch/related/by-user",
|
||||||
|
|
28
lib/db.ts
28
lib/db.ts
|
@ -5,6 +5,7 @@ import type {
|
||||||
AccountStrike,
|
AccountStrike,
|
||||||
Bot,
|
Bot,
|
||||||
Channel,
|
Channel,
|
||||||
|
Invite,
|
||||||
Message,
|
Message,
|
||||||
Report,
|
Report,
|
||||||
Server,
|
Server,
|
||||||
|
@ -286,6 +287,33 @@ export async function fetchServers(query: Filter<Server>) {
|
||||||
.toArray();
|
.toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `vanity` should eventually be added to the backend as well
|
||||||
|
export type ChannelInvite = Invite & { vanity?: boolean }
|
||||||
|
|
||||||
|
export async function fetchInvites(query: Filter<ChannelInvite>) {
|
||||||
|
await checkPermission("channels/fetch/invites", query);
|
||||||
|
|
||||||
|
const invites = await mongo()
|
||||||
|
.db("revolt")
|
||||||
|
.collection<ChannelInvite>("channel_invites")
|
||||||
|
.find(query)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
const channels = await mongo()
|
||||||
|
.db("revolt")
|
||||||
|
.collection<Channel>("channels")
|
||||||
|
.find({ _id: { $in: invites.map((invite) => invite.channel) } })
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
const users = await mongo()
|
||||||
|
.db("revolt")
|
||||||
|
.collection<User>("users")
|
||||||
|
.find({ _id: { $in: invites.map((invite) => invite.creator) } })
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
return { invites, channels, users }
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchMessageById(id: string) {
|
export async function fetchMessageById(id: string) {
|
||||||
await checkPermission("messages/fetch/by-id", id);
|
await checkPermission("messages/fetch/by-id", id);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue