generated from exteraGramPlugins/pluginBase
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>
This commit is contained in:
1 parent
84bdc44a76
commit
46a1f6ee1f
29 files changed
+3293
-220
No files matched your search
@@ -0,0 +1,106 @@
|
||||
"""Telegram-style HTML -> (text, entities), for responses of commands that
|
||||
run on the device (server responses arrive parsed). Offsets are UTF-16 code
|
||||
units, as Telegram counts them. Pure Python: tested offline.
|
||||
|
||||
Tags: b strong i em u ins s strike del code pre (`<code class="language-x">`
|
||||
inside), a href (tg://user?id=N mentions), blockquote [expandable],
|
||||
tg-spoiler / span class="tg-spoiler", tg-emoji emoji-id, br.
|
||||
"""
|
||||
|
||||
from html.parser import HTMLParser
|
||||
|
||||
_SIMPLE = {
|
||||
"b": "bold",
|
||||
"strong": "bold",
|
||||
"i": "italic",
|
||||
"em": "italic",
|
||||
"u": "underline",
|
||||
"ins": "underline",
|
||||
"s": "strike",
|
||||
"strike": "strike",
|
||||
"del": "strike",
|
||||
"code": "code",
|
||||
"pre": "pre",
|
||||
"blockquote": "blockquote",
|
||||
"tg-spoiler": "spoiler",
|
||||
}
|
||||
|
||||
|
||||
def utf16_len(text):
|
||||
return len(text.encode("utf-16-le")) // 2
|
||||
|
||||
|
||||
class _Parser(HTMLParser):
|
||||
def __init__(self):
|
||||
super().__init__(convert_charrefs=True)
|
||||
self.parts = []
|
||||
self.length = 0 # UTF-16 units so far
|
||||
self.open = [] # [(tag, entity or None)]
|
||||
self.entities = []
|
||||
|
||||
def _entity(self, tag, attrs):
|
||||
attrs = dict(attrs)
|
||||
if tag == "code" and any(t == "pre" for t, _ in self.open):
|
||||
pre = next(e for t, e in reversed(self.open) if t == "pre")
|
||||
language = (attrs.get("class") or "").removeprefix("language-")
|
||||
if pre is not None and language:
|
||||
pre["language"] = language
|
||||
return None
|
||||
if tag == "span" and attrs.get("class") == "tg-spoiler":
|
||||
return {"type": "spoiler"}
|
||||
if tag in _SIMPLE:
|
||||
entity = {"type": _SIMPLE[tag]}
|
||||
if tag == "blockquote" and "expandable" in attrs:
|
||||
entity["collapsed"] = True
|
||||
return entity
|
||||
if tag == "a" and attrs.get("href"):
|
||||
href = attrs["href"]
|
||||
if href.startswith("tg://user?id="):
|
||||
try:
|
||||
return {"type": "mention_name", "user_id": int(href.split("=", 1)[1])}
|
||||
except ValueError:
|
||||
pass
|
||||
return {"type": "text_link", "url": href}
|
||||
if tag == "tg-emoji":
|
||||
try:
|
||||
return {"type": "custom_emoji", "document_id": int(attrs.get("emoji-id"))}
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return None
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "br":
|
||||
self.handle_data("\n")
|
||||
return
|
||||
entity = self._entity(tag, attrs)
|
||||
if entity is not None:
|
||||
entity["offset"] = self.length
|
||||
self.open.append((tag, entity))
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
for index in range(len(self.open) - 1, -1, -1):
|
||||
if self.open[index][0] == tag:
|
||||
_, entity = self.open.pop(index)
|
||||
if entity is not None:
|
||||
entity["length"] = self.length - entity["offset"]
|
||||
if entity["length"] > 0:
|
||||
self.entities.append(entity)
|
||||
return
|
||||
|
||||
def handle_data(self, data):
|
||||
self.parts.append(data)
|
||||
self.length += utf16_len(data)
|
||||
|
||||
|
||||
def parse(html):
|
||||
"""(plain text, entities) of `html`; entities ordered by offset."""
|
||||
parser = _Parser()
|
||||
parser.feed(html or "")
|
||||
parser.close()
|
||||
entities = sorted(parser.entities, key=lambda e: (e["offset"], -e["length"]))
|
||||
return "".join(parser.parts), entities
|
||||
|
||||
|
||||
def escape(text):
|
||||
"""`text` made safe to put into a response."""
|
||||
return str(text).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
Reference in new issue
Block a user