Files
cg_api_secure-webshare/docs/MODERATION.md

8.1 KiB

Moderation & Punishment System

This document describes the punishment system implemented in the bot.


Database Schema

Defined in migrations/004_punishments.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)

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.

[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:

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"):

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:

{"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.