Files
honeyDueAPI/internal/push/gorush.go
Trey t 1f12f3f62a Initial commit: MyCrib API in Go
Complete rewrite of Django REST API to Go with:
- Gin web framework for HTTP routing
- GORM for database operations
- GoAdmin for admin panel
- Gorush integration for push notifications
- Redis for caching and job queues

Features implemented:
- User authentication (login, register, logout, password reset)
- Residence management (CRUD, sharing, share codes)
- Task management (CRUD, kanban board, completions)
- Contractor management (CRUD, specialties)
- Document management (CRUD, warranties)
- Notifications (preferences, push notifications)
- Subscription management (tiers, limits)

Infrastructure:
- Docker Compose for local development
- Database migrations and seed data
- Admin panel for data management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-26 20:07:16 -06:00

200 lines
5.5 KiB
Go

package push
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/rs/zerolog/log"
"github.com/treytartt/mycrib-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
}