Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copilot Instructions for python-italy-telegram-bot

## Project Overview

**Electus** is the official Telegram bot for Italian Python community groups.
It handles welcome captcha verification, moderation (ban/mute/report), spam detection, and multi-group management.
Built with `python-telegram-bot` (async), PostgreSQL via `psycopg`, and deployed on Fly.io.

## Tech Stack

- **Language**: Python 3.14+
- **Bot Framework**: python-telegram-bot >= 22.0 (async API)
- **Database**: PostgreSQL via psycopg 3 with async connection pooling; in-memory fallback for dev
- **Config**: python-dotenv for environment variables
- **Build**: Hatchling; dependency management with uv
- **Linting/Formatting**: ruff
- **Type Checking**: mypy
- **Testing**: pytest

## Architecture

Layered architecture with strict separation of concerns:

```
Handlers (telegram.ext) → Services (business logic) → Repository (data access)
```

- **Handlers** (`src/python_italy_bot/handlers/`): Receive Telegram updates, delegate to services. Each module exposes a `create_*_handlers()` factory that returns a list of `telegram.ext` handler objects.
- **Services** (`src/python_italy_bot/services/`): Contain business logic (`CaptchaService`, `ModerationService`). Depend on `AsyncRepository`.
- **Repository** (`src/python_italy_bot/db/`): Abstract `AsyncRepository` base class with `InMemoryRepository` and `PostgresRepository` implementations. Factory function `create_repository()` selects based on `DATABASE_URL`.
- **Models** (`src/python_italy_bot/db/models.py`): Domain dataclasses (`Ban`, `Mute`, `Report`).
- **Config** (`src/python_italy_bot/config.py`): `Settings` class loads all env vars.
- **Strings** (`src/python_italy_bot/strings.py`): Centralized bot message templates.

Dependency injection is done via `context.bot_data` dictionary, populated in `_post_init`.

## Coding Standards

- **Type hints**: Use modern Python type syntax everywhere (`int | None`, `list[int]`), no `Optional` or `Union`.
- **Async/await**: All handlers, services, and repository methods are `async def`.
- **Docstrings**: Module-level docstring on every file. One-line docstrings on functions and classes.
- **Imports**: Use relative imports within the package (`from ..services.captcha import CaptchaService`).
- **Handler pattern**: Define private `async def _handle_*` functions; expose a public `create_*_handlers()` factory returning `list`.
- **Error handling**: Wrap Telegram API calls in `try/except`, log warnings, degrade gracefully.
- **Logging**: Use `logging.getLogger(__name__)` per module.
- **String formatting**: Use f-strings or `.format()` with named placeholders from `strings.py`.
- **No global mutable state**: Pass dependencies through services and `bot_data`.

## Testing Rules

- Use `pytest` with async support for testing async code.
- Test services and repository implementations independently.
- Use `InMemoryRepository` for unit tests instead of mocking the database.
- Keep tests in the `tests/` directory mirroring the `src/` structure.

## Common Pitfalls

- **Permissions**: Always check that the bot has admin permissions before calling `restrict_chat_member` or `ban_chat_member`.
- **Null checks**: `update.effective_chat`, `update.effective_user`, and `update.message` can all be `None`; guard every handler.
- **Global vs per-chat**: Verification and bans operate globally across all tracked chats. Use `register_chat()` to track new chats.
- **Connection pool**: `PostgresRepository` uses `psycopg_pool.AsyncConnectionPool`; always access connections via `async with self._pool.connection()`.
- **Environment variables**: Required vars raise `ValueError` if missing. Optional vars default to `None`. See `.env.example` for the full list.
- **Bot messages are in Italian**: Keep all user-facing strings in `strings.py` in Italian.
136 changes: 136 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# CLAUDE.md — Project Specification for python-italy-telegram-bot

<project>
<name>python-italy-telegram-bot (Electus)</name>
<description>
Official Telegram bot for the Italian Python community groups.
Handles welcome captcha verification, moderation (ban/mute/report),
spam detection, and multi-group management.
</description>
<language>Python 3.14+</language>
<license>MIT</license>
</project>

<tech_stack>
<runtime>Python 3.14+ (async/await throughout)</runtime>
<framework>python-telegram-bot >= 22.0 (async API via telegram.ext)</framework>
<database>PostgreSQL via psycopg 3 with AsyncConnectionPool; InMemoryRepository fallback</database>
<config>python-dotenv for environment variables</config>
<build>Hatchling build backend; uv for dependency management</build>
<deployment>Docker multi-stage build; deployed on Fly.io (polling mode)</deployment>
<linting>ruff (linter and formatter)</linting>
<type_checking>mypy</type_checking>
<testing>pytest</testing>
</tech_stack>

<architecture>
<overview>
Layered architecture: Handlers → Services → Repository → Database.
Dependency injection via context.bot_data dictionary populated at startup.
</overview>

<layer name="handlers" path="src/python_italy_bot/handlers/">
Receive Telegram updates and delegate to services.
Each module exposes a create_*_handlers() factory returning a list of telegram.ext handler objects.
Private async handler functions follow the _handle_* naming convention.
Modules: welcome.py, moderation.py, spam.py, settings.py, id.py, announce.py, ping.py.
</layer>

<layer name="services" path="src/python_italy_bot/services/">
Business logic layer. Classes: CaptchaService, ModerationService, SpamDetector.
Depend on AsyncRepository for persistence. Stateless except for repository reference.
</layer>

<layer name="repository" path="src/python_italy_bot/db/">
Abstract AsyncRepository base class (db/base.py) with two implementations:
- InMemoryRepository (db/in_memory.py) — for development and testing
- PostgresRepository (db/postgres.py) — production with psycopg AsyncConnectionPool
Factory function create_repository() in db/__init__.py selects implementation based on DATABASE_URL.
Domain models (Ban, Mute, Report) are dataclasses in db/models.py.
</layer>

<layer name="config" path="src/python_italy_bot/config.py">
Settings class loads environment variables. Required vars raise ValueError if missing.
See .env.example for the full list of configuration options.
</layer>

<layer name="strings" path="src/python_italy_bot/strings.py">
All user-facing bot messages centralized here, in Italian.
Uses named placeholders for .format() substitution.
</layer>

<layer name="entry_point" path="src/python_italy_bot/main.py">
Creates ApplicationBuilder, registers handlers via _post_init callback,
initializes repository and services, runs polling loop.
</layer>
</architecture>

<coding_conventions>
<rule name="type_hints">
Use modern Python type syntax: int | None, list[int], dict[str, Any].
Do not use Optional or Union from typing.
</rule>
<rule name="async">
All handlers, service methods, and repository methods must be async def.
</rule>
<rule name="docstrings">
Every file has a module-level docstring. Functions and classes have one-line docstrings.
</rule>
<rule name="imports">
Use relative imports within the package (from ..services.captcha import CaptchaService).
</rule>
<rule name="handler_pattern">
Define private async _handle_* functions. Expose a public create_*_handlers() factory returning list.
</rule>
<rule name="error_handling">
Wrap Telegram API calls in try/except, log warnings with logger, degrade gracefully.
</rule>
<rule name="logging">
Use logging.getLogger(__name__) per module.
</rule>
<rule name="strings">
All user-facing text lives in strings.py in Italian. Use named placeholders.
</rule>
<rule name="no_global_state">
No global mutable state. Pass dependencies through services and bot_data.
</rule>
</coding_conventions>

<commands>
<command name="install">uv sync --dev</command>
<command name="run">uv run python-italy-bot</command>
<command name="lint">uv run ruff check src/</command>
<command name="format">uv run ruff format src/</command>
<command name="typecheck">uv run mypy src/</command>
<command name="test">uv run pytest</command>
</commands>

<testing>
<framework>pytest with async support</framework>
<directory>tests/</directory>
<guidelines>
- Test services and repository implementations independently.
- Use InMemoryRepository for unit tests; do not mock the database interface.
- Mirror the src/ directory structure in tests/.
- Keep tests focused and avoid testing Telegram API internals.
</guidelines>
</testing>

<environment>
<variable name="TELEGRAM_BOT_TOKEN" required="true">Bot token from @BotFather</variable>
<variable name="DATABASE_URL" required="false">PostgreSQL connection string; omit for in-memory</variable>
<variable name="CAPTCHA_SECRET_COMMAND" required="false" default="python-italy">Secret command for captcha verification</variable>
<variable name="CAPTCHA_FILE_PATH" required="false" default="assets/regolamento.md">Path to rules file</variable>
<variable name="MAIN_GROUP_ID" required="false">Main group chat ID</variable>
<variable name="LOCAL_GROUP_IDS" required="false">Comma-separated local group IDs</variable>
<variable name="RULES_URL" required="false">External rules page URL</variable>
<variable name="BOT_OWNER_ID" required="false">Owner user ID for /announce</variable>
</environment>

<pitfalls>
<pitfall>Always guard against None for update.effective_chat, update.effective_user, and update.message.</pitfall>
<pitfall>Verification and bans are global across all tracked chats. Use register_chat() for new chats.</pitfall>
<pitfall>PostgresRepository requires async with self._pool.connection() for all DB access.</pitfall>
<pitfall>Bot must have admin permissions to restrict or ban members.</pitfall>
<pitfall>Bot messages are in Italian — keep strings.py consistent.</pitfall>
</pitfalls>