Files
Messenger/server/internal/api/push.go
T
vantablackTea 6fc6d977bf
CI / Go (build + vet + test) (push) Canceled after 0s
CI / Web (build + lint) (push) Canceled after 0s
added media fixes, search, push updates, pwa, metrics. preparing release 1.0
2026-09-27 23:26:15 +03:00

130 lines
3.5 KiB
Go

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)
}
}