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";
|
||||
|
||||
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 }) {
|
|||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Link
|
||||
className={`flex-1 ${buttonVariants()}`}
|
||||
href={`/panel/inspect/server/${server._id}/invites`}
|
||||
>
|
||||
Invites
|
||||
</Link>
|
||||
|
||||
<Button className="flex-1" variant="destructive">
|
||||
Quarantine
|
||||
</Button>
|
||||
|
|
|
@ -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",
|
||||
|
|
28
lib/db.ts
28
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<Server>) {
|
|||
.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) {
|
||||
await checkPermission("messages/fetch/by-id", id);
|
||||
|
||||
|
|
Loading…
Reference in New Issue