Files
chatSummaries/README.md
T
exteraGramPlugins a3b0b264c1 Initial commit
2026-09-12 02:03:47 +00:00

5.6 KiB

exteraGram Plugin Base

A build toolchain and ADB automation for writing exteraGram plugins as ordinary multi-file Python projects instead of hand-maintaining one giant .py file.

How exteraGram plugins actually work

A plugin, as exteraGram loads it, is exactly one Python file on the device: /data/user/0/com.exteragram.messenger/files/plugins/<plugin_id>.py. The distribution convention names the same content <id>.plugin.

The app reads plugin metadata statically via AST — it looks for top-level constant assignments (__id__, __name__, __description__, __author__, __version__, __icon__, __app_version__, __sdk_version__, __requirements__) without executing the file. Only __id__ and __name__ are required.

The file must define exactly one class inheriting BasePlugin (from base_plugin import BasePlugin). Lifecycle methods (on_plugin_load, on_plugin_unload, create_settings) and hooks (on_send_message_hook, pre_request_hook, post_request_hook, on_update_hook(s), on_app_event) are all called by the host; hooks must be registered inside on_plugin_load (e.g. self.add_on_send_message_hook()). A hook returns a HookResult(strategy=HookStrategy.MODIFY, params=params) to apply changes, or a bare HookResult() to pass through. Settings rows returned from create_settings come from ui.settings (Header, Input, Switch, Text, Selector, Divider) and persist through self.get_setting/self.set_setting. Logging goes through android_utils.log; Chaquopy also redirects sys.stdout/sys.stderr to the logcat tags python.stdout/python.stderr.

This repo does not use the official Elyx/ElyxBuilder archive format — see docs/sdk-notes.md for why.

Repo layout

plugin.json         single source of truth for plugin metadata
src/                the plugin, written as an ordinary Python package
  main.py           entry module — exactly one BasePlugin subclass, absolute imports only
  greeting/         sample package (proves relative + cross-module imports survive bundling)
  textutil.py       sample top-level sibling module
tools/
  bundle.py         compiles plugin.json + src/ into dist/<id>.plugin
  devclient.py      speaks the on-device dev-server protocol (127.0.0.1:42690)
  verify_bundle.py  offline harness that proves a built bundle behaves correctly, no phone needed
scripts/            bash automation for the ADB debug loop (see below)
docs/sdk-notes.md   condensed SDK API surface gathered from the official docs

The three commands that matter

python3 tools/bundle.py --release          # compile src/ -> dist/<id>.plugin
python3 tools/verify_bundle.py dist/<id>.plugin   # offline proof the bundle works, no device needed
bash scripts/deploy.sh --enable            # build (--dev) + push to a connected device + enable + reload

bundle.py walks src/ recursively, embeds every .py file's source and dotted module name into a compressed, base64-encoded payload, and appends a small runtime preamble that installs a private import hook (scoped to a per-module __builtins__ dict, never touching sys.modules) so cross-module and cross-package imports resolve exactly as they would if the files were still on disk. --dev (the default) embeds absolute local paths so a debugger resolves breakpoints against your real source files; --release embeds repo-relative paths for distribution. Both modes produce byte-reproducible output for a given src/ + plugin.json.

Device prerequisites

  • exteraGram 12.5.1+.
  • In-app: Preferences → Plugins → enable the plugin engine, then enable developer mode.
  • USB debugging enabled, device authorized (adb devices).

Release-build APKs disallow adb run-as, so you cannot adb push a file straight into the app's private plugin directory. The on-device dev server (tools/devclient.py, scripts/deploy.sh) is the actual install path — it talks to the running app over a TCP socket forwarded with adb forward, and the app writes the file into its own storage.

ADB automation

All scripts live in scripts/, are runnable from any working directory, and honor the standard ANDROID_SERIAL env var for device selection.

  • scripts/adb-connect.sh [--debug] — starts adb, waits for the device, (re)creates the tcp:42690 port forward (and tcp:5678 with --debug), pings the dev server.
  • scripts/deploy.sh [--release] [--enable] [--no-connect] — bundles, connects, writes the plugin to the device, optionally enables it, reloads it.
  • scripts/watch.sh [deploy args…] — watches src/ and plugin.json, redeploys on change (uses inotifywait if available, otherwise polls).
  • scripts/logs.sh [-c] [-t] [-g PATTERN] — streams logcat for the running app (-t for the raw Chaquopy stdio tags, -g to grep, -c to clear first).
  • scripts/debug.sh start|stop [--platform vscode|pycharm] — forwards the debugger port and tells the dev server to start/stop the in-app debugger. Requires debugpy (vscode) or pydevd-pycharm (pycharm) present in the app's Python runtime — add it to requirements in plugin.json if it's missing.
  • scripts/fetch-sdk-stubs.sh [--beta] — downloads the latest SDK type stubs into stubs/ for editor autocomplete.

Writing your own plugin

  1. Edit plugin.json (id, name, description, etc.).
  2. Write your plugin under src/, with src/main.py as the entry module (absolute imports only — it's executed at plugin-module top level, not inside a package).
  3. python3 tools/bundle.py --release && python3 tools/verify_bundle.py dist/<id>.plugin (substitute your id from plugin.json).
  4. bash scripts/deploy.sh --enable with a device connected.