Skip to content

Watchers

A watcher sees every message in every chat the account can read — not just commands.

go
func (m *Mod) Watchers() []goroku.WatcherHandler {
	return []goroku.WatcherHandler{
		func(msg *goroku.Message) error {
			if strings.Contains(strings.ToLower(msg.Text), "goroku") {
				return msg.Reply("👋")
			}
			return nil
		},
	}
}

WatcherHandler has the same shape as a command handler: func(msg *goroku.Message) error.

Filtering

Filter declaratively with WatcherMetas, positionally matched to the slice returned by Watchers. The dispatcher applies the filter before your code runs, which is both clearer and cheaper than an if at the top of every watcher:

go
func (m *Mod) WatcherMetas() []goroku.CommandMeta {
	return []goroku.CommandMeta{
		{
			OnlyGroups:  true,
			NoForwarded: true,
			Contains:    "goroku",
		},
	}
}

The same fields as CommandMeta are available.

Cost

Watchers run on every message. A slow or chatty watcher is felt across the whole bot.

  • Return nil immediately when the message is not yours to handle. Make the cheapest check first.
  • Do not write to the database per message — see the batching example in Storage.
  • Watchers run concurrently in a bounded pool. When the pool is full, watcher invocations are dropped rather than queued, so a slow watcher costs you messages.
go
func(msg *goroku.Message) error {
	if msg.Out || msg.Text == "" {
		return nil // cheapest possible rejection, first
	}
	if !strings.Contains(msg.Text, "goroku") {
		return nil
	}
	return m.handle(msg)
}

Panics

A panic in a watcher is recovered and logged with your module name; the bot keeps running.

Watchers or commands?

UseWhen
CommandThe user is asking for something explicitly
WatcherYou react to ordinary conversation

If you find yourself parsing a prefix inside a watcher, you want a command.

Released under the GNU AGPL v3 License.