package notification import ( "bytes" "encoding/json" "fmt" "log/slog" "net/http" "strings" "prism/internal/signal" ) type Dispatcher struct { store *Store signalClient *signal.Client logger *slog.Logger } func NewDispatcher(store *Store, signalClient *signal.Client, logger *slog.Logger) *Dispatcher { return &Dispatcher{ store: store, signalClient: signalClient, logger: logger, } } func (d *Dispatcher) Send(endpoint string, notif Notification) error { mapping, err := d.store.GetEndpointMapping(endpoint) if err != nil { return fmt.Errorf("failed to get mapping: %w", err) } if mapping == nil { appName := strings.TrimPrefix(endpoint, "ntfy-") appName = strings.TrimPrefix(appName, "proton-") if err := d.store.Register(endpoint, appName, ChannelSignal, nil, nil); err != nil { return fmt.Errorf("failed to register endpoint: %w", err) } mapping, err = d.store.GetEndpointMapping(endpoint) if err != nil { return fmt.Errorf("failed to get mapping after registration: %w", err) } } switch mapping.Channel { case ChannelWebhook: return d.sendWebhook(mapping, notif) case ChannelSignal: return d.sendSignal(mapping, notif) default: return fmt.Errorf("unknown channel: %s", mapping.Channel) } } func (d *Dispatcher) sendSignal(mapping *Mapping, notif Notification) error { groupID := mapping.GroupID if groupID == nil || *groupID == "" { newGroupID, err := d.createGroup(mapping.AppName) if err != nil { return fmt.Errorf("failed to create group: %w", err) } groupID = &newGroupID if err := d.store.UpdateGroupID(mapping.Endpoint, *groupID); err != nil { d.logger.Warn("Failed to update group ID", "error", err) } } if err := d.sendGroupMessage(*groupID, notif); err != nil { return fmt.Errorf("failed to send group message: %w", err) } d.logger.Debug("Sent Signal notification", "app", mapping.AppName, "message", notif.Message) return nil } func (d *Dispatcher) createGroup(appName string) (string, error) { params := map[string]interface{}{ "name": appName, } result, err := d.signalClient.Call("createGroup", params) if err != nil { return "", err } var response struct { GroupID string `json:"groupId"` } if err := json.Unmarshal(result, &response); err != nil { return "", fmt.Errorf("failed to parse createGroup response: %w", err) } return response.GroupID, nil } func (d *Dispatcher) sendGroupMessage(groupID string, notif Notification) error { message := notif.Message if notif.Title != "" { message = fmt.Sprintf("%s\n\n%s", notif.Title, notif.Message) } params := map[string]interface{}{ "groupId": groupID, "message": message, } _, err := d.signalClient.Call("sendGroupMessage", params) return err } func (d *Dispatcher) sendWebhook(mapping *Mapping, notif Notification) error { if mapping.UpEndpoint == nil || *mapping.UpEndpoint == "" { return fmt.Errorf("webhook endpoint not configured for %s", mapping.AppName) } payload, err := json.Marshal(notif) if err != nil { return fmt.Errorf("failed to marshal notification: %w", err) } resp, err := http.Post(*mapping.UpEndpoint, "application/json", bytes.NewReader(payload)) if err != nil { return fmt.Errorf("failed to send webhook: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("webhook returned status %d", resp.StatusCode) } d.logger.Debug("Sent webhook notification", "app", mapping.AppName, "endpoint", *mapping.UpEndpoint) return nil }