11 KiB
11 KiB
SaveAny-Bot Agent Guidelines
This document provides essential information for AI coding agents working on the SaveAny-Bot project.
Project Overview
SaveAny-Bot is a Telegram bot written in Go that saves files/messages from Telegram and various websites to multiple storage backends (local, S3, MinIO, WebDAV, AList, Telegram). It features a plugin system for parsing web content and extensible storage backends.
Tech Stack: Go 1.24.2, gotd/td (Telegram MTProto), Cobra (CLI), Viper (config), GORM (ORM), SQLite, Goja (JS runtime), Playwright (browser automation)
Build & Test Commands
Build
# Standard build
go build -o saveany-bot .
# Run directly
go run ./cmd
# Docker build (multi-stage, Alpine-based)
docker build -t saveany-bot .
docker compose up -d
Test
# Run all tests
go test ./...
# Run tests in specific package
go test ./pkg/queue
go test ./storage/telegram
# Run tests with verbose output
go test -v ./...
# Run a single test
go test -run TestQueueBasic ./pkg/queue
# Run with coverage
go test -cover ./...
Lint & Format
# Format code (standard Go formatting)
go fmt ./...
# Vet code for common issues
go vet ./...
# Generate code (i18n keys)
go generate ./...
Other Commands
# Update dependencies
go mod tidy
# View documentation
cd docs && hugo server -D
Code Style Guidelines
Imports
- Standard library first, then third-party, then project-internal
- Group imports with blank lines between groups
- Use explicit import aliases for clarity when needed (e.g.,
storconfig,storenum)
import (
"context"
"fmt"
"github.com/charmbracelet/log"
"github.com/krau/SaveAny-Bot/config"
"github.com/krau/SaveAny-Bot/pkg/enums/storage"
)
Formatting
- Line length: reasonable (no hard limit, but be sensible)
- Organize code with blank lines between logical sections
- Follow standard Go conventions for braces, spacing, etc.
Types & Interfaces
- Use clear, descriptive type names (PascalCase for exported, camelCase for unexported)
- Define interfaces where abstraction is needed (e.g.,
Executable,StorageConfig) - Embed context in method signatures, not structs:
func (s *Service) Do(ctx context.Context) error - Prefer composition over inheritance
// Interfaces define behavior
type Executable interface {
Type() tasktype.TaskType
Title() string
TaskID() string
Execute(ctx context.Context) error
}
// Structs compose behavior
type Local struct {
config config.LocalStorageConfig
logger *log.Logger
}
Naming Conventions
- Packages: lowercase, single word when possible (avoid underscores)
- Files: lowercase with underscores for multiword (e.g.,
auth_terminal.go,progress_reader.go) - Variables: camelCase for unexported, PascalCase for exported
- Constants: PascalCase for exported, camelCase for unexported (not ALL_CAPS)
- Functions/Methods: PascalCase for exported, camelCase for unexported
- Test files:
*_test.gopattern
Error Handling
- Always handle errors explicitly; never ignore them
- Wrap errors with context using
fmt.Errorf("context: %w", err) - Use
errors.Is()anderrors.As()for error checking - Log errors with appropriate level (Error, Warn, Info)
- Return errors from functions rather than panicking (except for truly unrecoverable situations)
// Good error handling
if err := db.Save(user).Error; err != nil {
return fmt.Errorf("failed to save user %d: %w", user.ChatID, err)
}
// Check specific errors
if errors.Is(err, context.Canceled) {
logger.Info("Operation was canceled")
return nil
}
Logging
- Use
github.com/charmbracelet/logpackage - Get logger from context:
log.FromContext(ctx) - Create prefixed loggers for components:
logger.WithPrefix("component") - Use appropriate levels: Debug, Info, Warn, Error
- Include context in log messages (e.g., task IDs, file names)
logger := log.FromContext(ctx)
logger.Infof("Processing task: %s", task.ID)
logger.Errorf("Failed to save file %s: %v", filename, err)
Concurrency
- Use channels for communication between goroutines
- Protect shared state with
sync.Mutexorsync.RWMutex - Use
sync.WaitGroupfor coordinating goroutine completion - Always pass
context.Contextfor cancellation support - Use
context.WithCancel/WithTimeoutfor managing goroutine lifetimes
// Example from queue implementation
func (tq *TaskQueue[T]) Add(task *Task[T]) error {
tq.mu.Lock()
defer tq.mu.Unlock()
// ... critical section
tq.cond.Signal()
return nil
}
Comments
- Document exported types, functions, and packages with doc comments
- Start doc comments with the name being documented
- Use
//for single-line comments - Explain why, not what (code should be self-explanatory for "what")
- Add
[NOTE],[WARN],[IMPORTANT]tags for important clarifications
// GetUserByChatID retrieves a user by their Telegram chat ID.
// Returns an error if the user is not found.
func GetUserByChatID(ctx context.Context, chatID int64) (*User, error) {
Architecture & Conventions
Application Structure
- Entry point:
main.go→cmd.Execute(ctx) - CLI root:
cmd/root.go(Cobra), implementation incmd/run.go - Startup sequence: Config → Cache → i18n → Database → Storage → Parsers → Userbot → Bot → Queue
- Follow this order when adding new initialization steps in
cmd/run.go::initAll
Configuration (Viper)
- Config defined in
config/viper.go::Config - Read from
config.toml(seeconfig.example.toml) - Environment variables:
SAVEANY_*prefix (e.g.,SAVEANY_TELEGRAM_TOKEN) - Access via
config.C()(returns a copy, don't modify the return value) - Storage configs validated via
config/storage/factory.go::LoadStorageConfigs
Telegram Client
- Bot client:
client/bot/bot.go::Init(uses gotgproto) - Handlers: Centralized in
client/bot/handlers/directory - Registration: All handlers registered in
handlers.Register - Commands: Add to
CommandHandlersslice for automatic/helpand bot command list updates - Middleware: Common middleware in
client/middleware/(floodwait, retry, etc.)
Tasks & Queue
- Task interface:
core/core.go::Executable(Type, Title, TaskID, Execute methods) - Queue:
pkg/queue.TaskQueue[Executable](generic, thread-safe) - Workers: Count from
config.C().Workers - Task types: Implementations in
core/tasks/**(tfile, parsed, telegraph, directlinks, batchtfile) - Lifecycle hooks:
TaskBeforeStart,TaskSuccess,TaskFail,TaskCancel(defined in config) - Adding tasks: Use
core.AddTask(ctx, task)
Database (GORM + SQLite)
- Init:
database.Initusingconfig.C().DB.Path - Models: User, Dir, Rule, WatchChat (in
database/*.go) - Migrations: Automatic via
db.AutoMigrate - User sync:
database.syncUserssyncs DB withconfig.C().Users(don't manually create/delete users) - Context: Always use
db.WithContext(ctx)for operations
Storage Backends
- Interface: Defined in
config/storage/types.goandstorage/ - Implementations: local, alist, s3/minio, webdav, telegram (each in subdirectory)
- Adding new storage:
- Add enum to
pkg/enums/storage - Create config struct in
config/storage/withValidate()method - Implement storage in
storage/<name>/ - Register in
storageFactoriesmapping - Update
config.example.tomlwith example
- Add enum to
Parser Plugins (JavaScript)
- Runtime: Goja (JS runtime) + Playwright (browser automation)
- Plugin API:
registerParser({ metadata, canHandle, parse })in JS - Integration: Defined in
parsers/directory - Documentation: See
plugins/README.md - Plugin
parsereturnsItem/Resourcewhich becomes download/transfer task
Internationalization (i18n)
- Usage:
i18n.T(i18nk.SomeKey, map[string]any{"Name": value}) - Locale files:
common/i18n/locale/*.yaml - Key generation: Run
go generate ./...to generatecommon/i18n/i18nk/keys.go - Adding new strings: Add to YAML → run
go generate→ use in code - All user-facing strings should be internationalized
Context Usage
- Always pass
context.Contextas first parameter - Use
log.FromContext(ctx)to get contextual logger - Respect context cancellation in long-running operations
- Store request-scoped data in context (e.g.,
ctxkey.ContentLength)
Special Rules from .github/copilot-instructions.md
- Never modify
config.C()return values - it returns a copy. Modify config inconfig.Initor via Viper. - Handlers must update
CommandHandlersslice - ensures/helpand bot commands stay in sync. - Task execution must preserve hooks - don't remove
TaskBeforeStart,TaskSuccess,TaskFail,TaskCancelhook calls. - User sync is automatic - don't manually create/delete users in DB; use config-based sync.
- Prefer context logger - use
log.FromContext(ctx)over global logger when context is available. - Storage factory pattern - new storage types must register in
storageFactoriesmapping. - Plugin API compatibility - changes to
Item/Resourcestructures require updatingplugins/README.md.
Common Patterns
Adding a New Command
- Create handler function in
client/bot/handlers/<name>.go - Add to
CommandHandlersslice inregister.go - Add i18n key to
common/i18n/locale/*.yaml - Run
go generate ./... - Test with Telegram bot
Adding a New Task Type
- Create struct implementing
core.Executableincore/tasks/<type>/ - Implement
Type(),Title(),TaskID(),Execute(ctx)methods - Add task type enum to
pkg/enums/tasktype - Use
core.AddTask(ctx, task)to enqueue
Adding a New Storage Backend
- Define config struct in
config/storage/<name>.gowithValidate()method - Implement storage interface in
storage/<name>/<name>.go - Add storage type enum to
pkg/enums/storage - Register factory in
config/storage/factory.go::storageFactories - Update
config.example.tomlwith configuration example
File References
When referencing code locations, use path/to/file.go:line format (e.g., core/core.go:23 for the worker function).
Testing Guidelines
- Write tests for new functionality (place in
*_test.gofiles) - Test files should be in same package as code being tested
- Use table-driven tests for multiple test cases
- Mock external dependencies (databases, network calls)
- Aim for meaningful tests, not just coverage numbers
Notes
- Binary size matters: use
CGO_ENABLED=0for static binaries - FFmpeg is included in Docker images for media processing
- Build process supports cross-compilation (amd64/arm64, Linux/macOS/Windows)
- Documentation site uses Hugo; edit files in
docs/directory - Session data stored in SQLite; delete
data/session.dbif changing bot token