- discord_oauth: POST /discord/oauth/start and GET /options take an optional system_id. The system must be the org's own or shared (another org's or unknown = 404). Its preferred bot is strict: one that can't connect is a 404, never a quiet swap to Automatic. No preference = the usual order. The picker's "bot for a system" now always sends system_id. - #257: _channel_state gives a bot whose token Discord rejected its own "invalid" state; the picker shows "Radio bot needs replacing" with no invite links, instead of "No radio bot". - #256: Start no longer stays disabled on "Needs fix". It re-checks with Discord first (refresh=1) and starts only if the fresh answer allows it. "Not a voice channel" still disables Start. - #258: own tooltip for "Not a voice channel"; "platform admin" -> "DRB support"; PreferredBotsCard says plainly that adding your own bot isn't self-serve (no support address exists yet, see #47), and a load failure gets a plain message + Try again instead of the raw C2 error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
13a3525e4c
commit
32d223e556
@@ -20,6 +20,7 @@ import {
|
||||
type ParsedC2Error,
|
||||
type VoiceCheck,
|
||||
type VoiceCheckBot,
|
||||
type VoiceCheckChannel,
|
||||
} from "@/lib/discordPicker";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { DIM, Field, INPUT, InviteLinks, Panel, Status, Table, TD } from "./ui";
|
||||
@@ -70,17 +71,19 @@ function AddChannelForm({
|
||||
|
||||
const cid = channelId.trim();
|
||||
const checkKey = SNOWFLAKE.test(cid) && guildId ? `${guildId}:${cid}` : "";
|
||||
const runCheck = useCallback(async (key: string, refresh: boolean) => {
|
||||
const runCheck = useCallback(async (key: string, refresh: boolean): Promise<VoiceCheck | null> => {
|
||||
const [gid, ch] = key.split(":");
|
||||
setCheck((c) => ({ key, data: c?.key === key ? c.data : null, busy: true, error: null, limited: false }));
|
||||
try {
|
||||
const data = normalizeVoiceCheck(await c2api.getGuildVoiceCheck(gid, ch, refresh));
|
||||
setCheck((c) => (c?.key === key ? { key, data, busy: false, error: null, limited: refresh && data.refreshed === false } : c));
|
||||
return data;
|
||||
} catch (err) {
|
||||
const e = parseStartError(err);
|
||||
setCheck((c) =>
|
||||
c?.key === key ? { key, data: c.data, busy: false, error: e.status === 404 ? null : e.message, limited: false } : c,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
// Check the channel once its ID looks complete (debounced while typing).
|
||||
@@ -98,8 +101,8 @@ function AddChannelForm({
|
||||
const verdict = liveCheck?.data && checkOn ? channelVerdict(checked) : null;
|
||||
const shownName = channelName.trim() || checked?.name || null;
|
||||
// Per-bot result, by token id (the admin route sends ids) or else by name.
|
||||
const botCheck = (b: RadioBot): VoiceCheckBot | undefined =>
|
||||
checked?.bots.find((x) => (x.token_id ? x.token_id === b.token_id : x.name === b.name || x.name === b.bot_name));
|
||||
const botCheck = (b: RadioBot, row: VoiceCheckChannel | undefined = checked): VoiceCheckBot | undefined =>
|
||||
row?.bots.find((x) => (x.token_id ? x.token_id === b.token_id : x.name === b.name || x.name === b.bot_name));
|
||||
|
||||
const org = guilds.find((g) => g.guild_id === guildId)?.acting_org_id ?? null;
|
||||
const systems = overview.systems.filter((s) => s.org_id == null || s.org_id === org);
|
||||
@@ -124,6 +127,13 @@ function AddChannelForm({
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
if (checkKey && (verdict?.kind === "fix" || chosenBotDenied)) {
|
||||
// "Needs fix" can change: ask Discord again before refusing (server-26#256).
|
||||
// Still refused = stay here, the row shows why; a failed check fails open.
|
||||
const fresh = await runCheck(checkKey, true);
|
||||
const row = fresh && fresh.enabled !== false ? fresh.channels[cid] : undefined;
|
||||
if (row && (blocksStart(channelVerdict(row)) || (chosenBot && botCheck(chosenBot, row)?.ok === false))) return;
|
||||
}
|
||||
const res = await c2api.startDiscordStream({
|
||||
guild_id: guildId,
|
||||
channel_id: channelId.trim(),
|
||||
@@ -221,7 +231,7 @@ function AddChannelForm({
|
||||
</div>
|
||||
{verdict && <FixText verdict={verdict} channelName={shownName} />}
|
||||
{chosenBotDenied && (
|
||||
<div className="text-xs text-sev-major">The bot you picked can't stream into this channel — pick another, or fix it in Discord and Re-check.</div>
|
||||
<div className="text-xs text-sev-major">The bot you picked can't stream into this channel — pick another, or fix it in Discord and press Start again.</div>
|
||||
)}
|
||||
{checkOn && <BotDetails channel={checked} channelName={shownName} />}
|
||||
<CheckFooter
|
||||
@@ -252,10 +262,16 @@ function AddChannelForm({
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={saving || !systemId || blocksStart(verdict) || chosenBotDenied}
|
||||
title={blocksStart(verdict) ? "No radio bot in this server can stream into this channel — fix the permission in Discord, then Re-check." : undefined}
|
||||
disabled={saving || !systemId || verdict?.kind === "not_voice"}
|
||||
title={
|
||||
verdict?.kind === "not_voice"
|
||||
? "That is not a voice channel. Pick a voice channel."
|
||||
: blocksStart(verdict) || chosenBotDenied
|
||||
? "Fixed the permission in Discord? Start checks again first."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{saving ? "Starting…" : "Start radio channel"}
|
||||
{saving ? (blocksStart(verdict) || chosenBotDenied ? "Checking…" : "Starting…") : "Start radio channel"}
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant="ghost" onClick={onCancel}>Cancel</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user