This document serves as the single source of truth for all development rules, coding standards, and agent behavior guidelines for this project.
- π€ Agent Development Rules & Guidelines
ngbot is a Telegram gatekeeper bot with CAPTCHA verification, LLM-powered spam detection, and community voting moderation.
Stack: Go 1.26.8, SQLite, Telegram Bot API, OpenAI/Gemini LLMs
Structure:
cmd/ngbot/- Entry point, runtime wiringinternal/bot/- Core service, update processorinternal/handlers/- Admin, Gatekeeper, Reactor, Moderationinternal/db/sqlite/- Persistence with embedded migrationsinternal/adapters/llm/- OpenAI/Gemini clientsresources/- i18n, challenges, migrations
For detailed architecture, see docs/CODEBASE_MAP.md.
- Language Policy π: Always reason and edit in English, but answer user in their prompt language.
- Response Format π: Always format responses using structured tables with emojis instead of long text blocks.
- Visual Clarity β¨: Use tables for better visual clarity and quick scanning. Replace lengthy paragraphs with concise, emoji-enhanced tabular format.
- Present in diagrams π: Present complex flows and business in Mermaid diagrams when appropriate.
- Continuation Style β‘: Continue without stopping to reiterate or provide feedback, and don't report until all planned work is finished.
- Web Search & Fetch π: Use ZAI MCP tools when needing to search or fetch web content.
- Architecture Style ποΈ: Hexagonal / Domain-Driven Design (DDD). Keep it modular and layered but compact. Avoid over-abstraction.
- Avoid Over-Abstraction π―: Don't abstract prematurely; keep solutions simple and focused.
- Interfaces Near Consumer π: Ports (interfaces) must be defined near the code that uses them.
cmd/ngbot: entrypoint and lifecycle wiring.internal/bot: core service, update processor, Telegram helpers.internal/handlers: admin, gatekeeper, reactor, moderation workflows.internal/db/sqlite: persistence with embedded migrations (including gatekeeper challenges).internal/adapters/llm: OpenAI/Gemini clients.resources: embedded i18n, gatekeeper challenges, migrations.- Observability stack is removed; logs only.
- Domain:
internal/dbentities and pure policies where applicable. - Application:
internal/botand workflow handlers. - Adapters: Telegram API, SQLite, LLM providers, banlist HTTP.
- Telegram Bot API.
- LLM APIs (OpenAI-compatible and Gemini).
- Banlist API (lols.bot).
- Join-Captcha WebApp π: The gatekeeper join-captcha WebApp server speaks plain HTTP and MUST run behind a TLS-terminating reverse proxy. Its listen address is configured via
GatekeeperWebApp.ListenAddr. In the Docker deployment it binds0.0.0.0:8080inside the container (mapped to127.0.0.1:18080on the host); the default must NOT be changed to loopback or the container port mapping breaks. - No-Rights Mode π‘οΈ: Before banlist, LLM, reaction, or voting moderation, check the bot's restrict-member capability. A confirmed Telegram privilege error is terminal and must not be retried. Public CAPTCHA may still run without restricting the user: success deletes the CAPTCHA; failure leaves a durable 30-minute notice.
- Manual Allowlist Priority β
: A matching per-chat
chat_not_spammer_overridesrow is the only exemption checked before cached or provider-backed banlist enforcement. For every non-allowlisted user, effective banlist membership remains an unconditional deny decision before remembered membership, admin status, commands, voting, or LLM. The always-on cached guard precedes the configuredadmin β gatekeeper β reactorchain, and join updates are checked even when Gatekeeper is disabled. Enforcement directly bans and deletes the available join/current-message artifact without creating a spam case. The guard may inspect the in-memory effective set to short-circuit routing, but it must verify moderation capability before Telegram I/O and must not perform online provider checks in no-rights mode. Allowlist lookup failures fail closed and continue normal moderation. - Durable Author Trust βοΈ: All chats use a typed
MessageAuthor(userorsender_chat);SenderChattakes precedence over technicalFrom. Three distinct safe new semantic messages grant 30 days of per-chat trust by default; one safe new message renews expired trust. SQLite atomically binds checked messages, increments the counter and grants trust. Membership bookkeeping is separate and best effort after persistence; reactions andchat_known_non_membersnever grant trust. Commands, bot mentions, empty media, reactions and edits never advance or renew admission. Before admission and after expiry all semantic edits are checked; during trust previously bound edits remain protected. Checked-message bindings are durable identity metadata: never expire them with context or trust, because renewal would otherwise make old checked edits unprotected; chat deletion may cascade them. Pending/resolving cases suspend trust; confirmed spam resets it, false positives retain any previous unexpired grant. Channels follow community voting without a mute or technical-user fallback. Keep user allowlist-before-banlist priority; user banlists never consume channel IDs. Preserve linked-channel and anonymous group-admin exemptions. - Bounded Conversation Context π¬: Persist message context separately from author trust in SQLite. Retain original send time, thread/reply links, edited text and Telegram update IDs for same-second edit ordering; snapshots cannot supersede newer authoritative edits. Use only same-chat/thread history younger than 24 hours, at most five earlier replies, 2,000 characters per saved text and 8,000 extra context characters. Prioritize direct reply/quote, source post, then nearby replies. Unknown discussion threads use reply chains, ordinary groups may use recent group history. Successful bot deletions leave short-lived tombstones. Successful user bans that revoke messages clear the matching user context; channel bans do not imply removal of previous channel messages. CAPTCHA context cleanup uses a distinct durable phase so a SQLite retry cannot repeat a completed ban. Ordinary user deletions are not reliably observable through Bot API. Pass history as untrusted structured evidence and classify only the candidate. Never log message texts. No external-reply automatic verdict.
- Author Trust Configuration βοΈ: Global
NG_SPAM_SAFE_MESSAGES_REQUIREDandNG_SPAM_AUTHOR_TRUST_DURATIONdefault to3and720h. Keep the existing per-chat LLM switch.NG_SPAM_MESSAGE_PROBATION_DURATIONis accepted as deprecated with a warning and has no effect. Migrate prior effective user trust for 30 days from migration, including suspended grants; active probations restart at zero and reaction-only records are excluded. Preserve cases, votes and durable actions.
- Cascading Menus π§: Admin settings must be structured as cascading category menus. Do not place many unrelated controls on a single page.
- Leaf Screens πΏ: Concrete value changes (presets/inputs) must live on leaf screens dedicated to one setting or one logical group.
- i18n Completeness π: Any new admin UI key must be added for all supported locales before merge.
- Self-documenting code π: No commentsβclear names and structure speak for themselves.
- No TODOs π«: Write complete code or nothing. No placeholders.
- Professional standards π¨βπ»: Write like a professional Go developer would, without unnecessary code bloat.
- Architecture first ποΈ: Audit before coding: scan repo, read related packages, plan all changes.
- Go Version π’: 1.26.8, as pinned in
go.modandDockerfile. Ref: Go Release Notes - Documentation Strategy π: Use
go doc,go tool,go listfor Go packages. - English Only πΊπΈ: Code and technical reasoning in English.
- Tool Directive π§: Use Go 1.24+
tooldirective ingo.modfor dev tools (golangci-lint, goimports, etc.). - No tools.go Hack π«: Avoid the
tools.goblank import pattern. - Refactor tooling π οΈ:
go tool gorenameβ safe, reference-aware renames.go tool godoctorβ extract/inline functions and move code.go tool gopatchβ template-driven patches.go tool goimportsβ auto-manage imports.
- Case Convention π€: Use MixedCaps/mixedCaps (no underscores).
- Acronyms π€: All uppercase (HTTP, URL, ID, API).
- Getters π£: No "Get" prefix (
user.Name()notuser.GetName()). - Interfaces π: Ends in "-er" (Reader) or "-able" (Readable).
- Organization π: Group related constants/variables/types together.
- Packages π: One package per directory with short, meaningful names.
- Formatting β¨: Use
gofumpt -w .(tabs, newline at EOF).
- Check Immediately
β οΈ : Check errors immediately, no panic for normal errors. - Wrapping π: Use
fmt.Errorf("op: %w", err). - Inspection π: Use
errors.Is/errors.As. - Interface Types π: Use
anyinstead ofinterface{}.
- Testing π§ͺ: Table-driven tests beside code (
*_test.go). Mock interfaces. - Context β±οΈ: Use
context.Contextfor cancellation/timeouts (first param). - Global Variables π«: Avoid them.
- Composition π: Prefer composition over inheritance.
- Embedding π: Use judiciously.
- Preallocation π§ : Preallocate slices when length is known.
- Philosophy π§ : Share memory by communicating.
- Coordination π‘: Channels for coordination, mutexes for state.
- Error Groups π₯: Use
errgroupfor concurrent tasks. - Leaks π°: Prevent goroutine leaks.
Reality check β : The persistence stack is SQLite, not PostgreSQL. There is no
pgx, nosqlc, nointernal/db/sql/, and nointernal/db/sqlc/in this repo. The notes below describe what the code actually uses.
- Location π:
resources/migrations/as numbered/timestamped plain-SQL files (e.g.,0-init.sql,20260613000000-add-gatekeeper-webapp-challenges.sql). - Embedding π: Embedded via
//go:embed *inresources/embed.go(resources.FS). - Runner π οΈ:
rubenv/sql-migrate(EmbedFileSystemMigrationSource, dialect"sqlite3"), applied at startup ininternal/db/sqlite/client.go. - Direction
βοΈ : Every migration uses-- +migrate Upand-- +migrate Down; never rewrite an applied migration, add a new file instead.
- Driver π:
modernc.org/sqlite(pure-Go, CGO-free β enables the distroless static build). - Access π§©: Hand-written SQL through
jmoiron/sqlx(db:struct tags,StructScan). No code generation. - Ports π: Consumer-owned interfaces live beside
bot.serviceand each handler. The concrete SQLite adapter satisfies them structurally; there is no repository-wide database interface. - Concurrency π: SQLite runs in WAL mode with
SetMaxOpenConns(42), a 1,000-page auto-checkpoint, and a per-connection 64 MiB journal size limit. One app-levelsync.RWMutexserializes ordinary writes. Banlist imports write an inactive generation in short batches, release the lock between batches, atomically activate the generation, then garbage-collect retired entries in bounded transactions. Race-sensitive state changes use transactions or atomic compare-and-set (UPDATE β¦ WHERE status=β¦+RowsAffected()==1). - Maintenance π§Ή:
ngbot --database-maintenanceis an offline-only mode. With the service stopped, it applies migrations, enables incremental auto-vacuum through a fullVACUUM, optimizes and validates the database, restores WAL mode, and exits before Telegram initialization.
- Architecture ποΈ: Flat β one concrete
sqliteClientadapter implements consumer-owned ports directly. No interface β Postgres β buffered/cached β factory chain. - Caching β‘: Lives in
bot.service(memberCache5-min TTL,settingsCacheprocess-lifetime) above persistence β not in a storage decorator. - Value semantics π§: Settings enter and leave the cache as clones; a new snapshot is published only after a successful DB write, and warmup never overwrites a newer cached value.
Single-Action Complete Revisions: Consolidate ALL necessary changes into bulk comprehensive updates. Deliver complete, functional code in a single edit action.
- Edge Case π: When restructuring requires updating imports across many files (30+), use
sedorfind ... -exec sed. - Example:
find . -name "*.go" -exec sed -i '' 's|old/path|new/path|g' {} \; - Verification: Always run
go vet ./...after mass replacements.
- β Audit First: Read and understand the complete file context.
- β Plan Comprehensively: Identify all changes needed across the file.
- β Verify Completeness: Ensure the edit delivers fully functional code.
- β No Placeholders: No "TODO" or incomplete states.
- Full Lint π:
go tool golangci-lint run --no-config --enable=unused --enable=unparam --enable=ineffassign --enable=goconst ./.... The repository has no tracked linter configuration;--no-configprevents parent or home configuration from silently weakening local checks compared with CI. - Quick Check β‘:
go vet ./...(Do not usego buildfor validation). - Compliance β : Never ignore lint warnings and fix them right away.
- Unused Code ποΈ: Remove unused files, functions, parameters.
- No Dead Code π: Don't leave dead code unless protected by a describing comment.
- Constants π¦: Extract repeated string literals into constants.
- Signature Refactoring βοΈ: Simplify function signatures by removing unused params/returns.
- Tool Usage π οΈ: Use provided tools extensively instead of guessing.
- Code Inspection π: Inspect code when unsure: list project structure, read whole files.
- Dependency Check π¦: Verify library existence in
go.modbefore importing.
- Interactive Feedback π¬: Always call
interactive_feedbackMCP when asking questions. - Continuous Feedback π: Continue calling until user feedback is empty.
- Reporting π: Request feedback or ask when finished or unsure.
- Audit First π: Read and understand the complete project structure (
tree -a -I .git). - Plan π: Identify all changes needed across files.
- Verify β
: Use
go vet ./...to check code;go test ./...if applicable. - Update Docs π: Update
AGENTS.mdif architecture changes or new rules introduced.