Module Pattern
This guide explains how to add new feature modules to Alita Robot, following the established patterns and conventions.
Module Structure Template
Section titled “Module Structure Template”Every module follows this structure:
package modules
import ( "fmt" "strings"
"github.com/PaulSonOfLars/gotgbot/v2" "github.com/PaulSonOfLars/gotgbot/v2/ext" "github.com/PaulSonOfLars/gotgbot/v2/ext/handlers" "github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/filters/callbackquery" log "github.com/sirupsen/logrus"
"github.com/divkix/Alita_Robot/alita/db" "github.com/divkix/Alita_Robot/alita/i18n" "github.com/divkix/Alita_Robot/alita/utils/chat_status" "github.com/divkix/Alita_Robot/alita/utils/extraction" "github.com/divkix/Alita_Robot/alita/utils/helpers")
// Module struct with name for help systemvar exampleModule = moduleStruct{moduleName: "Example"}
// Command handler methodfunc (m moduleStruct) exampleCommand(b *gotgbot.Bot, ctx *ext.Context) error { chat := ctx.EffectiveChat user := ctx.EffectiveSender.User msg := ctx.EffectiveMessage tr := i18n.MustNewTranslator(db.GetLanguage(ctx))
// Permission checks if !chat_status.RequireGroup(b, ctx, nil) { return ext.EndGroups } if !chat_status.RequireUserAdmin(b, ctx, nil, user.Id) { return ext.EndGroups }
// Business logic here text, _ := tr.GetString("example_success_message") _, err := msg.Reply(b, text, helpers.Shtml()) if err != nil { log.Error(err) // Return ext.EndGroups after user notification, not the error return ext.EndGroups }
return ext.EndGroups}
// Callback handler for inline buttonsfunc (m moduleStruct) exampleCallback(b *gotgbot.Bot, ctx *ext.Context) error { query, ok := callbackQueryFromContext(ctx) if !ok { return ext.EndGroups } tr := i18n.MustNewTranslator(db.GetLanguage(ctx))
// Parse callback data using the modules package wrapper // This handles both new codec format and legacy dot-notation fallback decoded, ok := decodeCallbackData(query.Data, "example") if !ok { log.Warn("[ExampleCallback] Invalid callback data format") _, _ = query.Answer(b, nil) return ext.EndGroups } action, _ := decoded.Field("action")
// Handle action var responseText string switch action { case "confirm": responseText, _ = tr.GetString("example_confirmed") case "cancel": responseText, _ = tr.GetString("example_cancelled") }
// Answer callback _, err := query.Answer(b, &gotgbot.AnswerCallbackQueryOpts{ Text: responseText, }) if err != nil { log.Error(err) return err }
return ext.EndGroups}
// LoadExample registers all handlers for this modulefunc LoadExample(dispatcher *ext.Dispatcher) { // Register in help system DefaultHelpRegistry().AbleMap.Store(exampleModule.moduleName, true)
// Register command handlers dispatcher.AddHandler(handlers.NewCommand("example", exampleModule.exampleCommand))
// Register callback handlers dispatcher.AddHandler(handlers.NewCallback( callbackquery.Prefix("example"), exampleModule.exampleCallback, ))}
// Register the module in init() so LoadAllModules picks it upfunc init() { RegisterLegacyModule("Example", 100, LoadExample)}Step-by-Step Guide
Section titled “Step-by-Step Guide”Step 1: Create Database Model (If Needed)
Section titled “Step 1: Create Database Model (If Needed)”Create a new file alita/db/example/repository.go (following the domain-package pattern):
package db
import ( "gorm.io/gorm")
// ExampleSettings stores per-chat example settingstype ExampleSettings struct { ID uint `gorm:"primaryKey;autoIncrement"` ChatID int64 `gorm:"uniqueIndex;not null"` Enabled bool `gorm:"default:false"` Value string `gorm:"type:text"` CreatedAt int64 `gorm:"autoCreateTime"` UpdatedAt int64 `gorm:"autoUpdateTime"`}
// GetExampleSettings retrieves settings for a chatfunc GetExampleSettings(chatID int64) *ExampleSettings { var settings ExampleSettings tx := db.Session(&gorm.Session{}).Where("chat_id = ?", chatID).First(&settings) if tx.Error != nil { return &ExampleSettings{ChatID: chatID, Enabled: false} } return &settings}
// SetExampleSettings saves settings for a chatfunc SetExampleSettings(chatID int64, enabled bool, value string) error { settings := ExampleSettings{ ChatID: chatID, Enabled: enabled, Value: value, }
tx := db.Session(&gorm.Session{}).Where("chat_id = ?", chatID). Assign(settings).FirstOrCreate(&settings)
if tx.Error != nil { return tx.Error }
// Invalidate cache deleteCache(exampleSettingsCacheKey(chatID)) return nil}Step 2: Create Migration File
Section titled “Step 2: Create Migration File”Create migrations/XXX_add_example_settings.sql:
-- Create example_settings tableCREATE TABLE IF NOT EXISTS example_settings ( id SERIAL PRIMARY KEY, chat_id BIGINT NOT NULL UNIQUE, enabled BOOLEAN DEFAULT FALSE, value TEXT, created_at BIGINT, updated_at BIGINT);
-- Create index for faster lookupsCREATE INDEX IF NOT EXISTS idx_example_settings_chat_id ON example_settings(chat_id);Step 3: Implement Database Operations
Section titled “Step 3: Implement Database Operations”Add cache helpers to alita/db/cache/ttl.go and alita/db/cache/keys.go using the CacheKey helper:
const ( CacheTTLExampleSettings = 30 * time.Minute)
// Use the CacheKey helper for consistent key formattingfunc exampleSettingsCacheKey(chatID int64) string { return CacheKey("example_settings", chatID)}Update the database operations to use caching with singleflight protection:
func GetExampleSettings(chatID int64) *ExampleSettings { result, err := getFromCacheOrLoad( exampleSettingsCacheKey(chatID), CacheTTLExampleSettings, func() (*ExampleSettings, error) { var settings ExampleSettings tx := db.Session(&gorm.Session{}).Where("chat_id = ?", chatID).First(&settings) if tx.Error != nil { return &ExampleSettings{ChatID: chatID, Enabled: false}, nil } return &settings, nil }, ) if err != nil { return &ExampleSettings{ChatID: chatID, Enabled: false} } return result}Step 4: Add Translations
Section titled “Step 4: Add Translations”Add to locales/en.yml:
# Example moduleexample_help: | <b>Example Module</b>
Commands: - /example: Run the example command - /exampleset <value>: Set the example value
example_success_message: "Example command executed successfully!"example_value_set: "Example value set to: %s"example_not_enabled: "Example feature is not enabled in this chat."example_confirmed: "Action confirmed!"example_cancelled: "Action cancelled."Add to other locale files (es.yml, fr.yml, hi.yml, id.yml, pt.yml, ru.yml) with appropriate translations.
Step 5: Register Module
Section titled “Step 5: Register Module”Modules self-register via init() using the registry system. The LoadAllModules function in alita/main.go loads all registered modules in priority order:
func LoadModules(dispatcher *ext.Dispatcher) { modules.DefaultHelpRegistry().AbleMap.Init() defer modules.LoadHelp(dispatcher)
// Loads all modules registered via RegisterLegacyModule / RegisterModule modules.LoadAllModules(dispatcher)}There are two registration patterns:
- Legacy (most modules):
RegisterLegacyModule(name, priority, loadFunc)ininit()— wraps existingLoadXxx(dispatcher)functions. - New interface:
RegisterModule(m Module)whereModuleimplementsName(),Priority(),Load(dispatcher).
Permission Check Functions
Section titled “Permission Check Functions”Use these functions to validate permissions before executing commands:
| Function | Description | Returns |
|---|---|---|
RequireGroup(b, ctx, chat) | Ensures command is in group | bool |
RequirePrivate(b, ctx, chat) | Ensures command is in PM | bool |
RequireUserAdmin(b, ctx, chat, userId) | User must be admin | bool |
RequireBotAdmin(b, ctx, chat) | Bot must be admin | bool |
RequireUserOwner(b, ctx, chat, userId) | User must be creator | bool |
CanUserRestrict(b, ctx, chat, userId) | User can ban/mute | bool |
CanBotRestrict(b, ctx, chat) | Bot can ban/mute | bool |
CanUserDelete(b, ctx, chat, userId) | User can delete messages | bool |
CanBotDelete(b, ctx, chat) | Bot can delete messages | bool |
CanUserPin(b, ctx, chat, userId) | User can pin messages | bool |
CanBotPin(b, ctx, chat) | Bot can pin messages | bool |
CanUserPromote(b, ctx, chat, userId) | User can promote/demote | bool |
CanBotPromote(b, ctx, chat) | Bot can promote/demote | bool |
CanUserChangeInfo(b, ctx, chat, userId) | User can change chat info | bool |
CanInvite(b, ctx, chat, msg) | Can generate invite links | bool |
Permission Error Handling
Section titled “Permission Error Handling”Permission checking functions are pure and return only boolean values without sending any error messages to users. When error messaging is desired on failure, the caller must explicitly call PermissionResponder to send the response.
Example usage:
if !chat_status.RequireUserAdmin(b, ctx, nil, user.Id) { chat_status.NewPermissionResponder(b).Respond(ctx, "chat_status_user_admin_cmd_error", "chat_status_user_admin_button_error", chat_status.WithReplyFallback()) return ext.EndGroups}Common Permission Patterns
Section titled “Common Permission Patterns”// Admin-only commandif !chat_status.RequireGroup(b, ctx, nil) { return ext.EndGroups}if !chat_status.RequireUserAdmin(b, ctx, nil, user.Id) { return ext.EndGroups}
// Command requiring bot to have restrict permissionsif !chat_status.RequireBotAdmin(b, ctx, nil) { return ext.EndGroups}if !chat_status.CanBotRestrict(b, ctx, nil) { return ext.EndGroups}
// Owner-only commandif !chat_status.RequireUserOwner(b, ctx, nil, user.Id) { return ext.EndGroups}Handler Return Values
Section titled “Handler Return Values”// Stop processing - no more handlers will runreturn ext.EndGroups
// Continue to next handler in same groupreturn ext.ContinueGroups
// Error - propagates to dispatcher error handlerreturn err
// Success with no errorreturn nilWhen to Use Each
Section titled “When to Use Each”| Return | Use When |
|---|---|
ext.EndGroups | Command handled successfully, stop processing |
ext.ContinueGroups | Allow other handlers to also process this update |
err | Something went wrong, let error handler deal with it |
nil | Same as ext.EndGroups for most purposes |
Translation Best Practices
Section titled “Translation Best Practices”Parameter Passing
Section titled “Parameter Passing”Use positional formatters in YAML with named parameters in code:
example_user_action: "User %s performed action: %s"// In handlertext, _ := tr.GetString("example_user_action")formattedText := fmt.Sprintf(text, userName, actionName)Escape Sequences
Section titled “Escape Sequences”Always use double quotes for strings with escape sequences:
# Correct - double quotes interpret \nexample_multiline: "Line 1\nLine 2\nLine 3"
# Wrong - single quotes preserve \n literallyexample_multiline: 'Line 1\nLine 2\nLine 3'Key Naming Convention
Section titled “Key Naming Convention”Follow the pattern: module_feature_description
bans_ban_normal_ban: "Banned %s!"bans_ban_ban_reason: "\nReason: %s"bans_kick_kicked_user: "Kicked %s!"bans_unban_unbanned_user: "Unbanned %s!"Complete Example: Welcome Toggle Module
Section titled “Complete Example: Welcome Toggle Module”Here’s a complete example showing all patterns together:
package modules
import ( "fmt"
"github.com/PaulSonOfLars/gotgbot/v2" "github.com/PaulSonOfLars/gotgbot/v2/ext" "github.com/PaulSonOfLars/gotgbot/v2/ext/handlers" log "github.com/sirupsen/logrus"
"github.com/divkix/Alita_Robot/alita/db" "github.com/divkix/Alita_Robot/alita/i18n" "github.com/divkix/Alita_Robot/alita/utils/chat_status" "github.com/divkix/Alita_Robot/alita/utils/helpers")
var welcomeModule = moduleStruct{moduleName: "Welcome"}
// welcomestatus shows whether welcome messages are enabledfunc (m moduleStruct) welcomestatus(b *gotgbot.Bot, ctx *ext.Context) error { chat := ctx.EffectiveChat msg := ctx.EffectiveMessage tr := i18n.MustNewTranslator(db.GetLanguage(ctx))
if !chat_status.RequireGroup(b, ctx, nil) { return ext.EndGroups }
settings := db.GetGreetingSettings(chat.Id) enabled := false if settings.WelcomeSettings != nil { enabled = settings.WelcomeSettings.ShouldWelcome }
var text string if enabled { text, _ = tr.GetString("welcome_status_enabled") } else { text, _ = tr.GetString("welcome_status_disabled") } _, err := msg.Reply(b, text, helpers.Shtml()) if err != nil { log.Error(err) return err }
return ext.EndGroups}
// togglewelcome enables or disables welcome messages (admin only)func (m moduleStruct) togglewelcome(b *gotgbot.Bot, ctx *ext.Context) error { chat := ctx.EffectiveChat user := ctx.EffectiveSender.User msg := ctx.EffectiveMessage tr := i18n.MustNewTranslator(db.GetLanguage(ctx))
if !chat_status.RequireGroup(b, ctx, nil) { return ext.EndGroups } if !chat_status.RequireUserAdmin(b, ctx, nil, user.Id) { return ext.EndGroups }
settings := db.GetGreetingSettings(chat.Id) current := false if settings.WelcomeSettings != nil { current = settings.WelcomeSettings.ShouldWelcome }
if err := db.SetWelcomeToggle(chat.Id, !current); err != nil { log.Error(err) return err }
var text string if !current { text, _ = tr.GetString("welcome_toggle_enabled") } else { text, _ = tr.GetString("welcome_toggle_disabled") } _, err := msg.Reply(b, text, helpers.Shtml()) if err != nil { log.Error(err) return err }
return ext.EndGroups}
func LoadWelcome(dispatcher *ext.Dispatcher) { DefaultHelpRegistry().AbleMap.Store(welcomeModule.moduleName, true)
dispatcher.AddHandler(handlers.NewCommand("welcomestatus", welcomeModule.welcomestatus)) dispatcher.AddHandler(handlers.NewCommand("togglewelcome", welcomeModule.togglewelcome))}
func init() { RegisterLegacyModule("Welcome", 210, LoadWelcome)}Security Considerations
Section titled “Security Considerations”HTML Escaping
Section titled “HTML Escaping”When displaying user-controlled data in HTML-formatted messages, always escape it:
import "github.com/divkix/Alita_Robot/alita/utils/helpers"
// Escape chat titles, usernames, and user-supplied texttext := fmt.Sprintf("Settings for %s", helpers.HtmlEscape(chat.Title))The helpers.MentionHtml() function already handles escaping for user names.
Database Operations and User Feedback
Section titled “Database Operations and User Feedback”Prefer synchronous operations when sending success confirmations:
// CORRECT: Synchronous operation before success messagedb.SetWelcomeText(chat.Id, db.DefaultWelcome, "", nil, db.TEXT)_, err := msg.Reply(b, "Welcome message reset successfully!", helpers.Shtml())// AVOID: Async operation with premature success messagego func() { db.SetWelcomeText(chat.Id, db.DefaultWelcome, "", nil, db.TEXT) // May fail silently}()_, err := msg.Reply(b, "Success!") // User sees success even if DB write failsWhen async operations are necessary, only use them for non-critical background tasks that don’t require user confirmation.
Handling Functions That Return Errors
Section titled “Handling Functions That Return Errors”Always check errors from database operations that can fail:
// CORRECT: Check error and handle nil casecaptchaSettings, err := db.GetCaptchaSettings(chat.Id)if err != nil { log.Errorf("Failed to get captcha settings: %v", err) captchaSettings = &db.CaptchaSettings{Enabled: false} // Use safe default}if captchaSettings != nil && captchaSettings.Enabled { // Safe to access}// AVOID: Ignoring errors can cause nil pointer panicscaptchaSettings, _ := db.GetCaptchaSettings(chat.Id)if captchaSettings.Enabled { // May panic if captchaSettings is nil! // ...}Async Database Operations (When Appropriate)
Section titled “Async Database Operations (When Appropriate)”When running DB operations in goroutines for non-critical background tasks, follow this pattern:
// 1. Capture loop/closure variableschatId := chat.Id
go func() { // 2. Add panic recovery defer error_handling.RecoverFromPanic("FunctionName", "module")
// 3. Handle errors explicitly if err := db.SomeOperation(chatId); err != nil { log.Errorf("[Module] Operation failed for chat %d: %v", chatId, err) }}()User Extraction
Section titled “User Extraction”Use extraction.ExtractUserAndText() for consistent user identification. It handles:
- Reply messages
- Text mentions
- Username lookups (with Telegram API fallback)
- Numeric user IDs
Checklist for New Modules
Section titled “Checklist for New Modules”- Create module struct with
moduleName - Implement handler methods on module struct
- Add appropriate permission checks
- Use
i18n.MustNewTranslator(db.GetLanguage(ctx))for translations - Handle errors properly (log and return)
- Escape user-controlled input with
helpers.HtmlEscape() - Add panic recovery to goroutines
- Create database models if needed
- Create migration file if needed
- Add cache helpers if needed
- Add translations to all locale files
- Register module in
init()withRegisterLegacyModuleorRegisterModule - Store module in help system with
DefaultHelpRegistry().AbleMap.Store - Test in development environment
Next Steps
Section titled “Next Steps”- Request Flow - Understanding the update pipeline
- Caching - Redis cache integration
- Project Structure - Where files belong