"""Comenzile slash ale puntii: declararea si inregistrarea lor in Discord. Tot ce depinde de `discord.app_commands` sta aici. Logica ramane in `bot.Bridge` (`cmd_new`, `cmd_cd`, ...), deci comenzile slash si allowlist-ul nu au a doua implementare care sa divergheze. Modulul se importa si FARA discord.py instalat (ca `bot.py`): atunci `build_tree` returneaza None si `sync_guilds` nu face nimic. """ from __future__ import annotations import logging log = logging.getLogger("discord-bridge.slash") try: # pragma: no cover - depinde de mediu import discord # type: ignore from discord import app_commands # type: ignore except ImportError: # pragma: no cover discord = None # type: ignore app_commands = None # type: ignore # Permisiunile din README (309237763136) + scope-ul de comenzi. Fara # `applications.commands` in invitatie, sync-ul da 403 Missing Access. INVITE_TEMPLATE = ( "https://discord.com/oauth2/authorize?client_id={app_id}" "&scope=bot%20applications.commands&permissions=309237763136" ) def invite_url(app_id) -> str: return INVITE_TEMPLATE.format(app_id=app_id or "APPLICATION_ID") def build_tree(client, bridge): """Construieste arborele de comenzi. Fara discord.py: None.""" if app_commands is None: # pragma: no cover - mediu fara discord.py log.warning("discord.py lipseste: nu declar comenzi slash") return None tree = app_commands.CommandTree(client) @tree.command(name="new", description="Sesiune Claude noua in firul curent") @app_commands.describe(fork="Porneste noua sesiune din contextul celei curente") async def _new(interaction, fork: bool = False): await bridge.handle_slash(interaction, "new", fork=fork) @tree.command(name="cd", description="Schimba directorul de lucru al firului") @app_commands.describe(cale="Cale absoluta, oriunde in /workspace") async def _cd(interaction, cale: str): await bridge.handle_slash(interaction, "cd", cale=cale) @tree.command(name="model", description="Schimba modelul pentru firul curent") @app_commands.describe(model="sonnet (implicit) sau opus") @app_commands.choices(model=[ app_commands.Choice(name="sonnet", value="sonnet"), app_commands.Choice(name="opus", value="opus"), ]) async def _model(interaction, model: app_commands.Choice[str]): await bridge.handle_slash(interaction, "model", model=model.value) @tree.command(name="status", description="Sesiune, director, model, cost, proces") async def _status(interaction): await bridge.handle_slash(interaction, "status") @tree.command(name="stop", description="Opreste turul in desfasurare din firul curent") async def _stop(interaction): await bridge.handle_slash(interaction, "stop") @tree.command(name="cleanup", description="Procese lasate in urma (implicit: rulare seaca)") @app_commands.describe(force="Opreste efectiv procesele gasite (implicit: doar lista)") async def _cleanup(interaction, force: bool = False): await bridge.handle_slash(interaction, "cleanup", force=force) @tree.command(name="permisiuni", description="Ce s-a aprobat pentru tot firul") @app_commands.describe(revoca="Sterge aprobarile firului; totul cere iar confirmare") async def _permisiuni(interaction, revoca: bool = False): await bridge.handle_slash(interaction, "permisiuni", revoca=revoca) @tree.command(name="help", description="Lista comenzilor puntii") async def _help(interaction): await bridge.handle_slash(interaction, "help") return tree async def sync_guilds(tree, guild_ids) -> dict[str, str]: """Inregistreaza comenzile pe fiecare guild din allowlist. Sync-ul pe guild e instantaneu; cel global se propaga in ~1h, deci nu-l folosim. NU arunca niciodata: daca invitatia botului nu are scope-ul `applications.commands`, Discord raspunde 403 Missing Access — logam linkul de reinvitare si mergem mai departe. Botul ramane functional pentru mesajele obisnuite. """ results: dict[str, str] = {} if tree is None or discord is None: log.warning("fara arbore de comenzi: sar peste inregistrarea comenzilor slash") return results gids = [str(g).strip() for g in (guild_ids or []) if str(g).strip()] if not gids: log.warning("DISCORD_GUILD_IDS gol: nu inregistrez comenzi slash nicaieri") return results app_id = getattr(getattr(tree, "client", None), "application_id", None) for gid in gids: try: guild = discord.Object(id=int(gid)) tree.copy_global_to(guild=guild) cmds = await tree.sync(guild=guild) results[gid] = f"ok ({len(cmds)} comenzi)" log.info("comenzi slash inregistrate pe guild %s: %d", gid, len(cmds)) except Exception as exc: results[gid] = f"esuat: {exc}" log.error("sync de comenzi slash esuat pe guild %s: %s", gid, exc) log.error( "daca e 403 Missing Access, botul a fost invitat fara scope-ul " "`applications.commands`. Reinvita-l cu: %s", invite_url(app_id), ) return results