""" Unlinked servers see only /drb link and /help (server-26#213). Discord registers slash commands globally, so an unlinked server still lists the others in its command picker; this gate answers them with how to link instead of running them. Per-guild command sync would hide them outright but costs one sync per server per change — not worth it while C2 refuses every unlinked request anyway (this gate is UX, C2 is the enforcement). Whether a server is "linked" is C2's answer: its own discord_guilds link, or DISCORD_UNLINKED_GUILD_ORG_ID when that is set — so with the default ("founding") nothing is gated, exactly as before. """ from __future__ import annotations import time from typing import Optional import discord from discord import app_commands from app.internal.c2_client import c2 from app.internal.logger import logger ALWAYS_ALLOWED = frozenset({"drb link", "help"}) LINKED_TTL = 60.0 UNLINKED_TTL = 10.0 # short, so a fresh /drb link (or a web-side link) shows up fast UNLINKED_TEXT = ( "This server is not linked to a DRB organization yet.\n" "An owner of the DRB organization generates a code under **Settings → Organization → Discord servers**; " "then someone with **Manage Server** here runs `/drb link code:`." ) _cache: dict[int, tuple[float, bool]] = {} async def is_linked(guild_id: int) -> Optional[bool]: """True/False from C2, or None when C2 could not be reached.""" hit = _cache.get(guild_id) if hit and time.monotonic() < hit[0]: return hit[1] data = await c2.guild_overview(guild_id) if data.get("error"): return None linked = bool(data.get("org_id")) _cache[guild_id] = (time.monotonic() + (LINKED_TTL if linked else UNLINKED_TTL), linked) return linked def forget(guild_id: int) -> None: _cache.pop(guild_id, None) class GatedTree(app_commands.CommandTree): async def interaction_check(self, interaction: discord.Interaction) -> bool: if interaction.guild_id is None: return True if interaction.type not in (discord.InteractionType.application_command, discord.InteractionType.autocomplete): return True cmd = interaction.command if cmd is not None and cmd.qualified_name in ALWAYS_ALLOWED: return True # None = C2 unreachable: let the command run and report that itself. if await is_linked(interaction.guild_id) is not False: return True try: if interaction.type is discord.InteractionType.autocomplete: await interaction.response.autocomplete([]) else: await interaction.response.send_message(UNLINKED_TEXT, ephemeral=True) except discord.HTTPException as e: logger.warning(f"unlinked-guild reply failed: {e}") return False