Files
cg_api_secure-webshare/docs/MODERATION.md

218 lines
8.1 KiB
Markdown

# 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 <dur> <unit> [reason]` | Ban the user for a specific duration. |
| `/smute` | `/smute @user <dur> <unit> [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`)
---
## Global Ban Configuration
The `[groups]` section in the config contains the optional `global_ban` flag (default `false`). When enabled, punishment commands (`/sban`, `/smute`, `/mute`, `/pban`, `/kick`) are propagated across all known chats where the bot is an administrator.
```toml
[groups]
admin_group_ids = [-1001234567890]
review_group_ids = [-1009876543210]
global_ban = false
```
- When `global_ban = true`, issuing a punishment in any admin group applies the same action to every known chat (source chats, destination chats, review groups, and configured `admin_group_ids` / `review_group_ids`) where the bot has admin rights.
- When `global_ban = false` (default), punishments are local to the group where the command was issued.
### Propagation Behavior
Each propagated punishment is recorded as a **separate row** in the `punishments` table, with its own `chat_id`. This means:
- The background expiration task naturally revokes each per-chat punishment independently.
- Manual `/rmute` or `/rban` only affects the chat where the revoke command was issued.
- The bot skips any chat where it is not an administrator and logs a warning.
---
## Hash Blacklist
Migration `007_hash_blacklist.sql` creates the `hash_blacklist` table:
```sql
CREATE TABLE hash_blacklist (
hash BLOB PRIMARY KEY,
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
reason TEXT
);
```
The `HashBlacklistRepo` (in `crates/cgcx-db/src/repos.rs`) provides:
- `insert(hash, reason)` — Adds a hash to the blacklist (ignored if already present).
- `contains(hash)` — Returns `true` if the hash is blacklisted.
During file ingestion (`crates/cgcx-file-pipeline/src/lib.rs`), the pipeline computes a **plaintext BLAKE3 hash** and checks it against `hash_blacklist` **before** deduplication and persistence. If the hash is blocked, ingestion is rejected with a `BlockedHash` error and the temporary file is discarded.
---
## Username Tracking
The bot can log username changes to a JSON file for audit and moderation purposes.
### Configuration
Set `uname_changes_path` at the top level of the config (default: `"data/uname_changes.json"`):
```toml
database_path = "data/db.sqlite"
uname_changes_path = "data/uname_changes.json"
```
### How It Works
On every message and callback interaction, the bot calls `UserRepo::ensure_exists(...)`, passing the configured path. If the user's stored username differs from the current one, a JSON line is appended to the file:
```json
{"timestamp":"2026-05-24T12:34:56Z","user_id":123456789,"chat_id":-1001234567890,"old_username":"old_name","new_username":"new_name"}
```
The file is opened in append mode and created automatically if it does not exist.