Client commands tokens cover all your panel accounts
Publish plugin / publish (push) Successful in 5s

One token runs the commands of every account of its owner on the panel: /me
lists them, the plugin asks for the commands of each one signed in on the
device, and every request names its account by Telegram user id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lyuksovannyyandClaude Opus 5.5 committed 2026-10-09 16:59:32 +02:00
1 parent ecb4c0d97f
commit 0b174fea7e
7 files changed
+112 -67

No files matched your search

+9 -8
View File
@@ -1,9 +1,10 @@
"""telegram-panel client API (`/api/v1/client/commands`).
Each token runs the client commands of one panel account, the Telegram
account it is bound to (panel: Settings -> API Tokens -> Client commands);
the plugin holds one per account and picks it by the Telegram user id
`/me` reports. Message logger tokens are refused here, and these there.
A token (panel: Settings -> API Tokens -> Client commands) runs the client
commands of any of its owner's accounts on the panel; requests name the
account by its Telegram user id (`/me` lists them). One token covers every
account of yours; several only for several panel users. Message logger
tokens are refused here, and these there.
Calls are blocking: run them off the UI thread (`jutil.in_background`).
"""
@@ -70,12 +71,12 @@ class Api:
return response.json()
def me(self, token):
"""{account_id, tg_id, account_name, running, ...} of the token."""
"""{name, accounts: [{account_id, tg_id, name, running}]} of the token."""
return self._call(token, "GET", "/me")
def commands(self, token):
"""{commands: [...], refresh_seconds}."""
return self._call(token, "GET", "/")
def commands(self, token, tg_id):
"""{commands: [...], refresh_seconds} of the account `tg_id`."""
return self._call(token, "GET", "/", params={"tg_id": int(tg_id)})
def execute(self, token, text, chat_id, reply_to_id=None, topic_id=None, send=False, sender_id=None):
"""{ok, error, text, entities, html, sent_message_id}. `sender_id`:
+49 -30
View File
@@ -1,8 +1,9 @@
"""Which commands each signed-in account has.
- The panel's: every token is asked for its account (`/me`) and that
account's commands, again once an hour (or when the panel says so), and
cached on disk, so a restart shows them at once, even offline.
- The panel's: a token runs any of its owner's accounts on the panel. It is
asked which (`/me`), then for the commands of each one signed in here,
again once an hour (or when the panel says so), and they are cached on
disk, so a restart shows them at once, even offline.
- The ones plugins registered through the library (`library.py`): for every
account.
@@ -26,6 +27,18 @@ def own_user_id(slot):
return int(UserConfig.getInstance(int(slot)).getClientUserId())
def signed_in():
"""Telegram user ids of the accounts signed in on this device."""
from org.telegram.messenger import UserConfig
out = set()
for slot in range(UserConfig.MAX_ACCOUNT_COUNT):
config = UserConfig.getInstance(slot)
if config.isClientActivated():
out.add(int(config.getClientUserId()))
return out
class Catalog:
def __init__(self, plugin, api, library):
self.plugin = plugin
@@ -73,13 +86,11 @@ class Catalog:
token = self._token_of.get(tg_id)
if token is None:
for candidate in self.api.tokens():
if candidate not in self._token_of.values():
try:
self._learn(candidate)
except ApiError:
continue
with self._lock:
token = self._token_of.get(tg_id)
try:
if tg_id in self._learn(candidate):
return candidate
except ApiError:
continue
return token
def account_name(self, slot):
@@ -151,40 +162,48 @@ class Catalog:
return (min(upcoming) - time.time()) if upcoming else REFRESH_SECONDS
def _learn(self, token):
"""{tg id: account name} of the token's accounts here; the token is
remembered for them."""
me = self.api.me(token)
tg_id = int(me.get("tg_id") or 0)
if not tg_id:
raise ApiError("the panel doesn't know this account's Telegram id yet")
here = signed_in()
mine = {
int(a["tg_id"]): a.get("name")
for a in me.get("accounts") or ()
if a.get("tg_id") and int(a["tg_id"]) in here
}
with self._lock:
self._token_of[tg_id] = token
return tg_id, me
for tg_id in mine:
self._token_of[tg_id] = token
return mine
def _refresh(self, token):
try:
tg_id, me = self._learn(token)
listing = self.api.commands(token)
commands = []
for data in listing.get("commands") or ():
try:
commands.append(Command.from_json(data))
except (KeyError, ValueError) as exc:
diag.log(f"skipped command {data.get('name')!r}: {exc}")
mine = self._learn(token)
listings = {tg_id: self.api.commands(token, tg_id) for tg_id in mine}
except ApiError as exc:
diag.log(f"refresh {token[:10]}…: {exc}")
with self._lock:
self._errors[token] = str(exc)
self._due[token] = time.time() + (REFRESH_SECONDS if exc.refused else RETRY_SECONDS)
return
refresh = int(listing.get("refresh_seconds") or REFRESH_SECONDS)
refresh = REFRESH_SECONDS
with self._lock:
self._errors.pop(token, None)
for tg_id, listing in listings.items():
commands = []
for data in listing.get("commands") or ():
try:
commands.append(Command.from_json(data))
except (KeyError, ValueError) as exc:
diag.log(f"skipped command {data.get('name')!r}: {exc}")
refresh = min(refresh, int(listing.get("refresh_seconds") or REFRESH_SECONDS))
self._server[tg_id] = {
"commands": commands,
"account": mine[tg_id],
"fetched": time.time(),
}
diag.log(f"{len(commands)} commands for {tg_id}")
self._due[token] = time.time() + max(60, refresh)
self._server[tg_id] = {
"commands": commands,
"account": me.get("account_name"),
"fetched": time.time(),
}
diag.log(f"{len(commands)} commands for {tg_id}")
self._save()
self._changed()
+15 -5
View File
@@ -112,15 +112,25 @@ class ClientCommandsPlugin(BasePlugin):
if not self.api.configured:
bulletin("Client Commands: set the panel URL and API tokens", error=True)
return
from catalog import signed_in
here = signed_in()
lines, failed = [], False
for token in self.api.tokens():
try:
me = self.api.me(token)
state = "✓" if me.get("running") else "✓ (not running on the panel)"
lines.append(f"{me.get('account_name')} {state}")
except ApiError as exc:
failed = True
lines.append(f"{token[:10]}…: {exc}")
continue
for account in me.get("accounts") or ():
if account.get("tg_id") not in here:
continue
state = "✓" if account.get("running") else "✓ (not running on the panel)"
lines.append(f"{account.get('name')} {state}")
if not lines:
failed = True
lines.append("none of the accounts here are on the panel")
bulletin("Accounts: " + "; ".join(lines), error=failed)
self.catalog.refresh()
@@ -163,10 +173,10 @@ class ClientCommandsPlugin(BasePlugin):
),
Input(
key=API_TOKENS,
text="API tokens",
text="API token",
default=DEFAULTS[API_TOKENS],
subtext="Client commands tokens, one per account, separated by spaces "
"(panel: Settings → API Tokens). Not the message logger ones.",
subtext="A Client commands token (panel: Settings → API Tokens); it covers all "
"your accounts. Several, separated by spaces, for several panel users.",
icon="msg_permissions",
on_change=self._tokens_changed,
),