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:
Chris Kruining 2026-04-16 09:06:57 +02:00
parent eeedb5268a
commit fe627f3aab
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
19 changed files with 1855 additions and 35 deletions

View file

@ -0,0 +1,137 @@
package onboarding
import (
"context"
"fmt"
"strings"
"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
)
const handledInviteEventType = "com.arrtrix.handled_invite"
func HandleBotInvite(ctx context.Context, bridge *bridgev2.Bridge, texts bridgeconfig.ManagementRoomTexts, evt *event.Event) {
if evt.Type != event.StateMember ||
evt.GetStateKey() != bridge.Bot.GetMXID().String() ||
evt.Content.AsMember().Membership != event.MembershipInvite {
return
}
log := zerolog.Ctx(ctx)
sender, err := bridge.GetUserByMXID(ctx, evt.Sender)
if err != nil {
log.Err(err).Msg("Failed to load sender for bot invite")
return
}
if !sender.Permissions.Commands {
return
}
if err = bridge.Bot.EnsureJoined(ctx, evt.RoomID); err != nil {
log.Err(err).Msg("Failed to accept invite to room")
return
}
members, err := bridge.Matrix.GetMembers(ctx, evt.RoomID)
if err != nil {
log.Err(err).Msg("Failed to get members of room after accepting invite")
return
}
if len(members) != 2 {
return
}
assignedManagementRoom := sender.ManagementRoom == ""
if assignedManagementRoom {
sender.ManagementRoom = evt.RoomID
if err = sender.Save(ctx); err != nil {
log.Err(err).Msg("Failed to update user's management room in database")
return
}
}
message := buildWelcomeMessage(bridge, texts, sender, assignedManagementRoom)
content := format.RenderMarkdown(message, true, false)
if _, err = bridge.Bot.SendMessage(ctx, evt.RoomID, event.EventMessage, &event.Content{Parsed: &content}, nil); err != nil {
log.Err(err).Msg("Failed to send welcome message to room")
return
}
evt.Type = event.Type{Type: handledInviteEventType}
}
func buildWelcomeMessage(bridge *bridgev2.Bridge, texts bridgeconfig.ManagementRoomTexts, sender *bridgev2.User, assignedManagementRoom bool) string {
return composeWelcomeMessage(
bridge.Network.GetName().DisplayName,
bridge.Config.CommandPrefix,
bridge.Bot.GetMXID(),
texts,
sender.GetDefaultLogin() != nil,
assignedManagementRoom,
)
}
func composeWelcomeMessage(
bridgeName string,
commandPrefix string,
botMXID id.UserID,
texts bridgeconfig.ManagementRoomTexts,
connected bool,
assignedManagementRoom bool,
) string {
replacer := strings.NewReplacer(
"$cmdprefix", commandPrefix,
"$bridge", bridgeName,
"$bot", string(botMXID),
)
var parts []string
base := strings.TrimSpace(texts.Welcome)
if base == "" {
base = fmt.Sprintf("Hello, I'm the %s bot.", bridgeName)
}
parts = append(parts, replacer.Replace(base))
if assignedManagementRoom {
parts = append(parts, "This room has been marked as your management room.")
} else {
parts = append(parts, fmt.Sprintf("Use `%s help` to see available commands in this room.", commandPrefix))
}
if connected {
connected := strings.TrimSpace(texts.WelcomeConnected)
if connected == "" {
connected = "You're connected. Use `help` to see the commands available right now."
}
parts = append(parts, replacer.Replace(connected))
} else {
unconnected := strings.TrimSpace(texts.WelcomeUnconnected)
if unconnected == "" {
unconnected = "Use `help` to see the commands available right now."
}
parts = append(parts, replacer.Replace(unconnected))
}
if extra := strings.TrimSpace(texts.AdditionalHelp); extra != "" {
parts = append(parts, replacer.Replace(extra))
}
return strings.Join(parts, "\n\n")
}
func IsHandledInviteEvent(evt *event.Event) bool {
return evt.Type.Type == handledInviteEventType
}
func IsBotInviteFor(roomBot id.UserID, evt *event.Event) bool {
return evt.Type == event.StateMember &&
evt.GetStateKey() == roomBot.String() &&
evt.Content.AsMember().Membership == event.MembershipInvite
}