Skip to content

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.

go
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.

go
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.

go
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:

KeyMeaning
nameDisplay name in .help
_cls_docWhat the module does
_cmd_doc_<command>What that command does

Everything else is yours to look up with m.T.

go
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.

go
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.

go
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.

go
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:

InterfaceMethodPurpose
ModuleWithMetaCommandMetas()Aliases and restrictions
ModuleWithConfigSchemaConfigSchema()Typed user settings
ModuleWithConfigReadyConfigReady(map[string]any)React to settings changes
ModuleWithTranslationAliasesTranslationAliases()Extra translation-file names

Full list in Optional interfaces.

Released under the GNU AGPL v3 License.