Files
Warp/server/internal/syncapi/token.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

152 lines
5.0 KiB
Go

package syncapi
import (
"fmt"
"strconv"
"strings"
"github.com/dariuszjaniak/warp/server/internal/storage"
)
// Token is a position in every stream a sync response carries.
//
// A sync response is assembled from streams that move independently: room
// events, read receipts, to-device messages, account data and presence. A
// client that has seen receipt 7 but only event 4 has to be able to say so
// in the one opaque string the API gives it, so the token names a position
// in each.
//
// The wire form is
//
// s{pdu}_{receipt}_{to_device}_{account_data}_{presence}_{device_list}_{notification}
//
// with trailing zeroes left off, so a client that has only ever seen room
// events carries "s12" rather than "s12_0_0_0_0_0_0". The device list
// position came with Phase 7 and the notification count position with
// Phase 8, each added at the end, which is what lets every token handed
// out before them go on parsing. The leading "s" is there
// to make a token recognisable in a log and to leave room for a second
// token shape later — Simplified Sliding Sync (PLAN.md phase 10) has its
// own — without any ambiguity about which is which.
//
// The spec says only that the token is opaque, and clients treat it that
// way. It is still worth being readable: a token is the first thing in
// every bug report about /sync.
type Token struct {
PDU int64
Receipt int64
ToDevice int64
AccountData int64
Presence int64
DeviceList int64
// Notification is the push server's notification count stream.
Notification int64
}
// tokenPositions is how many positions a token carries.
const tokenPositions = 7
// tokenPrefix marks a classic /sync token.
const tokenPrefix = "s"
// IsInitial reports whether the token is the start of every stream, which
// is what a sync with no ?since= means.
func (t Token) IsInitial() bool { return t == Token{} }
// String renders the wire form.
func (t Token) String() string {
parts := []int64{t.PDU, t.Receipt, t.ToDevice, t.AccountData, t.Presence, t.DeviceList, t.Notification}
// Trailing zeroes carry no information, and leaving them off keeps the
// common token short.
last := 0
for i, p := range parts {
if p != 0 {
last = i
}
}
var b strings.Builder
b.WriteString(tokenPrefix)
for i, p := range parts[:last+1] {
if i > 0 {
b.WriteByte('_')
}
b.WriteString(strconv.FormatInt(p, 10))
}
return b.String()
}
// ParseToken reads a token a client sent back.
//
// An empty string is the initial token: /sync without ?since= and
// /messages without ?from= both mean "from the beginning", and refusing
// the empty string would make every caller special-case it.
//
// A token with fewer fields than this build writes is accepted and the
// missing ones are zero. That is what lets a token stored by a client
// before a stream existed go on working: the stream it did not know about
// reads as "seen nothing", so the client is sent that stream in full once
// and is in step from then on.
func ParseToken(raw string) (Token, error) {
if raw == "" {
return Token{}, nil
}
body, ok := strings.CutPrefix(raw, tokenPrefix)
if !ok {
return Token{}, fmt.Errorf("%q is not a sync token: it must start with %q", raw, tokenPrefix)
}
fields := strings.Split(body, "_")
if len(fields) > tokenPositions {
return Token{}, fmt.Errorf("%q is not a sync token: it has %d positions, this server writes at most %d",
raw, len(fields), tokenPositions)
}
var positions [tokenPositions]int64
for i, field := range fields {
value, err := strconv.ParseInt(field, 10, 64)
// The PDU position alone may be negative: /messages hands out
// positions inside history backfilled from other servers, which
// is numbered downwards from -1. Every other stream starts at 0.
if err != nil || (value < 0 && i > 0) {
return Token{}, fmt.Errorf("%q is not a sync token: position %d is %q", raw, i+1, field)
}
positions[i] = value
}
return Token{
PDU: positions[0],
Receipt: positions[1],
ToDevice: positions[2],
AccountData: positions[3],
Presence: positions[4],
DeviceList: positions[5],
Notification: positions[6],
}, nil
}
// tokenAt is the token naming the top of every stream, which is what a
// sync response hands back when it has drained all of them.
func tokenAt(positions storage.StreamPositions) Token {
return Token{
PDU: positions.PDU,
Receipt: positions.Receipt,
ToDevice: positions.ToDevice,
AccountData: positions.AccountData,
Presence: positions.Presence,
DeviceList: positions.DeviceList,
Notification: positions.Notification,
}
}
// pduToken is the token for a position in the room event stream alone.
//
// It is what /messages and a room's prev_batch hand out: both are about
// room events only, and a token from either has to be usable as a ?since=
// for /sync, where the other streams then read as unseen. That is the
// conservative direction — the client is sent those streams again rather
// than having them skipped.
func pduToken(pos int64) Token { return Token{PDU: pos} }