A Rust Telegram bot that turns public GitHub/GitLab evidence into reviewed Android Repository channel posts. Staff load a repository, review a generated English caption and 1920 × 1080 banner, then explicitly confirm publication.
One binary owns the HTTP connection pool, SQLite pool, bounded rendering work and teloxide dispatcher in a single Rust module tree.
Install Rust through rustup.
rust-toolchain.toml pins Rust 1.99.1 with Clippy and rustfmt. Native builds need a C/C++ compiler, CMake and pkg-config for the
TLS dependencies and bundled SQLite.
cargo build --release --locked
cp config.example.toml config.toml
chmod 600 config.toml
# Fill in your Telegram, OpenCode and SQLite file settings.
cargo run --release --locked -- check-config
cargo run --release --locked -- migrate
cargo run --release --locked -- runOmitting the subcommand defaults to run. check-config does not contact any service;
migrate requires only database_url. Commands load config.toml from the
working directory. Use --config /path/to/config.toml before or after the
subcommand to select another file. Settings come only from TOML; .env and
process environment variables are not loaded.
Choose a writable local SQLite file path; its parent directory must exist.
migrate creates the file if missing. Run migrations successfully before starting
the bot. Assets and SQL migrations are embedded in
the executable; preserve the bundled asset licenses when distributing it.
Read-only provider inspection and offline rendering are available without bot, model or database credentials:
cargo run --release --locked -- inspect https://github.com/AntennaPod/AntennaPod \
--output /tmp/evidence.json
cargo run --release --locked -- render-banner --title AntennaPod \
--repository AntennaPod/AntennaPod --stack 'Java · GPL-3.0 · Podcast' \
--output /tmp/banner.pnginspect reads public provider APIs and writes evidence locally. It uses the
optional provider token if configured; it never invokes AI or Telegram.
render-banner always uses the bundled offline artwork.
| TOML key | Required for polling | Default | Purpose |
|---|---|---|---|
bot_token |
yes | — | Telegram bot credential |
staff_chat_id |
yes | — | Staff supergroup, nonzero ID |
post_topic_id |
yes | — | Positive staff workflow topic ID |
log_topic_id |
yes | — | Positive topic ID for best-effort audits |
channel_id |
yes | — | Target channel, nonzero ID |
opencode_zen_api_key |
yes | — | OpenCode Console API credential |
database_url |
yes | — | Persistent file URL, e.g. sqlite://androidrepo.db |
log_level |
no | INFO |
CRITICAL, ERROR, WARNING/WARN, INFO, DEBUG |
opencode_zen_model |
no | mimo-v2.6-flash-free |
Model with Chat Completions tool calling |
github_token |
no | — | Optional GitHub API token |
gitlab_token |
no | — | Optional GitLab API token |
Generation uses the OpenCode endpoint
https://opencode.ai/inference/openai/v1/chat/completions. Choose a model that
supports that API and tool calls; models exposed only through Responses or
Anthropic Messages are not interchangeable. Free model availability is temporary;
consult the provider's current model documentation.
Copy config.example.toml to config.toml. IDs are TOML
integers; credentials, URLs, model names and log levels are strings. Unknown keys,
invalid types and malformed TOML are rejected. Blank optional tokens are ignored.
inspect also works when the default file is absent; an explicit --config path
must exist. render-banner does not load configuration. SQLite relative paths
resolve from the working directory, including when --config points elsewhere.
Never commit a populated configuration file or log credentials, raw external
errors or private chat text. The default config.toml is ignored by Git; keep
custom configuration paths outside the repository.
/startis public./post,/cancel,/reconcileand callbacks require the configured staff chat and post topic. Commands must have an identified user./post https://github.com/owner/repositoryor a nested GitLab repository root loads public evidence. An invalid URL leaves the current draft intact.- Regenerate keeps the previous usable draft on failure. A replacement becomes active before the previous message is deleted.
- Publish opens confirmation. Publish now copies to the channel;
Back keeps the draft. Cancel and
/cancelclear the owned session. - Draft and missing-download controls are bound to their owner and exact active message. Restarting discards drafts, but never publication history or recovery reservations.
- Missing README content produces a notice. Missing download evidence requires a separate staff decision before generating without a Download button.
Only HTTPS repository roots are accepted. Credentials, custom or explicit ports, encoded/dot paths, query strings, fragments and provider subresources are rejected. API credentials are scoped to their provider; public visibility is checked even when authenticated. Stable provider IDs and observed aliases are stored in SQLite.
README links are parsed as CommonMark and inert HTML. Code, hidden markup and image sources are excluded. The model selects inspected IDs, never final URLs. Donation links cannot become optional post links; Download must resolve to a verified release, Android app store or package repository.
Generation has four outcomes: draft, affirmatively non-Android project, insufficient evidence, or missing-download warning. It permits at most three requests, 2,048 output tokens each, 6,144 total, within 120 seconds. Evidence fields and collections are bounded, README text is limited to 50,000 characters, and escaped evidence JSON to 80,000. Drafts require distinct features and tags, plain text, eligible link selections and a 950-character text budget. The final caption is also checked against 1,024 UTF-16 units.
A committed copying reservation precedes the only channel-copy request.
SQLite BEGIN IMMEDIATE transactions acquire the writer lock before reading
publication state; a unique open-operation index also prevents duplicate
reservations. The cooldown is three calendar months in UTC, clamped to the last
valid day of the destination month.
An expired lease means unresolved delivery, never permission to copy again.
The receipt and completed publication are committed atomically. If storing them fails, the bot records a receipt and compensation lease before attempting to delete the channel copy. Confirmed deletion releases the reservation. Failed or ambiguous deletion retains a blocking operation or a conservatively recorded publication. Both copy and compensation leases prevent concurrent staff reconciliation while an external effect is in flight.
After inspecting the channel, staff can resolve an uncertain operation:
/reconcile <operation-id> absent
/reconcile <operation-id> <channel-message-id> <ISO-8601-publication-time>
Example timestamp: 2026-10-22T18:00:00-03:00. Use the actual visible message and
its actual publication time. Reconciliation never sends another channel copy.
The bot supplies the operation ID and any known receipt in its recovery notice.
src/
├── main.rs # CLI, configuration and resource composition
├── config.rs # TOML settings; secrets have no Debug representation
├── error.rs # safe errors without external response bodies
├── http.rs # shared pool, bounded reads and GET retries
├── bot/
│ ├── mod.rs # teloxide dispatcher, per-user state, shutdown
│ ├── handlers.rs # admission, commands and callback transitions
│ ├── drafts.rs # evidence → generation → banner → review message
│ ├── publication.rs # Telegram delivery, persistence and compensation
│ └── ui.rs # caption, keyboards and message helpers
├── repositories/
│ ├── mod.rs # normalized evidence values and provider selection
│ ├── reference.rs # strict URL parsing
│ ├── client.rs # shared provider policy and normalization
│ ├── github.rs # GitHub metadata and README format
│ ├── gitlab.rs # GitLab namespaces and branch-aware README paths
│ └── links.rs # inert CommonMark/HTML link extraction
├── generation/ # output validation, evidence prompt, tool-call client
├── media/ # NASA fetch, bounded decoding, renderer, licensed assets
└── db/ # registration and transactional publication transitions
migrations/ # embedded SQLite SQLx revisions
bot/ contains Telegram-facing behavior; providers, generation and rendering do
not depend on Telegram. Database functions do not perform channel effects.
bot/publication.rs coordinates that boundary and db/publications.rs owns SQL
state transitions. The split keeps delivery recovery reviewable without creating
repository interfaces, service containers or generic workflow engines.
The dispatcher serializes updates by user. State mutexes protect only short map
operations, never network calls. Up to four draft jobs and two render jobs run
concurrently. HTTP requests share a connection pool; independent repository
resources load concurrently. Image decoding and rendering use spawn_blocking.
SIGINT/SIGTERM stop polling and drain dispatcher work before the database closes.
Allow sufficient shutdown grace time: forced termination leaves unfinished
reservations blocked for reconciliation.
SQLite uses WAL journaling, synchronous=FULL, foreign keys and a ten-second
busy timeout. All writers acquire BEGIN IMMEDIATE before reading state. Keep
the database on a local persistent filesystem with reliable file locks; multiple
bot processes must use the same file on the same host. Do not share it over a
network filesystem. Database URLs must use sqlite:// with a nonempty file path;
in-memory databases and URL query options are rejected so reservations survive
restarts. Relative paths resolve from the process working directory; prefer an
absolute path in deployments.
Stop and drain the bot before backing up the database. With every connection
closed, preserve the database and any remaining -wal/-shm sidecars together.
For a live backup, use SQLite's backup API rather than copying the main file.
Run migrate before starting the bot. The embedded initial migration creates
the SQLite schema with JSON tag arrays and UTC timestamp text. The bot has no
production database yet, so the former PostgreSQL/Alembic baseline has been
replaced. No PostgreSQL server or data import is required for a fresh installation.
Any local PostgreSQL development data is not imported automatically.
After deployment, never edit an applied SQL revision; add a new migration under
migrations/. No credentials or media assets change.
The baseline stores the following records. Draft sessions remain in memory and are not restored after a restart.
| Table | Purpose and identity |
|---|---|
repository_apps |
Stable project identity, unique by provider and provider repository ID; current path and first/last observation times |
repository_aliases |
Observed paths for a project, unique by project, full name and URL |
post_attempts |
Cooldown refusals with requesting user, attempted path and blocked-until time |
published_posts |
Completed publication title, JSON tags, actor, UTC timestamp and unique channel/message receipt |
publication_operations |
Copy/compensation ownership, recovery state and receipt; at most one open operation per project |
The rustdoc for db::publications lists state transitions and each function's
lease, receipt and reconciliation preconditions. SQL constraints remain defined
by the embedded migration, rather than by a separate schema representation.
Generate the internal reference with the pinned toolchain:
cargo doc --locked --no-deps
# Open target/doc/androidrepo_bot/index.html, or use:
cargo doc --locked --no-deps --openThis is a private binary crate, so Cargo documents private items by default. The landing page explains resource ownership and links to all modules. Module pages describe trust boundaries; item pages cover types, fields, variants, helpers, errors and publication cancellation requirements. CLI examples use shell blocks because these modules are not an externally importable library. The build/run and offline commands above provide operational examples.
Follow the Rustdoc Book and the Rust API Guidelines when changing these contracts:
- Use
//!for crate/module overviews and///for items. Start with a short summary, then explain behavior, units, side effects and caller obligations. - Link related Rust items with intra-doc links. Disambiguate functions/modules and enums/macros when their names overlap.
- Document fallible operations under
# Errors; add# Panicsfor known panic preconditions. Document cancellation where dropping a future affects durable work. Unsafe code remains forbidden, so no unsafe API contracts are needed. - Give meaningful examples at the relevant workflow boundary. Mark shell and
non-Rust examples with their actual language. If adding Rust examples, keep
them compilable and use
?for errors; do not hide broken examples withignoreor add test suites without explicit authorization. - Update the affected contract alongside implementation changes. Keep credentials, private text and populated configuration out of documentation.
Clippy rejects missing documentation even on private items. Rustdoc rejects broken intra-doc links, invalid HTML tags and bare URLs. To additionally treat every documentation warning as an error, run:
RUSTDOCFLAGS='-D warnings' cargo doc --locked --no-depscargo metadata --locked --format-version 1 --no-deps > /dev/null
cargo fmt --all -- --check
cargo clippy --locked --all-targets -- -D warnings
cargo build --release --locked
cargo doc --locked --no-deps
prek run --all-filesInstall prek as a development tool and activate the Git hook in each checkout:
uv tool install prek==0.5.7
prek installprek.toml uses built-in Rust hooks for file hygiene, with no remote
hook repositories or Python hook environments. Their versions follow the prek
binary; the installation command pins the validated release, and the config
requires at least that version. Review and validate the hooks when upgrading
prek. It is not a bot dependency. See the
installation guide for other installers.
Read-only hygiene checks run concurrently. Text fixers run sequentially to avoid
editing the same files concurrently; Rust formatting and Clippy checks then run
together. Rust checks include deleted files, and Clippy also runs for manifest,
lockfile and pinned toolchain changes. The toolchain stays managed by rustup via
rust-toolchain.toml.
For focused local checks, use the hook groups; use the full run above before finishing a change:
prek run --all-files --group hygiene
prek run --all-files --group rust
prek validate-config prek.tomlThere is no Rust test suite. AGENTS.md lists project constraints and verification commands; domain-specific manual scenarios live in the repository skills.
Release builds use thin LTO and one codegen unit. Connection reuse, bounded concurrency and one-pass image compositing target the useful work in this bot. Network/model latency still dominates post creation. No end-to-end speedup over previous releases is claimed without comparable measurements.
The application is AGPL-3.0-or-later. Bundled font, Android robot and fallback
artwork are unchanged; their source/license inventory is in
src/media/assets/README.md. Every banner retains
Android attribution and a NASA or generated-artwork credit.