generated from exteraGramPlugins/pluginBase
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>
174 lines
5.4 KiB
Python
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)
|