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>
8.2 KiB
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 ofui.settingsrows 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 aHookResult. HookResult(strategy=HookStrategy.MODIFY, params=params)applies the mutatedparamsback to the host;HookStrategy.DEFAULT(a bareHookResult()) passes through unchanged;HookStrategy.CANCELdrops the action;HookStrategy.MODIFY_FINALapplies 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
ui.alert.AlertDialogBuilder— native alert dialogs. https://plugins.exteragram.app/docs/ui-alertui.bulletin— transient bottom bulletins/toasts. https://plugins.exteragram.app/docs/ui-bulletinintents.IntentsManager— Android intent integration, exteraGram 12.6.4+ only. https://plugins.exteragram.app/docs/intents
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
.pluginfiles 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
intpassed where Java takesObject, or to an overloaded method, becomes aLong— even the result ofInteger.valueOf(). Usejava.jint/jfloatexplicitly:ArrayList<Integer>forTL_*_getMessages.id(aLongthere fails serialization on the network thread and the request silently never answers),Paint.setColor(anintotherwise picks thesetColor(long)ColorSpace overload), reflectiveMethod.invokearguments, and replacingparam.args[i]in hooks. - Requests.
client_utils.send_requestdid not invoke the callback for these requests;ConnectionsManager.getInstance(account).sendRequest(req, RequestDelegate)with adynamic_proxy(RequestDelegate)kept strongly referenced works (src/tg.py). - Logs. This device's logcat is silenced;
src/diag.pymirrors to…/files/exteraGram/exteraGram Files/message_logger/diag.log(readable overadb shell cat). - Deploys.
devclient.py deploy --enableonly 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.messagesdirectly (not inmessagesDict) breaks later arrivals in that chat; the privateChatActivity.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 andchatAdapter.notifyItemChanged(...), or the list keeps stale positions. Item animators reset cell alpha: scale it in asetAlphabefore-hook instead of setting it once. - Icon packs. exteraGram applies icon packs through the activity's
ExteraResources(getDrawableForDensity); the application context's plainResourcesandColoredImageSpan(int)give stock icons. Load drawables fromget_last_fragment().getParentActivity().getResources(). - Another account's chat.
BaseFragment.setCurrentAccount(slot)beforepresentFragmentopens e.g. aChatActivityas another signed-in account without switching accounts. - Links. Tapped links go through
org.telegram.messenger.browser.Browser.openUrl(8 overloads, the second argument aUriorString);adb shell am start -d URLneeds-p <package>. - Message menu rows.
MenuItemDataitems land in the menu's Plugins folder, and options added toChatActivity.fillMessageMenu's lists are dropped by exteraGram. A row in the main menu is anActionBarMenuSubIteminserted into the built popup aftercreateMenu(whose overloads call one another: insert once);selectedObjectis the message. - Topics as another account.
ForumUtilities.openTopicuses the presenting fragment's account; do its steps yourself (TopicsController.findTopic/loadTopic,ChatActivitywithsetCurrentAccountthensetThreadMessages) — seesrc/views/open_as.py.