Get rid of the unwanted spam joins out of the box
- Triggered by new chat members and join requests.
- Applies the chat's manual allowlist before checking known spammer sources. Non-allowlisted known spammers are declined/banned immediately and join artifacts are cleaned up.
- Restricts the newcomer while verification is active.
- Sends a CAPTCHA-style challenge with configurable option count and timeout.
- On success, the newcomer is approved/unrestricted and the challenge message is cleaned up.
- On failure or timeout, the newcomer is banned for the configured reject timeout.
- Optional greeting text can be shown immediately with the public CAPTCHA for direct joins, or after approval for join-request newcomers.
- Every previously untrusted author enters a per-chat message probation. Each new text, caption, or visible rich-message text is checked for at least three hours, and the first safe new message after the deadline is required for release:
- Manual allowlist ("Indulgence") override
- Known spammers lookup from local imports and online checks against LoLs bot and CAS/Combot
- External quote heuristic for obvious cross-chat spam patterns
- LLM-powered binary classification with built-in and chat-specific spam examples
- If the message is considered spam, the user is either immediately banned or sent into community voting, depending on chat settings.
- Chat users can report missed spam with
/votebanor by mentioning the bot in reply to the message. Reports are rechecked by the LLM first, then either moderated immediately or sent to community voting without pre-deleting the original message. - Clean messages before the deadline remain bound for future edit checks. A distinct clean message after the deadline durably completes probation; commands and media without text start the clock but cannot complete it.
- Run
/settingsin a group where the bot is an admin. - The bot sends a deep-link that opens a private admin panel for that chat.
- From there you can configure gatekeeper, new-user message probation, community voting, spam examples, language, and manual not-spammer overrides.
- The home screen includes a one-tap
Recommended Protectionpreset and a compact 7-day protection summary.
- Create a bot via BotFather and enable group messages access.
- Clone the repository:
git clone https://github.com/iamwavecut/ngbot.git
cd ngbot- Create a mode-
0600environment file and configure it.compose.yamlis the tracked deployment source; do not copy or maintain a second Compose file:
install -m 0600 /dev/null .env
sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' .env.example > .env
chmod 0600 .env
# Edit .env and set NG_TOKEN, NGBOT_DATA_PATH, and the selected LLM credential.Never commit .env, paste it into logs, or pass it on a command line. See Operator deployment guide for secret rotation and release identity checks.
4. Create the writable data directory named by NGBOT_DATA_PATH and give the distroless nonroot user ownership:
sudo install -d -m 0700 -o 65532 -g 65532 /home/username/.ngbotThe image runs as UID/GID 65532:65532; set NGBOT_DATA_PATH=/home/username/.ngbot. Compose always mounts that host path at /data, and the application always uses NG_DOT_PATH=/data in the container.
5. Start the bot:
docker compose up -d- Optional for join-request Mini App CAPTCHA: set
NG_GATEKEEPER_WEBAPP_PUBLIC_URL=https://antifraud.rtfm.rsvp, point Caddy atdeploy/caddy/ngbot-webapp.Caddyfile, then restart Compose. - Add your bot to chat and give it Ban, Delete, and Invite permissions.
- Optional: Change bot language with
/lang <code>(e.g.,/lang ru). - Optional: Set
NG_SPAM_DEBUG_USER_IDto your Telegram user ID for private/testspamand/skipreason; source-chat administrators may use those diagnostics in their chat. - Optional: Open
/settingsas a group admin and applyRecommended Protection.
- Create the bot and a mode-
0600.envas above. - Export the variables before starting the native binary. The Go binary does not load
.envby itself:
set -a
. ./.env
set +a
go mod download
go run ./cmd/ngbotThe native Mini App server defaults to loopback at 127.0.0.1:8080. Only the Compose deployment overrides it to 0.0.0.0:8080 inside the isolated container; Docker publishes it exclusively on host loopback.
All configuration is done through environment variables. You can:
- Set them in your environment
- Let Docker Compose read a mode-
0600.envfile - Source a mode-
0600.envbefore running the native binary
See .env.example for a quick reference. NGBOT_* variables configure only the Compose/build workflow; NG_* variables configure the application.
| Compose/build variable | Purpose | Default |
|---|---|---|
NGBOT_DATA_PATH |
Secured host directory mounted at /data |
Required |
NGBOT_WEBAPP_HOST_PORT |
WebApp port published on host loopback | 18080 |
NGBOT_VERSION |
Release version stored in the binary and OCI label | dev |
NGBOT_REVISION |
Exact Git revision stored in the binary and OCI label | unknown |
NGBOT_BUILD_DATE |
RFC 3339 build time stored in the binary and OCI label | unknown |
| Required | Variable name | Description | Default | Options |
|---|---|---|---|---|
| ✔️ | NG_TOKEN |
Telegram BOT API token | ||
NG_LANG |
Default language to use in new chats | en |
be, bg, cs, da, de, el, en, es, et, fi, fr, hu, id, it, ja, ko, lt, lv, nb, nl, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh |
|
NG_HANDLERS |
Enabled bot handlers | admin,gatekeeper,reactor |
Comma-separated list of handlers | |
NG_LOG_LEVEL |
Logging verbosity | 2 |
0=Panic, 1=Fatal, 2=Error, 3=Warn, 4=Info, 5=Debug, 6=Trace |
|
NG_DOT_PATH |
Bot data storage path | ~/.ngbot |
Any valid filesystem path | |
NG_TELEGRAM_POLL_TIMEOUT |
Telegram long poll timeout | 60s |
Any valid duration string | |
NG_TELEGRAM_REQUEST_TIMEOUT |
Telegram HTTP request timeout | 75s |
Must be greater than poll timeout | |
NG_TELEGRAM_RECOVERY_WINDOW |
Maximum degraded polling window before restart | 10m |
Must be greater than request timeout | |
NG_TELEGRAM_INBOX_MAX_PENDING_ROWS |
Maximum aggregate pending/retry inbox rows | 100000 |
Positive integer | |
NG_TELEGRAM_INBOX_MAX_PENDING_BYTES |
Maximum aggregate pending/retry payload bytes | 536870912 |
Positive integer | |
NG_TELEGRAM_INBOX_MAX_DISPATCH_PENDING_ROWS |
Maximum pending/retry rows for one dispatch key | 10000 |
Positive integer | |
NG_TELEGRAM_INBOX_MAX_DISPATCH_PENDING_BYTES |
Maximum pending/retry bytes for one dispatch key | 33554432 |
Positive integer | |
NG_TELEGRAM_INBOX_MIN_FREE_BYTES |
Minimum database filesystem free space for admission | 268435456 |
Positive integer | |
NG_GATEKEEPER_WEBAPP_PUBLIC_URL |
Public HTTPS origin for join-request CAPTCHA Mini App | Absolute URL, e.g. https://captcha.example.com |
||
NG_GATEKEEPER_WEBAPP_LISTEN_ADDR |
Native embedded Mini App server listen address | 127.0.0.1:8080 |
Compose enforces 0.0.0.0:8080 inside the container |
|
NG_GATEKEEPER_WEBAPP_MAX_CONCURRENT |
Maximum in-flight Mini App requests | 32 |
Integer greater than zero; 0 is invalid |
|
NG_GATEKEEPER_WEBAPP_REQUESTS_PER_MINUTE |
Per-client Mini App request limit | 120 |
Integer greater than zero; 0 is invalid |
|
NG_LLM_GEMINI_API_KEY |
Gemini credential; required when reactor uses Gemini |
Preferred over the legacy key | ||
NG_LLM_OPENAI_API_KEY |
OpenAI credential; required when reactor uses OpenAI |
Preferred over the legacy key | ||
NG_LLM_API_KEY |
Legacy credential fallback for the selected provider | Used only when its dedicated key is empty | ||
NG_LLM_API_MODEL |
Optional LLM model override | Provider-specific | Any valid OpenAI or Gemini model | |
NG_LLM_API_URL |
OpenAI-compatible API base URL | https://api.openai.com/v1 |
Used when NG_LLM_API_TYPE=openai |
|
NG_LLM_API_TYPE |
LLM provider | openai |
openai, gemini |
|
NG_LLM_REQUEST_TIMEOUT |
Maximum duration of one classification request | 45s |
Any positive duration string | |
NG_SPAM_LOG_CHANNEL_USERNAME |
Channel for spam logging | Any valid channel username | ||
NG_SPAM_DEBUG_USER_ID |
User allowed to run diagnostics in private chat | 0 |
Telegram user ID | |
NG_SPAM_VERBOSE |
Verbose in-chat notifications | false |
true, false |
|
NG_SPAM_MESSAGE_PROBATION_DURATION |
Minimum new-user message probation before a checked safe exit | 3h |
Any positive duration string | |
NG_SPAM_VOTING_TIMEOUT |
Voting time limit | 5m |
Any valid duration string | |
NG_SPAM_MIN_VOTERS |
Minimum required voters | 2 |
Any positive integer | |
NG_SPAM_MAX_VOTERS |
Maximum voters cap | 10 |
Any positive integer | |
NG_SPAM_MIN_VOTERS_PERCENTAGE |
Minimum voter percentage | 5 |
Any positive float | |
NG_SPAM_SUSPECT_NOTIFICATION_TIMEOUT |
Suspect notification timeout | 2m |
Any valid duration string |
The language codes in NG_LANG are the same complete locale catalog used by the admin UI and CAPTCHA resources. CI verifies that translation keys remain complete across that catalog.
The Docker Compose file binds the Mini App server to 127.0.0.1:${NGBOT_WEBAPP_HOST_PORT:-18080} on the host. A matching Caddy template is available at deploy/caddy/ngbot-webapp.Caddyfile.
Put the application values in the same mode-0600 .env that Compose reads automatically:
NG_GATEKEEPER_WEBAPP_PUBLIC_URL=https://antifraud.rtfm.rsvp
NGBOT_WEBAPP_HOST_PORT=18080These are file entries, not shell assignments to run before docker compose: Compose reads .env itself. It enforces the container listener, so leave NG_GATEKEEPER_WEBAPP_LISTEN_ADDR at its native default in .env.
Configure the separate Caddy service environment with the public domain and the same canonical host-port variable (for example through its systemd Environment= or EnvironmentFile= settings):
NGBOT_GATEKEEPER_WEBAPP_DOMAIN=antifraud.rtfm.rsvp
NGBOT_WEBAPP_HOST_PORT=18080Do not give Caddy the bot .env, because it contains application credentials that Caddy does not need.
The Mini App endpoint is intentionally hostile to indexing and unauthorized embedding:
/robots.txtdisallows all crawlers, including known search, SEO, and LLM training bots./sitemap.xmlis an empty sitemap.X-Robots-Tagdenies indexing, following, snippets, archives, image indexing, translation, AI use, and image-AI use.- CSP uses
default-src 'none'and per-request nonces for the Telegram script and local inline code. - CSP allows framing only by the official Telegram Web origin; attacker origins remain blocked.
- Browser capability APIs are disabled with
Permissions-Policy. - Referrers are suppressed with
Referrer-Policy: no-referrer. - Responses are marked
no-storeandprivate. - Cross-origin resource sharing is not enabled, and cross-site mutation requests are rejected through Fetch Metadata.
- POST bodies are size-limited before form parsing.
- Known crawler and LLM user agents are rejected before challenge lookup.
- MIME sniffing and legacy cross-domain policies are disabled.
- Concurrent admission and per-client request rates are bounded; access telemetry records only method, an allowlisted route name, status, and duration, never raw paths, query strings, authorization headers, or bearer tokens.
The embedded server exposes GET /livez for WebApp process liveness and GET /readyz for application readiness. Readiness is published only after every lifecycle component, including the durable update loop, starts successfully and is withdrawn before shutdown. A fatal serving error triggers graceful process shutdown with exit status 1 so Compose can restart the service. Container health checks use /readyz; use ./ngbot --version, the OCI revision label, and the running container image ID to verify a deployed artifact. See deploy/README.md for the executable release and rollback procedure.
Do not run PRAGMA quick_check, integrity_check, or other long scans against the live database file. Create a consistent online snapshot first, then run integrity and migration checks against the snapshot:
umask 077
sqlite3 /home/username/.ngbot/bot.db ".backup '/var/backups/ngbot/bot-audit.db'"
chmod 0600 /var/backups/ngbot/bot-audit.db
test "$(sqlite3 /var/backups/ngbot/bot-audit.db 'PRAGMA quick_check;')" = ok
test -z "$(sqlite3 /var/backups/ngbot/bot-audit.db 'PRAGMA foreign_key_check;')"Delete the audit snapshot after verification. A direct long-running read can hold a SQLite shared lock long enough for application writes to reach the configured busy timeout.
The application has an offline maintenance mode that applies pending migrations, performs a full VACUUM, enables incremental auto-vacuum, runs PRAGMA optimize, validates the database, restores WAL mode, and exits without starting Telegram polling:
docker compose stop ngbot
docker compose run --rm --no-deps ngbot --database-maintenance
docker compose up -d ngbotCreate and verify a database backup before running this command. The service must remain stopped for the complete maintenance run. Normal banlist refreshes use inactive generations, short batched writes, atomic activation, bounded garbage collection, passive WAL checkpoints, and incremental vacuum, so full maintenance is not required after each refresh.
Don't hesitate to contact me
- Gemini requests can reuse server-side explicit caching for the static moderation prefix when the provider supports it.
- Chat-specific settings, spam examples, and the private settings UI are already implemented.
This bot benefits from public anti-spam data shared with the community by:
- Combot Anti-Spam (CAS) for the CAS spammer database and API.
- LoLs bot for spammer lists and account checks.
Thank you to both projects for maintaining and sharing these community safety resources.
Feel free to add feature requests in issues.
