package api import ( "context" "encoding/json" "net/http" "strings" "github.com/SherClockHolmes/webpush-go" "messenger/internal/realtime" "messenger/internal/store" ) type pushRequest struct { Endpoint string `json:"endpoint"` Keys struct { P256dh string `json:"p256dh"` Auth string `json:"auth"` } `json:"keys"` } func (h *apiHandler) subscribePush(w http.ResponseWriter, r *http.Request) { userID, _ := userIDFrom(r.Context()) var req pushRequest if !decodeJSON(w, r, &req) { return } if req.Endpoint == "" || req.Keys.P256dh == "" || req.Keys.Auth == "" { writeError(w, http.StatusBadRequest, "endpoint, p256dh and auth are required") return } _, err := h.store.CreatePushSubscription(r.Context(), store.PushSubscription{ UserID: userID, Endpoint: req.Endpoint, P256dh: req.Keys.P256dh, Auth: req.Keys.Auth, }) if err != nil { writeError(w, http.StatusInternalServerError, "internal server error") return } writeJSON(w, http.StatusCreated, map[string]string{"status": "ok"}) } func (h *apiHandler) unsubscribePush(w http.ResponseWriter, r *http.Request) { userID, _ := userIDFrom(r.Context()) var req pushRequest if !decodeJSON(w, r, &req) { return } if req.Endpoint == "" { writeError(w, http.StatusBadRequest, "endpoint is required") return } if err := h.store.DeletePushSubscription(r.Context(), userID, req.Endpoint); err != nil { writeError(w, http.StatusInternalServerError, "internal server error") return } writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } type pushNotifier struct { store *store.Store hub *realtime.Hub public string private string subject string } func newPushNotifier(st *store.Store, hub *realtime.Hub, public, private, subject string) *pushNotifier { if public == "" || private == "" { return nil } return &pushNotifier{store: st, hub: hub, public: public, private: private, subject: subject} } // notifyMembers pushes a message notification to offline members. // Failed or stale endpoints are cleaned up asynchronously. func (n *pushNotifier) notifyMembers(ctx context.Context, conversationID, senderID, messageID int64, senderName, body string) { members, err := n.store.ListMembers(ctx, conversationID) if err != nil { return } title := senderName text := strings.TrimSpace(body) if text == "" { text = "📎 Вложение" } if len(text) > 140 { text = text[:140] + "…" } payload, _ := json.Marshal(map[string]any{ "title": title, "body": text, "conversation_id": conversationID, "message_id": messageID, }) for _, m := range members { if m.User.ID == senderID || n.hub.IsOnline(m.User.ID) { continue } subs, err := n.store.ListPushSubscriptionsForUser(ctx, m.User.ID) if err != nil { continue } for _, sub := range subs { go n.send(ctx, m.User.ID, sub, payload) } } } func (n *pushNotifier) send(ctx context.Context, userID int64, sub store.PushSubscription, payload []byte) { wp := &webpush.Subscription{Endpoint: sub.Endpoint, Keys: webpush.Keys{P256dh: sub.P256dh, Auth: sub.Auth}} resp, err := webpush.SendNotificationWithContext(ctx, payload, wp, &webpush.Options{ VAPIDPublicKey: n.public, VAPIDPrivateKey: n.private, Subscriber: n.subject, TTL: 24 * 60 * 60, }) if err != nil { return } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusGone { _ = n.store.DeletePushSubscription(context.Background(), userID, sub.Endpoint) } }