Skip to content

Module Pattern

This guide explains how to add new feature modules to Alita Robot, following the established patterns and conventions.

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 system
var exampleModule = moduleStruct{moduleName: "Example"}
// Command handler method
func (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, false) {
return ext.EndGroups
}
if !chat_status.RequireUserAdmin(b, ctx, nil, user.Id, false) {
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 err
}
return ext.EndGroups
}
// Callback handler for inline buttons
func (m moduleStruct) exampleCallback(b *gotgbot.Bot, ctx *ext.Context) error {
query := ctx.CallbackQuery
tr := i18n.MustNewTranslator(db.GetLanguage(ctx))
// Parse callback data
args := strings.Split(query.Data, ".")
action := args[1]
// 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 module
func LoadExample(dispatcher *ext.Dispatcher) {
// Register in help system
HelpModule.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,
))
}

Create a new file alita/db/example_db.go:

package db
import (
"gorm.io/gorm"
)
// ExampleSettings stores per-chat example settings
type 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 chat
func 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 chat
func 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
}

Create migrations/XXX_add_example_settings.sql:

-- Create example_settings table
CREATE 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 lookups
CREATE INDEX IF NOT EXISTS idx_example_settings_chat_id ON example_settings(chat_id);

Add cache helpers to alita/db/cache_helpers.go:

const (
CacheTTLExampleSettings = 30 * time.Minute
)
func exampleSettingsCacheKey(chatID int64) string {
return fmt.Sprintf("alita:example_settings:%d", chatID)
}

Update the database operations to use caching:

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
}

Add to locales/en.yml:

# Example module
example_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 (de.yml, etc.) with appropriate translations.

Add to alita/main.go in LoadModules:

func LoadModules(dispatcher *ext.Dispatcher) {
modules.HelpModule.AbleMap.Init()
defer modules.LoadHelp(dispatcher)
// ... existing modules ...
modules.LoadExample(dispatcher) // Add your module
}

Use these functions to validate permissions before executing commands:

FunctionDescriptionReturns
RequireGroup(b, ctx, chat, justCheck)Ensures command is in groupbool
RequirePrivate(b, ctx, chat, justCheck)Ensures command is in PMbool
RequireUserAdmin(b, ctx, chat, userId, justCheck)User must be adminbool
RequireBotAdmin(b, ctx, chat, justCheck)Bot must be adminbool
RequireUserOwner(b, ctx, chat, userId, justCheck)User must be creatorbool
CanUserRestrict(b, ctx, chat, userId, justCheck)User can ban/mutebool
CanBotRestrict(b, ctx, chat, justCheck)Bot can ban/mutebool
CanUserDelete(b, ctx, chat, userId, justCheck)User can delete messagesbool
CanBotDelete(b, ctx, chat, justCheck)Bot can delete messagesbool
CanUserPin(b, ctx, chat, userId, justCheck)User can pin messagesbool
CanBotPin(b, ctx, chat, justCheck)Bot can pin messagesbool
CanUserPromote(b, ctx, chat, userId, justCheck)User can promote/demotebool
CanBotPromote(b, ctx, chat, justCheck)Bot can promote/demotebool
CanUserChangeInfo(b, ctx, chat, userId, justCheck)User can change chat infobool
Caninvite(b, ctx, chat, msg, justCheck)Can generate invite linksbool
  • justCheck = false: Sends error message to user if check fails
  • justCheck = true: Silently returns false without messaging
// Admin-only command
if !chat_status.RequireGroup(b, ctx, nil, false) {
return ext.EndGroups
}
if !chat_status.RequireUserAdmin(b, ctx, nil, user.Id, false) {
return ext.EndGroups
}
// Command requiring bot to have restrict permissions
if !chat_status.RequireBotAdmin(b, ctx, nil, false) {
return ext.EndGroups
}
if !chat_status.CanBotRestrict(b, ctx, nil, false) {
return ext.EndGroups
}
// Owner-only command
if !chat_status.RequireUserOwner(b, ctx, nil, user.Id, false) {
return ext.EndGroups
}
// Stop processing - no more handlers will run
return ext.EndGroups
// Continue to next handler in same group
return ext.ContinueGroups
// Error - propagates to dispatcher error handler
return err
// Success with no error
return nil
ReturnUse When
ext.EndGroupsCommand handled successfully, stop processing
ext.ContinueGroupsAllow other handlers to also process this update
errSomething went wrong, let error handler deal with it
nilSame as ext.EndGroups for most purposes

Use positional formatters in YAML with named parameters in code:

locales/en.yml
example_user_action: "User %s performed action: %s"
// In handler
text, _ := tr.GetString("example_user_action")
formattedText := fmt.Sprintf(text, userName, actionName)

Always use double quotes for strings with escape sequences:

# Correct - double quotes interpret \n
example_multiline: "Line 1\nLine 2\nLine 3"
# Wrong - single quotes preserve \n literally
example_multiline: 'Line 1\nLine 2\nLine 3'

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!"

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 counterModule = moduleStruct{moduleName: "Counter"}
// getcount shows the current greeting count
func (m moduleStruct) getcount(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, false) {
return ext.EndGroups
}
count := db.GetGreetingCount(chat.Id)
text, _ := tr.GetString("counter_current_count")
_, err := msg.Reply(b, fmt.Sprintf(text, count), helpers.Shtml())
if err != nil {
log.Error(err)
return err
}
return ext.EndGroups
}
// resetcount resets the greeting count (admin only)
func (m moduleStruct) resetcount(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, false) {
return ext.EndGroups
}
if !chat_status.RequireUserAdmin(b, ctx, nil, user.Id, false) {
return ext.EndGroups
}
err := db.ResetGreetingCount(chat.Id)
if err != nil {
log.Error(err)
text, _ := tr.GetString("counter_reset_error")
_, _ = msg.Reply(b, text, nil)
return err
}
text, _ := tr.GetString("counter_reset_success")
_, err = msg.Reply(b, text, helpers.Shtml())
if err != nil {
log.Error(err)
return err
}
return ext.EndGroups
}
func LoadCounter(dispatcher *ext.Dispatcher) {
HelpModule.AbleMap.Store(counterModule.moduleName, true)
dispatcher.AddHandler(handlers.NewCommand("getcount", counterModule.getcount))
dispatcher.AddHandler(handlers.NewCommand("resetcount", counterModule.resetcount))
}
  • 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)
  • Create database models if needed
  • Create migration file if needed
  • Add cache helpers if needed
  • Add translations to all locale files
  • Register module in LoadModules
  • Store module in help system with HelpModule.AbleMap.Store
  • Test in development environment