Skip to content

Add AI spam monitoring via classifier.dev (monitor-only) - #36

Open
rezhajulio wants to merge 3 commits into
mainfrom
ai-spam-monitor
Open

rezhajulio wants to merge 3 commits into
mainfrom
ai-spam-monitor

Conversation

@rezhajulio

Copy link
Copy Markdown
Owner

Summary

Adds a monitor-only AI spam layer using the free classifier.dev zero-shot classification API, positioned as the last defense: a message handler at PTB group 7 only classifies messages that survived every existing enforcement handler (all of which raise ApplicationHandlerStop after acting).

  • Never blocks the bot: classification runs in a background task (context.application.create_task); the update pipeline never awaits the network
  • Fails soft: circuit breaker (3 consecutive failures → 10 min cooldown) + daily budget (15,000/day, reset at midnight WIB) stop a dead or rate-limited upstream from being hammered; every failure path is logged and discarded
  • Human-in-the-loop enforcement: high-confidence (≥0.9) detections alert the per-group ai_spam_alert_chat_id (private admin group or DM) with inline buttons — Hapus pesan / Abaikan / Hapus + batasi / Hapus + ban. The bot itself never deletes, restricts, or warns; the warning topic is never touched
  • Scoped authorization: alert buttons encode the target group_id (aispam:{action}:{group_id}:{user_id}:{message_id}) and verify the clicker via is_user_admin_in_group on that group
  • Privacy: only message text is sent to the API; profile metadata (photo/username) is fetched locally via check_user_profile for the alert text only
  • Audit trail: admin clicks edit the alert in place (✔ Ditangani oleh @admin); dedup keeps one alert per (group_id, message_id)
  • Alert mentions are plain text (alerts are sent without parse_mode to avoid Markdown injection from quoted message content)

Test plan

  • 68 new tests; both new modules at 100% coverage (full suite: 1,177 passed)
  • Hypothesis property tests for circuit-breaker transitions and daily budget logic
  • ruff check and mypy src/bot/ tests/ clean
  • Live API smoke test against classifier.dev with Indonesian spam/legit samples (correct labels, calibrated confidence)
  • Shared httpx client closed via new main.post_shutdown hook

New ai_spam_monitor plugin registers two handlers:
- Message handler at group=7 (highest group) so only messages that
  survived every enforcement handler get classified; runs as a
  background task so the update pipeline never awaits the network
- Callback handler at group=0 for admin action buttons on alerts

services/classifier_client.py wraps the free classifier.dev zero-shot
API with a circuit breaker (3 failures -> 10 min cooldown) and a daily
budget (default 15k, reset at midnight WIB) so a dead or rate-limited
upstream fails soft instead of hammering per message.

High-confidence results alert the per-group ai_spam_alert_chat_id
(private admin group or DM) with buttons: delete, delete+restrict,
delete+ban, dismiss. Profile metadata (photo/username) is fetched
locally for the alert only and never sent to the API. No auto-enforcement,
no UserWarning records, no ApplicationHandlerStop.

Per-group gating via guard_plugin (ai_spam_monitor / ai_spam_callback);
both new modules at 100% test coverage with Hypothesis property tests
for the breaker and budget logic.
- Use the module-level circuit breaker as the single source of truth:
  the in-task re-check read a bot_data CircuitState nothing wrote to,
  so it never fired; classifier_cooldown_seconds is now honored at
  both gate points
- Build alert mentions as plain text: get_user_mention returns
  Markdown, which rendered literally in alerts sent without parse_mode
- Answer the callback query once; the edit-failure fallback answered
  an already-answered query, so the admin never saw it
- Drop the hardcoded httpx client timeout so classifier_timeout_seconds
  is the single timeout knob; correct the breaker cooldown docstring
A single long-lived client with a bounded pool cannot leak in
production, but leaving it open emitted asyncio unclosed-session
warnings on shutdown. Add post_shutdown hook that closes the shared
client; tests now close it via an async fixture.
@rezhajulio rezhajulio self-assigned this Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant