Files
Warp/server/internal/syncapi/notifications.go
T
tajniak81andClaude Opus 5.5 b507753172 Phase 8: push and notifications
Implements PLAN.md Phase 8. internal/pushrules is the rule language: the
spec's default ruleset, all six condition kinds, glob and word-boundary
matching, intentional mentions switching off the legacy body rules, and
rule ordering as Synapse has it. internal/pushserver keeps the rules,
pushers and notifications and talks to push gateways.

Push rules are the m.push_rules account data, written only by the push
server; reads keep the user's own rules and the enabled/actions of each
default and take the rest from the current defaults, so new default rules
reach every user without a migration. PUT account_data/m.push_rules is
refused with 405.

The push server listens on the roomserver's emit stage and evaluates each
event on its own worker for every local member and an invite's target.
Notifications and per-room unread counts are stored in migration 0010;
counts are a seventh sync token stream, so /sync reports
unread_notifications and picks up count changes made after the event or by
a receipt from another device. m.read and m.read.private receipts clear
the counts; leaving a room clears it.

Pushers (http kind only) push through a bounded sender pool with retries,
honour event_id_only, drop push keys the gateway rejects, and go away with
their device. GET /notifications pages the history. New config section
push: enabled, include_content, gateway_timeout, notification_retention.

Account data writes now wake the user's blocked syncs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:43:36 +02:00

52 lines
1.5 KiB
Go

package syncapi
import (
"context"
"slices"
)
// pushRulesType is the account data push rules travel as. The push server
// renders it rather than the account data store, because a user who never
// changed a rule has none stored and still has the server defaults.
const pushRulesType = "m.push_rules"
// UnreadNotifications is a joined room's unread counts for the user
// syncing.
type UnreadNotifications struct {
NotificationCount int `json:"notification_count"`
HighlightCount int `json:"highlight_count"`
}
// unreadNotifications fills in the counts of every joined room in the
// response, in one read.
//
// Every joined room reported gets its counts, not only the ones whose
// counts moved: a room is in a response because something in it changed,
// and a client showing a new message wants the count beside it whether or
// not this particular sync is the one that moved it.
func (s *API) unreadNotifications(ctx context.Context, userID string, resp *Response) error {
if s.push == nil || len(resp.Rooms.Join) == 0 {
return nil
}
roomIDs := make([]string, 0, len(resp.Rooms.Join))
for roomID := range resp.Rooms.Join {
roomIDs = append(roomIDs, roomID)
}
slices.Sort(roomIDs)
counts, err := s.push.NotificationCounts(ctx, userID, roomIDs)
if err != nil {
return err
}
for _, roomID := range roomIDs {
room := resp.Rooms.Join[roomID]
c := counts[roomID]
room.UnreadNotifications = &UnreadNotifications{
NotificationCount: c.Notifications,
HighlightCount: c.Highlights,
}
resp.Rooms.Join[roomID] = room
}
return nil
}