tgwatchspam
A self-hosted Telegram bot that protects community groups from spam — crypto offers, job scams, and recruitment messages. Rule-based, predictable, zero cloud dependency.
Overview
tgwatchspam is a self-hosted spam filter for Telegram community groups. It blocks unwanted messages about crypto, job scams, and other common spam patterns. Runs on any private VPS, requires no web interface, and is managed entirely through Telegram commands.
Word & Regex Filters
Blocked words and RE2 regex patterns, per chat. Substring match with no word boundaries.
Lookalike Normalization
Cyrillic/Latin character substitution is detected and normalized before matching.
Button Verification
New members must tap an inline button to prove they're human before posting.
Sandbox Mode
Restricts new members to text-only (no media, no link previews) for a configurable period.
Name Filter
Checks new members' display name and username against spam filters on join.
Multi-Chat
One bot instance manages multiple groups simultaneously. All data is isolated per chat.
Setup
Requirements
- A Linux VPS (any size — the bot uses ~15 MB RAM)
- A Telegram bot token from @BotFather
- Your numeric Telegram user ID from @userinfobot
- Go 1.21+ (only needed to build; the binary is self-contained)
First Run
-
Clone and build
git clone https://github.com/sqerison/tgwatchspam cd tgwatchspam go build -o tgwatchspam ./cmd/bot
-
Create your
.envfilecp .env.example .env # Edit .env and fill in BOT_TOKEN and SUPERADMIN_ID
-
Run the bot
./tgwatchspam
On startup the bot automatically registers all commands with BotFather so users get autocomplete when typing
/tgwatch_. -
Add the bot to your group
Grant it admin rights with: Delete messages, Ban users, Restrict members.
-
Verify it works
/tgwatch_show settings
The bot should reply with the default settings for your chat.
Deployment
Copy the binary and .env to the server, then create a systemd service so it restarts automatically.
scp tgwatchspam .env user@yourserver:/opt/tgwatchspam/
Create /etc/systemd/system/tgwatchspam.service:
[Unit] Description=tgwatchspam Telegram bot After=network.target [Service] WorkingDirectory=/opt/tgwatchspam ExecStart=/opt/tgwatchspam/tgwatchspam Restart=always RestartSec=5 [Install] WantedBy=multi-user.target
systemctl enable --now tgwatchspam journalctl -u tgwatchspam -f # view live logs
Spam Filtering
Every incoming message passes through a three-stage pipeline:
- Normalize — Cyrillic lookalike characters are mapped to their Latin equivalents and the text is lowercased.
- Word match — The normalized text is checked against the blocked word list using substring matching (no word boundaries).
- Regex match — The normalized text is tested against all RE2 patterns with
(?i)prepended automatically.
Word Filtering (FEAT-001)
Words are stored normalized. Matching is case-insensitive and uses substring logic — доход matches доходності, and binance matches binance/bybit (the slash does not break the match, fixing the main flaw of the previous bot).
/tgwatch_add word bitcoin /tgwatch_add word гарна плата # Bulk import — one word per line: /tgwatch_add word bitcoin usdt крипта заробіток digital nomad
Regex Filtering (FEAT-002)
Patterns use Go's RE2 syntax. The (?i) flag is added automatically — you do not need to write it. Example patterns:
| Pattern | Matches |
|---|---|
u[\.\s]?s[\.\s]?d[\.\s]?t | usdt, u.s.d.t, u s d t |
\+3[4-9]\d{7,} | Non-Italian phone numbers |
зп.{0,5}\d+\$ | Salary spam like "ЗП: 200$" |
earn.{0,10}\$\d+ | "earn $200/day" patterns |
Cyrillic/Latin Normalization (FEAT-003)
Spammers bypass filters by mixing scripts — for example writing Rоblox with a Cyrillic о. The bot normalizes these before matching:
| Cyrillic | Maps to | Cyrillic | Maps to |
|---|---|---|---|
а е о р с х у і | a e o p c x y i | А В Е К М Н О Р С Т Х У | A B E K M H O P C T X Y |
/tgwatch_check <text> to test any message against the current filters without taking any action. The bot will show exactly which rule matched and what would have happened.
Spam Actions
When a message matches a filter, the bot takes a configurable action. The message is always deleted first.
| Action | What happens | Default |
|---|---|---|
ban | Message deleted + user permanently banned | Yes |
kick | Message deleted + user removed (can rejoin via invite) | |
mute | Message deleted + user muted for N hours | |
delete | Message deleted only, no further action |
/tgwatch_set action ban /tgwatch_set action mute /tgwatch_set mute_duration 24 # hours, used when action=mute
After every spam detection the bot posts a notification in the group:
Matched word:
bitcoinAction: permanently banned
New Member Controls
Button Verification (FEAT-015)
When enabled, new members must tap an inline button before they can post anything. This stops the vast majority of bot accounts that join and immediately post spam.
Flow:
- User joins → bot fully restricts them and posts a public message with a button: "I'm not a bot — let me in"
- User taps the button → bot verifies it's the same person → removes the verification message
- If they don't tap within the timeout → bot kicks them automatically
/tgwatch_set verification on /tgwatch_set verification_timeout 5 # minutes before auto-kick (default: 2)
Sandbox Mode (FEAT-007)
After passing verification (or immediately if verification is off), new members can be placed in sandbox mode. During sandbox they can post regular text messages but cannot send media, stickers, GIFs, or link previews.
Sandbox and verification work together:
- Verification ON + Sandbox ON — Must tap button first, then text-only for sandbox_hours
- Verification ON + Sandbox OFF — Must tap button, then full access
- Verification OFF + Sandbox ON — Text-only immediately, full access after sandbox_hours
- Both OFF — No restrictions on new members
/tgwatch_set sandbox on /tgwatch_set sandbox_duration 24 # hours (default: 24) /tgwatch_unrestrict # reply to a user's message to lift sandbox early
Name Filter (FEAT-010)
When enabled, the bot runs a new member's display name and username through the same word/regex filters on join. Accounts like crypto_earn_fast or usdt_exchanger are kicked before they post anything.
/tgwatch_set name_filter on
Admin Authorization
All management commands require the user to be a group admin. Non-admins who send commands are silently ignored — no error reply, no indication that the command was received. This avoids tipping off spammers.
| Level | Who | What they can do |
|---|---|---|
| Admin | Any Telegram group admin | All filter and settings commands within their chat |
| Superadmin | User ID set in SUPERADMIN_ID |
Everything + /tgwatch_copy via DM with the bot |
Admin status is verified live via Telegram's getChatMember API on every command call — no caching, no stale permissions.
Spam Log
Every filtered message is recorded in SQLite with full context: timestamp, user, matched rule, original message text, and action taken.
/tgwatch_log # last 10 entries /tgwatch_log 25 # last 25 entries (max 50) /tgwatch_log clear # clear the log for this chat (inline button)
Multi-Chat Support
One bot instance manages multiple groups simultaneously. All data — word lists, regex patterns, settings, spam log — is completely isolated per chat_id. No data is shared between chats unless explicitly copied.
# Superadmin only, send this in a DM with the bot: /tgwatch_copy -100123456789 -100987654321
The bot will ask for confirmation before overwriting. Existing data in the target chat is replaced, not merged.
Command Reference
All commands use the /tgwatch_ prefix to avoid conflicts with other bots in the same group.
Word Filters
| Command | Who | Description |
|---|---|---|
/tgwatch_add word <text> | Admin | Add a blocked word or phrase |
/tgwatch_add word + lines | Admin | Bulk add — one word per line after the command |
/tgwatch_remove word <text> | Admin | Remove a word |
/tgwatch_list words | Admin | List all blocked words |
/tgwatch_clear words | Admin | Remove all words (inline button confirmation) |
Regex Filters
| Command | Who | Description |
|---|---|---|
/tgwatch_add regex <pattern> | Admin | Add a RE2 regex pattern ((?i) applied automatically) |
/tgwatch_remove regex <pattern> | Admin | Remove a pattern |
/tgwatch_list regex | Admin | List all patterns |
/tgwatch_clear regex | Admin | Remove all patterns (inline button confirmation) |
Spam Action Settings
| Command | Who | Description |
|---|---|---|
/tgwatch_set action delete|mute|ban|kick | Admin | Action when spam is detected (default: ban) |
/tgwatch_set mute_duration <hours> | Admin | Mute duration (used when action=mute) |
New Member Controls
| Command | Who | Description |
|---|---|---|
/tgwatch_set verification on|off | Admin | Require new members to tap a button before posting |
/tgwatch_set verification_timeout <min> | Admin | Minutes to verify before auto-kick (default: 2) |
/tgwatch_set sandbox on|off | Admin | Restrict new members to text-only for sandbox_duration |
/tgwatch_set sandbox_duration <hours> | Admin | How long sandbox lasts (default: 24h) |
/tgwatch_set name_filter on|off | Admin | Kick new members whose name/username matches spam filters |
/tgwatch_unrestrict | Admin | Reply to a message to manually lift sandbox on that user |
Management
| Command | Who | Description |
|---|---|---|
/tgwatch_show settings | Admin | Show all current settings for this chat |
/tgwatch_check <text> | Admin | Test text against filters without taking action |
/tgwatch_log [n] | Admin | Show last N spam entries (default 10, max 50) |
/tgwatch_log clear | Admin | Clear the spam log for this chat (inline button confirmation) |
/tgwatch_clean | Admin | Delete all bot replies and admin commands from chat |
/tgwatch_help | Admin | Show all available commands in chat |
/tgwatch_copy <src_id> <dst_id> | Superadmin | Copy all settings from one chat to another (use in DM) |
Configuration
Configuration is loaded from a .env file in the working directory.
| Variable | Required | Default | Description |
|---|---|---|---|
BOT_TOKEN | Yes | — | Telegram bot token from @BotFather |
SUPERADMIN_ID | Yes | — | Numeric Telegram user ID of the owner (from @userinfobot) |
DATABASE_PATH | No | ./data/bot.db | Path to the SQLite database file |
LOG_LEVEL | No | info | debug / info / warn / error |
Per-Chat Settings
These are stored in SQLite and changed via bot commands. Each chat has independent settings.
| Setting | Default | Command |
|---|---|---|
| Spam action | ban | /tgwatch_set action |
| Mute duration | 24h | /tgwatch_set mute_duration |
| Verification | off | /tgwatch_set verification |
| Verification timeout | 2 min | /tgwatch_set verification_timeout |
| Sandbox | off | /tgwatch_set sandbox |
| Sandbox duration | 24h | /tgwatch_set sandbox_duration |
| Name filter | on | /tgwatch_set name_filter |
Feature Index
Each feature has a tag in source code comments (e.g. [FEAT-001]). Search with grep -r "FEAT-001" .
| ID | Feature | Primary File |
|---|---|---|
FEAT-001 | Word/phrase filtering | internal/filter/words.go |
FEAT-002 | Regex filtering | internal/filter/regex.go |
FEAT-003 | Cyrillic/Latin normalization | internal/filter/normalize.go |
FEAT-004 | Spam actions | internal/bot/handlers.go |
FEAT-005 | Admin authorization | internal/admin/admin.go |
FEAT-006 | Multi-chat support | internal/storage/storage.go |
FEAT-007 | Sandbox mode | internal/bot/handlers.go |
FEAT-008 | /tgwatch_check command | internal/bot/handlers.go |
FEAT-009 | Spam log | internal/storage/storage.go |
FEAT-010 | Name/username filter on join | internal/bot/handlers.go |
FEAT-011 | Bulk word import | internal/bot/handlers.go |
FEAT-012 | Copy settings between chats | internal/bot/handlers.go |
FEAT-013 | /tgwatch_clean command | internal/bot/handlers.go |
FEAT-014 | Spam notification in chat | internal/bot/handlers.go |
FEAT-015 | Button verification for new members | internal/bot/handlers.go |
tgwatchspam — self-hosted, open source, written in Go. github.com/sqerison/tgwatchspam