feat: add web push and sadhna tracker
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
webpush "github.com/SherClockHolmes/webpush-go"
|
||||
"github.com/prabhupadasays/spsays-bff/internal/repository"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// PushPayload is the JSON the service worker receives in its `push` event.
|
||||
type PushPayload struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
}
|
||||
|
||||
// PushSender delivers Web Push messages via VAPID. Construct with
|
||||
// NewPushSender; it returns nil when VAPID isn't configured, and every method
|
||||
// is nil-safe so callers needn't branch.
|
||||
type PushSender struct {
|
||||
repo *repository.PushRepo
|
||||
pub string
|
||||
priv string
|
||||
subject string
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewPushSender returns nil if the VAPID keys are absent (push disabled).
|
||||
func NewPushSender(repo *repository.PushRepo, publicKey, privateKey, subject string, logger *zap.Logger) *PushSender {
|
||||
if publicKey == "" || privateKey == "" || repo == nil {
|
||||
return nil
|
||||
}
|
||||
return &PushSender{repo: repo, pub: publicKey, priv: privateKey, subject: subject, logger: logger}
|
||||
}
|
||||
|
||||
// PublicKey is the VAPID application server key the browser subscribes with.
|
||||
func (p *PushSender) PublicKey() string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return p.pub
|
||||
}
|
||||
|
||||
// Enabled reports whether push delivery is configured.
|
||||
func (p *PushSender) Enabled() bool { return p != nil }
|
||||
|
||||
// SendToUser pushes a payload to every subscription the user has, pruning any
|
||||
// endpoint the push service reports as gone (404/410). Best-effort: failures
|
||||
// are logged, never returned.
|
||||
func (p *PushSender) SendToUser(ctx context.Context, userID string, payload PushPayload) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
subs, err := p.repo.ListByUser(ctx, userID)
|
||||
if err != nil {
|
||||
p.logger.Warn("push: list subscriptions failed", zap.String("user", userID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
if len(subs) == 0 {
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, s := range subs {
|
||||
sub := &webpush.Subscription{
|
||||
Endpoint: s.Endpoint,
|
||||
Keys: webpush.Keys{P256dh: s.P256dh, Auth: s.Auth},
|
||||
}
|
||||
resp, err := webpush.SendNotification(body, sub, &webpush.Options{
|
||||
Subscriber: p.subject,
|
||||
VAPIDPublicKey: p.pub,
|
||||
VAPIDPrivateKey: p.priv,
|
||||
TTL: 60,
|
||||
Urgency: webpush.UrgencyNormal,
|
||||
})
|
||||
if err != nil {
|
||||
p.logger.Warn("push: send failed", zap.String("user", userID), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
status := resp.StatusCode
|
||||
_ = resp.Body.Close()
|
||||
// Subscription expired/gone — drop it so we stop trying.
|
||||
if status == http.StatusNotFound || status == http.StatusGone {
|
||||
if derr := p.repo.DeleteByEndpoint(ctx, s.Endpoint); derr != nil {
|
||||
p.logger.Warn("push: prune failed", zap.Error(derr))
|
||||
}
|
||||
} else if status >= 400 {
|
||||
p.logger.Warn("push: non-2xx", zap.Int("status", status), zap.String("user", userID))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user