"""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"🎲 {random.randint(1, sides)}" # 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)