Files
go-telegram/dispatch/groups.go
T
lukaszraczylo ac7cae8fa7 Initial release of go-telegram
A fully-generated, strongly-typed Go client for the Telegram Bot API.

* 176 methods + 301 types generated from Bot API v10.0
* 1408 auto-generated tests (8 scenarios per method)
* Typed unions throughout — no 'any' in the public surface
* Pluggable HTTP transport and JSON codec (default goccy/go-json)
* Built-in retry middleware honouring Telegram's retry_after
* Generic dispatcher with filters and conversation handlers
* Self-verifying codegen pipeline (regen → audit → emit → run tests)
* 14 example bots covering common patterns
2026-05-09 13:09:27 +01:00

187 lines
5.4 KiB
Go

package dispatch
import (
"errors"
"regexp"
"sort"
"github.com/lukaszraczylo/go-telegram/api"
)
// ErrEndGroups stops dispatch from running any further handlers in any
// group for this update when returned by a handler. Use it to indicate
// the update has been definitively handled.
//
// errors.Is(err, ErrEndGroups) is the canonical check, though dispatch
// itself recognises it by exact identity.
var ErrEndGroups = errors.New("dispatch: end groups")
// ErrContinueGroups signals that this group's handler should be treated
// as not-matching when returned by a handler: dispatch moves on to the
// next handler in the same group, then to subsequent groups.
//
// Without ErrContinueGroups, a non-error return from a matched handler
// stops dispatch (default first-match-wins semantics).
var ErrContinueGroups = errors.New("dispatch: continue groups")
// RouterScope registers handlers into a specific priority group on its parent
// Router. Group 0 runs first, then group 1, etc. Within a group, handlers run
// in registration order; the first non-skipped match terminates dispatch
// unless the handler returns ErrContinueGroups.
type RouterScope struct {
router *Router
group int
}
// Group returns a RouterScope that registers handlers in the given group.
// Group 0 (the default) runs first, then group 1, etc. Within a group,
// handlers run in registration order; the first non-skipped match
// terminates dispatch unless the handler returns ErrContinueGroups.
func (r *Router) Group(group int) *RouterScope {
return &RouterScope{router: r, group: group}
}
// OnCommand registers a command handler in this group.
func (s *RouterScope) OnCommand(cmd string, h Handler[*api.Message]) {
s.router.groupCommands = append(s.router.groupCommands, groupCommandRoute{
cmd: cmd, group: s.group, handler: h,
})
}
// OnText registers a regex text handler in this group.
// Panics at registration time if pattern is not a valid regular expression.
func (s *RouterScope) OnText(pattern string, h Handler[*api.Message]) {
s.router.groupTexts = append(s.router.groupTexts, groupTextRoute{
re: regexp.MustCompile(pattern), group: s.group, handler: h,
})
}
// OnMessageFilter registers a filter-based message handler in this group.
func (s *RouterScope) OnMessageFilter(f Filter[*api.Message], h Handler[*api.Message]) {
s.router.groupMessageFilters = append(s.router.groupMessageFilters, groupMessageFilterRoute{
filter: f, group: s.group, handler: h,
})
}
// group-aware route types
type groupCommandRoute struct {
cmd string
group int
handler Handler[*api.Message]
}
type groupTextRoute struct {
re *regexp.Regexp
group int
handler Handler[*api.Message]
}
type groupMessageFilterRoute struct {
filter Filter[*api.Message]
group int
handler Handler[*api.Message]
}
// dispatchGroups runs message handlers registered via RouterScope.Group().
// It collects all matching groups, sorts by group number, and applies
// first-match-wins semantics within each group. Handlers may return
// ErrContinueGroups (skip to next handler/group) or ErrEndGroups (stop all groups).
// A non-sentinel error stops dispatch and is returned to the caller.
func (r *Router) dispatchGroups(c *Context, m *api.Message) error {
// Collect group numbers present.
groupSet := map[int]struct{}{}
for _, gr := range r.groupCommands {
groupSet[gr.group] = struct{}{}
}
for _, gr := range r.groupTexts {
groupSet[gr.group] = struct{}{}
}
for _, gr := range r.groupMessageFilters {
groupSet[gr.group] = struct{}{}
}
if len(groupSet) == 0 {
return nil
}
groups := make([]int, 0, len(groupSet))
for g := range groupSet {
groups = append(groups, g)
}
sort.Ints(groups)
for _, g := range groups {
matched, err := r.runGroupHandlers(c, m, g)
if err != nil {
if errors.Is(err, ErrEndGroups) {
return nil
}
return err
}
if matched {
// First-match-wins: stop further groups.
return nil
}
// No match or ErrContinueGroups from all handlers: try next group.
}
return nil
}
// runGroupHandlers runs all handlers in group g against m, in registration
// order. Returns (true, nil) when a handler matched (returned nil). Returns
// (false, nil) when all handlers returned ErrContinueGroups. Returns
// (false, err) for ErrEndGroups or any non-sentinel error.
func (r *Router) runGroupHandlers(c *Context, m *api.Message, g int) (matched bool, err error) {
// Commands.
if cmd, args, ok := extractCommand(m); ok {
for _, route := range r.groupCommands {
if route.group != g || route.cmd != cmd {
continue
}
c.Values["command"] = cmd
c.Values["command_args"] = args
if err := route.handler(c, m); err != nil {
if errors.Is(err, ErrContinueGroups) {
continue
}
return false, err
}
return true, nil
}
}
// Text regex.
if m.Text != "" {
for _, route := range r.groupTexts {
if route.group != g {
continue
}
subs := route.re.FindStringSubmatch(m.Text)
if subs == nil {
continue
}
c.Values["regex_match"] = subs
if err := route.handler(c, m); err != nil {
if errors.Is(err, ErrContinueGroups) {
continue
}
return false, err
}
return true, nil
}
}
// Filter-based.
for _, route := range r.groupMessageFilters {
if route.group != g || !route.filter(m) {
continue
}
if err := route.handler(c, m); err != nil {
if errors.Is(err, ErrContinueGroups) {
continue
}
return false, err
}
return true, nil
}
return false, nil
}