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>
327 lines
12 KiB
Go
327 lines
12 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
)
|
|
|
|
// Errors the sync store returns for cases a caller has to tell apart.
|
|
var (
|
|
// ErrNoSuchFilter is returned for a filter ID the user has not
|
|
// uploaded.
|
|
ErrNoSuchFilter = errors.New("no such filter")
|
|
)
|
|
|
|
// StreamPositions is the top of every stream a sync token points into, read
|
|
// in one call.
|
|
//
|
|
// A sync response is assembled from several independent streams, and the
|
|
// token it returns has to name a position in each. Reading them one at a
|
|
// time would let a stream move between two of the reads, and the client
|
|
// would then be handed a token that skips whatever landed in the gap.
|
|
type StreamPositions struct {
|
|
// PDU is the room event stream: the highest stream ordering in
|
|
// roomserver_events.
|
|
PDU int64
|
|
|
|
Receipt int64
|
|
ToDevice int64
|
|
AccountData int64
|
|
Presence int64
|
|
|
|
// DeviceList is the keyserver's device list stream: the users whose
|
|
// end-to-end keys a client should fetch again.
|
|
DeviceList int64
|
|
|
|
// Notification is the push store's notification count stream: the
|
|
// rooms whose unread counts moved for a user.
|
|
Notification int64
|
|
}
|
|
|
|
// TimelineDirection is which way a timeline read walks the stream.
|
|
type TimelineDirection string
|
|
|
|
// The two directions, spelled as the Client-Server API spells them in
|
|
// /messages' `dir` parameter.
|
|
const (
|
|
// DirectionForward reads oldest first, which is the order /sync hands
|
|
// events to a client.
|
|
DirectionForward TimelineDirection = "f"
|
|
|
|
// DirectionBackward reads newest first, which is how /messages pages
|
|
// back through a room's history.
|
|
DirectionBackward TimelineDirection = "b"
|
|
)
|
|
|
|
// TimelineRequest is one window of a room's event stream.
|
|
//
|
|
// The window is named by its two ends rather than by a direction and a
|
|
// starting point, because the two are independent: which end the limit
|
|
// trims, and which order the rows come back in, is Direction's business,
|
|
// and nothing else about the request changes with it. Spelling the bounds
|
|
// as "from" and "to" instead would mean each of them being the low bound in
|
|
// one direction and the high bound in the other, which is exactly the
|
|
// off-by-one every pagination bug is made of.
|
|
//
|
|
// Low is exclusive and High inclusive, in both directions. That is what
|
|
// lets a token a client was handed be passed straight back without
|
|
// re-reading the event it names: a position is somewhere a client has got
|
|
// to, not an event it still wants.
|
|
//
|
|
// Positions can be negative: history backfilled from other servers is
|
|
// numbered downwards from -1, below everything that was already here (see
|
|
// RoomStore.WriteBackfill). Zero is never a position, which is what lets
|
|
// it go on meaning "unbounded" here — and why the position just below an
|
|
// event is PositionBefore and not a plain subtraction.
|
|
type TimelineRequest struct {
|
|
RoomID string
|
|
|
|
// Low is an exclusive lower bound. Zero means unbounded: the start of
|
|
// the room, backfilled history included.
|
|
Low int64
|
|
|
|
// High is an inclusive upper bound. Zero means the top of the stream,
|
|
// and goes on meaning that as the stream grows.
|
|
High int64
|
|
|
|
// Direction decides which end of the window the limit trims — the
|
|
// bottom when reading backwards, the top when reading forwards — and
|
|
// the order the rows come back in.
|
|
Direction TimelineDirection
|
|
|
|
// Limit caps the rows returned. It is required: an unbounded timeline
|
|
// read is a way to ask the server to serialise a whole room.
|
|
Limit int
|
|
|
|
// Filter narrows which events come back. The store applies it in SQL
|
|
// because a filter applied after the limit would return fewer events
|
|
// than the client asked for and then look like the end of the room.
|
|
Filter TimelineFilter
|
|
}
|
|
|
|
// PositionBefore is the position just below pos: the bound that names
|
|
// "everything up to, but not including, the event at pos".
|
|
//
|
|
// It is pos-1 except at 1, whose predecessor is -1. Zero is not a
|
|
// position — it is the "unbounded" bound of every window — so a token
|
|
// below the first event this server ever persisted has to skip it, or
|
|
// handing that token back as a High bound would read the whole room again
|
|
// instead of the backfilled history beneath it.
|
|
func PositionBefore(pos int64) int64 {
|
|
if pos == 1 {
|
|
return -1
|
|
}
|
|
return pos - 1
|
|
}
|
|
|
|
// TimelineFilter is the part of a Client-Server API event filter that the
|
|
// store can apply itself.
|
|
//
|
|
// Only the fields that are a plain comparison on an indexed or stored
|
|
// column are here. The rest of a filter — `contains_url`, lazy-loading,
|
|
// `include_redundant_members` — is about what an event means or about how a
|
|
// response is assembled, and is applied by the syncapi.
|
|
type TimelineFilter struct {
|
|
// Types, when non-empty, admits only these event types. A trailing '*'
|
|
// is a wildcard, as the spec defines for filter patterns.
|
|
Types []string
|
|
|
|
// NotTypes excludes types, and wins over Types for an event matching
|
|
// both, which is what the spec requires.
|
|
NotTypes []string
|
|
|
|
// Senders, when non-empty, admits only these senders.
|
|
Senders []string
|
|
|
|
// NotSenders excludes senders, and likewise wins.
|
|
NotSenders []string
|
|
}
|
|
|
|
// IsEmpty reports whether the filter admits everything.
|
|
func (f TimelineFilter) IsEmpty() bool {
|
|
return len(f.Types) == 0 && len(f.NotTypes) == 0 &&
|
|
len(f.Senders) == 0 && len(f.NotSenders) == 0
|
|
}
|
|
|
|
// SyncMembership is a user's current membership in one room, with the
|
|
// position of the event that set it.
|
|
//
|
|
// The position is what bounds what a user who has left may see: their view
|
|
// of the room ends at their own leave event, and a room they were kicked
|
|
// out of must not keep streaming to them.
|
|
type SyncMembership struct {
|
|
RoomID string
|
|
Membership string
|
|
EventID string
|
|
|
|
// StreamOrdering is the position of the membership event.
|
|
StreamOrdering int64
|
|
}
|
|
|
|
// Receipt is one read receipt.
|
|
type Receipt struct {
|
|
RoomID string
|
|
UserID string
|
|
ReceiptType string
|
|
|
|
// ThreadID is the thread the receipt is for, or empty for the room's
|
|
// main timeline.
|
|
ThreadID string
|
|
|
|
EventID string
|
|
|
|
// TimestampMS is when the receipt was sent, which is what a client
|
|
// shows as "read at".
|
|
TimestampMS int64
|
|
|
|
// StreamPos is the receipt's position in the receipt stream.
|
|
StreamPos int64
|
|
}
|
|
|
|
// ToDeviceMessage is one message addressed to a single device.
|
|
type ToDeviceMessage struct {
|
|
// StreamPos is the message's position in the to-device stream.
|
|
StreamPos int64
|
|
|
|
Localpart string
|
|
DeviceID string
|
|
Sender string
|
|
Type string
|
|
|
|
// Content is the message body as the sender wrote it.
|
|
Content []byte
|
|
|
|
SentAtMS int64
|
|
}
|
|
|
|
// NewToDeviceMessage is one message to enqueue.
|
|
type NewToDeviceMessage struct {
|
|
Localpart string
|
|
|
|
// DeviceID is the device to deliver to. The caller has already expanded
|
|
// a wildcard addressee into one of these per device: which devices a
|
|
// user has is the userapi's business, not the store's.
|
|
DeviceID string
|
|
|
|
Sender string
|
|
Type string
|
|
Content []byte
|
|
SentAtMS int64
|
|
}
|
|
|
|
// PresenceState is a user's presence as the store holds it.
|
|
type PresenceState struct {
|
|
UserID string
|
|
|
|
// Presence is "online", "offline" or "unavailable".
|
|
Presence string
|
|
|
|
StatusMsg string
|
|
|
|
// LastActiveMS is when the user was last seen doing something.
|
|
LastActiveMS int64
|
|
|
|
// StreamPos is the position of the last change to this row.
|
|
StreamPos int64
|
|
}
|
|
|
|
// AccountDataEntry locates one account data blob that has changed. The
|
|
// content is not carried: a sync response needs to know which blobs to
|
|
// re-read, and reading them is a separate, cacheable call.
|
|
type AccountDataEntry struct {
|
|
// RoomID is empty for global account data.
|
|
RoomID string
|
|
DataType string
|
|
|
|
StreamPos int64
|
|
}
|
|
|
|
// StoredFilter is an uploaded filter.
|
|
type StoredFilter struct {
|
|
Localpart string
|
|
FilterID string
|
|
|
|
// JSON is the filter exactly as the client uploaded it.
|
|
JSON []byte
|
|
|
|
CreatedAtMS int64
|
|
}
|
|
|
|
// SyncStore persists the streams a sync response carries that are not room
|
|
// events, plus the filters clients upload: the storage half of the syncapi
|
|
// component.
|
|
//
|
|
// Typing notifications are not here on purpose. They expire in seconds and
|
|
// are worthless across a restart, so the syncapi keeps them in memory.
|
|
type SyncStore interface {
|
|
// StreamPositions reads the top of every stream in one call.
|
|
StreamPositions(ctx context.Context) (StreamPositions, error)
|
|
|
|
// SetReceipt records a read receipt, replacing any earlier one from the
|
|
// same user for the same type and thread, and returns its stream
|
|
// position.
|
|
//
|
|
// A receipt that would move backwards is not written and reports the
|
|
// current position with moved false: receipts only ever advance, and a
|
|
// client syncing on two devices would otherwise flap.
|
|
SetReceipt(ctx context.Context, r Receipt) (pos int64, moved bool, err error)
|
|
|
|
// Receipts returns the current receipts of the given rooms whose
|
|
// position is after from and at or before to. A from of 0 returns every
|
|
// current receipt, which is what an initial sync needs.
|
|
Receipts(ctx context.Context, roomIDs []string, from, to int64) ([]Receipt, error)
|
|
|
|
// ReceiptsForUser returns one user's own current receipts in the given
|
|
// rooms, whatever their position. A client needs its own read markers
|
|
// on an initial sync even when it set them long ago.
|
|
ReceiptsForUser(ctx context.Context, userID string, roomIDs []string) ([]Receipt, error)
|
|
|
|
// SendToDevice enqueues messages for delivery and returns the highest
|
|
// position it wrote. An empty batch is not an error and returns the
|
|
// current position.
|
|
SendToDevice(ctx context.Context, msgs []NewToDeviceMessage) (int64, error)
|
|
|
|
// ToDeviceMessages returns the messages queued for one device whose
|
|
// position is after from and at or before to, oldest first.
|
|
ToDeviceMessages(ctx context.Context, localpart, deviceID string, from, to int64, limit int) ([]ToDeviceMessage, error)
|
|
|
|
// DeleteToDeviceMessages removes the messages for a device at or before
|
|
// pos. A device acknowledges a batch by syncing past it, so this is
|
|
// what the next sync does with the batch before it.
|
|
DeleteToDeviceMessages(ctx context.Context, localpart, deviceID string, pos int64) error
|
|
|
|
// SetPresence records a user's presence and returns its stream
|
|
// position, reporting whether anything a peer would care about
|
|
// changed.
|
|
//
|
|
// A write that only refreshes LastActiveMS does not move the stream.
|
|
// A client says it is still online by syncing, so every sync is one of
|
|
// these — and a stream that moved each time would wake every peer's
|
|
// long poll, which would sync, which would move it again.
|
|
SetPresence(ctx context.Context, p PresenceState) (pos int64, moved bool, err error)
|
|
|
|
// Presence reads one user's presence, returning ErrNoSuchUser when
|
|
// nothing has ever been recorded for them.
|
|
Presence(ctx context.Context, userID string) (PresenceState, error)
|
|
|
|
// PresenceForUsers returns the presence of the given users whose
|
|
// position is after from and at or before to. A from of 0 returns every
|
|
// current state.
|
|
PresenceForUsers(ctx context.Context, userIDs []string, from, to int64) ([]PresenceState, error)
|
|
|
|
// AccountDataChanges lists the account data of a user that changed
|
|
// after from and at or before to.
|
|
AccountDataChanges(ctx context.Context, localpart string, from, to int64) ([]AccountDataEntry, error)
|
|
|
|
// AccountDataAt lists every account data blob a user has, as an initial
|
|
// sync needs.
|
|
AccountDataAt(ctx context.Context, localpart string, to int64) ([]AccountDataEntry, error)
|
|
|
|
// SaveFilter stores a filter under a new ID and returns it.
|
|
SaveFilter(ctx context.Context, localpart string, filter []byte, createdAtMS int64) (string, error)
|
|
|
|
// Filter reads an uploaded filter, returning ErrNoSuchFilter when the
|
|
// ID is unknown.
|
|
Filter(ctx context.Context, localpart, filterID string) ([]byte, error)
|
|
}
|