feat: realtime status, mentions, push notifications

This commit is contained in:
2026-06-29 15:52:34 -04:00
parent 282a436995
commit 6c8defb6fd
10 changed files with 387 additions and 34 deletions
+71
View File
@@ -183,6 +183,77 @@ func (h *Handler) SendPush(ctx context.Context, userID string, payload map[strin
}
}
// SendChannelNotification sends a push notification to all server members (except the author)
// when a new message is posted in a channel.
func (h *Handler) SendChannelNotification(ctx context.Context, channelID, authorID, channelName, content string) {
if h.vapidPub == "" || h.vapidPriv == "" {
return
}
var serverID string
err := h.db.QueryRowContext(ctx, `SELECT server_id FROM channels WHERE id = $1`, channelID).Scan(&serverID)
if err != nil {
h.logger.Error("failed to find channel server", "error", err)
return
}
body := content
if len(body) > 200 {
body = body[:200] + "..."
}
payload, _ := json.Marshal(map[string]interface{}{
"title": "New message in #" + channelName,
"body": body,
"url": "/channels/" + channelID,
})
rows, err := h.db.QueryContext(ctx, `
SELECT DISTINCT ps.user_id, ps.endpoint, ps.p256dh, ps.auth
FROM push_subscriptions ps
JOIN members m ON m.user_id = ps.user_id
WHERE m.server_id = $1 AND ps.user_id != $2
`, serverID, authorID)
if err != nil {
h.logger.Error("failed to query push subscriptions", "error", err)
return
}
defer rows.Close()
for rows.Next() {
var userID, endpoint, p256dh, auth string
if err := rows.Scan(&userID, &endpoint, &p256dh, &auth); err != nil {
continue
}
sub := &webpush.Subscription{
Endpoint: endpoint,
Keys: webpush.Keys{
P256dh: p256dh,
Auth: auth,
},
}
resp, err := webpush.SendNotification(payload, sub, &webpush.Options{
Subscriber: h.vapidSubj,
VAPIDPublicKey: h.vapidPub,
VAPIDPrivateKey: h.vapidPriv,
TTL: 30,
})
if err != nil {
h.logger.Error("failed to send push", "error", err, "user_id", userID)
if resp != nil && resp.StatusCode == 410 {
h.db.ExecContext(ctx, `DELETE FROM push_subscriptions WHERE endpoint = $1`, endpoint)
}
continue
}
if resp != nil {
resp.Body.Close()
}
}
}
// GenerateVAPIDKeys generates a new VAPID key pair.
func GenerateVAPIDKeys() (publicKey, privateKey string, err error) {
privateKeyBytes := make([]byte, 32)