network/pkg/gateway/push_notifications.go
anonpenguin23 b983066016 refactor: rename DeBros to Orama and update configuration paths
- Replaced all instances of DeBros with Orama throughout the codebase, including CLI commands and configuration paths.
- Updated documentation to reflect the new naming convention and paths for configuration files.
- Removed the outdated PRODUCTION_INSTALL.md file and added new scripts for local domain setup and testing.
- Introduced a new interactive TUI installer for Orama Network, enhancing the installation experience.
- Improved logging and error handling across various components to provide clearer feedback during operations.
2025-11-26 15:36:11 +02:00

184 lines
5.0 KiB
Go

package gateway
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"go.uber.org/zap"
)
// PushNotificationService handles sending push notifications via Expo
type PushNotificationService struct {
logger *zap.Logger
client *http.Client
}
// ExpoTicket represents the response from Expo API
type ExpoTicket struct {
ID string `json:"id"`
Error string `json:"error,omitempty"`
}
// ExpoPushMessage represents a message to send via Expo
type ExpoPushMessage struct {
To string `json:"to"`
Title string `json:"title"`
Body string `json:"body"`
Data map[string]interface{} `json:"data,omitempty"`
Sound string `json:"sound,omitempty"`
Badge int `json:"badge,omitempty"`
Priority string `json:"priority,omitempty"`
// iOS specific
MutableContent bool `json:"mutableContent,omitempty"`
IosIcon string `json:"iosIcon,omitempty"`
// Android specific
AndroidBigLargeIcon string `json:"androidBigLargeIcon,omitempty"`
ChannelID string `json:"channelId,omitempty"`
}
// NewPushNotificationService creates a new push notification service
func NewPushNotificationService(logger *zap.Logger) *PushNotificationService {
return &PushNotificationService{
logger: logger,
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// SendNotification sends a push notification via Expo
func (pns *PushNotificationService) SendNotification(
ctx context.Context,
expoPushToken string,
title string,
body string,
data map[string]interface{},
avatarURL string,
) error {
if expoPushToken == "" {
return fmt.Errorf("empty expo push token")
}
message := ExpoPushMessage{
To: expoPushToken,
Title: title,
Body: body,
Data: data,
Sound: "default",
Priority: "high",
// Enable mutable content for iOS to allow Notification Service Extension
MutableContent: true,
ChannelID: "messages",
AndroidBigLargeIcon: avatarURL,
}
// For iOS, include avatar in data so Notification Service Extension can fetch it
if avatarURL != "" {
if message.Data == nil {
message.Data = make(map[string]interface{})
}
message.Data["avatar_url"] = avatarURL
}
return pns.sendExpoRequest(ctx, message)
}
// SendBulkNotifications sends notifications to multiple users
func (pns *PushNotificationService) SendBulkNotifications(
ctx context.Context,
expoPushTokens []string,
title string,
body string,
data map[string]interface{},
avatarURL string,
) []error {
errors := make([]error, 0)
for _, token := range expoPushTokens {
if err := pns.SendNotification(ctx, token, title, body, data, avatarURL); err != nil {
errors = append(errors, fmt.Errorf("failed to send to token %s: %w", token, err))
}
}
return errors
}
// sendExpoRequest sends a request to the Expo push notification API
func (pns *PushNotificationService) sendExpoRequest(ctx context.Context, message ExpoPushMessage) error {
const expoAPIURL = "https://exp.host/--/api/v2/push/send"
body, err := json.Marshal(message)
if err != nil {
pns.logger.Error("failed to marshal push notification",
zap.Error(err),
zap.String("to", message.To))
return fmt.Errorf("marshal error: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, expoAPIURL, bytes.NewBuffer(body))
if err != nil {
pns.logger.Error("failed to create push notification request",
zap.Error(err),
zap.String("to", message.To))
return fmt.Errorf("request creation error: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := pns.client.Do(req)
if err != nil {
pns.logger.Error("failed to send push notification",
zap.Error(err),
zap.String("to", message.To))
return fmt.Errorf("send error: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
pns.logger.Error("failed to read push notification response",
zap.Error(err),
zap.String("to", message.To))
return fmt.Errorf("response read error: %w", err)
}
// Check for API errors
if resp.StatusCode != http.StatusOK {
pns.logger.Warn("push notification API error",
zap.Int("status_code", resp.StatusCode),
zap.String("response", string(respBody)),
zap.String("to", message.To))
return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(respBody))
}
// Parse response
var tickets []ExpoTicket
if err := json.Unmarshal(respBody, &tickets); err != nil {
pns.logger.Error("failed to parse push notification response",
zap.Error(err),
zap.String("response", string(respBody)))
return fmt.Errorf("parse error: %w", err)
}
// Check for errors in tickets
for _, ticket := range tickets {
if ticket.Error != "" {
pns.logger.Warn("push notification error in ticket",
zap.String("error", ticket.Error),
zap.String("to", message.To))
return fmt.Errorf("ticket error: %s", ticket.Error)
}
}
pns.logger.Info("push notification sent successfully",
zap.String("to", message.To),
zap.String("title", message.Title))
return nil
}