An independent, group-native Telegram assistant for the Gooners community.
It behaves like a specific foul-mouthed member of the group, while still doing real assistant
work: it reads files and replied media, verifies current facts, remembers people and threads,
uses tools honestly, and can acquire durable read-only research capabilities.
Current release: 2.0.0. See CHANGELOG.md for breaking changes and migration notes.
- Highlights
- Quick start (no Docker)
- Telegram setup and Privacy Mode
- LLM providers
- NSFW routing
- Commands
- Built-in modes
- Voice (TTS and STT)
- Vision
- Web and image grounding
- Documents and attachments
- Capability Forge
- Per-user heat
- Knowledge base
- Images and autonomous posting
- Brain and memory
- Configuration
- Security
- Development and testing
- Troubleshooting
- License
- Living group persona: learns every member's interests, habits and style; tracks relationships, evolving callbacks and group norms; supports first and roasts only when the scene permits it.
- Multi-action turns: a validated dependency graph can research, transform and generate several deliverables in one request, then Telegram sends every successful artifact.
- Document understanding: reads PDF, DOCX, text/Markdown, code, JSON, CSV, XML and HTML from the current message or the replied-to message; scanned PDFs are reported honestly when OCR is needed.
- Capability Forge: learns safe read-only research recipes, persists them, and exposes them as new slash commands without loading model-generated source code into the bot process.
- Per-chat modes you add, select and delete at runtime, plus built-in Gooners modes.
- Memory: every accepted chat message is mined asynchronously; only provenance-backed, durable social changes and episodic lore are retained and retrieved when relevant.
- Per-user heat: hostility escalates with users who push the bot and cools when they back off.
- Auto-engage: a scorer decides when to jump in (cooldowns, hourly cap, confidence, risk).
- Voice in and out: local whisper.cpp STT (voice, audio, video) and Kokoro TTS voice notes.
- Vision: looks at photos and video frames through a separate vision endpoint.
- Free grounding: web search and reverse-image lookup via a self-hosted SearXNG, no API keys.
- Image sending: fetches a waifu/anime image online and vision-checks it before posting.
- Autonomous posting: timed, opt-in takes on current events (RSS) or a commented image, plus
/news. - Music:
/playand/sing(or natural language like "mi canti X", "suona X", "play X", "cantame X") search YouTube, extract the audio and send it as a voice note. - Link media rehost: when a media URL is posted, the bot re-uploads it as a native Telegram attachment. Video streams (YouTube, TikTok, adult/cam, ...) are downloaded with yt-dlp; social posts (X, Instagram, Bluesky) are sent as images plus context (post text, likes, reposts). Results are cached by file_id, toggle per chat with
/linkmedia. - Translation:
/translate(alias/traduci) translates the replied message into any language. - NSFW routing to a separate uncensored model, decided before generation, with a refusal backstop.
- Pluggable LLM backends (GemRouter, OpenAI, DeepSeek, Ollama, any OpenAI-compatible host) with an optional fallback endpoint.
- No Docker and no Python. Node plus a local MongoDB. Strict TypeScript, ESM, eslint, prettier, vitest.
Requirements: Node.js 23.3 (see
.nvmrc) or a recent LTS, pnpm, and a running MongoDB.
# 1. Node
nvm use # picks up .nvmrc (23.3.0); or: nvm install 23.3.0
# 2. Install
pnpm install
# 3. MongoDB (any local instance). A helper for a user-local, auth-enabled mongod is included:
scripts/mongo-local.sh start # or: sudo systemctl start mongod / mongod --dbpath ./.mongo-data
# 4. Configure
cp .env.example .env
# edit .env: set TELEGRAM_BOT_TOKEN, MONGO_URI and your LLM provider
# 5. Run
pnpm dev # watch mode (tsx)
# or production:
pnpm build && pnpm start| Script | Purpose |
|---|---|
pnpm dev |
run with hot reload (tsx) |
pnpm build |
compile TypeScript to dist/ |
pnpm start |
run the compiled bot (node dist/main.js) |
pnpm typecheck |
strict type check, no emit |
pnpm lint / pnpm lint:fix |
eslint |
pnpm format / pnpm format:check |
prettier |
pnpm test / pnpm test:watch |
vitest |
- Create a bot with @BotFather and copy the token into
TELEGRAM_BOT_TOKEN. - Add the bot to your group.
- Put the deployer's
@handleinADMIN_HANDLESso they can run control commands anywhere, even without being a group admin.ALLOWED_HANDLES=*lets everyone chat.
By default Telegram bots run with Privacy Mode ON: the bot only receives commands, replies to its own
messages, and messages that mention it. Make the bot a group admin or disable Privacy Mode in
@BotFather (/setprivacy, then remove and re-add the bot) only when you want it to retain unaddressed
text as lightweight conversation context. Unaddressed messages never trigger STT, media handling,
scene analysis, Cortex/evaluator calls, or any LLM request. A command, @mention, or reply to the bot
is required for inference. No group ID is ever hardcoded.
Pick a provider with LLM_PROVIDER. Base URL and model are configurable, nothing is hardcoded in
business logic. Media capabilities activate only when you set the matching model var; if unset, that
capability is disabled and the bot degrades gracefully instead of crashing.
# GemRouter (OpenAI-compatible root surface)
LLM_PROVIDER=custom_openai_compatible
LLM_BASE_URL=http://192.168.178.27:4024
LLM_API_KEY=<GemRouter app bearer token>
LLM_MODEL=gemini-2.5-flash
SCENE_MODEL=gemini-2.5-flash-lite
REALISTIC_EVALUATOR_MODEL=gemini-2.5-flash-lite
CORTEX_MODEL=gemini-2.5-flash-lite
EMBEDDING_BASE_URL=http://192.168.178.27:4024/v1
EMBEDDING_MODEL=bge-m3
LLM_VISION_ENDPOINT_URL=http://192.168.178.27:4024/v1/vision
LLM_VISION_MODEL=minicpm-v4.5:8b
# Every Free-group LLM stage uses this economy model instead of LLM_MODEL.
FREE_LLM_MODEL=gemma-4-26b-a4b-it
# Background learning is pinned independently and never consumes chat-plan quota.
MINING_LLM_BASE_URL=http://192.168.178.27:4024
MINING_LLM_MODEL=gemma-4-31b-it
MINING_LLM_REQUEST_TIMEOUT_MS=180000
MINING_LLM_MAX_REQUESTS_PER_MINUTE=3
MINING_LLM_MAX_TOKENS_PER_MINUTE=15000
# DeepSeek
LLM_PROVIDER=deepseek
DEEPSEEK_API_KEY=<key>
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-chat
# Ollama / OpenAI / any OpenAI-compatible host
LLM_PROVIDER=ollama
LLM_BASE_URL=http://127.0.0.1:11434/v1
LLM_MODEL=llama3.1An optional fallback endpoint (LLM_FALLBACK_BASE_URL + LLM_FALLBACK_MODEL) can be used for chat
and reasoning calls when the primary throws. It can now be followed by a free-tier provider pool:
Groq, Gemini, OpenRouter and Cloudflare Workers AI. A short circuit breaker avoids hammering a
provider after its quota is exhausted. Vision, STT and TTS stay on their own providers.
Continuous social/lore extraction has a separate MINING_LLM_* route. Its model is pinned, request
model overrides are ignored, the group-plan header is never forwarded and its tokens are not charged
to the conversational quota. GemRouter calls are FIFO, strictly serial and paced to at most three
actual upstream starts in any rolling minute, with at least 20 seconds between starts. JSON-mode
fallbacks and repair attempts use the same budget, so an extraction cannot create a hidden burst.
The same gate reserves a conservative maximum of 15,000 estimated tokens in any rolling minute and
rejects an impossible request before HTTP dispatch.
The 180-second timeout belongs only to this asynchronous provider; interactive chat keeps
LLM_REQUEST_TIMEOUT_MS.
The worker drains all unseen messages by Telegram message_id in bounded batches. Each successful
window advances a durable cursor/checkpoint; a timeout, malformed result or transient upstream
failure retains the same window and resumes later. Transient provider failures additionally open a
60-second cooldown before another slot can be consumed. Backfill and live mining share the same
serialized lane, so they cannot overlap. Confidence, provenance, privacy and deduplication still
decide what becomes memory.
Mining never serializes the whole memory database. It considers up to 1,000 retained items locally for relevance and deduplication, sends at most 20 relevant items within 2.8 KB, focuses social context on people present in the window and byte-packs transcript windows within 12 KB. The two mining contracts omit a redundant generated JSON Schema but still apply strict local Zod validation and bounded repair. Every eligible message id is processed.
On token-constrained Gemma Free routes, reply generation uses one candidate call instead of three
identical parallel prompts. Do not populate LLM_ROUTER_FALLBACK_MODELS with models served by the
same intelligent GemRouter: it already performs account/model fallback, so client retries to that
same endpoint only multiply the payload.
Free quotas are useful resilience layers, not unlimited production capacity. Configure only the providers you actually have keys for and keep model ids explicit because catalogs change:
- Groq OpenAI compatibility and current free-plan limits
- Gemini OpenAI compatibility and current rate limits
- OpenRouter free-model router and rate-limit FAQ
- Cloudflare Workers AI OpenAI endpoint and free allocation/pricing
The provider reports capabilities (chat, vision, transcription, imageGeneration, tts);
a missing one is logged once and skipped.
GoonersBot can route adult turns to a separate uncensored model while keeping a normal model for
everyday banter. Set LLM_NSFW_MODEL. Routing is decided before generation (no extra LLM call) and
gated per chat by an admin.
/nsfw <mode> |
behaviour |
|---|---|
base (or on) |
the whole chat uses the uncensored model. |
off |
never use the uncensored model. |
smart |
default. Per message: an instant lexicon picks the uncensored model for NSFW-looking turns; for the rest the default model runs with a buffered refusal backstop, so a refusal is silently retried on the uncensored model and never shown. |
A custom mode created with a leading [nsfw] tag always routes to the uncensored model in
NSFW-enabled chats. Hard limits always apply regardless of model or mode: nothing involving minors,
no real-world non-consent, no sexual content about real named people without consent, nothing
illegal, no doxxing. NSFW is opt-in per chat and meant for private, consenting adult communities. If
LLM_NSFW_MODEL is empty, all routing is inert and the default model is always used.
| Command | Who | What |
|---|---|---|
/start |
admin | wake GoonersBot in this chat |
/stop |
admin | put it to sleep |
/reset |
admin | wipe conversation memory |
/mode |
admin | pick a mode |
/addmode <description> |
admin | add a custom mode ([nsfw] prefix flags it adult) |
/deletemode |
admin | delete a mode |
/introduce <text> |
anyone | tell GoonersBot who you are (saved as lore) |
/setfact @handle <text> |
admin | manually insert lore |
/facts [@handle] |
self / admin | show personal memory (/memory and /memoria aliases) |
/clearfacts [@handle] |
self / admin | expire stored lore (self anytime, others need admin) |
/lore |
anyone | top group lore (max 5) |
/forget |
reply / admin | reply to forget lore mined from a message; admin /forget <id> |
/translate <language> |
anyone | translate the replied message (alias /traduci) |
/voice |
anyone | turn the last message, or the replied one, into a voice note |
/play <query> |
anyone | search YouTube and send the audio as a voice note (aliases /suona, /riproduci) |
/sing <query> |
anyone | same as /play, phrased for songs (aliases /canta, /cantami) |
/news |
anyone | force an autonomous post now (alias /nuovo) |
/autopost |
admin | toggle timed autonomous posts in this chat |
/genera <prompt> |
anyone | generate an original image with Stable Diffusion (aliases /image, /img) |
/disegna <prompt> |
anyone | force manga planning/style while retaining capability routing (alias /draw) |
/genvid <prompt> |
anyone | generate a short video clip (aliases /video, /genvideo, /vid, /clip, /animazione) |
/usage |
anyone | your usage and limits |
/capabilities |
approved | list dynamically learned read-only capabilities |
/learn <goal> |
bot admin | explicitly design/install a safe research capability or save a setup proposal |
/community |
anyone | privacy-safe social-awareness coverage and current community themes |
/socialstatus |
admin | social-memory lifecycle/coverage diagnostics without exposing private scores |
/profile [free|plus|pro] |
admin | show or set the shared group plan and live quotas (aliases: /groupplan, /groupquota) |
/language |
admin | set chat language (it, en, ru, es) |
/terms |
anyone | terms of use and acceptance |
/conversationtracker |
admin | toggle passive tracking |
/autoengage |
admin | show passive-reply status |
/nsfw [off|base|smart] |
admin | NSFW model routing |
/ban @handle [seconds] |
bot admin | ban a Gooner (reply-aware, duration optional, 0 = permanent) |
/unban @handle |
bot admin | unban a Gooner |
/brain, /debuglast |
admin | inspect why the bot answered the way it did |
/approve [id] |
bot admin | approve a community chat or user (no id in a group = approve it) |
/unapprove [id] |
bot admin | revoke approval for a chat or user |
/approved |
bot admin | list approved chats and users |
/help |
anyone | help |
admin means group admin or bot admin (ADMIN_HANDLES). bot admin means listed in ADMIN_HANDLES.
Most commands that act on the chat need /terms accepted first. Outside the basic commands
(/start, /tos//terms, /help) everything requires approval (see below).
The model, media generation and link-media are gated: they work only for bot admins, approved
user ids, or approved community chats. Everyone else (including anyone who DMs the bot) is
limited to /start, /tos//terms and /help, and gets a notice to request access; the model
never replies and nothing is generated for them.
- Private DMs: a stranger who messages the bot is asked to sign the terms, then receives a notice
that this is an NSFW bot for approved private communities only and to DM the admin
(
ADMIN_HANDLES) for approval. No conversation, no generation. - Groups: a group only gets the full bot once its chat id is approved; non-approved groups stay silent.
- Approving: a bot admin runs
/approveinside a group to approve it, or/approve <id>from anywhere (negative id = chat, positive id = user)./unapproveand/approvedmanage the list. - Approval alone does not schedule background work. On boot the bot audits every approved group
with Telegram; mining, feedback and autonomous posts run only while its persisted status is
memberoradministrator./approvedshows that status and the last successful audit. my_chat_memberupdates are persisted in an append-onlychat_membership_eventsaudit. A transition toleftorkickedimmediately stops the chat and disables autoengage, autopost and conversation tracking, even when no ordinary message can arrive after the removal.- Approvals are seeded from
APPROVED_CHATS/APPROVED_USERSon first run and then persisted toAPPROVED_STORE_PATH(a JSON file, gitignored), so runtime/approvechanges survive restarts.
Every approved group has one persistent plan. New groups start on Free; a group admin changes
the plan with /profile free, /profile plus, or /profile pro. /profile without arguments
shows current counters and limits. Limits reset on calendar boundaries in the Europe/Rome timezone.
| Resource | Free | Plus | Pro |
|---|---|---|---|
| Conversational requests | 12/day, 3/hour | 32/day, 9/hour | 144/day, 30/hour |
| LLM tokens | 30k/day | 150k/day | 2M/day |
| Web searches | 8/day | 33/day | 75/day |
| Opened/scanned pages | 15/day | 75/day | 200/day |
| News retrievals | 2/day | 9/day | 24/day |
| Generated images | 1/day | 18/day | 48/day |
| Downloaded media | 3/day, 100 MB | 20/day, 600 MB | 40/day, 1.2 GB |
| Passive LLM replies | disabled | 9/hour | 12/hour |
| Per-user cooldown | 30 s | 6 s | 1 s |
| Per-chat cooldown | 20 s | 3 s | 1 s |
| User/chat burst | 1 / 3 per min | 6 / 16 per min | 20 / 60 per min |
Free groups are pinned to FREE_LLM_MODEL for direct conversational LLM operations (scene,
evaluator/Cortex, generation, translation and image-prompt preparation); embeddings retain their
separate configured endpoint. Free groups do not invoke the separate vision model or autonomous
posting. Continuous background learning still runs for every started chat through the independent
plan-independent MINING_LLM_* route, configured for one request at a time and three starts per
minute. The miner also waits for a foreground quiet window so background extraction does not start
while an interactive LLM operation is active.
All plans store passive messages as context. With /autoengage enabled, Plus and Pro may additionally
run a compact Flash-Lite gate and chime in within their passive hourly allowance; approved passive
turns skip redundant scene/Cortex model calls but retain social memory, tools, style and final
generation.
Semantic RAG uses EMBEDDING_MODEL (default bge-m3, 1024 dimensions) through GemRouter's
OpenAI-compatible /v1/embeddings endpoint. It helps group-memory retrieval, curated knowledge
matching and news ranking when the wording is not an exact keyword match. If embeddings are
unavailable or the vector dimension is wrong, the bot logs the failure and falls back to
keyword/Jaccard retrieval.
Live conversation attribution is tracked separately from durable RAG. The conversation_threads
and conversation_entities collections keep short-lived working memory such as "the RAV4 belongs
to @funboy" or "@miguel is commenting on @funboy's car thread". This state is injected compactly
before the generator so the bot can reply to the current speaker without stealing ownership of
topics introduced by someone else. Embeddings can help attach ambiguous follow-ups to the right
active thread, but ownership is always carried by Telegram metadata and structured entity fields,
not guessed from vector similarity. Configure it with THREAD_STATE_ENABLED,
THREAD_STATE_TTL_DAYS, and THREAD_STATE_MAX_ACTIVE.
The bot applies a per-user and per-chat anti-flood bucket before expensive work. Free is deliberately strict; Plus allows normal group use; Pro has a much wider burst allowance while retaining hard hour/day caps. Image generation is globally serialized: one image job runs at a time across every group and the rest wait in queue. Counters are persisted atomically in Mongo, so restarting the bot does not reset a group's budget.
| Mode | Vibe |
|---|---|
default |
natural group participant, funny, short, contextual |
roast |
light roast and banter, never hateful, no protected categories |
hype |
hypes the group: raids, announcements, wins, updates |
lorekeeper |
tracks recurring jokes, group and user facts, callbacks |
chaos |
unpredictable but rate-limited and safe |
market_degen |
crypto and degen vibes, never financial advice as certainty |
meme_recorder |
turns funny moments into quote/meme candidates and remembers them |
Add your own with /addmode <description> (the mode name is the first sentence). Prefix with
[nsfw] to make it adult.
- STT: a local whisper.cpp build transcribes incoming voice notes, audio files, videos and round video-notes. ffmpeg extracts the audio track from video containers, so the brain reads them as text and stores them as context. No cloud, modest CPU.
- TTS: an OpenAI-compatible
/v1/audio/speechserver (for example Kokoro-FastAPI) synthesizes replies. The bot finalizes the clip as Telegram OGG/Opus with a short silent tail (TTS_TAIL_PADDING_MS) when ffmpeg is available, which prevents clients from eating the last word. - The bot replies with a voice note when you sent it one (
TTS_REPLY_TO_VOICE), or occasionally on its own (TTS_AUTO_VOICE_PROBABILITY). /voicevoices the last chat message, or the replied-to message when used as a reply.- Multilingual: the TTS voice and whisper language follow the chat language (it
im_nicola, enam_michael, esem_alex; no Russian voice, so it falls back to the default).
# 1. Provision the local toolchain into vendor/ (gitignored): static ffmpeg, whisper.cpp, model
scripts/setup-voice.sh # or: scripts/setup-voice.sh small (better Italian, more CPU)
# 2. Enable in .env
TTS_ENABLED=true
TTS_BASE_URL=http://<kokoro-host>:8880
TTS_VOICE=im_nicola
STT_ENABLED=true # paths default to the vendor/ buildVerify the round-trip with pnpm tsx scripts/smoke-voice.ts. The default whisper model is base
(multilingual, ~142 MB); set WHISPER_MODEL to small for better Italian at a bit more CPU. No GPU
required.
The bot can fetch a track from YouTube and send it as a voice note. It searches with yt-dlp,
downloads the best audio, trims to MUSIC_MAX_DURATION_SECONDS (12 minutes by default) and
transcodes to Telegram OGG/Opus.
- Commands:
/play <query>(aliases/suona,/riproduci,/reproduce) and/sing <query>(aliases/canta,/cantami,/cantame). Used as a reply with no query, the replied message's text becomes the query. A direct YouTube URL also works. - Natural language (Italian, English, Spanish), recognized when the bot is addressed (mention or reply): "mi fai sentire X", "mi canti X", "suona X", "play X", "sing me X", "let me hear X", "cantame X", "ponme X", "reproduce X".
- yt-dlp is installed into
vendor/bin/byscripts/setup-voice.sh(alongside ffmpeg). Both are required; if either is missing the feature reports as unavailable and the rest of the bot is unaffected.
# Provisioned by scripts/setup-voice.sh; relevant .env knobs:
MUSIC_ENABLED=true
YTDLP_BIN=vendor/bin/yt-dlp
MUSIC_MAX_DURATION_SECONDS=720 # 12 minutesWhen someone posts a media URL in the group, the bot downloads the real content and re-uploads it as
a native Telegram attachment (so it plays inline, with no preview-stripping), then caches the source
URL to a Telegram file_id so the next post of the same link is instant. Toggle per chat with
/linkmedia (admin; on by default).
The bot picks the right kind of media per link:
- Video -> the actual clip, via yt-dlp. YouTube/Shorts, TikTok, Vimeo, Streamable, Twitch clips,
Facebook, Dailymotion, Kick, Reddit video, Instagram reels, video tweets, and adult/cam sites
are handled by the vendored
yt-dlpbinary (the same one/playuses). It picks the best clip up to 720p, merges video+audio with ffmpeg, and respects the size and duration caps. yt-dlp covers ~1800 sites, so most "here is a video" links just work. Reddit video and X video go through yt-dlp because their direct URLs are either split tracks (Reddit) or an uncapped 4K master (X). - Social photos -> image(s) + context. A photo tweet (via the fxtwitter API) and Bluesky posts are sent as the image(s) with a caption carrying the context: the post text, the author, and the engagement counts (likes, reposts, replies, views). That context is also fed to the brain when the bot is tagged, so it can comment on what the post says, not just show it.
- Live streams / unbounded video -> a single snapshot. When a link is a live stream (or a video we cannot download within the caps), the bot grabs one frame with ffmpeg and posts that still instead, optionally with a vision description.
Direct file links (.mp4, .gif, .jpg, .mp3, ...), Imgur, Giphy and Tenor are fetched
directly; anything else falls back to a generic OpenGraph/JSON-LD scan.
Instagram needs cookies. Logged-out Instagram no longer exposes media to scrapers or yt-dlp, so IG reels/posts only work if you set
LINK_MEDIA_COOKIES_INSTAGRAMto either a raw Cookie header string or a path to a Netscapecookies.txtexported from a logged-in browser. Without it, IG links are silently skipped.
- Scope is deliberately small: a couple of links per message, a few files at most, with hard caps on count, size and duration. It is not a profile/feed crawler.
- Videos are sent as inline, autoplaying Telegram players: the mp4 is remuxed
+faststart(moov atom moved to the front, no re-encode when already small) and uploaded withsupports_streamingplus dimensions, duration and a generated poster thumbnail. GIFs become muted mp4 animations, audio becomes mp3. - Short clips can be transcribed (STT) or frame-described (vision); that, plus the social post text and stats, is fed to the brain when the bot is tagged, so it can actually comment on the link.
- SSRF-guarded: only http/https, and hosts resolving to localhost/private/link-local/cloud-metadata addresses are refused. Per-chat and per-user cooldowns prevent spam.
- Adult/cam sites are supported but gated: they are skipped unless
LINK_MEDIA_NSFW_ALLOW=true. Live cam streams (no fixed duration) are not captured, only recorded videos.
# Relevant .env knobs (full list in .env.example):
LINK_MEDIA_ENABLED=true
LINK_MEDIA_NSFW_ALLOW=false # set true to allow adult/cam video hosts
LINK_MEDIA_MAX_URLS_PER_MESSAGE=2
LINK_MEDIA_MAX_UPLOAD_MB=45
LINK_MEDIA_MAX_DURATION_SECONDS=180
YTDLP_BIN=vendor/bin/yt-dlp # shared with /play; scripts/setup-voice.sh installs itYouTube note: without a JavaScript runtime installed, yt-dlp can only fetch progressive (~360p) formats. That is fine for a Telegram rehost; install a JS runtime if you want higher resolutions.
The bot can look at photos and at a frame extracted from a video, then react. Vision is gated by
LLM_VISION_MODEL. Production uses GemRouter's dedicated /v1/vision endpoint, backed by
minicpm-v4.5:8b; do not point the bot directly at Ollama for this flow:
# in .env:
LLM_VISION_MODEL=minicpm-v4.5:8b
LLM_VISION_ENDPOINT_URL=http://192.168.178.27:4024/v1/vision
LLM_VISION_API_KEY= # empty reuses LLM_API_KEYImages are analysed only when the bot is addressed (mention or reply), for the current message or the replied-to one. If the endpoint is down, vision degrades gracefully.
When a turn needs facts the model cannot know, a grounding layer fetches them and injects a context block; the persona model still writes the reply. Two heuristic-gated triggers run in parallel with memory retrieval, both backed by a free self-hosted SearXNG (no API keys):
- Web search for recency or factual questions (who won yesterday, how much is the 5090, latest news).
- Image lookup for "who/what is this" or product questions: the vision model identifies the subject of a photo or video frame, then SearXNG searches that identification for confirmation and product links. This is the free equivalent of Google Lens, which now needs a headless browser and a public image URL.
Everything degrades to nothing on failure, and the model is told never to claim it searched the web.
# 1. One-time: clone SearXNG, venv, deps, settings, and install a systemd --user service
scripts/searxng.sh setup
# 2. Run it on 127.0.0.1:8888 (systemd --user service: auto-restart, survives reboot via lingering;
# falls back to a plain process where systemd --user is unavailable)
scripts/searxng.sh start # stop | restart | status
# 3. Enable in .env
WEB_SEARCH_ENABLED=true
SEARXNG_URL=http://127.0.0.1:8888
IMAGE_LOOKUP_ENABLED=true # needs WEB_SEARCH_ENABLED and a vision modelVerify with pnpm tsx scripts/smoke-search.ts. Gating lives in src/search/groundingService.ts,
the SearXNG client in src/search/searxng.ts.
PDF, DOCX, text/Markdown, source code, JSON, CSV, XML and HTML attachments are inputs to the same assistant turn as the Telegram text. The bot inspects both the current message and the replied-to message, so replying to an uploaded PDF with “summarise this” does not lose the file relationship. Extraction is inert: macros, scripts, HTML JavaScript and code attachments are read as data and never executed.
Selectable-text PDFs are extracted directly. Long documents are summarized from chunk-level notes before synthesis rather than silently truncating to the first page. If a PDF is scanned or a format is unsupported, the response names the actual limitation instead of claiming that no attachment exists. Telegram's hosted Bot API download ceiling and the configured per-file/per-turn limits still apply.
/learn <goal> lets a bot admin turn a missing, read-only research workflow into a persistent
declarative recipe. An installed recipe becomes a stable slash command, survives restart and uses
grounded search plus constrained synthesis; /capabilities lists what is currently available.
Requests that need credentials, authenticated APIs, package installation, compilation, shell access, local-machine control or external writes are saved as explicit setup proposals. Even when source code is attached, the bot may inspect it to design the proposal but never executes it merely because it arrived from Telegram. This keeps “learn while talking” useful without turning a message or a prompt-injected document into remote code execution.
Hostility is tracked per user, per chat as a heat score from 0 to 100 (collection user_heat). It
starts gruff (HEAT_BASELINE), rises when someone attacks or pushes the bot, and decays over time,
faster when the user de-escalates (apologizes, calms down). The score maps to an escalation level
(baseline, irritato, ostile, incazzato, furia) that raises the aggression dial and injects a hostility
directive aimed at that specific user. So the bot can be venomous with one person and normal with the
rest. Logic in src/services/heat.ts; knobs HEAT_ENABLED, HEAT_BASELINE, HEAT_DECAY_PER_MINUTE.
A curated knowledge collection (anime, manga, otaku and Asian pop culture, gaming, IT and dev,
crypto, sci-fi and TV) is recalled only when relevant: a keyword match against the message surfaces
the top KNOWLEDGE_MAX_ITEMS entries as a short, clearly optional context block. Most turns match
nothing, so it adds no prompt weight and never makes the character monothematic. Seeded on boot from
src/knowledge/seed.ts (KNOWLEDGE_SEED_ON_BOOT, idempotent); retrieval in
src/knowledge/knowledgeRetriever.ts. Extend the seed freely.
Sending images, free and without an image-generation model: the bot occasionally posts a waifu or
anime image that fits its taste. The image is fetched online through SearXNG image search, then
downloaded and looked at by the vision model before it is ever sent; off-theme, unsafe or real-person
results are rejected. In replies it attaches one at IMAGE_SEND_PROBABILITY when the topic is anime
or waifu. See src/media/imageFinder.ts (needs SearXNG and a vision model).
Autonomous posting: every AUTOPOST_INTERVAL_MINUTES, with AUTOPOST_PROBABILITY per eligible chat,
the bot drops an unprompted line. It is either a styled take on a current event pulled from RSS
(RSS_FEEDS) with the source link, or a commented waifu image, split by AUTOPOST_IMAGE_RATIO. It is
opt-in per chat (/autopost, default off) and can be forced on demand with /news (alias /nuovo).
Composer in src/services/autonomousPoster.ts, feeds in src/news/newsService.ts.
/genera, /disegna, autonomous generated images and the cortex image_gen tool share one visual
pipeline. A loose request is not forwarded verbatim to a model:
request + relevant continuity -> validated scene plan
-> Agnes natural-language prompt
-> PonyXL tag prompt + native negative prompt
-> capability-aware provider routing
-> generated bitmap
-> vision QA -> optional corrected retry -> best bitmap
The prompt model first returns a Zod-validated scene plan containing the locked medium, content rating and aspect ratio; exact subject counts and per-subject descriptions/actions/positions; interaction, framing, camera, setting, lighting, palette and mood; required and excluded details; and any exact visible text. It must translate visual fields to English and cannot silently replace, merge or add subjects. If structured generation fails, a deterministic scene contract preserves the request instead of dropping image generation. Chat lore is considered only for an explicitly referenced member/series or continuity request such as "same as before".
That single contract is compiled twice:
- Agnes receives an instruction-following natural-language prompt with literal subject counts, spatial relationships, composition, environment, hard requirements and exclusions.
- PonyXL receives concise booru-compatible content tags. Its checkpoint-specific quality, source and rating chain and its negative prompt are added only by the Forge provider.
The first provider is selected by capability, not by a fixed global preference. Explicit content stays on Pony; focused anime, manga and pixel art normally prefer Pony; requested exact text, multiple subjects, detail-dense contracts and the other visual media prefer Agnes. A pose reference always uses local PonyXL + OpenPose. Either backend can fall back to the other when the requested capability permits it. Independent five-minute circuits open after two consecutive Agnes or Forge failures, while caller cancellations do not poison provider health.
Agnes uses the router's OpenAI-compatible POST /v1/images/generations with
the GemRouter-compatible exact sizes 1024x1024, 1792x1024 and 1024x1792. These preserve the
resolved 1:1, 16:9 or 9:16 orientation through the router, whose current OpenAI validator
rejects Agnes' otherwise documented native size: "1K" plus ratio body. An explicit tool ratio
wins; otherwise square/avatar requests become 1:1, vertical/story requests 9:16, and
landscape/banner requests 16:9, with 1:1 as the neutral default.
AGNES_BASE_URL and AGNES_API_KEY fall back to the main LLM route/key, while
AGNES_IMAGE_ENABLED=false removes Agnes from image routing without disabling local generation.
When vision is configured and IMAGE_GENERATION_QA_ENABLED=true, the generated bitmap is resized to
a bounded JPEG for inspection when ffmpeg is available, then scored against a compact quality brief
built from the original scene plan. Missing/extra main subjects, a wrong central action, medium or
framing, unreadable required text and severe anatomy are hard failures. A failed candidate gets a
bounded, focused retry: Pony-first focused art may move to Agnes, Agnes-first instruction-dense work
stays on Agnes, and pose-guided/explicit retries stay on Pony. The best inspected candidate is
returned if no attempt reaches the threshold. QA also reports
whether every visible person is unambiguously adult and independently classifies the pixels as
safe, suggestive, explicit or uncertain. Output above the requested rating is regenerated and
never returned after the retry budget: a safe request cannot leak an unspoilered explicit bitmap,
and suggestive work cannot leak explicit content. Suggestive and explicit scenes containing people
are not delivered when the adult-only check is missing or ambiguous; object-only adult scenes use
no_people instead. Any bitmap positively identified as minor/age-ambiguous is blocked at every
rating. Safe work remains fail-open only when vision itself is malformed or unavailable, so a flaky
inspector does not turn an otherwise safe image request into an error.
/genvid <prompt> renders a short clip with the remote agnes-video-v2.0 model (aliases /video,
/genvideo, /generavideo, /vid, /clip, /animazione, /genclip). It is also reachable from
the classifier, so "generami un video dove un cane si morde la coda" works without a command: the
cortex video_gen tool is deliberately distinguished from link_media (which downloads media that
already exists) and from image_gen (a still image).
Practical notes:
- The request blocks until the clip is rendered (~1-2 minutes), and upstream allows one video per minute; a local cooldown gates callers before the slot is spent and a rate-limited request answers "try again in Ns" instead of failing silently.
- Generated mp4s ship with the moov atom at the end, which Telegram cannot stream, so every clip is
remuxed
+faststart(stream copy) and sent withsupports_streaming, dimensions, duration and a poster: it arrives as an inline autoplaying video, not a file to download. - A clip spends the group's generated-image quota, and NSFW prompts are sent with a spoiler overlay.
AGNES_VIDEO_ENABLED=true
AGNES_VIDEO_MODEL=agnes-video-v2.0
AGNES_VIDEO_MIN_INTERVAL_MS=60000 # upstream allows 1 video/minute/genera <prompt> generates an original bitmap through a self-hosted Forge/Automatic1111 API;
/image and /img are aliases. All local workflows use Pony Diffusion XL: it stays loaded as the
single checkpoint on the shared Forge host, avoiding the RAM-heavy swaps that destabilize it. Model
selection still understands anime/manga/comic, general/photographic and explicit profiles, so an
operator may configure separate checkpoints later without changing the scene compiler.
/disegna <prompt> is intentionally separate: it forces the manga medium/profile, including manga
prompting, clean ink lineart and screentone negatives when Pony is selected. It still retains
capability routing, so exact-text, multi-subject or instruction-dense manga can use Agnes.
/genera infers both medium and routing.
Every Pony prompt receives the complete positive score chain
score_9, score_8_up, score_7_up, score_6_up, score_5_up, score_4_up, the appropriate
source_anime/source_cartoon tag when applicable, and one of
rating_safe, rating_questionable or rating_explicit. The native Forge negative_prompt merges
the operator baseline, medium/rating defects and scene-specific exclusions while deduplicating them;
low score tags score_3, score_2, score_1 remain negative rather than contaminating the positive
prompt. The compiler also translates prose that Pony commonly misreads into checkpoint-native
framing/background tags (upper body, waist up, simple background) and contextual negatives:
for example, a requested bust portrait suppresses full-body framing, while an unarmed warrior does
not silently acquire a sword.
The defaults are deliberately sized for a shared 12 GB RTX 3080 Ti: normal Pony renders use 28 steps;
the selected aspect ratio maps to 1024x1024, 1152x640 or 640x1152. OpenPose-guided renders use
a reduced 22-step canvas to preserve VRAM headroom. Before a request the bot polls Forge's global
queue and re-verifies the process-wide checkpoint because the frontend may have changed it. Forge
response bodies remain under the render timeout and a 32 MB cap. Image jobs and their QA retries
remain globally serialized. OpenPose search is restricted to genuinely complex interactions, checks
at most two candidates and caches a verified neutral pose for 30 minutes.
The normal news/web-image autopost pipeline never sends generated images. A separate generated-image
scheduler exists behind GENERATED_IMAGE_AUTOPOST_ENABLED=false and must remain off until explicitly
approved. Generated bitmaps are kept in memory for Telegram delivery, never written under the repo;
the .gitignore also excludes generated image artifact paths as a second guardrail.
GoonersBot does not dump facts into every prompt. Each reply runs a bounded pipeline so it behaves like a real group member rather than a deterministic bot:
message + replied media -> Perception -> Scene + Social Awareness + Memory Retriever ->
Cortex -> Multi-action DAG -> verified tools/artifacts ->
Reply Planner -> Style Engine -> Generator -> Ranker ->
semantic Repetition Guard -> reply + every artifact
+ (background) Social/Memory/Feedback Learning
- Scene Analyzer reads topic, energy, intent and whether the bot is being roasted (LLM with a deterministic fallback).
- Memory Retriever pulls only the few memories relevant to this turn (scored by handle, keyword, topic and salience), skips recently-used ones, and returns nothing when the chat is roasting the bot for repetition.
- Reply Planner and Style Engine pick intent, tone, length and one of ten voice variants. A dynamic banned-phrases list plus premise/mechanism history kills repeated openings, recycled roast shapes and catchphrase tics. Gratitude, distress and serious requests suppress gratuitous hostility.
- The Generator samples three candidates by default (configurable). The Ranker and Repetition Guard
use stale openings, premises and comedy strategies as ranking penalties instead of rejecting every
otherwise useful answer. Lexical clones above
REPETITION_SIMILARITY_THRESHOLD, stricter high-confidence semantic clones, explicitly banned/canned phrases, unauthorized verbatim-memory callbacks, internal deflection messages and social-floor violations are hard blocks. The best acceptable candidate is used immediately. - Ranking has a deterministic local fallback. If every candidate is hard-blocked, the bot performs one bounded regeneration; if that still produces only repetitive but socially safe, substantive text, it sends the best usable answer instead of the old evasive “rephrase and try again” message. Last-resort recovery may relax only repetition/canned-style blocks: it never revives unauthorized memory, a social-floor violation or an internal deflection. If no safe candidate exists, the fallback gives concise support or states the exact missing evidence/context without discussing the hidden generation pipeline.
- Social memory keeps evolving profiles, relationships, shared norms and running jokes with provenance, confidence, contradiction handling, lifecycle decay and fatigue. Reactions to the exact bot message teach which style variants and comedy mechanisms work for each person. A checkpointed startup backfill covers the full retained history; invalid structured output never advances the learning cursor, and a privacy-filtered local baseline keeps explicit declarations usable during an upstream outage.
- A multi-action planner may chain and parallelize available tools. Dependency failures are isolated, successful outputs are verified, and partial completion is reported honestly.
- The reply always addresses the current speaker, and attached media carries who posted it so the roast target is unambiguous.
- Memory lives in
memory_items(mined lore with confidence, salience and toxicity), not raw text. Social profiles live separately, so changing interests supersede stale claims instead of creating contradictory lore. Both projections evolve continuously in every started chat, including Free groups, from a dedicated pinned model./factand/autofactno longer exist:/facts,/clearfacts,/forget,/introduceand admin/setfactremain as transparency, erasure and correction controls. - Admins use
/brainand/debuglastto see exactly why the bot answered the way it did.
Internal pipeline instructions are written in English (the model follows them best) while the bot is
told to reply in the chat language. The legacy facts collection is auto-migrated into memory_items
on first boot.
Validated with zod at startup; the bot fails fast on a missing or invalid required var. Optional
capabilities never block startup. Copy .env.example to .env (gitignored; never commit secrets).
The tables below list the common vars; see .env.example for the full set with comments.
| Variable | Default | Description |
|---|---|---|
TELEGRAM_BOT_TOKEN |
required | Token from @BotFather. |
BOT_USERNAME |
GoonersBot |
Hint only; the real username is resolved at boot. |
ALLOWED_HANDLES |
* |
Comma @handles allowed to use the bot. Empty or * means everyone. |
ADMIN_HANDLES |
none | Comma @handles that are bot admins. |
MONGO_URI |
mongodb://127.0.0.1:27017/goonerbot |
Connection string. |
MONGO_DB |
goonerbot |
Database name. |
NODE_ENV |
development |
production gives JSON logs. |
LOG_LEVEL |
info |
pino level. |
| Variable | Default | Description |
|---|---|---|
LLM_PROVIDER |
ollama |
solclawn, openai, deepseek, ollama, custom_openai_compatible. |
LLM_BASE_URL |
per-provider | OpenAI-compatible base URL. |
LLM_API_KEY |
none | Bearer token. |
LLM_MODEL |
none | Chat model (required for text replies). |
FREE_LLM_MODEL |
gemma-4-26b-a4b-it |
Economy model forced for every LLM operation in Free groups. |
MINING_LLM_BASE_URL / MINING_LLM_API_KEY |
main LLM route/key | Independent OpenAI-compatible endpoint/key for continuous learning. |
MINING_LLM_MODEL |
gemma-4-31b-it |
Pinned background lore/social model; request overrides are ignored. |
MINING_LLM_REQUEST_TIMEOUT_MS |
180000 |
Timeout only for one queued background structured call. |
MINING_LLM_MAX_REQUESTS_PER_MINUTE |
3 |
Mining-provider cap; default pacing starts calls at least 20s apart. |
MINING_LLM_MAX_TOKENS_PER_MINUTE |
15000 |
Conservative rolling mining token envelope, including output reserve. |
MINING_LLM_FOREGROUND_QUIET_MS |
15000 |
Defer new mining calls while/just after interactive LLM work. |
LLM_VISION_MODEL |
none | Enables image and video-frame understanding. |
LLM_VISION_ENDPOINT_URL |
none | Full dedicated vision endpoint, e.g. GemRouter /v1/vision. |
LLM_VISION_BASE_URL / LLM_VISION_API_KEY |
none | Separate chat-compatible vision base; empty reuses the main one. |
LLM_TRANSCRIPTION_MODEL |
none | Remote STT fallback; local whisper covers this otherwise. |
LLM_TTS_MODEL / LLM_IMAGE_MODEL |
none | Enable remote TTS / image generation if your backend has them. |
LLM_FALLBACK_BASE_URL / LLM_FALLBACK_MODEL / LLM_FALLBACK_API_KEY |
none | Fallback chat endpoint when the primary throws. |
LLM_ROUTER_FALLBACK_MODELS |
none | Ordered alternative models on the primary gateway, reusing its URL/key. |
GROQ_API_KEY / GROQ_MODEL |
none / GPT-OSS | Optional Groq free-tier fallback. |
GEMINI_API_KEY / GEMINI_MODEL |
none | Optional Gemini free-tier fallback; model must be explicit. |
OPENROUTER_API_KEY / OPENROUTER_MODEL |
none / openrouter/free |
Optional free-model router fallback. |
CLOUDFLARE_AI_API_KEY / CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_AI_MODEL |
none | Optional Workers AI free-allocation fallback. |
LLM_REQUEST_TIMEOUT_MS |
60000 |
Per-request timeout. |
Structured internal calls use JSON mode plus a generated JSON Schema, strict Zod validation and one validation-error-guided repair. Circuit breakers are operation-specific: a model that temporarily fails social/planner JSON is not automatically removed from ordinary chat.
The mining timeout and pacing apply only to the dedicated provider. They do not replace
LLM_REQUEST_TIMEOUT_MS, delay an interactive reply or expand a conversational group's plan. The
minimum start gap is derived from the configured RPM (ceil(60000 / RPM)); leave the production
value at 3 for GemRouter.
| Variable | Default | Description |
|---|---|---|
TTS_ENABLED / TTS_BASE_URL / TTS_VOICE / TTS_FORMAT |
off | Kokoro TTS. Server audio is finalized for Telegram when ffmpeg is available. |
TTS_TAIL_PADDING_MS |
600 |
Silent tail appended after TTS so Telegram clients do not clip the last word. |
STT_ENABLED / WHISPER_MODEL / FFMPEG_BIN |
off | Local whisper.cpp STT (vendor/ defaults). |
WEB_SEARCH_ENABLED / SEARXNG_URL |
off | Web grounding via SearXNG. |
DOCUMENTS_ENABLED / DOCUMENT_MAX_CHARS_PER_FILE |
on / 50000 |
Read current/replied PDF, DOCX and text-like documents as inert content. |
CAPABILITY_FORGE_ENABLED / CAPABILITY_STORE_PATH |
on / data/capabilities |
Persist safe read-only research recipes and setup proposals. |
IMAGE_LOOKUP_ENABLED |
off | Reverse-image grounding (needs web search and vision). |
IMAGE_SEND_ENABLED / IMAGE_SEND_PROBABILITY |
on / 0.15 |
Attach a verified waifu image on anime topics. |
IMAGE_QUERY_POOL |
defaults | Comma-separated image query seeds. |
IMAGE_GENERATION_QA_ENABLED |
on | Vision-check generated images against their structured scene plan. |
IMAGE_GENERATION_QA_MIN_SCORE / IMAGE_GENERATION_QA_MAX_RETRIES |
0.72 / 1 |
Acceptance threshold / bounded corrective generations (0..2). |
AGNES_BASE_URL / AGNES_API_KEY |
main LLM route/key | Optional dedicated router endpoint and bearer token for Agnes media. |
AGNES_IMAGE_ENABLED / AGNES_IMAGE_MODEL |
on / agnes-image-2.1-flash |
Enable the instruction-following image route; Pony remains available. |
AGNES_IMAGE_TIMEOUT_MS / AGNES_IMAGE_MAX_MB |
120000 / 25 |
Remote render timeout and bounded returned-image size. |
SD_ENABLED / SD_API_URL |
on / Forge URL | Enable the self-hosted Forge/Automatic1111 generator. |
SD_ANIME_MODEL / SD_REALISTIC_MODEL / SD_NSFW_MODEL |
PonyXL | Keep all three set to the same PonyXL checkpoint to avoid Forge model swaps. |
SD_NEGATIVE_PROMPT / SD_STEPS |
tuned defaults | Pony baseline negative and sampling floor. |
SD_WIDTH / SD_HEIGHT / SD_CFG_SCALE |
compatibility values | Legacy/direct workflow values; planned requests use the tuned ratio presets. |
SD_TIMEOUT_MS / SD_QUEUE_TIMEOUT_MS / SD_QUEUE_POLL_MS |
300000 / 300000 / 2000 |
Per-render timeout and wait policy when Forge is busy. |
SD_CONTROLNET_ENABLED / SD_CONTROLNET_OPENPOSE_MODEL / SD_CONTROLNET_PROCESSOR_RESOLUTION |
on / OpenPoseXL2 / 512 |
SearXNG pose-reference workflow for complex poses, tuned for the shared GPU. |
AUTOPOST_ENABLED / AUTOPOST_DEFAULT_ENABLED |
on / off | Scheduler switch / per-chat default (opt-in). |
AUTOPOST_INTERVAL_MINUTES / AUTOPOST_PROBABILITY |
10 / 0.05 |
Tick interval / chance per eligible chat. |
AUTOPOST_IMAGE_RATIO |
0.4 |
Share of autoposts that are an image vs a news take. |
GENERATED_IMAGE_AUTOPOST_ENABLED |
off | Separate generated-image scheduler; leave off until quality is approved. |
GENERATED_IMAGE_AUTOPOST_INTERVAL_MINUTES / GENERATED_IMAGE_AUTOPOST_PROBABILITY |
10 / 0.05 |
Separate generated-image scheduler cadence, when enabled. |
RSS_FEEDS |
BBC, CNN, ANSA, Verge | Comma-separated feed URLs. |
| Variable | Default | Description |
|---|---|---|
LLM_NSFW_MODEL |
none | Uncensored model. Empty disables NSFW routing. |
LLM_NSFW_DEFAULT_MODE |
smart |
Initial per-chat mode: off, base, smart. |
LLM_REFUSAL_FALLBACK |
true |
Retry on the NSFW model if the default refuses. |
HEAT_ENABLED / HEAT_BASELINE / HEAT_DECAY_PER_MINUTE |
on / 12 / 1 |
Per-user hostility escalation. |
KNOWLEDGE_ENABLED / KNOWLEDGE_MAX_ITEMS / KNOWLEDGE_SEED_ON_BOOT |
on / 2 / on |
On-demand knowledge recall. |
REPLY_TEMPERATURE / REPLY_CANDIDATE_COUNT |
0.95 / 3 |
Generation temperature / candidates per reply. |
REPLY_MAX_REGENERATIONS |
1 |
One bounded retry only when every candidate is blocked. |
MAX_REPLY_LINES / MAX_REPLY_CHARS |
3 / 420 |
Reply length caps. |
MEMORY_MINING_ENABLED / FEEDBACK_LEARNING_ENABLED |
on / on | Continuous lore/social mining and feedback learning. |
MEMORY_MINING_BATCH_MESSAGES / MEMORY_MINING_CONTEXT_MESSAGES |
20 / 30 |
New evidence per call / bounded look-behind count. |
MEMORY_MINING_MAX_WINDOW_BYTES |
12000 |
UTF-8 transcript budget; messages are byte-packed. |
MEMORY_MINING_INTERVAL_SECONDS |
60 |
Backlog watchdog; idle cursor checks spend no LLM call. |
Backfill and continuous mining both use one global FIFO lane. The provider remains serial and its rolling RPM budget includes native JSON, prompt-only JSON fallback and repair requests; failures retain their cursor and are retried after cooldown instead of being replayed immediately.
| Variable | Default | Description |
|---|---|---|
DEFAULT_LANGUAGE |
italian |
italian, english, russian, spanish; per chat via /language. |
AUTOENGAGE_DEFAULT_ENABLED / CONVERSATION_TRACKER_DEFAULT_ENABLED |
on / on | Initial toggles for new chats. |
MAX_REPLIES_PER_CHAT_PER_HOUR |
72 |
Global safety ceiling; the active /profile plan enforces the lower per-group cap. |
AUTOENGAGE_MIN_COOLDOWN_SECONDS / AUTOENGAGE_USER_COOLDOWN_SECONDS |
45 / 20 |
Passive-reply cooldowns. |
AUTOENGAGE_MODEL / AUTOENGAGE_MAX_TOKENS |
main / 160 |
Optional fast passive gate model and its strict JSON output cap. |
MESSAGE_HISTORY_RETENTION_DAYS / MAX_CONTEXT_MESSAGES |
30 / 25 |
Message TTL / context window. |
COMMAND_RATE_LIMIT_SECONDS |
1 |
Min seconds between accepted commands per user. |
GoonersBot is built for an authorized, self-hosted deployment.
| Area | Posture |
|---|---|
| Secrets | Only in .env (gitignored). No hardcoded tokens or keys in source. The LLM key is sent as a Bearer header and never logged. |
| Logging | Structured (pino). The bot token, LLM key and Mongo URI are never logged. |
| Auth | Centralized permission service. Control commands require group admin or bot admin; /ban requires bot admin. Callback queries are permission-checked. |
| Bans | Gated on commands and in the message handler; timed bans auto-expire. |
| NoSQL injection | Mongo queries use fixed field names with user input only as scalar values; no $where or eval; ids guarded by ObjectId.isValid. |
| Rate limiting | Per-user command cooldown, plan-aware per-group anti-flood, durable hourly/daily quotas, a serial three-RPM mining lane, globally serialized image jobs, usage limits and media download caps. |
| Media and SSRF | Inbound files come only from Telegram's file API. Outbound hosts are operator-configured, not user input. Fetched images are size-capped and vision-checked. |
| MongoDB | Run it bound to 127.0.0.1 with --auth and a least-privilege app user (scripts/mongo-local.sh does this). |
| Content safety | NSFW is opt-in per chat with non-negotiable hard limits in the system prompt. |
Prompt-injection and jailbreak attempts in user messages are mitigated by system-prompt guardrails
but not eliminated; treat model output as untrusted. Keep ADMIN_HANDLES tight and Mongo off the
public network. To report a vulnerability, open a private security advisory on the repository.
pnpm typecheck # strict TS
pnpm lint # eslint
pnpm format:check # prettier
pnpm test # vitest (unit tests use fakes, no live Mongo needed)Optional integration and smoke harnesses live in scripts/ and need a real Mongo or the matching
backend:
pnpm tsx scripts/smoke-integration.ts # storage, LLM, reply and routing, end to end
pnpm tsx scripts/smoke-telegram.ts # synthetic Telegram updates through the real bot
pnpm tsx scripts/smoke-voice.ts # TTS to OGG/Opus to whisper round-trip
pnpm tsx scripts/smoke-search.ts # SearXNG query and grounding gating| Symptom | Cause and fix |
|---|---|
/start says you cannot do that here |
You are not a group admin and not in ADMIN_HANDLES. Add your @handle. |
| Bot ignores normal messages | Privacy Mode is ON. Disable it in @BotFather (then re-add the bot) or make the bot a group admin. |
| Replies in the wrong language | Existing chats keep their stored language; run /language. New chats use DEFAULT_LANGUAGE. |
| A capability is unavailable | The relevant LLM_*_MODEL is not set (vision, image, transcription). Set it or ignore. |
| Web search or images do nothing | SearXNG is not running or SEARXNG_URL is wrong. Start it with scripts/searxng.sh start. |
| Memory backfill looks slow | Expected: production mining is serial and capped at three starts/minute. Inspect checkpoints; do not raise the interactive timeout. |
| A mining window keeps retrying | Its structured result or provider failed. The cursor is retained and retried after cooldown. |
| Bot will not start | Read the fail-fast error, usually a missing TELEGRAM_BOT_TOKEN or unreachable MONGO_URI. |
See LICENSE.
Free for personal, educational, research, and other non-commercial use. Commercial use requires a separate license from 0xfunboy. Open an issue or use the contact information available on this GitHub profile to request permission.
