Files
honeyDueAPI/internal/push/fcm.go
Trey t 4976eafc6c Rebrand from Casera/MyCrib to honeyDue
Total rebrand across all Go API source files:
- Go module path: casera-api -> honeydue-api
- All imports updated (130+ files)
- Docker: containers, images, networks renamed
- Email templates: support email, noreply, icon URL
- Domains: casera.app/mycrib.treytartt.com -> honeyDue.treytartt.com
- Bundle IDs: com.tt.casera -> com.tt.honeyDue
- IAP product IDs updated
- Landing page, admin panel, config defaults
- Seeds, CI workflows, Makefile, docs
- Database table names preserved (no migration needed)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 06:33:38 -06:00

143 lines
3.6 KiB
Go

package push
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/rs/zerolog/log"
"github.com/treytartt/honeydue-api/internal/config"
)
const fcmEndpoint = "https://fcm.googleapis.com/fcm/send"
// FCMClient handles direct communication with Firebase Cloud Messaging
type FCMClient struct {
serverKey string
httpClient *http.Client
}
// FCMMessage represents an FCM message payload
type FCMMessage struct {
To string `json:"to,omitempty"`
RegistrationIDs []string `json:"registration_ids,omitempty"`
Notification *FCMNotification `json:"notification,omitempty"`
Data map[string]string `json:"data,omitempty"`
Priority string `json:"priority,omitempty"`
ContentAvailable bool `json:"content_available,omitempty"`
}
// FCMNotification represents the notification payload
type FCMNotification struct {
Title string `json:"title,omitempty"`
Body string `json:"body,omitempty"`
Sound string `json:"sound,omitempty"`
Badge string `json:"badge,omitempty"`
Icon string `json:"icon,omitempty"`
}
// FCMResponse represents the FCM API response
type FCMResponse struct {
MulticastID int64 `json:"multicast_id"`
Success int `json:"success"`
Failure int `json:"failure"`
CanonicalIDs int `json:"canonical_ids"`
Results []FCMResult `json:"results"`
}
// FCMResult represents a single result in the FCM response
type FCMResult struct {
MessageID string `json:"message_id,omitempty"`
RegistrationID string `json:"registration_id,omitempty"`
Error string `json:"error,omitempty"`
}
// NewFCMClient creates a new FCM client
func NewFCMClient(cfg *config.PushConfig) (*FCMClient, error) {
if cfg.FCMServerKey == "" {
return nil, fmt.Errorf("FCM server key not configured")
}
return &FCMClient{
serverKey: cfg.FCMServerKey,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}, nil
}
// Send sends a push notification to Android devices
func (c *FCMClient) Send(ctx context.Context, tokens []string, title, message string, data map[string]string) error {
if len(tokens) == 0 {
return nil
}
msg := FCMMessage{
RegistrationIDs: tokens,
Notification: &FCMNotification{
Title: title,
Body: message,
Sound: "default",
},
Data: data,
Priority: "high",
}
body, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("failed to marshal FCM message: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", fcmEndpoint, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("failed to create FCM request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "key="+c.serverKey)
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send FCM request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("FCM returned status %d", resp.StatusCode)
}
var fcmResp FCMResponse
if err := json.NewDecoder(resp.Body).Decode(&fcmResp); err != nil {
return fmt.Errorf("failed to decode FCM response: %w", err)
}
// Log individual results
for i, result := range fcmResp.Results {
if i >= len(tokens) {
break
}
if result.Error != "" {
log.Error().
Str("token", truncateToken(tokens[i])).
Str("error", result.Error).
Msg("FCM notification failed")
}
}
log.Info().
Int("total", len(tokens)).
Int("success", fcmResp.Success).
Int("failure", fcmResp.Failure).
Msg("FCM batch send complete")
if fcmResp.Success == 0 && fcmResp.Failure > 0 {
return fmt.Errorf("all FCM notifications failed")
}
return nil
}