218 lines
8.1 KiB
Markdown
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.
|