mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
152 lines
4.5 KiB
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)
|
|
}
|