- Build:
go build -o shield ./app - Run tests:
go test -race ./... - Run single test:
go test -v -race ./path/to/package -run TestName - Lint:
golangci-lint run - Coverage report:
go test -race -coverprofile=coverage.out ./... && go tool cover -func=coverage.out - Normalize code comments:
make unfuck-ai-comments
- Always run tests, linter and normalize comments before committing
- For linter use
golangci-lint run - Run tests and linter after making significant changes to verify functionality
- Go version: 1.24+
- Don't add "Generated with Claude Code" or "Co-Authored-By: Claude" to commit messages or PRs
- Do not include "Test plan" sections in PR descriptions
- Do not add comments that describe changes, progress, or historical modifications. Avoid comments like "new function," "added test," "now we changed this," or "previously used X, now using Y." Comments should only describe the current state and purpose of the code, not its history or evolution.
- Use
go:generatefor generating mocks, never modify generated files manually. Mocks are generated withmoqand stored in themockspackage. - After important functionality added, update README.md accordingly
- When adding new CLI parameters or environment variables, update BOTH:
- The "All Application Options" section in README.md (should match
--helpoutput exactly) - The appropriate descriptive section in README.md (e.g., spam detection modules, OpenAI integration, etc.)
- The "All Application Options" section in README.md (should match
- When merging master changes to an active branch, make sure both branches are pulled and up to date first
- Don't add "Test plan" section to PRs
- Logging:
github.com/go-pkgz/lgr - CLI flags:
github.com/jessevdk/go-flags - HTTP/REST:
github.com/go-pkgz/restwithgithub.com/go-pkgz/routegroup - Middleware:
github.com/didip/tollbooth/v8 - Database:
github.com/jmoiron/sqlxwithmodernc.org/sqlite - Testing:
github.com/stretchr/testify - Mock generation:
github.com/matryer/moq - OpenAI:
github.com/sashabaranov/go-openai - Frontend: HTMX v2. Try to avoid using JS.
- For containerized tests use
github.com/go-pkgz/testutils - To access libraries, figure how to use ang check their documentation, use
go doccommand andghtool
- Create server with routegroup:
router := routegroup.New(http.NewServeMux()) - Apply middleware:
router.Use(rest.Recoverer(), rest.Throttle(), rest.BasicAuth()) - Define routes with groups:
router.Mount("/api").Route(func(r *routegroup.Bundle) {...}) - Start server:
srv := &http.Server{Addr: addr, Handler: router}; srv.ListenAndServe()
- Format: Use
gofmt(enforced by linter) - exclude mocks:gofmt -s -w $(find . -type f -name "*.go" -not -path "./vendor/*" -not -path "*/mocks/*") - goimports:
goimports -w $(find . -type f -name "*.go" -not -path "./vendor/*" -not -path "*/mocks/*") - Line length: Maximum 140 characters
- Error handling: Return errors with context, use multierror for aggregation
- Naming: CamelCase for variables, PascalCase for exported types/functions
- Test tables: Use table-driven tests with descriptive test cases
- Comments: Keep in-code comments lowercase
- Documentation: All exported functions and types must be documented with standard Go comments
- Interfaces: Define interfaces in consumer packages
- Mocks: Generate with github.com/matryer/moq and store in mocks package
- Automatic spam detection checks only the current message text from the sender.
- Do not include
ReplyTo.Textor TelegramQuotetext inapp/bot/spam.go:OnMessage; it biases LLM checks by making replied-to content look authored by the current user. - Admin
/spam(app/events/admin.go:directReport) and user/report(app/events/reports.go:DirectUserReport) still appendorigMsg.Quote.Textto the reported message text for explicit moderation/training workflows. - The reporting-path concatenation uses a newline separator:
msgTxt + "\n" + origMsg.Quote.Text. - Empty quote text is ignored (no extra newline added).
- Quote concatenation in reporting paths is placed AFTER the transform fallback block so image-only messages with quotes get both caption text and quote text.
- When a channel posts in a group, Telegram uses a shared fake user
Channel_Bot(ID136817688) inmsg.From - The actual channel identity is in
msg.SenderChatwith unique ID and username SenderChat.IDis used for locator tracking (AddMessage,AddSpam), and for banning viaBanChatSenderChatConfigbot.OnMessagesetsResponse.ChannelID = msg.SenderChat.IDwhen SenderChat is present- Admin
/spamcommand (directReport) detectsorigMsg.SenderChatand passes channel ID for ban and cleanup - Anonymous admin posts (where
SenderChat.ID == group chat ID) skip spam check entirely - Linked channel (resolved via
ChatFullInfo.LinkedChatIDat startup) is treated as superuser for/ban,/spam,/warncommands - Linked channel messages also skip spam checking (same as anonymous admin posts)
isLinkedChannel(msg)helper checksl.linkedChannelID != 0 && msg.SenderChat != nil && msg.SenderChat.ID == l.linkedChannelIDchannelDisplayNameresolves display name from*tbapi.Chat: UserName > Title >channel_<ID>ReportBanuseshttps://t.me/<username>links for channels (nottg://userwhich doesn't resolve negative IDs); plain name+ID for channels without username- Admin
/warncommand (DirectWarnReport) targets the channel display name instead of@Channel_Bot - Admin notification text in
directReportshows channel display name and channel ID instead of Channel_Bot identity extractUsernamesupportstg://userlinks,t.mechannel links, plain channel name+ID, and{id name...}formats
spamcheck.ResponseincludesExtraDeleteIDs []intfield for additional message IDs to delete when spam is detected- Any spam checker can populate this field to request deletion of related messages
- Currently used by duplicate detector to delete all previous duplicates when threshold is reached
- The listener handles these deletions with rate limiting (35ms between deletions) to respect Telegram API limits
- Deletion errors are logged but don't fail the operation (messages might be already deleted or too old)
- Design principle: When a spammer is detected, aggressively clean up ALL their spam messages, not just the triggering one
- Shared provider-agnostic LLM flow lives in
lib/tgspam/llm.go - Keep provider-specific transport and request construction in
lib/tgspam/openai.go,lib/tgspam/gemini.go, etc - Common behavior such as history formatting, retry handling, and response detail formatting should be implemented once in
llm.goto avoidduplviolations - Shared behavior belongs in
lib/tgspam/llm_test.go; provider tests should focus on API-specific behavior such as request shape, model options, truncation, and safety settings context.Contextis threaded fromDetector.collectLLMCheck→check()→runLLMProviderCheck→sendRequest()→ API call; the detector creates a per-request context withLLMRequestTimeout(default 30s)