diff --git a/CHANGELOG.md b/CHANGELOG.md index d739903..8f956aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Semantic Versioning](https://semver.org/). ## [Unreleased] ### Added - `@ai ` in a channel: the AI (Cloudflare Workers AI, `ai_chat.py`) answers in that channel, as short plain text for screen readers, followed by `AI_CHAT_DISCLAIMER` ("AI bisa salah. Periksa kembali info penting."). It remembers the channel's last few questions for follow-ups, never gets nicknames, and has a per-person cooldown and a daily limit; answers with clear badwords are replaced by a refusal. Off by default (`/abt aichat`); users can read `/help ai`. +- Guest accounts (`tamu`, `hadirin`) are blocked from using `@ai ` in channels by default (`AI_CHAT_BLOCKED_USERS`), with a private notification (`AI_CHAT_BLOCKED_MESSAGE`). - Optional AI context check (`ai_review.py`, Cloudflare Workers AI): when every badword in a message is an ambiguous one (`AI_AMBIGUOUS_WORDS`, e.g. anjing, babi, tahi, telanjang, gay), the bot asks the AI whether it was an insult instead of counting it right away, so "anjing tetanggaku berisik" is not punished but "anjing lah" is. The AI also sees up to `AI_CONTEXT_MESSAGES` earlier messages and two follow-ups (kept in memory only, without names), so a lone "anjing" after "aku punya binatang baru" is read as the pet. Runs in the background; if the AI does not answer the message is not counted. Off by default, switched with `/abt ai` (refused until the credentials are in `config.json`); `/bwt` shows the AI's verdict; `tools/ai_eval.py` measures a model on 42 labelled sentences. Setup: `docs/ai.md`. - GitHub Actions workflow (`.github/workflows/deploy.yml`): pull requests into `main` are checked (all Python files must compile), and merging into `main` updates and restarts the bot on the server over SSH through `tools/deploy.sh`, using a key that can only run that script. Setup: `docs/deploy.md`. diff --git a/README.md b/README.md index df8bce8..5d8b154 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ All settings live in `config.json`. Highlights: | **Channel Rules** | `CHANNEL_CREATION_MAX_PER_USER`, limit/block messages, `CHANNEL_CREATION_BLOCKED_USERNAMES` | | **Registration** | `REGISTRATION_ALLOWED_USERNAMES`, per-IP limits/messages | | **Shared accounts** | `SHARED_ACCOUNTS` – accounts many people log in with (default `tamu`, `murid`, `hadirin`, `osis`, `pemateri`, `f.osis`, `guest`): not deletable via the bot, moderation tells their users apart by nickname + IP, and unless they are admin accounts they cannot create, delete or transfer channels via the bot | -| **AI context check** | `AI_ENABLED`, `AI_CLOUDFLARE_ACCOUNT_ID`, `AI_CLOUDFLARE_API_TOKEN`, `AI_MODEL`, `AI_TIMEOUT_SEC`, `AI_CONTEXT_MESSAGES`, `AI_AMBIGUOUS_WORDS`, `AI_CHAT_ENABLED`, `AI_CHAT_PREFIX`, `AI_CHAT_MODEL`, `AI_CHAT_MAX_TOKENS`, `AI_CHAT_COOLDOWN_SEC`, `AI_CHAT_DAILY_LIMIT`, `AI_CHAT_HISTORY`, `AI_CHAT_DISCLAIMER` (see [`docs/ai.md`](docs/ai.md)) | +| **AI context check** | `AI_ENABLED`, `AI_CLOUDFLARE_ACCOUNT_ID`, `AI_CLOUDFLARE_API_TOKEN`, `AI_MODEL`, `AI_TIMEOUT_SEC`, `AI_CONTEXT_MESSAGES`, `AI_AMBIGUOUS_WORDS`, `AI_CHAT_ENABLED`, `AI_CHAT_PREFIX`, `AI_CHAT_MODEL`, `AI_CHAT_MAX_TOKENS`, `AI_CHAT_COOLDOWN_SEC`, `AI_CHAT_DAILY_LIMIT`, `AI_CHAT_HISTORY`, `AI_CHAT_DISCLAIMER`, `AI_CHAT_BLOCKED_USERS`, `AI_CHAT_BLOCKED_MESSAGE` (see [`docs/ai.md`](docs/ai.md)) | | **Abuse Throttling** | `ABUSE_LOGIN_ENABLED`, `ABUSE_JOIN_ENABLED`, `ABUSE_LOGIN_COUNT`, `ABUSE_JOIN_COUNT`, `BADWORD_ABUSE_COUNT`, `ABUSE_WINDOW_SEC`, `ABUSE_TEMP_BAN_MINUTES`, `BAN_TARGET`, `ABUSE_WHITELIST_FILE`, warning message arrays | | **Message Anti-Spam** | `ANTISPAM_MESSAGE_ENABLED`, `ANTISPAM_MESSAGE_COUNT`, `ANTISPAM_MESSAGE_WINDOW_SEC`, `ANTISPAM_INTERCEPT_TYPES`, `ANTISPAM_IGNORE_ADMINS`, warning message arrays | | **Badwords** | `BADWORDS_ENABLED`, `BADWORDS_INTERCEPT_TYPES` (`"PRIVATE"`, `"CHANNEL"`, `"BROADCAST"`), `BADWORDS_FILE`, `BADWORDS_IGNORE_ADMINS`, `BADWORDS_PROFILE_CHECK_ENABLED` | diff --git a/config.py b/config.py index fa132be..3f42db0 100644 --- a/config.py +++ b/config.py @@ -142,6 +142,9 @@ AI_CHAT_DAILY_LIMIT: int = 200 # questions per day for the whole server AI_CHAT_HISTORY: int = 3 # earlier questions/answers of the channel the AI remembers AI_CHAT_DISCLAIMER: str = "AI bisa salah. Periksa kembali info penting." +# User accounts not permitted to use "@ai " in channels +AI_CHAT_BLOCKED_USERS: List[str] = ["tamu", "hadirin"] +AI_CHAT_BLOCKED_MESSAGE: str = "Fitur @ai tidak tersedia untuk akun tamu atau hadirin." # Badword entries with an ordinary meaning too; when every match in a message # is one of these, the AI decides whether it was an insult AI_AMBIGUOUS_WORDS: List[str] = [ @@ -236,6 +239,8 @@ "AI_CHAT_DAILY_LIMIT": AI_CHAT_DAILY_LIMIT, "AI_CHAT_HISTORY": AI_CHAT_HISTORY, "AI_CHAT_DISCLAIMER": AI_CHAT_DISCLAIMER, + "AI_CHAT_BLOCKED_USERS": AI_CHAT_BLOCKED_USERS, + "AI_CHAT_BLOCKED_MESSAGE": AI_CHAT_BLOCKED_MESSAGE, "AI_AMBIGUOUS_WORDS": AI_AMBIGUOUS_WORDS, "ANTISPAM_MESSAGE_ENABLED": ANTISPAM_MESSAGE_ENABLED, "ANTISPAM_MESSAGE_COUNT": ANTISPAM_MESSAGE_COUNT, diff --git a/docs/ai.md b/docs/ai.md index 0617109..653e9ed 100644 --- a/docs/ai.md +++ b/docs/ai.md @@ -101,6 +101,8 @@ AI bisa salah. Periksa kembali info penting. - Limits: one question per person every `AI_CHAT_COOLDOWN_SEC` (default 20) seconds and `AI_CHAT_DAILY_LIMIT` (default 200) questions per day for the whole server; people who hit a limit are told privately. +- Guest accounts (`AI_CHAT_BLOCKED_USERS`, default `tamu`, `hadirin`) cannot + use `@ai`; people on those accounts are told privately. - An answer that contains a clear badword is replaced by a short refusal. - The question itself is still an ordinary channel message: the badword filter and spam detection apply to it as usual. A question that gets a @@ -115,6 +117,8 @@ AI bisa salah. Periksa kembali info penting. Setup: the same Cloudflare keys as above, then `/abt aichat on` (off by default). Other settings: `AI_CHAT_PREFIX` (default `@ai`), `AI_CHAT_MODEL` -(empty: same as `AI_MODEL`), `AI_CHAT_MAX_TOKENS` (default 300) and -`AI_CHAT_DISCLAIMER` (the line after every answer; empty to leave it out). +(empty: same as `AI_MODEL`), `AI_CHAT_MAX_TOKENS` (default 300), +`AI_CHAT_BLOCKED_USERS` (default `["tamu", "hadirin"]`), +`AI_CHAT_BLOCKED_MESSAGE` and `AI_CHAT_DISCLAIMER` (the line after every +answer; empty to leave it out). Users can read `/help ai`. diff --git a/docs/index.html b/docs/index.html index 01db987..3191f6b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -91,7 +91,7 @@

Abuse Throttling

  • ABUSE_LOGIN_COUNT, ABUSE_JOIN_COUNT, BADWORD_ABUSE_COUNT: events per key within ABUSE_WINDOW_SEC before escalation.
  • ABUSE_TEMP_BAN_MINUTES: temporary ban duration at stage 3.
  • AI_ENABLED, AI_CLOUDFLARE_ACCOUNT_ID, AI_CLOUDFLARE_API_TOKEN, AI_MODEL, AI_TIMEOUT_SEC, AI_CONTEXT_MESSAGES, AI_AMBIGUOUS_WORDS: optional AI check (Cloudflare Workers AI) that decides whether a message whose only badwords are ambiguous ones (anjing, babi, tahi, ...) was an insult, looking at a few surrounding messages as context; unanswered checks are not counted. Off by default; switch with /abt ai. Setup: docs/ai.md.
  • -
  • AI_CHAT_ENABLED, AI_CHAT_PREFIX, AI_CHAT_MODEL, AI_CHAT_MAX_TOKENS, AI_CHAT_COOLDOWN_SEC, AI_CHAT_DAILY_LIMIT, AI_CHAT_HISTORY, AI_CHAT_DISCLAIMER: channel messages starting with @ai are answered by the AI in that channel, with a disclaimer line; per-person cooldown and a daily limit. Off by default; switch with /abt aichat.
  • +
  • AI_CHAT_ENABLED, AI_CHAT_PREFIX, AI_CHAT_MODEL, AI_CHAT_MAX_TOKENS, AI_CHAT_COOLDOWN_SEC, AI_CHAT_DAILY_LIMIT, AI_CHAT_HISTORY, AI_CHAT_DISCLAIMER, AI_CHAT_BLOCKED_USERS, AI_CHAT_BLOCKED_MESSAGE: channel messages starting with @ai are answered by the AI in that channel, with a disclaimer line; per-person cooldown, daily limit, and guest accounts (tamu, hadirin) blocked by default. Off by default; switch with /abt aichat.
  • ABUSE_LOGIN_ENABLED / ABUSE_JOIN_ENABLED: default on/off state of login-spam and join-spam detection (like ANTISPAM_MESSAGE_ENABLED and BADWORDS_ENABLED, admins can override them at runtime with /abt).
  • ABUSE_WHITELIST_FILE: file (one username or IP per line) of users the automatic moderation never punishes; managed with /abw. Default abuse_whitelist.txt.
  • ABUSE_LOGIN_WARNINGS, ABUSE_JOIN_WARNINGS, BADWORDS_WARNINGS: text templates shown at each stage (use {duration} placeholder for minutes).
  • diff --git a/help_texts.py b/help_texts.py index 045ee8b..939f086 100644 --- a/help_texts.py +++ b/help_texts.py @@ -190,7 +190,8 @@ "important information.\n" "Only works when the admins have switched it on (/abt aichat). There is a short wait " "between your questions and a daily limit for the server. A question with a badword " - "gets the usual warning and is not answered.\n" + "gets the usual warning and is not answered. Not available on guest accounts (e.g. " + "tamu, hadirin).\n" "Example:\n" " @ai apa itu fotosintesis?\n" " @ai jelasin lebih simpel\n", diff --git a/tt_client.py b/tt_client.py index 254bfa3..344b1a6 100644 --- a/tt_client.py +++ b/tt_client.py @@ -2563,7 +2563,40 @@ def _ai_chat_question(self, text: str) -> Optional[str]: return None return rest.lstrip(" :,\t\n").strip() + def _is_ai_chat_blocked_user(self, userid: int) -> bool: + """Whether this user's account is blocked from using @ai in channels.""" + username = (self._get_username(userid) or "").strip().lower() + blocked = { + str(u).strip().lower() + for u in getattr(config, "AI_CHAT_BLOCKED_USERS", ["tamu", "hadirin"]) or [] + if str(u).strip() + } + if username and username in blocked: + return True + if not username: + try: + user = self.getUser(userid) + nick = from_tt_char(user.szNickname).strip().lower() if user is not None else "" + if nick in blocked: + return True + except Exception: + pass + return False + def _handle_ai_question(self, userid: int, channel_id: int, question: str): + if self._is_ai_chat_blocked_user(userid): + msg = getattr( + config, + "AI_CHAT_BLOCKED_MESSAGE", + "Fitur @ai tidak tersedia untuk akun tamu atau hadirin.", + ) + safe_call(self.send_pm, userid, msg) + logger.info( + "AI chat refused: user %s (%s) is blocked from using AI chat", + userid, + self._get_username(userid), + ) + return if not question: prefix = str(getattr(config, "AI_CHAT_PREFIX", "@ai") or "@ai").strip() self._send_channel_text(