mautrix-go/commands/processor.go

152 lines
4.5 KiB
Go

// Copyright (c) 2026 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package commands
import (
"context"
"runtime/debug"
"strings"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
)
// Processor implements boilerplate code for splitting messages into a command and arguments,
// and finding the appropriate handler for the command.
type Processor[MetaType any] struct {
*CommandContainer[MetaType]
Client *mautrix.Client
LogArgs bool
PreValidator PreValidator[MetaType]
Meta MetaType
ReactionCommandPrefix string
}
// UnknownCommandName is the name of the fallback handler which is used if no other handler is found.
// If even the unknown command handler is not found, the command is ignored.
const UnknownCommandName = "__unknown-command__"
func NewProcessor[MetaType any](cli *mautrix.Client) *Processor[MetaType] {
proc := &Processor[MetaType]{
CommandContainer: NewCommandContainer[MetaType](),
Client: cli,
PreValidator: ValidatePrefixSubstring[MetaType]("!"),
}
proc.Register(MakeUnknownCommandHandler[MetaType]("!"))
return proc
}
func (proc *Processor[MetaType]) Process(ctx context.Context, evt *event.Event) {
log := zerolog.Ctx(ctx).With().
Stringer("sender", evt.Sender).
Stringer("room_id", evt.RoomID).
Stringer("event_id", evt.ID).
Logger()
defer func() {
panicErr := recover()
if panicErr != nil {
logEvt := log.Error().
Bytes(zerolog.ErrorStackFieldName, debug.Stack())
if realErr, ok := panicErr.(error); ok {
logEvt = logEvt.Err(realErr)
} else {
logEvt = logEvt.Any(zerolog.ErrorFieldName, panicErr)
}
logEvt.Msg("Panic in command handler")
_, err := proc.Client.SendReaction(ctx, evt.RoomID, evt.ID, "💥")
if err != nil {
log.Err(err).Msg("Failed to send reaction after panic")
}
}
}()
var parsed *Event[MetaType]
switch evt.Type {
case event.EventReaction:
parsed = proc.ParseReaction(ctx, evt)
case event.EventMessage:
parsed = proc.ParseEvent(ctx, evt)
}
if parsed == nil || (!proc.PreValidator.Validate(parsed) && parsed.StructuredArgs == nil) {
return
}
parsed.Proc = proc
parsed.Meta = proc.Meta
parsed.Ctx = ctx
handler := proc.GetHandler(parsed.Command)
if handler == nil {
return
}
parsed.Handler = handler
if handler.PreFunc != nil {
handler.PreFunc(parsed)
}
handlerChain := zerolog.Arr()
handlerChain.Str(handler.Name)
for handler.subcommandContainer != nil && len(parsed.Args) > 0 {
subHandler := handler.subcommandContainer.GetHandler(strings.ToLower(parsed.Args[0]))
if subHandler != nil {
parsed.ParentCommands = append(parsed.ParentCommands, parsed.Command)
parsed.ParentHandlers = append(parsed.ParentHandlers, handler)
handler = subHandler
handlerChain.Str(subHandler.Name)
parsed.Command = strings.ToLower(parsed.ShiftArg())
parsed.Handler = subHandler
if subHandler.PreFunc != nil {
subHandler.PreFunc(parsed)
}
} else {
break
}
}
if parsed.StructuredArgs != nil && len(parsed.Args) > 0 {
// TODO allow unknown command handlers to be called?
// The client sent MSC4391 data, but the target command wasn't found
log.Debug().Msg("Didn't find handler for MSC4391 command")
return
}
logWith := log.With().
Str("command", parsed.Command).
Array("handler", handlerChain)
if len(parsed.ParentCommands) > 0 {
logWith = logWith.Strs("parent_commands", parsed.ParentCommands)
}
if proc.LogArgs {
logWith = logWith.Strs("args", parsed.Args)
if parsed.StructuredArgs != nil {
logWith = logWith.RawJSON("structured_args", parsed.StructuredArgs)
}
}
log = logWith.Logger()
parsed.Ctx = log.WithContext(ctx)
parsed.Log = &log
if handler.Parameters != nil && parsed.StructuredArgs == nil {
// The handler wants structured parameters, but the client didn't send MSC4391 data
var err error
parsed.StructuredArgs, err = handler.Spec().ParseArguments(parsed.RawArgs)
if err != nil {
log.Debug().Err(err).Msg("Failed to parse structured arguments")
// TODO better error, usage info? deduplicate with WithParsedArgs
parsed.Reply("Failed to parse arguments: %v", err)
return
}
if proc.LogArgs {
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.RawJSON("structured_args", parsed.StructuredArgs)
})
}
}
log.Debug().Msg("Processing command")
handler.Func(parsed)
}