- Update ports for Alloy, Grafana, Loki, Prometheus, Promtail, Tempo, and Uptime Kuma to new ranges - Add Arrtrix content management commands and subscriptions - Implement Radarr and Sonarr client logic for movie and series management - Add matrix commands for download and subscription management - Add subscription repository with database schema and logic - Update Arrtrix config and example config for content section - Update help text and command processor to include new commands - Update vendor hash for Arrtrix package
227 lines
5.5 KiB
Go
227 lines
5.5 KiB
Go
package matrixcmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"runtime/debug"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
|
|
"maunium.net/go/mautrix/bridgev2"
|
|
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
|
|
"maunium.net/go/mautrix/bridgev2/status"
|
|
"maunium.net/go/mautrix/event"
|
|
"maunium.net/go/mautrix/format"
|
|
"maunium.net/go/mautrix/id"
|
|
|
|
"sneeuwvlok/packages/arrtrix/pkg/observability"
|
|
)
|
|
|
|
type Handler interface {
|
|
Meta() Meta
|
|
Run(*Context)
|
|
}
|
|
|
|
type Meta struct {
|
|
Name string
|
|
Description string
|
|
Usage string
|
|
Aliases []string
|
|
}
|
|
|
|
type HandlerFunc struct {
|
|
meta Meta
|
|
run func(*Context)
|
|
}
|
|
|
|
func NewHandler(meta Meta, run func(*Context)) Handler {
|
|
return HandlerFunc{meta: meta, run: run}
|
|
}
|
|
|
|
func (h HandlerFunc) Meta() Meta {
|
|
return h.meta
|
|
}
|
|
|
|
func (h HandlerFunc) Run(ctx *Context) {
|
|
h.run(ctx)
|
|
}
|
|
|
|
type Processor struct {
|
|
bridge *bridgev2.Bridge
|
|
bot bridgev2.MatrixAPI
|
|
texts bridgeconfig.ManagementRoomTexts
|
|
command map[string]Handler
|
|
alias map[string]string
|
|
order []string
|
|
}
|
|
|
|
type Context struct {
|
|
Bridge *bridgev2.Bridge
|
|
Bot bridgev2.MatrixAPI
|
|
RoomID id.RoomID
|
|
OrigRoomID id.RoomID
|
|
EventID id.EventID
|
|
ReplyTo id.EventID
|
|
User *bridgev2.User
|
|
Portal *bridgev2.Portal
|
|
Command string
|
|
Args []string
|
|
RawArgs string
|
|
Ctx context.Context
|
|
Log *zerolog.Logger
|
|
Processor *Processor
|
|
}
|
|
|
|
var _ bridgev2.CommandProcessor = (*Processor)(nil)
|
|
|
|
func NewProcessor(bridge *bridgev2.Bridge, texts bridgeconfig.ManagementRoomTexts) *Processor {
|
|
proc := &Processor{
|
|
bridge: bridge,
|
|
bot: bridge.Bot,
|
|
texts: texts,
|
|
command: make(map[string]Handler),
|
|
alias: make(map[string]string),
|
|
}
|
|
proc.Add(NewHelpHandler(proc))
|
|
proc.Add(NewDownloadHandler())
|
|
proc.Add(NewSubscriptionsHandler())
|
|
return proc
|
|
}
|
|
|
|
func (p *Processor) Add(handler Handler) {
|
|
meta := handler.Meta()
|
|
p.command[meta.Name] = handler
|
|
p.order = append(p.order, meta.Name)
|
|
for _, alias := range meta.Aliases {
|
|
p.alias[alias] = meta.Name
|
|
}
|
|
}
|
|
|
|
func (p *Processor) Handlers() []Handler {
|
|
names := append([]string(nil), p.order...)
|
|
sort.Strings(names)
|
|
|
|
handlers := make([]Handler, 0, len(names))
|
|
for _, name := range names {
|
|
handler, ok := p.command[name]
|
|
if ok {
|
|
handlers = append(handlers, handler)
|
|
}
|
|
}
|
|
return handlers
|
|
}
|
|
|
|
func (p *Processor) Handle(ctx context.Context, roomID id.RoomID, eventID id.EventID, user *bridgev2.User, message string, replyTo id.EventID) {
|
|
ctx, span := observability.StartSpan(ctx, "arrtrix.matrix.command")
|
|
defer span.End()
|
|
|
|
ms := &bridgev2.MessageStatus{
|
|
Step: status.MsgStepCommand,
|
|
Status: event.MessageStatusSuccess,
|
|
}
|
|
|
|
logCopy := zerolog.Ctx(ctx).With().Logger()
|
|
log := &logCopy
|
|
outcome := "success"
|
|
commandName := "unknown-command"
|
|
|
|
defer func() {
|
|
statusInfo := &bridgev2.MessageStatusEventInfo{
|
|
RoomID: roomID,
|
|
SourceEventID: eventID,
|
|
EventType: event.EventMessage,
|
|
Sender: user.MXID,
|
|
}
|
|
|
|
if recovered := recover(); recovered != nil {
|
|
logEvt := log.Error().Bytes(zerolog.ErrorStackFieldName, debug.Stack())
|
|
if err, ok := recovered.(error); ok {
|
|
logEvt = logEvt.Err(err)
|
|
ms.InternalError = err
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
} else {
|
|
logEvt = logEvt.Any(zerolog.ErrorFieldName, recovered)
|
|
ms.InternalError = fmt.Errorf("%v", recovered)
|
|
span.SetStatus(codes.Error, "panic")
|
|
}
|
|
logEvt.Msg("Panic in arrtrix Matrix command handler")
|
|
ms.Status = event.MessageStatusFail
|
|
ms.IsCertain = true
|
|
ms.ErrorAsMessage = true
|
|
outcome = "panic"
|
|
}
|
|
|
|
observability.RecordCommand(ctx, commandName, outcome)
|
|
p.bridge.Matrix.SendMessageStatus(ctx, ms, statusInfo)
|
|
}()
|
|
|
|
args := strings.Fields(message)
|
|
if len(args) == 0 {
|
|
args = []string{"unknown-command"}
|
|
}
|
|
|
|
commandName = strings.ToLower(args[0])
|
|
if actual, ok := p.alias[commandName]; ok {
|
|
commandName = actual
|
|
}
|
|
span.SetAttributes(
|
|
attribute.String("arrtrix.matrix.command.name", commandName),
|
|
attribute.String("matrix.room_id", roomID.String()),
|
|
)
|
|
|
|
portal, err := p.bridge.GetPortalByMXID(ctx, roomID)
|
|
if err != nil {
|
|
log.Err(err).Msg("Failed to get portal")
|
|
}
|
|
|
|
commandCtx := &Context{
|
|
Bridge: p.bridge,
|
|
Bot: p.bot,
|
|
RoomID: roomID,
|
|
OrigRoomID: roomID,
|
|
EventID: eventID,
|
|
ReplyTo: replyTo,
|
|
User: user,
|
|
Portal: portal,
|
|
Command: commandName,
|
|
Args: args[1:],
|
|
RawArgs: strings.TrimSpace(strings.TrimPrefix(message, args[0])),
|
|
Ctx: ctx,
|
|
Log: log,
|
|
Processor: p,
|
|
}
|
|
|
|
handler, ok := p.command[commandName]
|
|
if !ok {
|
|
log.Debug().Str("mx_command", commandName).Msg("Received unknown Matrix room command")
|
|
span.SetStatus(codes.Error, "unknown command")
|
|
outcome = "unknown"
|
|
commandCtx.Reply("Unknown command, use the `help` command for help.")
|
|
return
|
|
}
|
|
|
|
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
|
return c.Str("mx_command", commandName)
|
|
})
|
|
log.Debug().Msg("Received Matrix room command")
|
|
handler.Run(commandCtx)
|
|
span.SetStatus(codes.Ok, "")
|
|
}
|
|
|
|
func (c *Context) Reply(message string, args ...any) {
|
|
message = strings.ReplaceAll(message, "$cmdprefix ", c.Bridge.Config.CommandPrefix+" ")
|
|
if len(args) > 0 {
|
|
message = fmt.Sprintf(message, args...)
|
|
}
|
|
|
|
content := format.RenderMarkdown(message, true, false)
|
|
content.MsgType = event.MsgNotice
|
|
if _, err := c.Bot.SendMessage(c.Ctx, c.OrigRoomID, event.EventMessage, &event.Content{Parsed: &content}, nil); err != nil {
|
|
c.Log.Err(err).Msg("Failed to reply to Matrix room command")
|
|
}
|
|
}
|