From 944ad2896db6840660c03deb67a1bd85884eaccb Mon Sep 17 00:00:00 2001 From: Sivert <316918682+grytchat@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:04:50 +0100 Subject: [PATCH 1/3] Bots: a known bot asking for more permissions (GRYT-2006) An approved bot that asks for more now shows under Waiting for an answer as " wants new permissions": what it holds, ticked and greyed out, then the new ones as checkboxes. Approve sends what it had plus the ticked new ones; Decline leaves it as it was. The intro no longer says the list is fixed. Co-Authored-By: Claude Opus 5.5 --- .../socket/src/components/ServerBotsTab.tsx | 94 ++++++++++++++++++- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/src/packages/socket/src/components/ServerBotsTab.tsx b/src/packages/socket/src/components/ServerBotsTab.tsx index dd21bca06..ca6299138 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,9 +183,65 @@ 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.length === 0 ? null : ( pending.map((bot) => { const chosen = ticked[bot.registrationId] ?? new Set(); return ( @@ -361,3 +423,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 ( + + ); +} From ee80b3dc8df55d1a16621a90d63d900058eea4bd Mon Sep 17 00:00:00 2001 From: Sivert <316918682+grytchat@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:07:14 +0100 Subject: [PATCH 2/3] Server settings counts waiting bots with waiting reports (GRYT-2006) Nothing told an admin a bot was at the door: a knock wrote the audit log and refreshed the Bots list only if it was open. The red count on Server settings now adds bots waiting to be let in and known bots asking for more, for anyone holding manage_bots. It asks again on every server:details, which is what a knock sends round. Co-Authored-By: Claude Opus 5.5 --- .../socket/src/components/serverView.tsx | 6 +++- .../socket/src/hooks/useWaitingBots.ts | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/packages/socket/src/hooks/useWaitingBots.ts 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; +} From e166fa76321a6f16cced90a964aacaac1c34219c Mon Sep 17 00:00:00 2001 From: Sivert <316918682+grytchat@users.noreply.github.com> Date: Tue, 3 Nov 2026 13:12:21 +0100 Subject: [PATCH 3/3] Bots tab: no nested ternary (GRYT-2006) Co-Authored-By: Claude Opus 5.5 --- src/packages/socket/src/components/ServerBotsTab.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/packages/socket/src/components/ServerBotsTab.tsx b/src/packages/socket/src/components/ServerBotsTab.tsx index ca6299138..f709ec2b5 100644 --- a/src/packages/socket/src/components/ServerBotsTab.tsx +++ b/src/packages/socket/src/components/ServerBotsTab.tsx @@ -239,10 +239,10 @@ export function ServerBotsTab({ ); })} - {pending.length === 0 && wantsMore.length === 0 ? ( + {pending.length === 0 && wantsMore.length === 0 && ( Nothing waiting. - ) : pending.length === 0 ? null : ( - pending.map((bot) => { + )} + {pending.map((bot) => { const chosen = ticked[bot.registrationId] ?? new Set(); return ( @@ -321,8 +321,7 @@ export function ServerBotsTab({ ); - }) - )} + })}