diff --git a/src/packages/socket/src/components/ServerBotsTab.tsx b/src/packages/socket/src/components/ServerBotsTab.tsx index dd21bca06..f709ec2b5 100644 --- a/src/packages/socket/src/components/ServerBotsTab.tsx +++ b/src/packages/socket/src/components/ServerBotsTab.tsx @@ -21,6 +21,8 @@ type BotEntry = { awaitingClaim: boolean; createdAt: string | Date; decidedAt: string | Date | null; + /** Asked for since it was let in (GRYT-2006). An older server sends none. */ + more?: string[]; }; /** @@ -63,6 +65,9 @@ export function ServerBotsTab({ if (b.status === "pending" && !next[b.registrationId]) { next[b.registrationId] = new Set(b.requested); } + if (b.status === "approved" && b.more?.length && !next[b.registrationId]) { + next[b.registrationId] = new Set(b.more); + } } return next; }); @@ -103,6 +108,7 @@ export function ServerBotsTab({ }; const pending = useMemo(() => (bots ?? []).filter((b) => b.status === "pending"), [bots]); + const wantsMore = useMemo(() => (bots ?? []).filter((b) => b.status === "approved" && (b.more?.length ?? 0) > 0), [bots]); const settled = useMemo(() => (bots ?? []).filter((b) => b.status !== "pending"), [bots]); if (!bots) { @@ -113,8 +119,8 @@ export function ServerBotsTab({
A bot is a member like any other — it holds only what you give it, and it can never - give itself more. What it asked for is fixed the first time it turns up, so a bot - that has been tampered with since cannot come back asking for the keys. + give itself more. If it asks for more later, it keeps what it has and the new ask + waits here for you, so a bot that has been tampered with can ask but can't take. @@ -177,10 +183,66 @@ export function ServerBotsTab({
- {pending.length === 0 ? ( + {wantsMore.map((bot) => { + const chosen = ticked[bot.registrationId] ?? new Set(); + const more = bot.more ?? []; + return ( + +
+
+ {bot.nickname} wants new permissions + {bot.botId && ( + + {bot.botId} + + )} +
+
+ {bot.granted.map((permission) => ( + + ))} + {more.map((permission) => ( + toggle(bot.registrationId, permission, checked)} + /> + ))} +
+
+ + +
+
+
+ ); + })} + + {pending.length === 0 && wantsMore.length === 0 && ( Nothing waiting. - ) : ( - pending.map((bot) => { + )} + {pending.map((bot) => { const chosen = ticked[bot.registrationId] ?? new Set(); return ( @@ -259,8 +321,7 @@ export function ServerBotsTab({ ); - }) - )} + })}
@@ -361,3 +422,27 @@ function RegisterBot({ busy, onCreate }: { busy: boolean; onCreate: (nickname: s ); } + +/** One permission with its explanation. A disabled, ticked one is something the bot already has. */ +function PermissionRow({ + permission, + checked, + disabled, + onChange, +}: { + permission: string; + checked: boolean; + disabled?: boolean; + onChange?: (checked: boolean) => void; +}) { + const meta = describePermission(permission); + return ( + + ); +} diff --git a/src/packages/socket/src/components/serverView.tsx b/src/packages/socket/src/components/serverView.tsx index 92f808031..ec76423a4 100644 --- a/src/packages/socket/src/components/serverView.tsx +++ b/src/packages/socket/src/components/serverView.tsx @@ -53,6 +53,7 @@ import { SIDEBAR_HOVER_PX, SIDEBAR_WIDTH_PX, useMediaAutoShow, useSidebarHover, import { useSidebarEditor } from "../hooks/useSidebarEditor"; import { useSockets } from "../hooks/useSockets"; import { getUpdateAvailable } from "../hooks/useVersionStatus"; +import { useWaitingBots } from "../hooks/useWaitingBots"; import { takeVoiceJoin, usePendingVoiceJoin } from "../hooks/voiceJoinRequest"; import { AVATAR_COLUMN_WIDTH, CHANNEL_ICON_LIST_WIDTH, THREAD_PANEL_WIDTH } from "../lib/narrowLayout"; import { withSharing } from "../lib/screenItems"; @@ -542,12 +543,15 @@ export const ServerView = ({ dmSpace = false }: { dmSpace?: boolean }) => { const viewerPermissions = useServerPermissions(currentlyViewingServer?.host || ""); - const { pendingReportCount, memberListMap } = useServerReports({ + const { pendingReportCount: waitingReports, memberListMap } = useServerReports({ currentConnection, accessToken, currentlyViewingServer, memberLists, /* `has`, not `can`: this drives an automatic `reports:list` on join, before `server:details` arrives, so `can` made a refusal a new member's first sight. */ canViewReports: viewerPermissions.has("view_reports"), }); + // One badge on Server settings for anything waiting in there: reports and bots. + const waitingBots = useWaitingBots(currentConnection, accessToken, viewerPermissions.has("manage_bots")); + const pendingReportCount = waitingReports + waitingBots; const viewingHost = currentlyViewingServer?.host; const [updateAvailable, setUpdateAvailable] = useState(() => diff --git a/src/packages/socket/src/hooks/useWaitingBots.ts b/src/packages/socket/src/hooks/useWaitingBots.ts new file mode 100644 index 000000000..6d464c546 --- /dev/null +++ b/src/packages/socket/src/hooks/useWaitingBots.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import type { Socket } from "socket.io-client"; + +/** Bots at the door, and known bots asking for more (GRYT-2006). Re-asked on every + `server:details`, which is what a knock sends round. */ +export function useWaitingBots(connection: Socket | null, accessToken: string | null, canManageBots: boolean): number { + const [waiting, setWaiting] = useState(0); + + useEffect(() => { + if (!connection || !canManageBots || !accessToken) { + setWaiting(0); + return; + } + const onBots = (payload: { bots?: { status?: string; more?: string[] }[] }) => { + const bots = Array.isArray(payload?.bots) ? payload.bots : []; + setWaiting(bots.filter((b) => b.status === "pending" || (b.status === "approved" && (b.more?.length ?? 0) > 0)).length); + }; + const ask = () => connection.emit("server:bots:list", { accessToken }); + connection.on("server:bots", onBots); + connection.on("server:details", ask); + ask(); + return () => { + connection.off("server:bots", onBots); + connection.off("server:details", ask); + }; + }, [connection, accessToken, canManageBots]); + + return waiting; +}