Add Arrtrix runtime, config, onboarding, and webhook support
- Implement runtime package for bridge startup, config loading, and env overrides - Add onboarding package for management room welcome messages - Add matrixcmd package for command processing and help - Add webhook package with Radarr webhook support and validation - Extend connector config for webhooks and validation - Update default config and example config for new options - Add tests for new packages and config validation - Change database type default to sqlite3-fk-wal
This commit is contained in:
parent
eeedb5268a
commit
fe627f3aab
19 changed files with 1855 additions and 35 deletions
60
packages/arrtrix/pkg/matrixcmd/help.go
Normal file
60
packages/arrtrix/pkg/matrixcmd/help.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
package matrixcmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NewHelpHandler(proc *Processor) Handler {
|
||||
return NewHandler(Meta{
|
||||
Name: "help",
|
||||
Description: "Show this help message.",
|
||||
}, func(ctx *Context) {
|
||||
ctx.Reply(formatHelp(proc, ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func formatHelp(proc *Processor, ctx *Context) string {
|
||||
var builder strings.Builder
|
||||
|
||||
switch {
|
||||
case ctx.RoomID == ctx.User.ManagementRoom:
|
||||
builder.WriteString(fmt.Sprintf("This is your management room: prefixing commands with `%s` is not required.\n", ctx.Bridge.Config.CommandPrefix))
|
||||
case ctx.Portal != nil:
|
||||
builder.WriteString(fmt.Sprintf("**This is a portal room**: you must always prefix commands with `%s`. Management commands will not be bridged.\n", ctx.Bridge.Config.CommandPrefix))
|
||||
default:
|
||||
builder.WriteString(fmt.Sprintf("This is not your management room: prefixing commands with `%s` is required.\n", ctx.Bridge.Config.CommandPrefix))
|
||||
}
|
||||
|
||||
builder.WriteString("Parameters in [square brackets] are optional, while parameters in <angle brackets> are required.\n\n")
|
||||
builder.WriteString("#### General\n")
|
||||
|
||||
handlers := proc.Handlers()
|
||||
sort.SliceStable(handlers, func(i, j int) bool {
|
||||
return handlers[i].Meta().Name < handlers[j].Meta().Name
|
||||
})
|
||||
for _, handler := range handlers {
|
||||
meta := handler.Meta()
|
||||
builder.WriteString("**")
|
||||
builder.WriteString(meta.Name)
|
||||
builder.WriteString("**")
|
||||
if meta.Usage != "" {
|
||||
builder.WriteByte(' ')
|
||||
builder.WriteString(meta.Usage)
|
||||
}
|
||||
if meta.Description != "" {
|
||||
builder.WriteString(" - ")
|
||||
builder.WriteString(meta.Description)
|
||||
}
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
|
||||
if extra := strings.TrimSpace(ctx.Processor.texts.AdditionalHelp); extra != "" {
|
||||
builder.WriteByte('\n')
|
||||
builder.WriteString(extra)
|
||||
builder.WriteByte('\n')
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
42
packages/arrtrix/pkg/matrixcmd/help_test.go
Normal file
42
packages/arrtrix/pkg/matrixcmd/help_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package matrixcmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
func TestFormatHelpManagementRoom(t *testing.T) {
|
||||
roomID := id.RoomID("!arrtrix:test")
|
||||
proc := &Processor{
|
||||
texts: bridgeconfig.ManagementRoomTexts{AdditionalHelp: "Extra help text."},
|
||||
command: make(map[string]Handler),
|
||||
alias: make(map[string]string),
|
||||
}
|
||||
proc.Add(NewHelpHandler(proc))
|
||||
|
||||
out := formatHelp(proc, &Context{
|
||||
Bridge: &bridgev2.Bridge{
|
||||
Config: &bridgeconfig.BridgeConfig{
|
||||
CommandPrefix: "!arr",
|
||||
},
|
||||
},
|
||||
RoomID: roomID,
|
||||
User: &bridgev2.User{User: &database.User{ManagementRoom: roomID}},
|
||||
Processor: proc,
|
||||
})
|
||||
|
||||
for _, fragment := range []string{
|
||||
"prefixing commands with `!arr` is not required",
|
||||
"**help** - Show this help message.",
|
||||
"Extra help text.",
|
||||
} {
|
||||
if !strings.Contains(out, fragment) {
|
||||
t.Fatalf("expected help output to contain %q, got:\n%s", fragment, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
204
packages/arrtrix/pkg/matrixcmd/processor.go
Normal file
204
packages/arrtrix/pkg/matrixcmd/processor.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package matrixcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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))
|
||||
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) {
|
||||
ms := &bridgev2.MessageStatus{
|
||||
Step: status.MsgStepCommand,
|
||||
Status: event.MessageStatusSuccess,
|
||||
}
|
||||
|
||||
logCopy := zerolog.Ctx(ctx).With().Logger()
|
||||
log := &logCopy
|
||||
|
||||
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
|
||||
} else {
|
||||
logEvt = logEvt.Any(zerolog.ErrorFieldName, recovered)
|
||||
ms.InternalError = fmt.Errorf("%v", recovered)
|
||||
}
|
||||
logEvt.Msg("Panic in arrtrix Matrix command handler")
|
||||
ms.Status = event.MessageStatusFail
|
||||
ms.IsCertain = true
|
||||
ms.ErrorAsMessage = true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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")
|
||||
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)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue