Discord: channel list and presence without the main bot (server-26#225 S5)
A server linked to an org now shows in the node picker while a radio bot the org may use is in it, even when the DRB server bot is absent or left. Its voice-channel list is read from that radio bot's cached REST snapshot (discord_rest.guild_directory, the same snapshot the permission check reads, so no extra Discord request): inline when no list is stored, in the background when the list is older than discord_directory_refresh_seconds. The server bot's own fresh list still wins (back-compat) and its directory push now drops the server's cached snapshots. Guards: a server acting only through the unlinked-server fallback org still needs the server bot; REST never creates a discord_guilds doc, never writes org_id or bot_present, and never writes onto a server unlinked meanwhile. Also fixes server-26#251: an interactive start (/join, admin page, web picker, node discord_join) re-reads a cached "denied" snapshot older than 10 s once before refusing, so "fix it in Discord, then try again" works on the next try. The reconciler never re-reads (no REST loop). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e54191a5d0
commit
a976464e72
@@ -53,6 +53,7 @@ A retained MQTT message therefore never stores a bot credential on the broker.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional
|
||||
|
||||
@@ -352,14 +353,39 @@ async def check_bots(tokens: dict[str, dict], token_ids: list[str], guild_id: st
|
||||
for t in token_ids}
|
||||
|
||||
|
||||
# server-26#251: an interactive start (a person pressed Start / ran /join)
|
||||
# re-reads a bot's snapshot that says "denied" once it is this old, so a
|
||||
# permission fixed in Discord a moment ago is seen without waiting out the
|
||||
# cache. The reconciler never does (plan finding 4: no REST loop).
|
||||
RECHECK_DENIED_AFTER_SECONDS = 10.0
|
||||
|
||||
|
||||
async def _perm_check(*, org_id: str, guild_id: str, channel_id: str, stream_id: str,
|
||||
preferred_token_id: Optional[str], node_ids: set) -> Optional[dict]:
|
||||
preferred_token_id: Optional[str], node_ids: set,
|
||||
recheck_denied: bool = False) -> Optional[dict]:
|
||||
if not settings.discord_perm_check_enabled:
|
||||
return None
|
||||
tokens = {t["token_id"]: t for t in await fstore.collection_list(leases.TOKENS) if t.get("token_id")}
|
||||
ids = tokens_to_check(tokens, guild_id=guild_id, org_id=org_id, node_ids=node_ids,
|
||||
exclude_stream_id=stream_id, preferred_token_id=preferred_token_id)
|
||||
return await check_bots(tokens, ids, guild_id, channel_id)
|
||||
snaps = await gather_snapshots(tokens, ids, guild_id)
|
||||
|
||||
def verdict(t: str) -> dict:
|
||||
return evaluate_access(snaps[t], channel_id, bot=tokens[t].get("name") or t) if snaps.get(t) \
|
||||
else _unanswered()
|
||||
|
||||
out = {t: verdict(t) for t in ids}
|
||||
if recheck_denied:
|
||||
now = time.time()
|
||||
stale = [t for t in ids if out[t]["status"] == leases.PERM_DENIED
|
||||
and now - (getattr(snaps.get(t), "fetched_at", None) or now) >= RECHECK_DENIED_AFTER_SECONDS]
|
||||
if stale:
|
||||
fresh = await gather_snapshots(tokens, stale, guild_id, refresh=True)
|
||||
for t, snap in fresh.items():
|
||||
if snap is not None: # not answered in time: keep the cached verdict
|
||||
snaps[t] = snap
|
||||
out[t] = verdict(t)
|
||||
return out
|
||||
|
||||
|
||||
def _channel_ref(perms: Optional[dict], channel_name: Optional[str]) -> str:
|
||||
@@ -484,6 +510,7 @@ async def start_stream(
|
||||
preferred_token_id: Optional[str] = None,
|
||||
max_streams: Optional[int] = None,
|
||||
stream_id: Optional[str] = None,
|
||||
interactive: bool = False,
|
||||
) -> dict:
|
||||
"""Put ``system_id`` on a voice channel.
|
||||
|
||||
@@ -504,6 +531,12 @@ async def start_stream(
|
||||
channel first (``_perm_check``; server-26#225). Every entry point — the
|
||||
bot's /join, the web picker, the node command and the reconciler —
|
||||
comes through here, so none can hand a node a bot Discord will refuse.
|
||||
|
||||
``interactive``: a person asked for this start (the routes pass True,
|
||||
the reconciler never does). A bot whose cached check says "denied" is
|
||||
then asked again if that answer is more than
|
||||
``RECHECK_DENIED_AFTER_SECONDS`` old (server-26#251), so "fix it in
|
||||
Discord, then try again" works on the next try.
|
||||
"""
|
||||
if mode not in ("persistent", "adhoc"):
|
||||
raise StreamError("bad_mode", "mode must be persistent or adhoc", status=400)
|
||||
@@ -527,14 +560,14 @@ async def start_stream(
|
||||
org_id=org_id, guild_id=guild_id, channel_id=channel_id, system_id=system_id, mode=mode,
|
||||
channel_name=channel_name, guild_name=guild_name, created_by=created_by, node_id=node_id,
|
||||
preferred_token_id=preferred_token_id, max_streams=max_streams, stream_id=stream_id,
|
||||
existing=existing,
|
||||
existing=existing, interactive=interactive,
|
||||
)
|
||||
|
||||
|
||||
async def _start_locked(*, org_id: str, guild_id: str, channel_id: str, system_id: str, mode: str,
|
||||
channel_name: Optional[str], guild_name: Optional[str], created_by: Optional[str],
|
||||
node_id: Optional[str], preferred_token_id: Optional[str], max_streams: Optional[int],
|
||||
stream_id: Optional[str], existing: dict) -> dict:
|
||||
stream_id: Optional[str], existing: dict, interactive: bool = False) -> dict:
|
||||
already_on = False
|
||||
if stream_id:
|
||||
existing = await fstore.doc_get(STREAMS, stream_id) or existing # re-read under the lock
|
||||
@@ -612,7 +645,8 @@ async def _start_locked(*, org_id: str, guild_id: str, channel_id: str, system_i
|
||||
|
||||
node_ids = {n["node_id"] for n in candidates}
|
||||
perms = await _perm_check(org_id=org_id, guild_id=guild_id, channel_id=channel_id, stream_id=stream_id,
|
||||
preferred_token_id=preferred_token_id, node_ids=node_ids)
|
||||
preferred_token_id=preferred_token_id, node_ids=node_ids,
|
||||
recheck_denied=interactive)
|
||||
if perms and any(p.get("status") == "bad_channel" for p in perms.values()):
|
||||
raise StreamError("bad_channel", f"{_channel_ref(perms, channel_name)} is not a voice channel. "
|
||||
"Radio can only play in a voice channel — pick one from the list.",
|
||||
@@ -638,7 +672,7 @@ async def _start_locked(*, org_id: str, guild_id: str, channel_id: str, system_i
|
||||
# Bots that just turned out to be members get checked too.
|
||||
perms = await _perm_check(org_id=org_id, guild_id=guild_id, channel_id=channel_id,
|
||||
stream_id=stream_id, preferred_token_id=preferred_token_id,
|
||||
node_ids=node_ids)
|
||||
node_ids=node_ids, recheck_denied=interactive)
|
||||
res = await leases.lease_for_stream(stream_id, node_id=node["node_id"], org_id=org_id,
|
||||
preferred_token_id=preferred_token_id, stream_fields=fields,
|
||||
soft_preferred_token_id=soft_pref, perms=perms)
|
||||
|
||||
Reference in New Issue
Block a user