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>
6.3 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 (client_commands)
- Class checks.
hook_utils.find_classreturns a Chaquopy class wrapper withoutisInstance. Usejutil.java_class(name), which gives the realjava.lang.Class. - Plugin-made modules. A module a plugin puts into
sys.modulesdoesn't reliably survive that plugin's reload. State that must outlive a reload (the library registry) hangs offbuiltinsinstead. - Suggestion list.
MentionsAdapter.notifyDataSetChangeddiffs rows by their item (the command string). Rows whose label stays the same aren't rebound, so callnotifyItemRangeChangedtoo. The adapter's:emoji search is async and replaces the rows, so replace the search (skip the method) instead of patching its result. - Recycled cells. A cell restyled by an earlier load of the plugin keeps that style. Remember the original in the view's tag, not in plugin memory.
- Ephemeral messages. A tap opens their menu; a long press does nothing. Deleting one goes
through
MessagesController.deleteEphemeralMessage, which also asks Telegram to delete it.