package roomserver import ( "context" "encoding/json" "fmt" "github.com/dariuszjaniak/warp/server/internal/matrix/auth" "github.com/dariuszjaniak/warp/server/internal/matrix/event" "github.com/dariuszjaniak/warp/server/internal/matrix/roomversion" "github.com/dariuszjaniak/warp/server/internal/storage" ) // currentState reads a room's current state as the authorisation rules see // it. func (r *API) currentState(ctx context.Context, room storage.Room, rules roomversion.Rules) (auth.State, error) { stored, err := r.store.CurrentStateEvents(ctx, room.ID) if err != nil { return nil, err } events, err := parseEvents(stored, rules) if err != nil { return nil, err } return auth.NewState(events), nil } // ClientEvent is an event in the shape the Client-Server API returns it. // // It is not the event as it was signed. A client is given the event ID // explicitly — in every room version since 3 it is a hash, not a property — // and is not given the hashes, signatures, auth_events or depth, which are // a server's business. What it does get that the signed event never carried // is `unsigned`, which is per-server annotation: the age of the event, the // state it replaced, and the redaction that stripped it. type ClientEvent struct { EventID string `json:"event_id"` // RoomID is omitted when empty, because a /sync response carries its // events without one: they are already nested under the room they // belong to, and repeating it on every event of a client with a // thousand rooms is a measurable part of every response. Every other // endpoint sets it, and the spec has a separate name for the shape // without it — ClientEventWithoutRoomID — rather than two event types. RoomID string `json:"room_id,omitempty"` Type string `json:"type"` StateKey *string `json:"state_key,omitempty"` Sender string `json:"sender"` Content json.RawMessage `json:"content"` OriginServerTS int64 `json:"origin_server_ts"` Unsigned json.RawMessage `json:"unsigned,omitempty"` // Redacts is only on an m.room.redaction event, and only in the room // versions that keep it at the top level. Clients still read it there. Redacts string `json:"redacts,omitempty"` } // clientEvent projects a stored event into the client shape. func (r *API) clientEvent(stored storage.StoredEvent, e *event.Event, roomID string) ClientEvent { out := ClientEvent{ EventID: e.EventID(), RoomID: roomID, Type: e.Type(), Sender: e.Sender(), Content: e.Content(), OriginServerTS: e.OriginServerTS(), Redacts: e.Redacts(), } if stateKey, ok := e.StateKey(); ok { out.StateKey = &stateKey } out.Unsigned = r.unsigned(stored, e) return out } // unsigned builds the `unsigned` block a client is given. // // `age` is computed on the way out rather than stored: it is how long ago // the event happened as far as this server can tell, and a value written // down at persist time would be wrong by however long the event sat in the // database. func (r *API) unsigned(stored storage.StoredEvent, e *event.Event) json.RawMessage { block := map[string]json.RawMessage{} if existing := e.Unsigned(); len(existing) > 0 { if err := json.Unmarshal(existing, &block); err != nil { // The unsigned block is this server's own annotation and is // written as an object everywhere; anything else is a bug here // rather than something a client sent, and dropping it is // better than failing the read. r.log.Warn("stored event has an unreadable unsigned block", "event_id", e.EventID()) block = map[string]json.RawMessage{} } } if age := r.nowMS() - e.OriginServerTS(); age >= 0 { block["age"] = mustMarshal(age) } if stored.RedactedBy != "" { block["redacted_because"] = mustMarshal(map[string]any{"event_id": stored.RedactedBy}) } if len(block) == 0 { return nil } return mustMarshal(block) } // Event returns one event of a room, if the user may see it. func (r *API) Event(ctx context.Context, roomID, userID, eventID string) (ClientEvent, error) { _, rules, err := r.roomAndRules(ctx, roomID) if err != nil { return ClientEvent{}, err } visible, err := r.requireReadable(ctx, roomID, userID) if err != nil { return ClientEvent{}, err } stored, err := r.store.Event(ctx, roomID, eventID) if err != nil { return ClientEvent{}, err } // An event outside the user's window is reported as unknown rather // than as forbidden: whether a particular event ID exists in a room is // itself something they are not entitled to know. if !visible.Contains(stored.StreamOrdering) { return ClientEvent{}, fmt.Errorf("event %q: %w", eventID, ErrNoSuchEvent) } e, err := parseEvent(stored, rules) if err != nil { return ClientEvent{}, err } return r.clientEvent(stored, e, roomID), nil } // RoomState returns a room's state in the client shape: the current state // for a member, and the state as it was when they left for a user who has. func (r *API) RoomState(ctx context.Context, roomID, userID string) ([]ClientEvent, error) { _, rules, err := r.roomAndRules(ctx, roomID) if err != nil { return nil, err } visible, err := r.requireReadable(ctx, roomID, userID) if err != nil { return nil, err } return r.stateAt(ctx, roomID, rules, visible.To) } // StateEventContent returns the content of one piece of a room's current // state, which is what GET /rooms/{roomID}/state/{type}/{key} answers. // // The endpoint returns the content alone, not the event: a quirk of the // Client-Server API that predates the rest of it, and one clients depend on. func (r *API) StateEventContent(ctx context.Context, roomID, userID, eventType, stateKey string) (json.RawMessage, error) { e, err := r.StateEvent(ctx, roomID, userID, eventType, stateKey) if err != nil { return nil, err } return e.Content, nil } // StateEvent returns one piece of a room's current state as a whole event. // // It is what ?format=event asks for on the state endpoint, and the only way // a client can see who set a piece of state and when. func (r *API) StateEvent(ctx context.Context, roomID, userID, eventType, stateKey string) (ClientEvent, error) { _, rules, err := r.roomAndRules(ctx, roomID) if err != nil { return ClientEvent{}, err } visible, err := r.requireReadable(ctx, roomID, userID) if err != nil { return ClientEvent{}, err } // A member reads the one row they asked for. A user who has left has to // be served from the state as of their departure, which means reading // the snapshot and picking the tuple out of it — the room has moved on // since, and the current row is not theirs to see. if visible.To != 0 { state, err := r.stateAt(ctx, roomID, rules, visible.To) if err != nil { return ClientEvent{}, err } for _, e := range state { if e.Type == eventType && e.StateKey != nil && *e.StateKey == stateKey { return e, nil } } return ClientEvent{}, fmt.Errorf("state %s/%s of %q: %w", eventType, stateKey, roomID, ErrNoSuchEvent) } stored, err := r.store.CurrentStateEvent(ctx, roomID, eventType, stateKey) if err != nil { return ClientEvent{}, err } e, err := parseEvent(stored, rules) if err != nil { return ClientEvent{}, err } return r.clientEvent(stored, e, roomID), nil } // MembersRequest filters GET /rooms/{roomID}/members. type MembersRequest struct { RoomID string // User is the full user ID asking. User string // Membership and NotMembership filter the result; either may be empty. Membership string NotMembership string // At is the stream position to report the membership as of, or 0 for // the current one. // // It is what ?at= asks for, and what a client that is rendering an // older part of a room's timeline needs: the names and avatars to show // against those messages are the ones their senders had then, not now. At int64 } // Members returns a room's m.room.member events. func (r *API) Members(ctx context.Context, req MembersRequest) ([]ClientEvent, error) { _, rules, err := r.roomAndRules(ctx, req.RoomID) if err != nil { return nil, err } visible, err := r.requireReadable(ctx, req.RoomID, req.User) if err != nil { return nil, err } // The position to report as of: the one asked for, held inside what // the user may see. A user who has left is pinned to their departure // whatever they asked for — the room has moved on since, and the // current membership is not theirs to read. at := req.At if visible.To != 0 && (at == 0 || at > visible.To) { at = visible.To } if at != 0 && at <= visible.From { return nil, fmt.Errorf("%w: %s may not see %s as it was at %d", ErrForbidden, req.User, req.RoomID, req.At) } var members []ClientEvent if at != 0 { state, err := r.stateAt(ctx, req.RoomID, rules, at) if err != nil { return nil, err } members = filterClientEvents(state, storage.TimelineFilter{Types: []string{event.TypeMember}}) } else { var filter []string if req.Membership != "" { filter = append(filter, req.Membership) } stored, err := r.store.Members(ctx, req.RoomID, filter) if err != nil { return nil, err } if members, err = r.clientEvents(stored, rules, req.RoomID); err != nil { return nil, err } } out := make([]ClientEvent, 0, len(members)) for _, e := range members { membership := membershipOf(e) if req.Membership != "" && membership != req.Membership { continue } if req.NotMembership != "" && membership == req.NotMembership { continue } out = append(out, e) } return out, nil } // membershipOf reads content.membership out of a projected member event. // // A member event that reached a room always has one — the authorisation // rules refuse one that does not — so an empty answer means the event is // not a member event at all. func membershipOf(e ClientEvent) string { var content struct { Membership string `json:"membership"` } _ = json.Unmarshal(e.Content, &content) return content.Membership } // RoomMember is one joined member, as /joined_members reports them. type RoomMember struct { DisplayName string `json:"display_name,omitempty"` AvatarURL string `json:"avatar_url,omitempty"` } // JoinedMembers returns the joined members of a room with their profiles. // // Unlike /members this one is for members only: the spec says so, and the // reason is that it exists to let a client that is in a room render it, not // to let anyone enumerate who is in it. func (r *API) JoinedMembers(ctx context.Context, roomID, userID string) (map[string]RoomMember, error) { if err := r.requireJoined(ctx, roomID, userID); err != nil { return nil, err } _, rules, err := r.roomAndRules(ctx, roomID) if err != nil { return nil, err } stored, err := r.store.Members(ctx, roomID, []string{event.MembershipJoin}) if err != nil { return nil, err } out := make(map[string]RoomMember, len(stored)) for _, s := range stored { e, err := parseEvent(s, rules) if err != nil { return nil, err } stateKey, ok := e.StateKey() if !ok { continue } var content struct { DisplayName string `json:"displayname"` AvatarURL string `json:"avatar_url"` } // A member event with a malformed profile still counts as a member; // the profile is simply not shown. _ = json.Unmarshal(e.Content(), &content) out[stateKey] = RoomMember{DisplayName: content.DisplayName, AvatarURL: content.AvatarURL} } return out, nil } // JoinedRooms lists the rooms a user is joined to. func (r *API) JoinedRooms(ctx context.Context, userID string) ([]string, error) { rooms, err := r.store.RoomsForUser(ctx, userID, []string{event.MembershipJoin}) if err != nil { return nil, err } if rooms == nil { rooms = []string{} } return rooms, nil } // Membership returns a user's membership in a room, which is empty for a // user who was never in it. func (r *API) Membership(ctx context.Context, roomID, userID string) (string, error) { entry, err := r.store.Membership(ctx, roomID, userID) if err != nil { return "", err } return entry.Membership, nil } // roomAndRules reads a room and the rules its events are under. func (r *API) roomAndRules(ctx context.Context, roomID string) (storage.Room, roomversion.Rules, error) { room, err := r.store.Room(ctx, roomID) if err != nil { return storage.Room{}, roomversion.Rules{}, err } rules, err := r.rules(room) if err != nil { return storage.Room{}, roomversion.Rules{}, err } return room, rules, nil } // requireJoined refuses a caller who is not currently in the room. func (r *API) requireJoined(ctx context.Context, roomID, userID string) error { entry, err := r.store.Membership(ctx, roomID, userID) if err != nil { return err } if entry.Membership != event.MembershipJoin { return fmt.Errorf("%w: %s is not joined to %s", ErrForbidden, userID, roomID) } return nil } // CanSendStateEvent reports whether a user may send a state event of a type // into a room. // // It exists for the two endpoints that are *about* a room rather than in it // — publishing the room in the directory, and deleting one of its aliases — // where the spec's permission is "could set the canonical alias". Nothing // that sends an event uses it: an event's permission is decided by the // authorisation rules when it is sent, and asking twice would be two // answers to maintain. func (r *API) CanSendStateEvent(ctx context.Context, roomID, userID, eventType string) (bool, error) { room, rules, err := r.roomAndRules(ctx, roomID) if err != nil { return false, err } st, err := r.currentState(ctx, room, rules) if err != nil { return false, err } if st.Membership(userID) != event.MembershipJoin { return false, nil } create := st.Create() if create == nil { return false, fmt.Errorf("%w: %s has no create event in its state", ErrRejected, roomID) } powerLevels, err := auth.ResolvePowerLevels(st.PowerLevels(), create, rules) if err != nil { return false, fmt.Errorf("%w: %v", ErrRejected, err) } return powerLevels.UserLevel(userID) >= powerLevels.SendLevel(eventType, true), nil } // RoomVersion returns a room's version, for the handful of callers that // need to report it. func (r *API) RoomVersion(ctx context.Context, roomID string) (string, error) { room, err := r.store.Room(ctx, roomID) if err != nil { return "", err } return room.Version, nil }