# Moderation & Punishment System This document describes the punishment system implemented in the bot. --- ## Database Schema Defined in `migrations/004_punishments.sql`. ```sql CREATE TABLE punishments ( id INTEGER PRIMARY KEY AUTOINCREMENT, chat_id INTEGER NOT NULL, target_user_id INTEGER NOT NULL, action_type TEXT NOT NULL, -- 'ban', 'mute', 'kick' duration_seconds INTEGER, -- NULL = permanent / indefinite reason TEXT, created_by INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT datetime('now'), revoked_at TEXT, revoked_by INTEGER, active INTEGER NOT NULL DEFAULT 1 ); CREATE INDEX idx_punishments_chat_target ON punishments(chat_id, target_user_id); CREATE INDEX idx_punishments_active ON punishments(active); ``` ### Field Reference | Field | Type | Description | |-------|------|-------------| | `id` | `INTEGER` | Primary key. | | `chat_id` | `INTEGER` | The Telegram group/chat where the punishment was applied. | | `target_user_id` | `INTEGER` | The punished user's Telegram ID. | | `action_type` | `TEXT` | One of `ban`, `mute`, or `kick`. | | `duration_seconds` | `INTEGER` | Duration before auto-expiration. `NULL` means **permanent / indefinite**. | | `reason` | `TEXT` | Optional moderator-provided reason. | | `created_by` | `INTEGER` | Telegram ID of the moderator who issued the punishment. | | `created_at` | `TEXT` | ISO timestamp when the punishment was created. | | `revoked_at` | `TEXT` | ISO timestamp when the punishment was manually or automatically revoked. | | `revoked_by` | `INTEGER` | Telegram ID of the revoker, or `0` for the system (auto-expiry). | | `active` | `INTEGER` | `1` if the punishment is still in effect, `0` if revoked. | --- ## Command Reference All punishment commands require the caller to be an **administrator or owner** of the group. ### Duration-based Punishments | Command | Syntax | Description | |---------|--------|-------------| | `/sban` | `/sban @user [reason]` | Ban the user for a specific duration. | | `/smute` | `/smute @user [reason]` | Mute the user for a specific duration. | ### Permanent / Indefinite Punishments | Command | Syntax | Description | |---------|--------|-------------| | `/mute` | `/mute @user [reason]` | Mute the user **indefinitely** (`duration_seconds = NULL`). | | `/pban` | `/pban @user [reason]` | Permanently ban the user (`duration_seconds = NULL`). | | `/kick` | `/kick @user [reason]` | Kick the user from the group. Always recorded with `NULL` duration. | ### Revoke Commands | Command | Syntax | Description | |---------|--------|-------------| | `/rmute` | `/rmute @user` | Revoke the active mute for this user and restore chat permissions. | | `/rban` | `/rban @user` | Revoke the active ban for this user and unban them. | ### Target Resolution The `@user` argument is resolved in the following order: 1. Numeric user ID. 2. `@username` — matched against chat administrators. If the target cannot be resolved, the bot replies with *"Could not resolve target user."* --- ## Duration Units Reference The `parse_duration` function in `crates/cgcx-bot/src/main.rs` accepts the following units (case-insensitive): | Unit(s) | Seconds | Example | |---------|---------|---------| | `s`, `sec`, `secs`, `second`, `seconds` | 1 | `/sban @user 30 s spam` | | `m`, `min`, `mins`, `minute`, `minutes` | 60 | `/smute @user 10 m offtopic` | | `h`, `hr`, `hrs`, `hour`, `hours` | 3,600 | `/sban @user 24 h raid` | | `d`, `day`, `days` | 86,400 | `/sban @user 7 d trolling` | | `w`, `week`, `weeks` | 604,800 | `/smute @user 2 w` | | `mo`, `month`, `months` | 2,592,000 (30 days) | `/sban @user 1 mo` | | `y`, `year`, `years` | 31,536,000 (365 days) | `/pban @user 1 y` | --- ## How Expiration Works (Background Task) When the bot starts, a background Tokio task is spawned that runs every **60 seconds**: 1. **Query** — Calls `PunishmentRepo::list_expired()`, which selects rows where: - `active = 1` - `duration_seconds IS NOT NULL` - `datetime(created_at, '+' || duration_seconds || ' seconds') <= datetime('now')` 2. **Action per expired punishment**: - **`ban`** — Calls `unban_chat_member(chat_id, target_user_id)` to lift the ban. - **`mute`** — Calls `restrict_chat_member` with restored permissions: - `SEND_MESSAGES` - `SEND_MEDIA_MESSAGES` - `SEND_OTHER_MESSAGES` - `ADD_WEB_PAGE_PREVIEWS` - **Other types** — No automatic Telegram action is taken. 3. **Record update** — Calls `repo.revoke(p.id, 0)`: - Sets `active = 0` - Sets `revoked_at = datetime('now')` - Sets `revoked_by = 0` (system) --- ## How Revoke Works ### Manual Revoke (`/rmute`, `/rban`) 1. The bot resolves the target user. 2. Queries `get_active_for_chat_target(chat_id, target_user_id, action_type)` to find the active punishment. 3. If found: - **`/rmute`** — Restores the user's chat permissions via `restrict_chat_member(...)`. - **`/rban`** — Unbans the user via `unban_chat_member(...)`. - Calls `repo.revoke(p.id, admin_user_id)` to mark the punishment inactive. 4. If no active punishment is found, the bot replies with *"No active mute/ban found for this user."* ### Revoke via `PunishmentRepo::revoke(id, revoked_by)` ```sql UPDATE punishments SET active = 0, revoked_at = datetime('now'), revoked_by = ?1 WHERE id = ?2; ``` This is used both for: - **Automatic expiration** (`revoked_by = 0`) - **Manual moderator revocation** (`revoked_by = moderator_user_id`)