[no bump] Move technical docs to docs/, add icon and PNG export tool
Publish plugin / publish (push) Successful in 4s

README now carries the icon, TL;DR, description and features; the
technical sections live in docs/. tools/icon_png.py renders
images/icon.svg to a square PNG (512x512 by default, --resolution N).
plugin.json points at the plugin's own icon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lyuksovannyyandClaude Opus 5.5 committed 2026-10-09 14:16:37 +02:00
1 parent a2e4075a03
commit b86cc8a1c8
9 files changed
+412 -218

No files matched your search

+41 -217
View File
@@ -1,229 +1,53 @@
# Message Logger (exteraGram plugin)
<p align="center">
<img src="images/icon.svg" width="128" height="128" alt="Message Logger icon">
</p>
Shows what a [telegram-panel](../../telegram-panel) message logger saved, natively in chats:
<h1 align="center">Message Logger</h1>
- messages others delete stay (the delete updates are dropped), at 75% opacity with a red trash
icon (from the active icon pack) in the time row, and are reported to the server;
- logged messages deleted while the app was closed (or never seen deleted by the server, e.g.
bot-logged chats) are confirmed with Telegram, reported, and restored from the logger copy;
- *Edit history* in the message menu (right above Forward, for edited messages) opens every
version as a mock chat — also on the logger groups' own posts — with an *Open topic* button
for the history topic in the activity group. Versions and their media are read from the activity group through
a signed-in account that can (the current one, the master, or any member); otherwise the
server's text is shown and media are marked unavailable;
- the comments button of a deleted channel post (whose thread Telegram deletes with it) opens
the app's own comments section for it — avatars, names, replies, media — filled from the
logged comments (the thread's history request is answered locally);
- message links into a chat the current account isn't in (e.g. a log post's "Message" link)
open that chat as a signed-in account that is in it; going back returns to your account.
Every signed-in account's chats are cached client-side for this.
**TL;DR:** an [exteraGram](https://exteragram.app) plugin that keeps deleted messages in
your chats and shows the edit history of edited ones, using what your
[telegram-panel](../../telegram-panel) message logger saved.
The server only points at copies (logger/activity group message ids); the client reads them
through its own session, so the account must be a member of those groups.
## Description
The server is asked about a chat only when needed: when it wasn't checked within the last hour
(from the oldest loaded message on; the answer replaces the chat's cached marks), when older
history is loaded past the checked range (that part only), or after an edit/delete event in the
chat. Answers hold at most 100 marks (edited/deleted) and 100 missing ids, newest first. Marks
are cached per chat in the plugin's SQLite (`state.db`, no size cap), so a chat opened again —
even offline — shows them at once, and deleted messages are restored from the cache. An expired
chat keeps its cache until the next successful check, which replaces it and starts over. Cached
answers older than *Cache lifetime* (1/7/30/90/365 days or never; 30 by default) are removed,
checked once a day (from local midnight) and when the setting changes.
Message Logger connects exteraGram to a telegram-panel message logger and shows its data
natively in chats. Messages other people delete stay visible, edited messages get a full
edit history, and comments of deleted channel posts can still be opened. Marks are cached on
the device, so a chat opened again, even offline, shows them at once.
Setup: in the panel create a token per master — the account owning the logger groups —
(Settings → API Tokens); in the plugin settings enter the panel URL and the tokens (separated by
spaces), then *Test connection*. Any Telegram account can use a token. Diagnostics: see `docs/sdk-notes.md` (gotchas section).
## Features
The rest of this README describes the build toolchain this repo came from (pluginBase).
- **Deleted messages stay.** Messages others delete remain in the chat at 75% opacity with a
red trash icon (from the active icon pack) in the time row. Messages deleted while the app
was closed are confirmed with Telegram and restored from the logger copy.
- **Edit history.** *Edit history* in the message menu (right above Forward, for edited
messages) opens every version as a mock chat, also for the logger groups' own posts, with
an *Open topic* button for the history topic in the activity group.
- **Comments of deleted channel posts.** The comments button of a deleted post opens the
app's own comments section for it (avatars, names, replies, media), filled from the logged
comments.
- **Links into chats you're not in.** Message links into a chat the current account isn't
in (e.g. a log post's "Message" link) open that chat as a signed-in account that is in it;
going back returns to your account.
- **Cache lifetime.** Cached answers are kept for 1, 7, 30, 90 or 365 days, or forever
(30 by default).
## Plugin base toolchain
The account reading the logged copies must be a member of the logger and activity groups.
A build toolchain and ADB automation for writing [exteraGram](https://exteragram.app) plugins
as ordinary multi-file Python projects instead of hand-maintaining one giant `.py` file.
## Setup
## How exteraGram plugins actually work
1. In the panel, create a token per master, the account owning the logger groups
(Settings → API Tokens). Any Telegram account can use a token.
2. In the plugin settings, enter the panel URL and the tokens (separated by spaces).
3. Tap *Test connection*.
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`.
For diagnostics, see the gotchas section of [docs/sdk-notes.md](docs/sdk-notes.md).
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.
## Documentation
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
release.py publishes a built bundle to a Gitea release (used by CI)
.gitea/workflows/ CI that builds, verifies and publishes the .plugin to Releases
scripts/ ADB debug-loop automation and the git hooks (see below)
docs/sdk-notes.md condensed SDK API surface gathered from the official docs
```
## The three commands that matter
```bash
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`.
Its first run in a clone also enables the repo's git hooks (see
[Git hooks and versioning](#git-hooks-and-versioning)).
## 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.
## Git hooks and versioning
`scripts/pre-commit` bumps the patch number of `version` in `plugin.json` on every commit
(`1.0.0` → `1.0.1`) and stages it into that same commit. `scripts/post-commit` re-stages
`plugin.json` afterwards, because commits of a file selection (`git commit -- <paths>`, which
some IDEs use) are built from a temporary index and would otherwise leave it looking modified.
**The hooks enable themselves.** Git never activates hooks from repository content, so a
fresh clone, or a repo created from this template, starts without them. The first run of
`tools/bundle.py` fixes that by running `git config core.hooksPath scripts`, and every
workflow runs it: directly, or through `scripts/deploy.sh` and `scripts/watch.sh`. That
includes `--check`, so after cloning run
```bash
python3 tools/bundle.py --check
```
once, before your first commit. A commit made before any build runs without the hooks.
`bundle.py` leaves `core.hooksPath` alone when it is already set (so pointing it elsewhere on
purpose sticks), skips the step in CI (whenever `CI` is set) and outside a git work tree, and
never fails a build over it. To turn the hooks off for a clone, run
`git config core.hooksPath .git/hooks`; `git config --unset core.hooksPath` only lasts
until the next build.
The bump is skipped when:
- the commit message contains `[no bump]`, `[skip bump]`, `[no version bump]` or
`[skip version]` (case-insensitive; `nobump`, `no-bump` and `no_bump` also work),
- `NO_BUMP=1` (or `GIT_NO_BUMP=1`) is set, e.g. `NO_BUMP=1 git commit`, which is the only way
for commits written in an editor, since their message doesn't exist yet when the hook runs,
- `version` in `plugin.json` already differs from `HEAD` (you bumped it by hand, e.g. for a
minor or major release),
- a merge, rebase, cherry-pick or revert is in progress.
Because every version change publishes a release (see below), a normal commit + push to
`main` ships a new patch release. Use a stop word for commits that should not.
## Publishing a release
`.gitea/workflows/release.yml` builds, verifies and publishes the `.plugin` to the repo's
Releases tab on every push to `main`. `dist/` is gitignored, so the artifact is always built
by CI rather than committed. Nothing in the workflow is plugin-specific — it reads the id and
version straight out of `plugin.json`, so it keeps working after you rename the plugin.
The `version` field in `plugin.json` is the single source of truth:
| Push to `main` with… | Result |
| --- | --- |
| a new `version` | creates tag `v<version>` and a new release |
| the same `version` | rebuilds the existing release, replacing its attachment |
So every push leaves a downloadable build attached to the current version, and bumping
`version` is what cuts a new release. With the git hooks enabled every commit bumps it,
so every push of a normal commit cuts a new patch release. Pushing a `v*` tag by hand still works and uses that
tag verbatim, as does a manual run (Actions → Publish plugin → Run workflow).
Every path runs `--check`, builds with `--release`, and gates the upload on
`verify_bundle.py`, so a bundle that fails verification is never published. A rebuild
replaces the attachment rather than piling up duplicates, and refreshes the release notes to
name the commit it came from. The tag itself is never moved: it keeps pointing at the commit
that first introduced that version.
Authentication uses `secrets.API_TOKEN`, an organisation-level Actions secret holding a user
token with the `write:repository` scope (which covers releases). Set it once under
**Org → Settings → Actions → Secrets** and every repo in the organisation inherits it; a
repo-level secret of the same name takes precedence if one repo ever needs different
credentials.
The workflow passes it to `release.py` as `RELEASE_TOKEN`, **not** `GITEA_TOKEN`. The Actions
runner injects its own `GITEA_TOKEN` into every step, and that value overrides a step-level
`env:` entry of the same name, so the org secret would be silently ignored.
If the org secret does not reach a repository the publish still works: `release.py` tries
`RELEASE_TOKEN` first and then falls back to the runner's `GITEA_TOKEN`, which can publish
releases in its own repository, and prints which one it used. An organisation secret is only
exposed to a repository that has Actions enabled, so check **Repo → Settings → Actions** if a
repo unexpectedly falls back.
If you would rather not manage a token at all, that injected `GITEA_TOKEN` can create
releases in its own repo: pass `--token-env GITEA_TOKEN` and drop the org secret. It is
scoped to the single job and expires with it, so it is the safer default; `API_TOKEN` is what
you want when publishing needs to reach beyond this repo.
## Writing your own plugin
1. Run `python3 tools/bundle.py --check` once. It enables the git hooks that auto-bump the
version on commit (see [Git hooks and versioning](#git-hooks-and-versioning)).
2. Edit `plugin.json` (id, name, description, etc.).
3. 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).
4. `python3 tools/bundle.py --release && python3 tools/verify_bundle.py dist/<id>.plugin`
(substitute your `id` from `plugin.json`).
5. `bash scripts/deploy.sh --enable` with a device connected.
- [How Message Logger works](docs/how-it-works.md): server sync and the local cache
- [Development](docs/development.md): getting started, repo layout, building and verifying, icon export
- [How exteraGram plugins work](docs/how-plugins-work.md): the plugin format and lifecycle
- [Device setup and ADB automation](docs/device-and-adb.md): deploy, watch, logs, debugger
- [Versioning and releases](docs/versioning-and-releases.md): git hooks, auto-bump, CI publishing
- [SDK notes](docs/sdk-notes.md): condensed exteraGram SDK API surface