Files
2026-09-13 10:18:12 +00:00

132 lines
5.7 KiB
Python

"""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
import infra_actions
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")
@tree.command(name="infra", description="Infrastructura: stare, oprire/pornire cluster, UPS, DR")
@app_commands.describe(actiune="Ce faci", simulare="Doar simulare (implicit da unde exista)")
@app_commands.choices(actiune=[
app_commands.Choice(name=a["label"][:100], value=k) for k, a in infra_actions.ACTIONS.items()
])
async def _infra(interaction, actiune: app_commands.Choice[str], simulare: bool | None = None):
await bridge.handle_slash(interaction, "infra", actiune=actiune.value, simulare=simulare)
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