package storage import ( "context" "errors" ) // Errors the room store returns for cases a caller has to tell apart. var ( // ErrNoSuchRoom is returned for a room this server does not know. ErrNoSuchRoom = errors.New("no such room") // ErrRoomExists is returned by CreateRoom for a room ID already taken. // It can only happen through a room ID collision, which for a room // version 12 room would mean two identical create events. ErrRoomExists = errors.New("room already exists") // ErrNoSuchEvent is returned for an event ID the room store does not // hold. ErrNoSuchEvent = errors.New("no such event") // ErrNoSuchAlias is returned for an unknown room alias. ErrNoSuchAlias = errors.New("no such room alias") // ErrAliasExists is returned by CreateAlias for an alias already // pointing somewhere. ErrAliasExists = errors.New("room alias already exists") ) // Room is one room this server takes part in. // // The version is fixed when the room is created and is what every later read // needs first: an event cannot even be parsed without knowing the rules it // was written under. type Room struct { ID string // Version is the room version identifier, e.g. "11". Version string // Creator is the full user ID that sent the m.room.create event. Creator string // Published reports whether the room is in this server's public room // directory. Published bool CreatedAtMS int64 } // NewRoom describes a room to create. The create event itself is written by // the same Write call that appends any other event. type NewRoom struct { ID string Version string Creator string Published bool CreatedAtMS int64 } // RoomProfile is the denormalised view of a room's state that a room // listing is searched and ordered by. // // It is all derivable from the room's current state, and is stored anyway: // the public room directory sorts every published room by member count and // pages the result, which cannot be done by reading each room's state in // turn. The roomserver recomputes it whenever one of the state events behind // it changes and hands it to Write; the store only keeps it, because // knowing that m.room.name means a name is the roomserver's business. // // JoinedMembers is the exception: the store maintains it itself, by counting // the current-state rows whose membership is "join". type RoomProfile struct { Name string Topic string AvatarURL string CanonicalAlias string JoinRule string HistoryVisibility string GuestAccess string // RoomType is m.room.create's content.type: empty for a normal room, // "m.space" for a space. RoomType string } // StateEntry is one piece of room state and the event that holds it. type StateEntry struct { Type string StateKey string EventID string // Membership is the membership value of an m.room.member entry, and // empty on every other kind of state. It travels with the entry so that // the store can keep "who is joined to this room" answerable with an // index instead of by parsing event JSON. Membership string } // StoredEvent is an event as the room store holds it: the canonical JSON // plus the columns worth indexing separately. // // JSON is the authority. The other fields are projections of it, written out // so that the store can answer "the members of this room" or "events in // stream order" without parsing every row. type StoredEvent struct { RoomID string EventID string Type string // StateKey is nil for a non-state event. The empty string is a // perfectly good state key, which is why this is a pointer. StateKey *string Sender string Depth int64 OriginServerTS int64 // JSON is the event as canonical JSON, exactly as it was signed. JSON []byte // StreamOrdering is this server's own position for the event: the // order /sync and /messages hand events to clients. For an event that // arrived live it is the order it was persisted in, counting up from 1; // for history backfilled from another server it is negative, counting // down from -1, so that it sorts below everything that was already // here. It is never zero. StreamOrdering int64 // RedactedBy is the event ID of the redaction that applies to this // event, or empty. The JSON is rewritten in place when a redaction is // applied, so a redacted event is already stripped on the way out; this // field is what lets unsigned.redacted_because be filled in. RedactedBy string // Outlier reports an event held without the room's state at it: part // of a join's state or auth chain, an auth event fetched to judge // another, an invite to a room nobody here is in, or an event that // soft-failed. An outlier can be cited and read by ID, and is never in // a timeline. Outlier bool // RelatesTo and RelType are content.m.relates_to's event_id and // rel_type, empty for an event that relates to nothing. A reply's // m.in_reply_to is not a relation in this sense and is not recorded. // They are cleared when the event is redacted, because the redaction // algorithm strips the content they came from. RelatesTo string RelType string // SearchKey and SearchText are what the full-text index holds for the // event: the field ("content.body", "content.name" or "content.topic") // and its text. Both are empty for an event with nothing to search. // They are written, never read back: the index is the only place they // are kept. SearchKey string SearchText string } // IsState reports whether the event is a state event. func (e StoredEvent) IsState() bool { return e.StateKey != nil } // TransactionRef records a client transaction ID against the event it // produced, so that a retried PUT returns the first event rather than // sending a second one. type TransactionRef struct { Localpart string DeviceID string TxnID string } // RoomWrite is one event and everything its arrival changes, written in a // single transaction. // // Persisting an event is never just an insert: it moves the room's forward // extremities, it may replace a piece of current state, and it may strip an // earlier event. Splitting that across calls would leave a window in which // the room's current state disagrees with its events, so the whole lot goes // down together. type RoomWrite struct { // Event is the event being appended. Its StreamOrdering is assigned by // the store and returned. Event StoredEvent // StateBefore is the room state the event was authorised against: the // full state of the room as of its prev_events. It is stored as a // deduplicated snapshot, so appending a message to a quiet room adds no // state rows at all. StateBefore []StateEntry // StateDelta are the pieces of current state this event changes — for // an ordinary state event, the single tuple it writes. It is empty for // an event that changes no state. StateDelta []StateEntry // StateDeltaIsWholeState says that StateDelta is the room's entire new // current state, and that any tuple not in it must be removed. State // resolution across a forked room produces such a result; a single // state event does not, and rewriting the whole of a large room's state // on every join would make filling a room quadratic. StateDeltaIsWholeState bool // Profile updates the denormalised room listing columns. It is nil when // this event changes none of them. Profile *RoomProfile // Extremities replaces the room's forward extremities. For a locally // created event that is the event itself, since it descends from every // extremity there was. Extremities []string // Transaction, when set, records the client transaction ID this event // came from. Transaction *TransactionRef // Redacts is the event ID this event redacts, when the redaction is to // be applied. RedactedJSON is the target's redacted form, which // replaces the stored JSON. Redacts string RedactedJSON []byte // Outlier writes the event as an outlier: no state snapshot is // recorded for it and StateBefore is ignored. StateDelta and // Extremities still apply when they are set, which is how an invite to // a room nobody here is in becomes the one piece of that room's state // this server knows. Outlier bool // InviteState is the stripped state an invite over federation arrived // with, kept for the invitee until the invite is answered. Any // m.room.member write clears what was stored for its state key first, // so the invite's own write is the only one that leaves a row behind. InviteState []byte } // BackfillEvent is one event of a room's history, fetched from another // server after this server already held later events, with the room state // it was authorised against. type BackfillEvent struct { Event StoredEvent // StateBefore is the state of the room as of the event's prev_events, // recorded as its state snapshot exactly as RoomWrite.StateBefore is. // It is what lets a client paging back through backfilled history be // told the state at any point of it. StateBefore []StateEntry } // RoomAlias is a local alias and the room it points at. type RoomAlias struct { Alias string RoomID string // Creator is the full user ID that created the alias. Creator string CreatedAtMS int64 } // MembershipEntry is one member of a room. type MembershipEntry struct { UserID string Membership string EventID string } // MemberChange is one membership event in a window of the event stream. type MemberChange struct { RoomID string // UserID is the state key: the user whose membership it set. UserID string StreamOrdering int64 // Membership is the user's membership of the room now, which may // have moved again since this event. Membership string } // RoomFilter narrows and pages a room listing. type RoomFilter struct { // Search matches a substring of the room ID, name, topic or canonical // alias, case-insensitively. Empty matches everything. Search string // Limit caps the returned rows; 0 means the store's own default. Limit int // Offset skips rows, for paging. Offset int } // RoomSummary is a room as a listing shows it: the room row, its // denormalised state and its member count. type RoomSummary struct { Room RoomProfile // JoinedMembers is how many members of the room are joined. JoinedMembers int } // RoomStats are the room counts the admin dashboard shows. type RoomStats struct { Rooms int Published int Events int } // RoomStore persists rooms, their events and their state: the storage half // of the roomserver component. // // Every method takes identifiers the caller has already validated. The store // interprets nothing: it does not know the authorisation rules, it does not // know which room versions exist, and it never parses an event. What it // guarantees is that a room's events, its state and its extremities are only // ever seen in a consistent set. type RoomStore interface { RelationStore // CreateRoom inserts the room row, returning ErrRoomExists when the ID // is taken. CreateRoom(ctx context.Context, room NewRoom) error // Room reads one room, returning ErrNoSuchRoom if it is absent. Room(ctx context.Context, roomID string) (Room, error) // Write appends one event and applies everything it changes. It returns // the stream ordering the event was given. Write(ctx context.Context, w RoomWrite) (int64, error) // Event reads one event of a room, returning ErrNoSuchEvent if it is // absent. Event(ctx context.Context, roomID, eventID string) (StoredEvent, error) // EventsByID reads events by ID within one room. Unknown IDs are left // out rather than reported: state resolution needs exactly that // behaviour for events this server does not have. EventsByID(ctx context.Context, roomID string, eventIDs []string) ([]StoredEvent, error) // CurrentState returns the room's resolved current state. CurrentState(ctx context.Context, roomID string) ([]StateEntry, error) // CurrentStateEvents returns the events behind the room's current // state, which is what the authorisation rules are run against. CurrentStateEvents(ctx context.Context, roomID string) ([]StoredEvent, error) // CurrentStateEvent returns one piece of current state, or // ErrNoSuchEvent when the room has no such state. CurrentStateEvent(ctx context.Context, roomID, eventType, stateKey string) (StoredEvent, error) // StateBefore returns the state snapshot recorded against an event: the // state of the room as of that event's prev_events. StateBefore(ctx context.Context, roomID, eventID string) ([]StateEntry, error) // Extremities returns the room's forward extremities, which are the // prev_events of the next event to be sent. Extremities(ctx context.Context, roomID string) ([]StoredEvent, error) // Members returns the m.room.member events of a room. memberships // filters by membership value; empty returns every member event, // including the users who have left. Members(ctx context.Context, roomID string, memberships []string) ([]StoredEvent, error) // MembershipEntries returns the memberships of a room without the event // bodies, which is what /joined_members needs. MembershipEntries(ctx context.Context, roomID string, memberships []string) ([]MembershipEntry, error) // Membership returns a user's membership in a room and the event that // set it. A user who was never in the room gets an empty membership and // no error: "never joined" and "left" are the same answer to every rule // that asks. Membership(ctx context.Context, roomID, userID string) (MembershipEntry, error) // RoomsForUser lists the rooms a user has one of the given memberships // in, ordered by room ID. RoomsForUser(ctx context.Context, userID string, memberships []string) ([]string, error) // MembershipsForUser is RoomsForUser with the membership and the // position of the event that set it: one query for everything a sync // needs to know about where a user stands. MembershipsForUser(ctx context.Context, userID string, memberships []string) ([]SyncMembership, error) // UserMembershipEvents returns every m.room.member event of one user in // one room, oldest first. // // It is how far back a user's own view of a room reaches: a user who // joined, left and rejoined has several spans, and only their own // membership events say where each one began. There is one row per // membership change of one user in one room, so this stays small // however busy the room is. UserMembershipEvents(ctx context.Context, roomID, userID string) ([]StoredEvent, error) // MaxStreamOrdering returns the top of this server's event stream, or 0 // for a server that has persisted no events. MaxStreamOrdering(ctx context.Context) (int64, error) // Timeline reads one window of a room's event stream. See // TimelineRequest: the bounds are exclusive and the limit is required. Timeline(ctx context.Context, req TimelineRequest) ([]StoredEvent, error) // StreamEvents reads the server's whole event stream after a position, // in order, across every room: the events with a stream ordering above // after and at or below upTo (0 for no upper bound), outliers left out, // at most limit of them. It is what the application services are fed // from. StreamEvents(ctx context.Context, after, upTo int64, limit int) ([]StoredEvent, error) // RoomsWithEvents returns the subset of roomIDs that have an event // after from and at or before to. // // It is what keeps an incremental sync down to one query for the common // case of nothing having happened: a user in two hundred rooms would // otherwise need two hundred timeline reads to find that out. RoomsWithEvents(ctx context.Context, roomIDs []string, from, to int64) ([]string, error) // LatestEventPositions returns, for each of roomIDs that has one, the // position of its newest timeline event of one of the given types at // or before upTo (zero for no bound). It is what sliding sync orders a // room list by: the last thing that happened in each room that a user // would call activity. LatestEventPositions(ctx context.Context, roomIDs, types []string, upTo int64) (map[string]int64, error) // StateAtStream returns the room's state as of a stream position: the // state after the last event at or before pos. The position may be a // negative one inside backfilled history; zero has no events at or // before it that mean anything and returns nothing. // // It answers the two questions current state cannot. A user who has // left sees the room as it was when they left, and a sync with a gap in // it has to say which state changed inside that gap. StateAtStream(ctx context.Context, roomID string, pos int64) ([]StateEntry, error) // TransactionEvent returns the event a client transaction ID already // produced, or ErrNoSuchEvent when this is the first time it is seen. TransactionEvent(ctx context.Context, ref TransactionRef) (string, error) // CreateAlias points an alias at a room, returning ErrAliasExists when // the alias is taken. CreateAlias(ctx context.Context, alias RoomAlias) error // DeleteAlias removes an alias, returning ErrNoSuchAlias if it is // absent. DeleteAlias(ctx context.Context, alias string) error // Alias resolves an alias, returning ErrNoSuchAlias if it is absent. // The creator comes back with it because the permission check on // deleting an alias is partly about who made it. Alias(ctx context.Context, alias string) (RoomAlias, error) // AliasesForRoom lists the local aliases pointing at a room, sorted. AliasesForRoom(ctx context.Context, roomID string) ([]string, error) // SetPublished adds or removes a room from the public room directory, // returning ErrNoSuchRoom for a room that does not exist. SetPublished(ctx context.Context, roomID string, published bool) error // PublishedRooms returns a page of the public room directory ordered by // descending joined-member count, plus the total number of published // rooms matching the filter. PublishedRooms(ctx context.Context, filter RoomFilter) ([]RoomSummary, int, error) // ListRooms returns a page of every room for the admin API, plus the // total matching the filter. ListRooms(ctx context.Context, filter RoomFilter) ([]RoomSummary, int, error) // RoomSummary returns one room with its denormalised state, returning // ErrNoSuchRoom if it is absent. RoomSummary(ctx context.Context, roomID string) (RoomSummary, error) // PurgeRoom deletes a room and everything belonging to it, returning // ErrNoSuchRoom for a room that does not exist. It is the admin API's // last resort and has no undo. PurgeRoom(ctx context.Context, roomID string) error // RoomStats returns the aggregate counts for the admin dashboard. RoomStats(ctx context.Context) (RoomStats, error) // WriteOutliers stores events as outliers in one transaction, skipping // any already held. It is how a join over federation stores the room // state and auth chain it is handed, which can run to tens of thousands // of events: one Write per event would be one transaction per event. WriteOutliers(ctx context.Context, roomID string, events []StoredEvent) error // WriteBackfill stores a room's history, fetched from another server, // below everything the room already holds, in one transaction. // // The events are given oldest first and keep that order: each is // numbered by a counter that runs downwards from -1, so the whole batch // sorts below every event this server held when it arrived, and a // later batch sorts below this one. An event already in the timeline is // skipped; an outlier is promoted into the timeline in place, keeping // its stored JSON. Nothing else about the room changes — not its // current state, not its forward extremities, not its member counts — // because history is by definition already reflected in them. WriteBackfill(ctx context.Context, roomID string, events []BackfillEvent) error // InviteState returns the stripped state stored with a user's invite, // or nil when there is none. InviteState(ctx context.Context, roomID, userID string) ([]byte, error) // HasJoinedMemberOn reports whether any user of a server is joined to // a room. It is asked about every event that arrives over federation — // is this server still in the room, is the sender's? — and answering // it by listing the members would read every row of a room of tens of // thousands. HasJoinedMemberOn(ctx context.Context, roomID, serverName string) (bool, error) // UsersSharingRooms lists the users joined to at least one room the // given user is joined to, that user included when they are joined // anywhere. It is whose device list changes a client is told about, // and who is woken when this user's keys change. UsersSharingRooms(ctx context.Context, userID string) ([]string, error) // MemberChanges lists the m.room.member events of the given rooms // after from and at or before to: whose membership moved in a window, // which is what tells a sync that it now shares a room with somebody // it did not before, or no longer does. MemberChanges(ctx context.Context, roomIDs []string, from, to int64) ([]MemberChange, error) // EventByID reads an event without knowing its room, returning // ErrNoSuchEvent if it is absent. The federation API's /event names // only the event; everything else asks within a room. EventByID(ctx context.Context, eventID string) (StoredEvent, error) }