diff --git a/drb-c2-core/app/internal/discord_streams.py b/drb-c2-core/app/internal/discord_streams.py index 1f7a907..4950d3c 100644 --- a/drb-c2-core/app/internal/discord_streams.py +++ b/drb-c2-core/app/internal/discord_streams.py @@ -626,7 +626,7 @@ async def _start_locked(*, org_id: str, guild_id: str, channel_id: str, system_i 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) - if not res.ok and res.error == "no_bot_in_guild" and not refreshed: + if not res.ok and res.error in ("no_bot_in_guild", "pool_full") and not refreshed: # Membership is cached on the token doc; someone may have just # invited a bot. Re-read it from Discord once, then retry. refreshed = True diff --git a/drb-c2-core/app/routers/discord.py b/drb-c2-core/app/routers/discord.py index 4e8a069..70129e2 100644 --- a/drb-c2-core/app/routers/discord.py +++ b/drb-c2-core/app/routers/discord.py @@ -95,7 +95,7 @@ async def guild_overview(guild_id: str, _: dict = Depends(require_service_key_or "member": None if member is None else guild_id in member, # A pool bot at discord_pool_bot_guild_cap is never offered (#225, owner Q3). "invite_url": (leases.invite_url(t.get("application_id"), guild_id) - if member or leases.invitable(t) else None), + if guild_id in (member or []) or leases.invitable(t) else None), }) return { "guild_id": guild_id, diff --git a/drb-c2-core/tests/test_discord_perm_check.py b/drb-c2-core/tests/test_discord_perm_check.py index 8324f0b..8667b28 100644 --- a/drb-c2-core/tests/test_discord_perm_check.py +++ b/drb-c2-core/tests/test_discord_perm_check.py @@ -446,6 +446,32 @@ def test_voice_check_lists_invites_within_the_pool_cap(fs, monkeypatch): assert [i["name"] for i in out["invite"]] == ["DRB Radio room"] +def test_pool_full_rereads_membership_once(fs, monkeypatch): + # A full pool bot an admin just invited by hand: guild_ids is stale, so + # the first pick says pool_full; one membership re-read must still find it. + monkeypatch.setattr(settings, "discord_pool_bot_guild_cap", 2) + _token(fs, "full", guilds=("1", "2")) + + async def _refresh(token_ids): + assert token_ids == ["full"] + fs.put("bot_tokens", "full", {**fs.get("bot_tokens", "full"), "guild_ids": ["1", "2", GUILD]}) + return 1 + monkeypatch.setattr(discord_rest, "refresh_membership", _refresh) + assert run(_start())["token_id"] == "full" + + +def test_bot_overview_never_offers_a_full_pool_bot(fs, monkeypatch): + # A pool bot in other servers (non-empty guild_ids) at the cap must not get + # an invite link; one already in this server keeps its link. + monkeypatch.setattr(settings, "discord_pool_bot_guild_cap", 2) + _token(fs, "full", guilds=("1", "2")) + _token(fs, "room", guilds=("1",)) + _token(fs, "here", guilds=("1", GUILD)) + out = run(discord_router.guild_overview(GUILD, {"service": True})) + links = {b["token_id"]: b["invite_url"] for b in out["bots"]} + assert links["full"] is None and links["room"] and links["here"] + + def test_voice_check_busy_and_other_node(fs): _token(fs, "t1", lease={"node_id": "n2", "streams": [], "lease_id": "L"}) out = run(checks.node_voice_check("n1", GUILD, refresh=0, channel_id="1001", decoded=OWNER))