CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Implements PLAN.md Phase 5: the Server-Server API. Warp joins rooms on other servers and lets their users join its rooms, invites across servers with the invitee's server countersigning, rejects remote invites (locally if the inviter is unreachable), and exchanges events, typing, receipts and to-device messages with other homeservers — all behind the runtime switch, which now starts and stops real outbound traffic. The protocol lives in internal/federation with no knowledge of rooms: X-Matrix signing (fuzzed parser), the spec's discovery algorithm keeping the dial address and the TLS name apart, a signed client that refuses every request while the switch is off or the policy denies the destination, and a key ring that caches other servers' keys for at most a week, with optional notaries. The roomserver takes events from elsewhere through the spec's intake: signatures and hashes (a hash mismatch keeps the redacted form), the rules against the event's own auth events, against the state before it — with /get_missing_events for small gaps and /state_ids for wide ones — and against the current state, where a failure is a soft failure. Events held without their state are outliers: a column on roomserver_events, because the current state of a room joined over federation is almost all outliers, and excluded from every timeline query. Forks are resolved at the room's head. Membership across servers is make_join/send_join, make_leave/send_leave and countersigned invites, in both directions; m.room.server_acl is enforced on everything another server sends. History before a remote join is backfilled when /messages pages past it, stored at negative stream positions. internal/federationapi is the running component: the inbound endpoints, and an outbound queue that persists what it owes each destination, sends it in transactions of 50 PDUs and 100 EDUs, backs off from ten seconds to six hours, survives restarts, and drops events persisted while federation is off rather than delivering a backlog in one burst. Its Module is what the switch starts and stops. Also: a TLS listener for port 8448, trusted key servers, key validity and an outbound CA bundle in the config; per-destination health and backoff reset in the admin API; remote profiles, aliases and directories in the Client-Server API; a Complement federation profile. See docs/federation.md, including its known gaps (no partial-state joins, no key rotation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
319 lines
11 KiB
Go
319 lines
11 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
|
|
}
|
|
|
|
// 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)
|
|
}
|