generated from exteraGramPlugins/pluginBase
Publish plugin / publish (push) Successful in 4s
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>
154 lines
8.2 KiB
Markdown
154 lines
8.2 KiB
Markdown
# 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
|
||
|
||
- `ui.alert.AlertDialogBuilder` — native alert dialogs. <https://plugins.exteragram.app/docs/ui-alert>
|
||
- `ui.bulletin` — transient bottom bulletins/toasts. <https://plugins.exteragram.app/docs/ui-bulletin>
|
||
- `intents.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 `.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`.
|