// Node-page Discord picker (server-26#213 item 3, #109 item 13). // Shapes returned by c2-core app/routers/discord_web.py. No token strings — // the server never sends them to the browser. export type StreamState = | "pending" | "connecting" | "connected" | "error" | "stopping" | "stopped" | "node_offline" | "paused_locally"; export interface DiscordStreamView { stream_id: string; guild_id: string; channel_id: string; guild_name: string | null; channel_name: string | null; system_id?: string | null; system_name: string | null; /** The radio bot carrying it — tells apart several streams in one voice channel. */ token_name?: string | null; mode: "persistent" | "adhoc" | null; node_id: string | null; node_name: string | null; actual: { state: StreamState; error: string | null; code?: string | null; since?: string; at?: string } | null; updated_at: string | null; } export interface DiscordInvite { name: string; invite_url: string; } export interface DiscordVoiceChannel { channel_id: string; name: string; category: string | null; /** This node's system's stream in the channel, if any — what a pick restarts or moves. */ stream: DiscordStreamView | null; /** Every live stream in the channel (several systems can share it, each its own bot). */ streams?: DiscordStreamView[]; } export interface DiscordGuildOption { guild_id: string; name: string | null; linked: boolean; synced: boolean; synced_at: string | null; voice_channels: DiscordVoiceChannel[]; streams_on: number; bot: { ok: boolean; code: string | null; message: string | null; invite: DiscordInvite[] }; } export interface DiscordJoinOptions { node_id: string; node_name: string | null; node_online: boolean; system_id: string | null; system_name: string | null; can_control: boolean; /** Permanent removal: platform admin or the org owner only (operators can stop, not remove). */ can_remove?: boolean; limits: { node_max: number; node_used: number; guild_max: number }; guilds: DiscordGuildOption[]; streams: DiscordStreamView[]; } export interface DiscordStartResult { ok: boolean; stream_id: string; token_id: string | null; delivered: boolean | null; reused: boolean | null; } /** c2api's request() throws `C2 API error : `. Pull the * detail + invite links back out of a JSON body when there is one. */ export function parseC2Error(err: unknown): { message: string; invite: DiscordInvite[] } { const raw = err instanceof Error ? err.message : String(err); const idx = raw.indexOf(": "); if (idx >= 0) { try { const body = JSON.parse(raw.slice(idx + 2)); const detail = typeof body?.detail === "string" ? body.detail : null; const invite = Array.isArray(body?.invite) ? (body.invite as DiscordInvite[]).filter((i) => i && typeof i.invite_url === "string") : []; if (detail) return { message: detail, invite }; } catch { /* not JSON — fall through */ } } return { message: raw, invite: [] }; }