package main import ( "context" "fmt" "log" "strings" "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" ) type MatrixHandler struct { AS *appservice.AppService DB *Database Ntfy *NtfyClient Config *BridgeConfig HSDomain string GhostPrefix string } func (h *MatrixHandler) HandleEvent(ctx context.Context, evt *event.Event) { if evt.Sender == h.AS.BotMXID() { return } if h.GhostPrefix != "" && strings.HasPrefix(string(evt.Sender), h.GhostPrefix) { return } switch evt.Type { case event.EventMessage: h.handleMessage(ctx, evt) case event.StateMember: h.handleMembership(ctx, evt) } } func (h *MatrixHandler) handleMessage(ctx context.Context, evt *event.Event) { msg := evt.Content.AsMessage() if msg == nil { return } text := strings.TrimSpace(msg.Body) if strings.HasPrefix(text, "bridge ") { h.handleCommand(ctx, evt, text) return } topic, err := h.DB.GetBridgeByRoom(string(evt.RoomID)) if err != nil { log.Printf("[matrix] db lookup error for room %s: %v", evt.RoomID, err) return } if topic == "" { return } if err := h.Ntfy.PublishToNtfy(topic, text); err != nil { botIntent := h.AS.BotIntent() botIntent.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Failed to publish to ntfy: %v", err)) log.Printf("[matrix] publish to ntfy failed: %v", err) } } func (h *MatrixHandler) handleCommand(ctx context.Context, evt *event.Event, text string) { parts := strings.SplitN(text, " ", 3) if len(parts) < 2 { return } switch parts[1] { case "create": topicName := "" if len(parts) > 2 { topicName = strings.TrimSpace(parts[2]) } h.handleCreateBridge(ctx, evt, topicName) default: botIntent := h.AS.BotIntent() botIntent.SendNotice(ctx, evt.RoomID, "Available commands:\n bridge create — Bridge an ntfy topic to a new Matrix room") } } func (h *MatrixHandler) handleCreateBridge(ctx context.Context, evt *event.Event, topicName string) { botIntent := h.AS.BotIntent() if topicName == "" { botIntent.SendNotice(ctx, evt.RoomID, "Usage: bridge create ") return } if !h.Config.HasPermission(string(evt.Sender), "admin") { botIntent.SendNotice(ctx, evt.RoomID, "You do not have permission to create bridges. Admin access required.") return } existing, err := h.DB.GetBridgeByTopic(topicName) if err != nil { log.Printf("[matrix] db error checking topic: %v", err) botIntent.SendNotice(ctx, evt.RoomID, "Internal database error.") return } if existing != "" { botIntent.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Topic '%s' is already bridged to room %s.", topicName, existing)) return } spaceRoomID, err := h.DB.GetSpaceRoomID(string(evt.Sender)) if err != nil { log.Printf("[matrix] db error checking space: %v", err) botIntent.SendNotice(ctx, evt.RoomID, "Internal database error.") return } if spaceRoomID == "" { resp, err := botIntent.Client.CreateRoom(ctx, &mautrix.ReqCreateRoom{ Name: "Notifications", Preset: "private_chat", CreationContent: map[string]interface{}{ "type": "m.space", }, Invite: []id.UserID{evt.Sender}, }) if err != nil { botIntent.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Failed to create Notifications space: %v", err)) return } spaceRoomID = string(resp.RoomID) if err := h.DB.SetSpaceRoomID(string(evt.Sender), spaceRoomID); err != nil { log.Printf("[matrix] failed to save space: %v", err) botIntent.SendNotice(ctx, evt.RoomID, "Failed to save space reference to database.") return } userIntent := h.AS.Intent(evt.Sender) userIntent.IsCustomPuppet = true if err := userIntent.EnsureJoined(ctx, id.RoomID(spaceRoomID)); err != nil { log.Printf("[matrix] double puppet join space failed: %v", err) } botIntent.SendNotice(ctx, evt.RoomID, "Created 'Notifications' space.") } roomResp, err := botIntent.Client.CreateRoom(ctx, &mautrix.ReqCreateRoom{ Name: topicName, Topic: fmt.Sprintf("ntfy topic: %s", topicName), Preset: "private_chat", Invite: []id.UserID{evt.Sender}, }) if err != nil { botIntent.SendNotice(ctx, evt.RoomID, fmt.Sprintf("Failed to create bridged room: %v", err)) return } bridgeRoomID := roomResp.RoomID viaServers := []string{h.HSDomain} if _, err := botIntent.SendStateEvent(ctx, id.RoomID(spaceRoomID), event.StateSpaceChild, string(bridgeRoomID), &event.SpaceChildEventContent{ Via: viaServers, }); err != nil { log.Printf("[matrix] failed to add room to space: %v", err) } userIntent := h.AS.Intent(evt.Sender) userIntent.IsCustomPuppet = true if err := userIntent.EnsureJoined(ctx, bridgeRoomID); err != nil { log.Printf("[matrix] double puppet join bridge room failed: %v", err) } if err := h.DB.SetBridge(topicName, string(bridgeRoomID)); err != nil { log.Printf("[matrix] failed to save bridge: %v", err) botIntent.SendNotice(ctx, evt.RoomID, "Bridge created but failed to save to database.") return } h.Ntfy.StartNtfyListener(topicName, string(bridgeRoomID), h.AS.BotIntent(), h.AS.BotMXID()) botIntent.SendNotice(ctx, evt.RoomID, fmt.Sprintf( "Bridge created!\n• Topic: %s\n• Room: %s\nMessages sent in that room will be published to ntfy, and ntfy messages will appear there.", topicName, bridgeRoomID, )) } func (h *MatrixHandler) handleMembership(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.AS.BotMXID() { botIntent := h.AS.BotIntent() if err := botIntent.EnsureJoined(ctx, evt.RoomID); err != nil { log.Printf("[matrix] failed to auto-join room %s: %v", evt.RoomID, err) } } }