package push import ( "bytes" "context" "encoding/json" "fmt" "net/http" "time" "github.com/rs/zerolog/log" "github.com/treytartt/casera-api/internal/config" ) // Platform constants const ( PlatformIOS = "ios" PlatformAndroid = "android" ) // GorushClient handles communication with Gorush server type GorushClient struct { baseURL string httpClient *http.Client config *config.PushConfig } // NewGorushClient creates a new Gorush client func NewGorushClient(cfg *config.PushConfig) *GorushClient { return &GorushClient{ baseURL: cfg.GorushURL, httpClient: &http.Client{ Timeout: 30 * time.Second, }, config: cfg, } } // PushNotification represents a push notification request type PushNotification struct { Tokens []string `json:"tokens"` Platform int `json:"platform"` // 1 = iOS, 2 = Android Message string `json:"message"` Title string `json:"title,omitempty"` Topic string `json:"topic,omitempty"` // iOS bundle ID Badge *int `json:"badge,omitempty"` // iOS badge count Sound string `json:"sound,omitempty"` // Notification sound ContentAvailable bool `json:"content_available,omitempty"` // iOS background notification MutableContent bool `json:"mutable_content,omitempty"` // iOS mutable content Data map[string]string `json:"data,omitempty"` // Custom data payload Priority string `json:"priority,omitempty"` // high or normal ThreadID string `json:"thread_id,omitempty"` // iOS thread grouping CollapseKey string `json:"collapse_key,omitempty"` // Android collapse key } // GorushRequest represents the full Gorush API request type GorushRequest struct { Notifications []PushNotification `json:"notifications"` } // GorushResponse represents the Gorush API response type GorushResponse struct { Counts int `json:"counts"` Logs []GorushLog `json:"logs,omitempty"` Success string `json:"success,omitempty"` } // GorushLog represents a log entry from Gorush type GorushLog struct { Type string `json:"type"` Platform string `json:"platform"` Token string `json:"token"` Message string `json:"message"` Error string `json:"error,omitempty"` } // SendToIOS sends a push notification to iOS devices func (c *GorushClient) SendToIOS(ctx context.Context, tokens []string, title, message string, data map[string]string) error { if len(tokens) == 0 { return nil } notification := PushNotification{ Tokens: tokens, Platform: 1, // iOS Title: title, Message: message, Topic: c.config.APNSTopic, Sound: "default", MutableContent: true, Data: data, Priority: "high", } return c.send(ctx, notification) } // SendToAndroid sends a push notification to Android devices func (c *GorushClient) SendToAndroid(ctx context.Context, tokens []string, title, message string, data map[string]string) error { if len(tokens) == 0 { return nil } notification := PushNotification{ Tokens: tokens, Platform: 2, // Android Title: title, Message: message, Data: data, Priority: "high", } return c.send(ctx, notification) } // SendToAll sends a push notification to both iOS and Android devices func (c *GorushClient) SendToAll(ctx context.Context, iosTokens, androidTokens []string, title, message string, data map[string]string) error { var errs []error if len(iosTokens) > 0 { if err := c.SendToIOS(ctx, iosTokens, title, message, data); err != nil { errs = append(errs, fmt.Errorf("iOS: %w", err)) } } if len(androidTokens) > 0 { if err := c.SendToAndroid(ctx, androidTokens, title, message, data); err != nil { errs = append(errs, fmt.Errorf("Android: %w", err)) } } if len(errs) > 0 { return fmt.Errorf("push notification errors: %v", errs) } return nil } // send sends the notification to Gorush func (c *GorushClient) send(ctx context.Context, notification PushNotification) error { req := GorushRequest{ Notifications: []PushNotification{notification}, } body, err := json.Marshal(req) if err != nil { return fmt.Errorf("failed to marshal request: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/api/push", bytes.NewReader(body)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } httpReq.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(httpReq) if err != nil { return fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("gorush returned status %d", resp.StatusCode) } var gorushResp GorushResponse if err := json.NewDecoder(resp.Body).Decode(&gorushResp); err != nil { return fmt.Errorf("failed to decode response: %w", err) } log.Debug(). Int("counts", gorushResp.Counts). Int("tokens", len(notification.Tokens)). Msg("Push notification sent") return nil } // HealthCheck checks if Gorush is healthy func (c *GorushClient) HealthCheck(ctx context.Context) error { httpReq, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/stat/go", nil) if err != nil { return err } resp, err := c.httpClient.Do(httpReq) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("gorush health check failed: status %d", resp.StatusCode) } return nil }