Skip to content

Commands

Registering

go
func (m *Weather) Commands() map[string]goroku.CommandHandler {
	return map[string]goroku.CommandHandler{
		"weather": m.WeatherCmd,
	}
}

The map key is the command word. With the default . prefix that registers .weather. Keys are matched case-insensitively.

Command and alias names are unique across all loaded modules. If another module already owns weather, registration fails with a collision error naming the conflict rather than silently overriding it.

Aliases

Add CommandMetas — an optional interface, so declaring it is enough:

go
func (m *Weather) CommandMetas() map[string]goroku.CommandMeta {
	return map[string]goroku.CommandMeta{
		"weather": {
			Aliases: []string{"w", "wx"},
		},
	}
}

.weather, .w and .wx now all work.

Restricting where a command runs

CommandMeta fields are filters. The dispatcher checks them before your handler runs, so a command that does not apply simply does not fire.

go
func (m *Weather) CommandMetas() map[string]goroku.CommandMeta {
	return map[string]goroku.CommandMeta{
		"weather": {
			Aliases:   []string{"w"},
			OnlyOwner: true,   // only you
			OnlyGroups: true,  // only in groups
			NoForwarded: true, // ignore forwarded messages
		},
	}
}

Common ones:

FieldEffect
OnlyOwnerOnly the account owner
OnlyPM / NoPMOnly / never in private chats
OnlyGroups, OnlyChannels, OnlyChatsRestrict by chat type
OnlyReply / NoReplyRequire / forbid a replied-to message
OnlyMedia, OnlyPhotos, OnlyVideos, OnlyDocsRequire an attachment
NoMedia, NoStickers, NoAudio, NoDocReject attachments
RatelimitSubject the command to per-user and per-chat rate limits
Filterfunc(*Message) bool for anything else

The full list is in CommandMeta.

Matching on content

go
"weather": {
	StartsWith: "город ",
	Regex:      `^\d{5}$`,
},

Regex is compiled once at registration. An invalid pattern fails registration rather than silently never matching.

Errors

Return an error and the user sees it:

go
func (m *Weather) WeatherCmd(msg *goroku.Message) error {
	city := msg.Args()
	if city == "" {
		return msg.Answer(m.T("no_city", "❌ <b>Which city?</b>"))
	}
	data, err := m.fetch(city)
	if err != nil {
		return fmt.Errorf("fetch weather for %q: %w", city, err)
	}
	return msg.Answer(data)
}

Two different outcomes:

  • msg.Answer(...) with a friendly message — an expected situation, like missing arguments. Return nil.
  • return err — something actually failed. Goroku logs it and shows the user ❌ Command execution error: <your message>.

Wrap errors with context (fmt.Errorf("...: %w", err)); that text is what you will read in the log at 3am.

Rate limits

Commands are not rate-limited by default. Opt in per command:

go
"weather": { Ratelimit: true },

The dispatcher then applies the configured per-user and per-chat windows, and tells the user when they are over the limit.

Concurrency

Handlers run concurrently, each in its own goroutine with a bounded pool. Two consequences:

  • Guard shared state on your module struct with a mutex. Your module is a single instance shared across every chat.
  • Long work is fine — it will not block other commands — but respect cancellation via msg.Context() so shutdown is not delayed.
go
type Weather struct {
	goroku.Base
	mu    sync.Mutex
	cache map[string]string
}

func (m *Weather) WeatherCmd(msg *goroku.Message) error {
	m.mu.Lock()
	cached, ok := m.cache[msg.Args()]
	m.mu.Unlock()
	if ok {
		return msg.Answer(cached)
	}
	return m.fetchAndAnswer(msg)
}

Released under the GNU AGPL v3 License.