ZaStoGram/Tools/check_plugin_client_utils_contract.py
loop-uh a651d43366
Some checks failed
Build three ZaStoGram APKs / build (arm64-v8a, ZaStoGram-standalone-arm64-v8a, Arm64, arm64) (push) Failing after 17m58s
Build three ZaStoGram APKs / build (armeabi-v7a, ZaStoGram-standalone-armeabi-v7a, Armv7, armv7) (push) Failing after 37m3s
ZaStoGram source guards / guards (push) Failing after 15s
Build three ZaStoGram APKs / build (x86, ZaStoGram-standalone-x86, X86, x86) (push) Has been cancelled
Плагины: совместимость с exteraGram SDK 1.4.5 — Elyx, класс-прокси, логгер, мультиаккаунт
- загрузчик: модуль плагина в sys.modules по __id__, импорт плагинов-библиотек
  (zwylib, cactuslib, quantahut), обход цикла debugpy.adapter, BasePlugin в
  пространстве имён, настройки доступны в __init__
- новые модули: markdown_utils, log_utils (self.logger), intents, plugin_settings,
  plugins_manager, elyx; FilesController в file_utils
- extera_utils.classes: настоящие Java-подклассы через DexMaker ProxyBuilder
- Elyx: установка .elyx/.eaf, изолированные импорты ElyxPlugins.<id>, assets,
  strings, settings, wheels, requires
- client_utils: account=, get_client/AccountClient, настоящие send_photo/video/audio,
  replyToMsg по id
- хуки: priority, HookResult.params, CANCEL отдельного обновления, приведение
  результата к типу возврата
- метаданные: разбор как в AST, __app_version__ с операторами, __sdk_version__
- общий Markdown-парсер: многострочные блоки кода, `code` без разметки,
  snake_case, экранирование, цитаты
- Java-прослойки com.exteragram: PluginsConstants, engines, pluginInstances,
  AppUtils, ChatUtils; авто-резолв com.* классов
- гард Tools/check_plugin_sdk_runtime.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 16:59:42 +03:00

161 lines
7.4 KiB
Python

#!/usr/bin/env python3
"""Static guard for exteraGram-facing client_utils compatibility exports."""
from __future__ import annotations
import ast
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CLIENT_UTILS = ROOT / "TMessagesProj/src/main/python/client_utils.py"
PLUGIN_UTILS = ROOT / "TMessagesProj/src/main/java/org/telegram/plugins/PluginUtils.java"
def fail(errors: list[str]) -> int:
print("Plugin client_utils contract check failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1
def call_name(node: ast.AST) -> str | None:
if isinstance(node, ast.Call):
return call_name(node.func)
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
return node.attr
return None
def has_dynamic_proxy_base(cls: ast.ClassDef, interface: str) -> bool:
for base in cls.bases:
if not isinstance(base, ast.Call):
continue
if call_name(base.func) != "dynamic_proxy":
continue
if base.args and call_name(base.args[0]) == interface:
return True
return False
def main() -> int:
try:
source = CLIENT_UTILS.read_text(encoding="utf-8")
except FileNotFoundError:
return fail([f"Missing {CLIENT_UTILS.relative_to(ROOT)}"])
tree = ast.parse(source, filename=str(CLIENT_UTILS))
classes = {node.name: node for node in tree.body if isinstance(node, ast.ClassDef)}
functions = {node.name: node for node in tree.body if isinstance(node, ast.FunctionDef)}
errors: list[str] = []
request_callback = classes.get("RequestCallback")
if request_callback is None:
errors.append("client_utils must export RequestCallback for exteraGram plugins")
else:
if not has_dynamic_proxy_base(request_callback, "RequestDelegate"):
errors.append("RequestCallback must proxy org.telegram.tgnet.RequestDelegate")
if not any(isinstance(node, ast.FunctionDef) and node.name == "run"
for node in request_callback.body):
errors.append("RequestCallback must implement run(response, error)")
if "RequestCallback(on_complete" not in source:
errors.append("send_request must wrap Python callbacks with RequestCallback")
for queue_name in (
"STAGE_QUEUE", "GLOBAL_QUEUE", "CACHE_CLEAR_QUEUE", "SEARCH_QUEUE",
"PHONE_BOOK_QUEUE", "THEME_QUEUE", "EXTERNAL_NETWORK_QUEUE", "PLUGINS_QUEUE",
):
if queue_name not in {node.targets[0].id for node in tree.body
if isinstance(node, ast.Assign)
and len(node.targets) == 1
and isinstance(node.targets[0], ast.Name)}:
errors.append(f"client_utils must export {queue_name} for exteraGram run_on_queue")
if "get_queue_by_name" not in functions:
errors.append("client_utils must export get_queue_by_name for raw DispatchQueue access")
run_on_queue = functions.get("run_on_queue")
if run_on_queue is None:
errors.append("client_utils must export run_on_queue")
else:
args = [arg.arg for arg in run_on_queue.args.args]
if args[:3] != ["fn", "queue", "delay"]:
errors.append("run_on_queue signature must accept fn, queue, delay")
if "PluginUtils.runOnQueue(str(queue or PLUGINS_QUEUE), _Runnable(fn), int(delay))" not in source:
errors.append("run_on_queue must pass queue name and delay to PluginUtils.runOnQueue")
for helper in ("send_text", "send_audio", "send_photo", "send_video", "edit_message"):
if helper not in functions:
errors.append(f"client_utils must export {helper} for common exteraGram plugins")
if "PluginUtils.editMessage(" not in source:
errors.append("edit_message must delegate through PluginUtils.editMessage")
for helper in ("get_notifications_settings", "get_media_controller"):
if helper not in functions:
errors.append(f"client_utils must export {helper} for exteraGram controller helpers")
notification_delegate = classes.get("NotificationCenterDelegate")
if notification_delegate is None:
errors.append("client_utils must export NotificationCenterDelegate")
else:
if not has_dynamic_proxy_base(notification_delegate, "NotificationCenterDelegateInterface"):
errors.append("NotificationCenterDelegate must proxy NotificationCenter.NotificationCenterDelegate")
if not any(isinstance(node, ast.FunctionDef) and node.name == "didReceivedNotification"
for node in notification_delegate.body):
errors.append("NotificationCenterDelegate must implement didReceivedNotification")
if "from extera_utils.text_formatting import parse_text" not in source:
errors.append("client_utils must import parse_text for parse_mode support")
if "parse_mode" not in (functions.get("send_message").args.args[-1].arg if functions.get("send_message") else ""):
errors.append("send_message must accept parse_mode")
if "_apply_parse_mode(params, parse_mode)" not in source:
errors.append("send_message/send_text must apply parse_mode through parse_text")
if "caption_entities" not in source:
errors.append("send_document must preserve parsed caption entities")
if "PluginUtils.sendDocument(" in source and "caption_entities" not in source:
errors.append("send_document must pass caption_entities to PluginUtils.sendDocument")
try:
java_source = PLUGIN_UTILS.read_text(encoding="utf-8")
except FileNotFoundError:
errors.append(f"Missing {PLUGIN_UTILS.relative_to(ROOT)}")
java_source = ""
if java_source:
if "Object captionEntities" not in java_source:
errors.append("PluginUtils.sendDocument must accept captionEntities")
if "coerceMessageEntities(captionEntities)" not in java_source:
errors.append("PluginUtils.sendDocument must coerce captionEntities")
if "SendMessagesHelper.prepareSendingDocuments(" not in java_source:
errors.append("PluginUtils.sendDocument must call prepareSendingDocuments overload with entities")
for name, kind in (("send_audio", "audio"), ("send_photo", "photo"), ("send_video", "video"),
("send_document", "document")):
fn = functions.get(name)
if fn is None:
errors.append(f"client_utils must export {name}")
continue
args = [arg.arg for arg in fn.args.args]
if args[:2] != ["peer_id", args[1]] or len(args) < 2:
errors.append(f"{name} signature must start with peer_id and a file path")
if "account" not in args:
errors.append(f"{name} must accept account= (exteraGram SDK 1.4.5.0 multi-account)")
if not any(isinstance(node, ast.Constant) and node.value == kind for node in ast.walk(fn)):
errors.append(f"{name} must send through _send_media({kind!r}, ...)")
if "PluginUtils.sendMedia(" not in source:
errors.append("_send_media must call PluginUtils.sendMedia")
if java_source and "prepareSendingMedia(" not in java_source:
errors.append("PluginUtils.sendMedia must send photos/videos through prepareSendingMedia")
if errors:
return fail(errors)
print("Plugin client_utils contract check passed")
return 0
if __name__ == "__main__":
sys.exit(main())