- 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
@@ -116,8 +116,8 @@ function DiscordPicker({
|
||||
const channelOnHere = channel?.stream?.node_id === nodeId;
|
||||
const check = guildId ? checks[guildId] : undefined;
|
||||
const checkOn = check?.data?.enabled !== false;
|
||||
const verdictOf = (c: DiscordGuildOption["voice_channels"][number]) => {
|
||||
const row = check?.data && checkOn ? check.data.channels[c.channel_id] : undefined;
|
||||
const verdictOf = (c: DiscordGuildOption["voice_channels"][number], data: VoiceCheck | null | undefined = check?.data) => {
|
||||
const row = data && data.enabled !== false ? data.channels[c.channel_id] : undefined;
|
||||
const v = channelVerdict(row);
|
||||
// This system already plays here: c2-core counts its bot as busy (it holds
|
||||
// the channel), so the channel can read "Needs fix"/"Bots busy" for a NEW
|
||||
@@ -128,19 +128,26 @@ function DiscordPicker({
|
||||
};
|
||||
const chosenVerdict = channel ? verdictOf(channel) : null;
|
||||
const definiteDeny = blocksStart(chosenVerdict);
|
||||
// "Not a voice channel" can't change, so Start stays off. "Needs fix" can:
|
||||
// Start re-checks with Discord first (server-26#256), so fixing it in
|
||||
// Discord and pressing Start again just works — no hunting for Re-check.
|
||||
const startOff = chosenVerdict?.kind === "not_voice";
|
||||
// Not for "invalid": that bot is already in the server; inviting won't help (#257).
|
||||
const showInvite = chosenVerdict?.kind === "fix" || chosenVerdict?.kind === "no_bot" || chosenVerdict?.kind === "busy";
|
||||
|
||||
const runCheck = useCallback(
|
||||
async (gid: string, refresh: boolean) => {
|
||||
async (gid: string, refresh: boolean): Promise<VoiceCheck | null> => {
|
||||
setChecks((m) => ({ ...m, [gid]: { data: m[gid]?.data ?? null, busy: true, error: null, limited: false } }));
|
||||
try {
|
||||
const data = normalizeVoiceCheck(await c2api.getNodeVoiceCheck(nodeId, gid, refresh));
|
||||
setChecks((m) => ({ ...m, [gid]: { data, busy: false, error: null, limited: refresh && data.refreshed === false } }));
|
||||
return data;
|
||||
} catch (err) {
|
||||
const e = parseC2Error(err);
|
||||
// 404 = route not deployed yet or server not visible: no badges (fail-open).
|
||||
const msg = e.status === 404 ? null : e.message;
|
||||
setChecks((m) => ({ ...m, [gid]: { data: m[gid]?.data ?? null, busy: false, error: msg, limited: false } }));
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[nodeId],
|
||||
@@ -166,6 +173,12 @@ function DiscordPicker({
|
||||
setSending(true);
|
||||
setError(null);
|
||||
try {
|
||||
if (definiteDeny) {
|
||||
// Fresh answer from Discord; still refused = stay here, the row shows why.
|
||||
// A failed check fails open, like everywhere else in the picker.
|
||||
const fresh = await runCheck(guild.guild_id, true);
|
||||
if (fresh && blocksStart(verdictOf(channel, fresh))) return;
|
||||
}
|
||||
await c2api.startNodeDiscordStream(nodeId, { guild_id: guild.guild_id, channel_id: channel.channel_id, mode });
|
||||
onStarted();
|
||||
onClose();
|
||||
@@ -382,11 +395,17 @@ function DiscordPicker({
|
||||
<button
|
||||
type="button"
|
||||
onClick={start}
|
||||
disabled={sending || !channel || !options.system_id || (atNodeCap && !channelOnHere) || definiteDeny}
|
||||
title={definiteDeny ? "No radio bot in this server can stream into this channel — fix the permission in Discord, then Re-check." : undefined}
|
||||
disabled={sending || !channel || !options.system_id || (atNodeCap && !channelOnHere) || startOff}
|
||||
title={
|
||||
startOff
|
||||
? "That is not a voice channel. Pick a voice channel."
|
||||
: definiteDeny
|
||||
? "Fixed the permission in Discord? Start checks again first."
|
||||
: undefined
|
||||
}
|
||||
className="flex-1 bg-accent hover:brightness-110 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg py-2 text-sm font-semibold"
|
||||
>
|
||||
{sending ? "Starting…" : channelOnHere ? "Update" : "Start radio"}
|
||||
{sending ? (definiteDeny ? "Checking…" : "Starting…") : channelOnHere ? "Update" : "Start radio"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user