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 } // 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) }