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.