package main import ( "context" "fmt" "log" "strings" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) type MatrixHandler struct { bridge *Bridge ghostPrefix string } func NewMatrixHandler(bridge *Bridge, ghostPrefix string) *MatrixHandler { return &MatrixHandler{ bridge: bridge, ghostPrefix: ghostPrefix, } } func (h *MatrixHandler) HandleEvent(ctx context.Context, evt *event.Event) { if evt.Sender == h.bridge.as.BotMXID() { return } if h.ghostPrefix != "" && strings.HasPrefix(string(evt.Sender), h.ghostPrefix) { return } switch evt.Type { case event.EventMessage: h.onMessage(ctx, evt) case event.StateMember: h.onMembership(ctx, evt) } } func (h *MatrixHandler) onMessage(ctx context.Context, evt *event.Event) { msg := evt.Content.AsMessage() if msg == nil { return } text := strings.TrimSpace(msg.Body) if text == "" { return } cmd, arg := parseCommand(text) switch cmd { case "help", "": h.cmdHelp(ctx, evt) case "list": h.cmdList(ctx, evt) case "create": h.cmdCreate(ctx, evt, arg) case "delete": h.cmdDelete(ctx, evt, arg) default: if topic, _ := h.bridge.GetBridgeByRoom(string(evt.RoomID)); topic != "" { h.forwardToNtfy(ctx, evt, topic) } else { h.reply(ctx, evt.RoomID, "Type 'help' to see available commands.") } } } func (h *MatrixHandler) onMembership(ctx context.Context, evt *event.Event) { member := evt.Content.AsMember() if member == nil { return } stateKey := evt.GetStateKey() if stateKey == "" { return } if member.Membership == event.MembershipInvite && id.UserID(stateKey) == h.bridge.as.BotMXID() { if err := h.bridge.as.BotIntent().EnsureJoined(ctx, evt.RoomID); err != nil { log.Printf("[matrix] auto-join room %s failed: %v", evt.RoomID, err) } } } func (h *MatrixHandler) cmdHelp(ctx context.Context, evt *event.Event) { h.reply(ctx, evt.RoomID, "ntfy-Matrix Bridge\n\n"+ " bridge create — Bridge an ntfy topic to a new room\n"+ " bridge list — List all active bridges\n"+ " bridge delete — Remove a bridge\n"+ " help — Show this message\n\n"+ "Bridged rooms forward messages between Matrix and ntfy.", ) } func (h *MatrixHandler) cmdList(ctx context.Context, evt *event.Event) { bridges, err := h.bridge.GetAllBridges() if err != nil { log.Printf("[matrix] list bridges: %v", err) h.reply(ctx, evt.RoomID, "Database error.") return } if len(bridges) == 0 { h.reply(ctx, evt.RoomID, "No active bridges.") return } var b strings.Builder b.WriteString("Active bridges:\n") for topic, roomID := range bridges { fmt.Fprintf(&b, " %s → %s\n", topic, roomID) } h.reply(ctx, evt.RoomID, strings.TrimRight(b.String(), "\n")) } func (h *MatrixHandler) cmdCreate(ctx context.Context, evt *event.Event, topicName string) { if topicName == "" { h.reply(ctx, evt.RoomID, "Usage: bridge create ") return } if !h.bridge.HasPermission(string(evt.Sender)) { h.reply(ctx, evt.RoomID, "Admin access required.") return } roomID, err := h.bridge.CreateBridge(ctx, evt.Sender, topicName) if err != nil { h.reply(ctx, evt.RoomID, fmt.Sprintf("Failed: %v", err)) return } h.reply(ctx, evt.RoomID, fmt.Sprintf("Bridge created!\n Topic: %s\n Room: %s", topicName, roomID)) } func (h *MatrixHandler) cmdDelete(ctx context.Context, evt *event.Event, topicName string) { if topicName == "" { h.reply(ctx, evt.RoomID, "Usage: bridge delete ") return } if !h.bridge.HasPermission(string(evt.Sender)) { h.reply(ctx, evt.RoomID, "Admin access required.") return } if err := h.bridge.DeleteBridge(topicName); err != nil { log.Printf("[matrix] delete bridge: %v", err) h.reply(ctx, evt.RoomID, "Failed to delete bridge.") return } h.reply(ctx, evt.RoomID, fmt.Sprintf("Bridge for '%s' deleted.", topicName)) } func (h *MatrixHandler) forwardToNtfy(ctx context.Context, evt *event.Event, topic string) { if err := h.bridge.PublishToNtfy(topic, evt.Content.AsMessage().Body); err != nil { log.Printf("[matrix] publish to ntfy: %v", err) h.reply(ctx, evt.RoomID, fmt.Sprintf("Failed to publish: %v", err)) } } func (h *MatrixHandler) reply(ctx context.Context, roomID id.RoomID, text string) { h.bridge.as.BotIntent().SendNotice(ctx, roomID, text) } func parseCommand(text string) (cmd, arg string) { if !strings.HasPrefix(text, "bridge ") { return "", text } rest := strings.TrimSpace(strings.TrimPrefix(text, "bridge ")) parts := strings.SplitN(rest, " ", 2) cmd = parts[0] if len(parts) > 1 { arg = strings.TrimSpace(parts[1]) } return }