Files
clientCommands/src/library.py
T
lyuksovannyyandClaude Opus 5.5 46a1f6ee1f Client Commands: telegram-panel slash commands with Discord-style parameters
Lists the panel account's client commands as Telegram slash commands (hourly
refresh, cached on the device), shows their parameters while typing, runs
complete commands on the panel instead of sending them and shows the answer
as an "Only visible to you" message (latest 20 kept in memory). Holding send
offers Run / Send as message / Send response in chat (red ! badge); an
answer's menu starts with Send in chat. Other plugins register their own
commands through the client_commands library module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:19:20 +02:00

174 lines
5.4 KiB
Python

"""The plugin as a library: other plugins register their own commands.
Published as the module `client_commands` (in `sys.modules`), so another
plugin can do:
import sys, types
def setup(api):
api.register(
"roll",
roll,
summary="Rolls a die",
params=[api.Param("sides", "integer", required=False, default=6)],
owner=__id__,
)
def roll(ctx, sides=6):
import random
return f"🎲 <b>{random.randint(1, sides)}</b>"
# in on_plugin_load: works whichever plugin loads first
api = sys.modules.setdefault("client_commands", types.ModuleType("client_commands"))
if hasattr(api, "register"):
setup(api)
else:
api.__dict__.setdefault("pending", []).append(setup)
# in on_plugin_unload
sys.modules["client_commands"].unregister_owner(__id__)
A handler gets a `CommandContext` and the parameters as keyword arguments,
runs off the UI thread, and returns the response as Telegram-style HTML (or
None). `raise api.CommandError("...")` shows a user-facing error.
The registry outlives the module object (the plugin engine drops modules a
plugin added when it unloads): it hangs off `builtins`, so registrations
survive this plugin being updated or turned off and on, and an `api` kept
from an earlier load still registers into it.
"""
import sys
import types
from commands.markup import escape
from commands.model import Command, Param
from commands.parse import CommandError
MODULE = "client_commands"
VERSION = 1
_STATE = "__client_commands_state__"
def shared():
"""State kept across loads of this plugin: {"registry": {name: Command},
...}; the hooks keep what they must not lose on a reload here too."""
import builtins
state = getattr(builtins, _STATE, None)
if state is None:
state = {"registry": {}}
setattr(builtins, _STATE, state)
return state
class CommandContext:
"""Where a command runs. `reply_to` is the replied MessageObject (or
None), `values` the parsed parameters, `send_to_chat` whether the
response goes into the chat (as the account) instead of only to you."""
def __init__(self, account, dialog_id, topic_id, reply_to, text, command, values, send_to_chat):
self.account = account
self.dialog_id = dialog_id
self.topic_id = topic_id
self.reply_to = reply_to
self.text = text
self.command = command
self.values = values
self.send_to_chat = send_to_chat
@property
def client(self):
"""The SDK's AccountClient of the account (send_text, send_request...)."""
from client_utils import get_client
return get_client(self.account)
class Library:
def __init__(self):
self.module = None
@property
def _registry(self):
return shared()["registry"]
def install(self):
"""Publishes the API (keeping earlier registrations) and runs the
setups other plugins queued before this plugin loaded."""
module = sys.modules.get(MODULE)
if module is None:
module = types.ModuleType(MODULE)
sys.modules[MODULE] = module
self.module = module
module.__doc__ = __doc__
module.VERSION = VERSION
module.Param = Param
module.CommandError = CommandError
module.CommandContext = CommandContext
module.escape = escape
module.register = self.register
module.unregister = self.unregister
module.unregister_owner = self.unregister_owner
module.commands = self.commands
module.available = True
module._registry = self._registry
pending = module.__dict__.pop("pending", [])
for setup in pending:
try:
setup(module)
except Exception as exc:
import diag
diag.error("library pending setup", exc)
def uninstall(self):
if self.module is not None:
self.module.available = False
def register(
self,
name,
handler,
*,
params=(),
summary="",
description="",
aliases=(),
reply="none",
owner=None,
):
"""Adds (or replaces) a command run by `handler(ctx, **values)`.
`params` are `Param`s or dicts like the panel lists them."""
if not callable(handler):
raise TypeError("handler must be callable")
params = [p if isinstance(p, Param) else Param.from_json(p) for p in params]
command = Command(
name,
params,
summary,
description,
aliases,
reply,
source=str(owner or "plugin"),
handler=handler,
)
self._registry[command.name] = command
return command
def unregister(self, name):
return self._registry.pop(str(name).lstrip("/").lower(), None) is not None
def unregister_owner(self, owner):
names = [n for n, c in self._registry.items() if c.source == str(owner)]
for name in names:
self._registry.pop(name)
return len(names)
def commands(self):
"""Every command plugins registered, as dicts."""
return [dict(c.to_json(), owner=c.source) for c in self.local_commands()]
def local_commands(self):
return sorted(self._registry.values(), key=lambda c: c.name)