Files
messageLogger/docs/sdk-notes.md
T
lyuksovannyyandClaude Opus 5.5 9b2f26a845
Publish plugin / publish (push) Successful in 4s
Message Logger: show logged edits and deletions from telegram-panel [no bump]
Keeps deleted messages (75% opacity, red icon-pack trash icon), restores
logged messages deleted while away, reports deletions, and shows edit
history as a mock chat (Edit history in the message menu, Open topic).
Message links and history open as any signed-in account with access.
Per-master API tokens; chat state synced per checked range with an hour's
validity, cached in SQLite with a configurable lifetime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:55:23 +02:00

8.2 KiB
Raw Blame History

exteraGram plugin SDK — condensed notes

Gathered from https://plugins.exteragram.app/docs, the SDK type stubs (stubs.zip from exteraSquad/plugins-pysdk-builds), the official exteragram-utils PyPI package (exteragram_utils/dev_client.py), and the app's gradle.properties. Not exhaustive — see the full docs site for anything not covered here.

Plugin identity and metadata

Read statically via AST from top-level constant assignments in the plugin's single .py file — the app never executes the file just to read metadata. Docs: https://plugins.exteragram.app/docs/metadata.

Constant Required Notes
__id__ yes 2–32 chars, starts with a letter, only A-Za-z0-9_-
__name__ yes display name
__description__ no
__author__ no
__version__ no
__icon__ no e.g. "exteraPlugins/1"
__app_version__ no e.g. ">=12.5.1"; legacy alias __min_version__
__sdk_version__ no e.g. ">=1.4.4.3"
__requirements__ no list of pip package specs the host installs before loading

BasePlugin lifecycle and hooks

https://plugins.exteragram.app/docs/base-plugin

  • on_plugin_load(self) — called once when the plugin is enabled/loaded. Hooks must be registered here (e.g. self.add_on_send_message_hook()); registering later has no effect.
  • on_plugin_unload(self) — called on disable/removal; release resources here.
  • create_settings(self) — returns a list of ui.settings rows rendered in the plugin's settings screen.
  • Hooks: on_send_message_hook(self, account, params), pre_request_hook(self, ...), post_request_hook(self, ...), on_update_hook(s)(self, ...), on_app_event(self, ...). Each returns a HookResult.
  • HookResult(strategy=HookStrategy.MODIFY, params=params) applies the mutated params back to the host; HookStrategy.DEFAULT (a bare HookResult()) passes through unchanged; HookStrategy.CANCEL drops the action; HookStrategy.MODIFY_FINAL applies changes and short-circuits any later hooks.
  • Settings persistence: self.get_setting(key, default) / self.set_setting(key, value).

ui.settings row types

https://plugins.exteragram.app/docs/ui-settings

Header, Input, Switch, Text, Selector, Divider — plain kwargs-holding row descriptors consumed by the host's settings renderer; construct and return them from create_settings.

Logging

https://plugins.exteragram.app/docs/logging

android_utils.log(...) writes to logcat. Chaquopy additionally redirects sys.stdout/sys.stderr to the logcat tags python.stdout / python.stderr — useful when a dependency logs with plain print().

Background work

https://plugins.exteragram.app/docs/client-utils

client_utils.run_on_queue(...) schedules work off the UI thread; use it for anything that shouldn't block message handling.

Reflection / private API access

https://plugins.exteragram.app/docs/hook-utils

hook_utils.find_class(...) and related helpers locate app classes and private fields for plugins that need to reach beyond the public SDK surface.

UI helpers

On-device dev server protocol

https://plugins.exteragram.app/docs/dev-server

TCP 127.0.0.1:42690, reachable from a PC via adb forward tcp:42690 tcp:42690. Wire format is consecutive UTF-8 JSON objects with no length framing — parse with json.JSONDecoder().raw_decode over an accumulating buffer, never assume one JSON object per recv(). Requests: {"@": <command>, "#": <id>, ...}; responses echo "#".

Commands used by this repo's tools/devclient.py: ping, get_plugins, enable_plugin, disable_plugin, reload_plugin, remove_plugin (field plugin_id), write_plugin (plugin_id, content — installs or updates), start_debugger/stop_debugger (host, port, platform ∈ vscode|pycharm). The official exteragram-utils client (PyPI) uses integer request ids from 1, a 30 s response timeout, a 1.0 s socket timeout, a write_plugin → sleep 0.3 s → reload_plugin deploy sequence, and adb forward (not reverse) for both the dev-server port and the debugger port. elyx_* commands exist for the Elyx engine and are out of scope here.

Runtime

Chaquopy 16 / Python 3.11. Anything shipped in a bundle must be 3.11-compatible.

Why not Elyx/ElyxBuilder

The official structured plugin format (Elyx/ElyxBuilder) is a real alternative to a single .plugin file, but it:

  • requires the separate Elyx engine to be installed and enabled on the device;
  • ships archives, not the .plugin files the base plugin engine understands;
  • needs a manual first install before live reload works.

This repo's bundler instead compiles a normal multi-file src/ project down to the one .plugin file the base engine already knows how to load, so the dev-server write/reload loop works from the first deploy with no extra on-device engine.

Gotchas found on exteraGram 12.10.6 (message_logger)

  • Java number boxing. A Python int passed where Java takes Object, or to an overloaded method, becomes a Long — even the result of Integer.valueOf(). Use java.jint / jfloat explicitly: ArrayList<Integer> for TL_*_getMessages.id (a Long there fails serialization on the network thread and the request silently never answers), Paint.setColor (an int otherwise picks the setColor(long) ColorSpace overload), reflective Method.invoke arguments, and replacing param.args[i] in hooks.
  • Requests. client_utils.send_request did not invoke the callback for these requests; ConnectionsManager.getInstance(account).sendRequest(req, RequestDelegate) with a dynamic_proxy(RequestDelegate) kept strongly referenced works (src/tg.py).
  • Logs. This device's logcat is silenced; src/diag.py mirrors to …/files/exteraGram/exteraGram Files/message_logger/diag.log (readable over adb shell cat).
  • Deploys. devclient.py deploy --enable only enables; an already enabled plugin needs a deploy without --enable (which reloads) to pick up a new build.
  • Adding messages to an open chat. Inserting into ChatActivity.messages directly (not in messagesDict) breaks later arrivals in that chat; the private ChatActivity.processNewMessages(ArrayList, boolean) places messages (also older ones) and keeps the app's dicts and adapter consistent.
  • Cell height. Changing a ChatMessageCell's measured height grows its bubble; after the state behind it changes, forceLayout() the cell and chatAdapter.notifyItemChanged(...), or the list keeps stale positions. Item animators reset cell alpha: scale it in a setAlpha before-hook instead of setting it once.
  • Icon packs. exteraGram applies icon packs through the activity's ExteraResources (getDrawableForDensity); the application context's plain Resources and ColoredImageSpan(int) give stock icons. Load drawables from get_last_fragment().getParentActivity().getResources().
  • Another account's chat. BaseFragment.setCurrentAccount(slot) before presentFragment opens e.g. a ChatActivity as another signed-in account without switching accounts.
  • Links. Tapped links go through org.telegram.messenger.browser.Browser.openUrl (8 overloads, the second argument a Uri or String); adb shell am start -d URL needs -p <package>.
  • Message menu rows. MenuItemData items land in the menu's Plugins folder, and options added to ChatActivity.fillMessageMenu's lists are dropped by exteraGram. A row in the main menu is an ActionBarMenuSubItem inserted into the built popup after createMenu (whose overloads call one another: insert once); selectedObject is the message.
  • Topics as another account. ForumUtilities.openTopic uses the presenting fragment's account; do its steps yourself (TopicsController.findTopic/loadTopic, ChatActivity with setCurrentAccount then setThreadMessages) — see src/views/open_as.py.