Skip to content

Repository files navigation

Android Repository Bot

Android Repository Bot turns a public GitHub or GitLab repository into a reviewed Telegram channel post. Staff remain in control: the bot loads and normalizes repository evidence, generates constrained English copy, renders a 1920 × 1080 PNG banner, and requires explicit confirmation before publication.

The project is one installable Python 3.14 application. Runtime code lives in src/androidrepo_bot, tests live in tests, and the androidrepo-bot console script and python -m androidrepo_bot use the same entry point.

What it preserves

  • Public GitHub and GitLab repository-root URLs, including nested GitLab namespaces.
  • Provider-stable repository identity and observed aliases in PostgreSQL.
  • A three-calendar-month publication cooldown, based on PostgreSQL calendar arithmetic rather than a fixed number of days.
  • Evidence-grounded structured generation with verified destination mapping.
  • Staff review, regeneration, cancellation, final confirmation, and safe publication retry behavior.
  • Full HD banners with attributed NASA artwork or a bundled offline fallback.
  • Best-effort staff audit messages and structured, secret-redacted process logs.

Quick start with Docker Compose

Requirements:

  • Docker with Compose
  • a Telegram bot with access to the configured staff chat, topics, and channel
  • an OpenCode Zen API key

Copy the environment template and replace every required placeholder:

cp .env.example .env

Build and start the bot:

docker compose up --build bot

Compose starts PostgreSQL, waits for its health check, runs the dedicated migrate service through alembic upgrade head, and starts the bot only after migrations succeed. Database data remains in the postgres-data named volume. The bot and migration containers run with all Linux capabilities dropped and no-new-privileges enabled; the application image runs as an unprivileged androidrepo user.

To apply migrations without starting the bot:

docker compose up --build migrate

The Compose PostgreSQL service is intentionally not published to the host. Use the complete Compose stack above, or provide a separately reachable PostgreSQL server for a host-run bot.

Local development

Requirements:

  • Python 3.14
  • uv
  • PostgreSQL reachable from the host

Install the locked application and all development groups:

uv sync --locked --all-groups

Copy .env.example to .env, replace the placeholders, and change AR_DATABASE_URL to the host-reachable PostgreSQL URL. Alembic reads AR_DATABASE_URL from the process environment rather than loading .env itself, so apply migrations explicitly:

AR_DATABASE_URL='postgresql+asyncpg://user:password@localhost:5432/androidrepo' \
  uv run alembic upgrade head

Run either supported entry point:

uv run androidrepo-bot
uv run python -m androidrepo_bot

At startup, the application validates settings, opens one shared aiohttp.ClientSession, verifies PostgreSQL connectivity, enters the PydanticAI agent, registers Telegram commands, and begins polling. Shutdown stops admission of new work, drains admitted update handlers, emits a best-effort stop audit, and closes resources through one central AsyncExitStack.

Configuration

Settings come from .env and process environment variables prefixed with AR_; process environment values take precedence. Empty optional token values are ignored. See .env.example for annotated placeholders.

Variable Required Default Purpose
AR_BOT_TOKEN yes Telegram bot token
AR_STAFF_CHAT_ID yes Staff chat admitted to the post workflow
AR_POST_TOPIC_ID yes Staff topic for /post, /cancel, and draft callbacks
AR_LOG_TOPIC_ID yes Staff topic receiving best-effort operational audits
AR_CHANNEL_ID yes Channel receiving confirmed posts
AR_OPENCODE_ZEN_API_KEY yes OpenCode Zen generation credential
AR_DATABASE_URL yes postgresql+asyncpg:// runtime database URL
AR_LOG_LEVEL no INFO CRITICAL, ERROR, WARNING, INFO, or DEBUG
AR_OPENCODE_ZEN_MODEL no deepseek-v4-flash OpenCode Zen model identifier
AR_GITHUB_TOKEN no Token for authenticated GitHub API requests
AR_GITLAB_TOKEN no Token for authenticated GitLab API requests

The staff chat and channel IDs must be non-zero, and both topic IDs must be positive. The database URL must use SQLAlchemy's postgresql+asyncpg driver.

Never commit .env. Do not put credentials, authorization headers, database passwords, private chat content, or secret-bearing URLs in logs, screenshots, issues, or pull requests.

Telegram behavior

Access

/start is admitted in any chat and links to the source repository, Android Repository channel, and community. Every other handled update must belong to the configured staff chat and post topic. Updates outside that scope are ignored.

Draft state uses aiogram's in-memory storage with per-user-in-chat isolation. An active draft therefore does not survive a process restart, while repository identity and publication history remain persistent in PostgreSQL.

Create and review a draft

  1. Send /post followed by a public repository-root URL:

    /post https://github.com/owner/repository
    /post https://gitlab.com/group/subgroup/repository
    
  2. With no argument, /post shows the expected URL form. An invalid URL gets a specific rejection without replacing an active draft.

  3. The parser accepts HTTPS GitHub and GitLab roots only. It rejects embedded credentials, custom ports, query strings, fragments, encoded or malformed paths, GitHub subpaths, and GitLab /-/ subresources.

  4. After a new URL is valid, the bot deactivates the previous owned draft controls and replaces its active session.

  5. The bot loads normalized provider metadata, README content when available, latest-release metadata, languages, license, topics, homepage, and verified links. Missing README content is allowed; the resulting draft uses metadata only and the bot sends a warning.

  6. The provider-stable identity and requested/canonical aliases are upserted. The cooldown is checked before generation and banner rendering. A blocked request is stored as an audited attempt.

  7. The bot generates, validates, renders, and sends a review message with Publish, Regenerate, and Cancel controls.

Generation treats every repository string, including README and release text, as untrusted evidence. Only the first 50,000 README characters are supplied. The structured result requires a canonical name, one summary, three to five distinct features, one to three supported tags, and at most four optional link selections. Free text, duplication, link IDs, semantic labels, project naming, and the 950-character generation budget are validated. The agent may make at most three model requests: the initial request and two output-correction retries. Each complete generation run has a 120-second deadline.

The model never supplies a destination URL to the final post. It selects stable IDs from inspected evidence; the service resolves those IDs through the same repository snapshot and always adds the mandatory repository destination. Unknown, mandatory-as-optional, and unresolved selections are not mapped.

The final Telegram caption contains the title, italic summary, key features, verified links, and hashtags, and is checked against Telegram's 1,024-character photo-caption limit.

Regenerate, cancel, and publish

  • Regenerate uses the already loaded repository snapshot. The existing usable draft remains active if generation, rendering, or Telegram delivery fails. A successful replacement is stored before deletion of the old draft is attempted.
  • Cancel and /cancel clear the session and delete the draft and optional missing-README notice on a best-effort basis.
  • Publish first switches to an explicit confirmation screen. Back returns to the draft without publishing; Publish now performs the channel copy.
  • Callbacks must belong to the session owner and originate from the active draft message. Stale, foreign, and malformed callbacks receive an alert and cannot mutate or publish the session.

Publication is serialized per repository in the running process, and the cooldown is checked again immediately before the channel copy. After a successful copy, the channel message receipt is stored in the FSM before the database write. If the database write fails, pressing Publish now again retries only the idempotent publication record; it does not copy the channel post twice. Until that receipt is saved, back, cancel, and replacement actions are blocked so the retry data cannot be discarded. The database also enforces uniqueness on channel ID and message ID. If Telegram rejects the copy, the draft remains available for another attempt.

The staff log topic receives best-effort lifecycle, draft creation/failure, replacement/cancellation, and publication success/failure events. Failure to deliver an audit message does not fail the underlying workflow.

Architecture

src/androidrepo_bot/
├── app.py              # composition root, Telegram middleware, lifecycle
├── config.py           # validated AR_* settings
├── start.py            # global /start presentation
├── posts/              # commands, callbacks, FSM, UI, workflow service
├── repositories/       # URL parsing, shared HTTP policy, GitHub/GitLab
├── generation/         # evidence prompt, schema, validation, AI service
├── media/              # NASA artwork and packaged banner renderer assets
└── db/                 # SQLAlchemy models, operations, packaged migrations

app.py directly composes concrete services. Repository and NASA access share one application-owned HTTP session; PostgreSQL uses operation-scoped async sessions; the application owns the bot, database engine, and generation-agent lifecycle. There are no internal workspace distributions or compatibility layers.

Repository provider responses are bounded, parsed as untrusted JSON, and validated before normalization. Transient provider failures use bounded retries, and safe user messages distinguish not-found, rate-limit, timeout, and temporary-provider failures.

PostgreSQL and migrations

Alembic configuration is in alembic.ini. Migration scripts are packaged under androidrepo_bot.db.migrations, alongside the installed application.

The schema stores:

  • provider-stable repository identities and observed aliases
  • successful channel publications
  • cooldown-blocked attempts

The current migrations preserve first-seen identity data, normalize aliases, enforce blocked-attempt shape, deduplicate legacy publication receipts, and make channel publication receipts unique. Do not stamp or edit a production schema manually; apply committed revisions with:

AR_DATABASE_URL='postgresql+asyncpg://user:password@host:5432/androidrepo' \
  uv run alembic upgrade head

Banners and asset attribution

BannerService attempts to load a curated image from the NASA Image and Video Library. Remote image downloads are restricted to NASA's HTTPS asset host, bounded to 12 MiB, and time-limited. Invalid, unavailable, or undecodable artwork falls back to the packaged black-hole image. Every result is a 1920 × 1080 RGB PNG and includes visible artwork credit and identifier text.

Bundled assets and their requirements are listed in the asset inventory:

  • The Android robot artwork is based on work created and shared by Google and is used under the Creative Commons 3.0 Attribution License terms referenced by the Android brand guidelines. The renderer includes the attribution.
  • Figtree is redistributed under the SIL Open Font License; its complete license text is packaged beside the font.
  • black-hole-fallback.webp was generated with OpenAI for this repository.

Keep the asset inventory and license files with redistributed builds.

Development and verification

Run checks from the repository root:

uv lock --check
uv sync --locked --all-groups
uv run ruff format --check .
uv run ruff check .
uv run pyright
uv run pre-commit run --all-files
uv build

Pyright runs in strict mode.

The built wheel must contain the complete package, database migrations, banner assets, and androidrepo-bot console-script metadata. The package is marked Private :: Do Not Upload.

License and support

Copyright (C) 2026 Hitalo M.

This project is licensed under the GNU Affero General Public License, version 3 or later (AGPL-3.0-or-later). Modified versions offered for remote network interaction must provide corresponding source as required by AGPL section 13.

Use the repository's GitHub issues for reproducible bugs and scoped improvements. Never include secrets or private Telegram content in a report.

About

The bot responsible for keeping Android Repository up to date.

Topics

Resources

Stars

11 stars

Watchers

2 watching

Forks

Used by

Contributors

Languages