Anatomy of a module
Every module satisfies goroku.Module. Embedding goroku.Base supplies a default for all of it except Name and Commands, so those two are the only methods you must write.
type Module interface {
Name() string
Strings() map[string]string
Init(client *CustomTelegramClient, db *Database) error
ClientReady() error
OnUnload() error
OnDlmod() error
Commands() map[string]CommandHandler
Watchers() []WatcherHandler
}Required
Name
The module's identity in .help, .unloadmod and the config UI. It is also the Go type name, and the key under which your data is stored.
func (m *Weather) Name() string { return "Weather" }Commands
Maps a command word to its handler. Users type the word after the bot prefix, so "weather" becomes .weather.
func (m *Weather) Commands() map[string]goroku.CommandHandler {
return map[string]goroku.CommandHandler{
"weather": m.WeatherCmd,
"forecast": m.ForecastCmd,
}
}A CommandHandler is func(msg *goroku.Message) error. Returning an error reports it to the user; the dispatcher also recovers panics, so a crash in your handler does not take down the bot.
Optional, with defaults from Base
Strings
User-facing text, and the source for m.T. Two key prefixes are special:
| Key | Meaning |
|---|---|
name | Display name in .help |
_cls_doc | What the module does |
_cmd_doc_<command> | What that command does |
Everything else is yours to look up with m.T.
func (m *Weather) Strings() map[string]string {
return map[string]string{
"name": "Weather",
"_cls_doc": "Current weather for a city",
"_cmd_doc_weather": "[city] — show the weather",
"no_city": "❌ <b>Which city?</b>",
}
}Init
Runs once when the module is registered, before any command can fire. Base has already populated m.Client, m.DB and m.Translator by then, so use Init for your own setup only.
func (m *Weather) Init(client *goroku.CustomTelegramClient, db *goroku.Database) error {
if err := m.Base.Init(client, db); err != nil {
return err
}
m.cache = make(map[string]string)
return nil
}Call m.Base.Init first
The loader wires the fields before calling Init, so this call is redundant in normal operation. Include it anyway: it makes the module work when constructed directly in a test, without going through the loader.
ClientReady
Runs once the Telegram connection is up. Use it for anything that needs to talk to Telegram at startup; Init may run before the client is connected.
func (m *Weather) ClientReady() error {
me := m.Client.Me()
_ = me
return nil
}OnUnload
Runs when the module is removed. Release goroutines, timers and file handles here.
func (m *Weather) OnUnload() error {
close(m.stop)
return nil
}WARNING
A panic in OnUnload is recovered and logged, but the module is still removed. Do not rely on OnUnload for correctness-critical cleanup.
OnDlmod
Runs right after installation via .dlmod. Useful for a one-time greeting or first-run setup.
Watchers
Handlers that see every message, not just commands. See Watchers.
Beyond the interface
Several capabilities are opt-in through separate interfaces — implement the method and Goroku picks it up:
| Interface | Method | Purpose |
|---|---|---|
ModuleWithMeta | CommandMetas() | Aliases and restrictions |
ModuleWithConfigSchema | ConfigSchema() | Typed user settings |
ModuleWithConfigReady | ConfigReady(map[string]any) | React to settings changes |
ModuleWithTranslationAliases | TranslationAliases() | Extra translation-file names |
Full list in Optional interfaces.