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
15 changes: 15 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM mcr.microsoft.com/devcontainers/python:3-3.12-trixie

ENV PYTHONUNBUFFERED 1

# [Optional] If your requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
# && rm -rf /tmp/pip-tmp

# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>



9 changes: 9 additions & 0 deletions .devcontainer/devcontainer-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"features": {
"ghcr.io/cirolosapio/devcontainers-features/alpine-bash:0": {
"version": "0.0.3",
"resolved": "ghcr.io/cirolosapio/devcontainers-features/alpine-bash@sha256:36cab39fa6859980ba9ac31e52737f855612bb18707222c92fef1441b29fd634",
"integrity": "sha256:36cab39fa6859980ba9ac31e52737f855612bb18707222c92fef1441b29fd634"
}
}
}
27 changes: 27 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/postgres
{
"name": "Python 3 & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"features": {
// "ghcr.io/cirolosapio/devcontainers-features/alpine-bash:0": {}
},

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or the host.
// "forwardPorts": [5000, 5432],

// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "pip install --user -r requirements.txt",

// Configure tool-specific properties.
// "customizations": {},

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
"remoteUser": "vscode"
}
35 changes: 35 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
version: '3.8'

services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile

volumes:
- ../..:/workspaces:cached

# Overrides default command so things don't shut down after the process ends.
command: sleep infinity

# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db

# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)

db:
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres

# Add "forwardPorts": [5432] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)

volumes:
postgres-data:
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true

[*.py]
indent_style = space
indent_size = 4

[*.{yml,yaml,json,toml}]
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false
15 changes: 15 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Normalize line endings: LF in the repository and in the working tree.
# Prevents editors on Windows hosts from turning the whole tree into a CRLF diff.
* text=auto eol=lf

# Binary assets — never touch these.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.zip binary
*.woff binary
*.woff2 binary
*.ttf binary
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot

version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly
32 changes: 32 additions & 0 deletions .github/workflows/dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Development
on:
push:
branches-ignore:
- stage
- master
jobs:
deploy:
name: Check code
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libpango-1.0-0 libpangoft2-1.0-0 poppler-utils
- name: Install requirements
run: |
pip install --upgrade pip
pip install -r requirements-dev.txt
- name: Check pylint
run: |
pylint src
- name: Run tests
run: |
pytest
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.vscode/settings.json
*__pycache__*
.pytest_cache/
13 changes: 13 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[MAIN]
init-hook="from pylint.config import find_default_config_files; import os, sys; sys.path.append(os.path.dirname(next(find_default_config_files())))"
[SIMILARITIES]
ignore-imports=y
[MESSAGES CONTROL]
disable=
W,
too-many-function-args,
too-many-public-methods,
too-many-arguments,
too-few-public-methods,
not-callable,
duplicate-code
179 changes: 177 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,184 @@
# ChatIconRotBot

This is a bot designed to update chat avatars.
A Telegram bot for communities of trophy hunters. It collects platinum-trophy
screenshots posted by chat members, rotates them as the group chat avatar on a
schedule, and runs a monthly rating survey on the games that were shown.

Built with [aiogram 3](https://docs.aiogram.dev/) (async), APScheduler,
SQLAlchemy 2.0 + PostgreSQL. All bot messages are in Russian.

## How it works

1. **Submit a trophy.** A member posts a photo captioned `@YourBotName Game Title`
(append `Xbox` for the Xbox platform, otherwise PlayStation is assumed).
The bot stores it in the chat's queue and replies with a random gaming quote
from [`src/data/quotations.yaml`](src/data/quotations.yaml).
2. **Rotate the avatar.** Every `delta` days a scheduled job takes the next entry
from the queue, sets it as the chat photo, posts and pins an announcement with
the hunter and the game, then removes the entry from the queue and moves the
history record forward. If the queue is empty, the chat's default avatar is
used instead.
3. **Rate the game.** The pinned announcement carries an inline *«Оценить»*
button that opens a private survey with the bot. Members score the game on
three axes from 1 to 10 — *Game*, *Picture*, *Difficulty*. The hunter who
submitted the trophy only scores *Difficulty*.
4. **Monthly results.** On the 1st of each month the bot aggregates the previous
month's scores, renders one table image per category, sends them as an album
and pins it.

## Commands

### Everyone

| Command | Description |
| --- | --- |
| `/start` | Start the bot in the chat (registers the chat and its schedule) |
| `/help` | Show the command reference |
| `/show_queue` | Show the queue of pending trophies as a table image |
| `/delete_game` | Delete your most recently submitted trophy |
| `/history [@username]` | List all trophies of yourself or another user |
| `/top [dd.mm.yyyy]` | Trophy leaderboard, all-time or since the given date |
| `/games_info` | Wikipedia links for the games currently in the queue |

### Chat owner / bot admins

| Command | Description |
| --- | --- |
| `/show_settings` | Show the next rotation date, the interval and the default avatar |
| `/set_date DD/MM/YYYY HH:MM` | Set the next avatar rotation (UTC) |
| `/set_delta N` | Set the interval between rotations, in days |

Posting a photo captioned `@YourBotName *Default*` as a user with
*change group info* rights sets the chat's fallback avatar, used whenever the
queue runs dry.

## Requirements

- Python 3.12
- PostgreSQL
- System packages for table rendering: `poppler-utils`, `pango`, and a font with
Cyrillic coverage (the Docker image installs `font-noto`)

## Configuration

The bot is configured entirely through environment variables:

| Variable | Description |
| --- | --- |
| `TOKEN` | Telegram Bot API token from [@BotFather](https://t.me/BotFather) |
| `DATABASE_URL` | Async SQLAlchemy DSN, e.g. `postgresql+asyncpg://user:pass@host:5432/db` |

Bot administrator IDs are currently hardcoded in
[`src/bot/settings.py`](src/bot/settings.py) (`admin_ids`) — change them there.
Admins receive startup/shutdown notices and every unhandled error traceback.

The bot must be an administrator in the group with permission to change chat
info and pin messages.

## Installation

1. Clone the repository:

```bash
git clone https://github.com/NickLyrick/ChatIconRotBot.git
cd ChatIconRotBot
```

2. Create a virtual environment and install the dependencies:

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

3. Install the native dependencies (Debian/Ubuntu):

```bash
sudo apt-get install poppler-utils libpango-1.0-0 fonts-noto
```

4. Provide the environment variables and run:

```bash
git clone https://github.com/your-username/ChatIconRotBot.git
export TOKEN="123456:ABC-DEF..."
export DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/postgres"
python main.py
```

> **Note:** the database schema is not created automatically. Create the tables
> described in [`src/database/schemas.py`](src/database/schemas.py) (`chats`,
> `platinum`, `history`, `scores`, `surveys`) before the first run.

### Docker

```bash
docker build -t chaticonrotbot .
docker run --rm -e TOKEN=... -e DATABASE_URL=... chaticonrotbot python main.py
```

### Dev container

The repository ships a VS Code dev container
([`.devcontainer/`](.devcontainer/)) with Python 3.12 and a PostgreSQL service,
so you can open the project in a container and get a local database for free.

## Project layout

```
main.py entry point: starts long polling
src/bot/ bot instance, settings, command descriptions
src/dispatcher/ dispatcher, router and middleware registration
src/handlers/ message and callback handlers
basic_handlers.py /start, /help, global error handler
records_handlers.py trophy submission and deletion
schedule_handlers.py /set_date, /set_delta, /show_settings
tables_handlers.py /show_queue, /top, /history
game_score_handlers.py inline survey flow
wiki_handlers.py /games_info
src/keyboards/inline/ survey keyboards and callback data
src/filters/ caption format, chat type and permission filters
src/middleware/ inject the DB request object and the scheduler
src/scheduler/ APScheduler wrapper and jobs
jobs.py change_avatar, finish_survey, check_db_connection
src/database/ SQLAlchemy models and all queries
src/utility/ PlatinumRecord model, HTML → PNG table renderer
src/data/quotations.yaml random quotes posted on submission
```

Table images are produced by rendering a pandas DataFrame to HTML, converting it
to PDF with WeasyPrint, rasterising it with pdf2image, and trimming the result
with Pillow — see [`src/utility/tools.py`](src/utility/tools.py).

## Database schema

| Table | Purpose |
| --- | --- |
| `chats` | Registered chats: next rotation date and interval in days |
| `platinum` | Pending queue of submitted trophies (hunter, game, platform, photo) |
| `history` | Archive of shown trophies, including the date the avatar was set |
| `scores` | Individual survey votes per trophy and user |
| `surveys` | Aggregated monthly results per trophy |

## Deployment

Deployment targets [Fly.io](https://fly.io) via GitHub Actions:

- **`master`** → deploys the `platinum` app ([`.github/workflows/fly.yml`](.github/workflows/fly.yml))
- **`stage`** → deploys the `platinum-dev` app ([`.github/workflows/stage.yml`](.github/workflows/stage.yml))
- **any other branch** → runs `pylint src` only ([`.github/workflows/dev.yml`](.github/workflows/dev.yml))

Deploys require the `FLY_API_TOKEN` repository secret. `TOKEN` and
`DATABASE_URL` are expected to be set as Fly secrets. A `Procfile` is included
for Heroku-style platforms.

## Development

Lint the codebase the same way CI does:

```bash
pip install pylint
pylint src
```

Rules live in [`.pylintrc`](.pylintrc).
17 changes: 17 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Shared pytest configuration.

``src.bot.settings`` reads its configuration from the environment at import
time and aiogram validates the token eagerly, so placeholders have to be in
place before anything under ``src`` is imported. Being at the repository root,
this module is loaded before any test module, which is early enough.

The root itself reaches ``sys.path`` through ``pythonpath`` in pytest.ini.
"""

import os

# A syntactically valid but fake token — aiogram rejects malformed ones.
os.environ.setdefault("TOKEN", "123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw")
os.environ.setdefault(
"DATABASE_URL", "postgresql+asyncpg://user:password@localhost:5432/test"
)
Loading
Loading