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 string `json:"room_id"` 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) { room, rules, err := r.roomAndRules(ctx, roomID) if err != nil { return ClientEvent{}, err } if err := r.requireVisibility(ctx, room, rules, userID); err != nil { return ClientEvent{}, err } stored, err := r.store.Event(ctx, roomID, eventID) if err != nil { return ClientEvent{}, err } e, err := parseEvent(stored, rules) if err != nil { return ClientEvent{}, err } return r.clientEvent(stored, e, roomID), nil } // RoomState returns a room's current state in the client shape. func (r *API) RoomState(ctx context.Context, roomID, userID string) ([]ClientEvent, error) { room, rules, err := r.roomAndRules(ctx, roomID) if err != nil { return nil, err } if err := r.requireVisibility(ctx, room, rules, userID); err != nil { return nil, err } stored, err := r.store.CurrentStateEvents(ctx, roomID) if err != nil { return nil, err } out := make([]ClientEvent, 0, len(stored)) for _, s := range stored { e, err := parseEvent(s, rules) if err != nil { return nil, err } out = append(out, r.clientEvent(s, e, roomID)) } return out, nil } // 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) { room, rules, err := r.roomAndRules(ctx, roomID) if err != nil { return nil, err } if err := r.requireVisibility(ctx, room, rules, userID); err != nil { return nil, err } stored, err := r.store.CurrentStateEvent(ctx, roomID, eventType, stateKey) if err != nil { return nil, err } e, err := parseEvent(stored, rules) 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) { room, rules, err := r.roomAndRules(ctx, roomID) if err != nil { return ClientEvent{}, err } if err := r.requireVisibility(ctx, room, rules, userID); err != nil { return ClientEvent{}, err } 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 } // Members returns a room's m.room.member events. func (r *API) Members(ctx context.Context, req MembersRequest) ([]ClientEvent, error) { room, rules, err := r.roomAndRules(ctx, req.RoomID) if err != nil { return nil, err } if err := r.requireVisibility(ctx, room, rules, req.User); err != nil { return nil, err } 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 } out := make([]ClientEvent, 0, len(stored)) for _, s := range stored { e, err := parseEvent(s, rules) if err != nil { return nil, err } if req.NotMembership != "" { membership, err := e.Membership() if err == nil && membership == req.NotMembership { continue } } out = append(out, r.clientEvent(s, e, req.RoomID)) } return out, nil } // 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 } // requireVisibility refuses a caller who may not see a room's state. // // This is a room-level test, not the per-event history visibility the spec // defines. A member sees the room; so does anyone at all when the room's // history is world_readable; a user who has left sees the state as it was // when they left, which is their own membership event's snapshot and which // this returns the current state for instead. // // That last case is the gap, and it is deliberate. Serving a departed // member the state as of their leave event means answering every state read // from a historical snapshot and filtering each event by the visibility in // force when it was sent — which is the same machinery /messages and // /context need, and so belongs with them in Phase 4 rather than being // built twice. Until then a user who has left a room is refused, which errs // towards showing them too little. func (r *API) requireVisibility(ctx context.Context, room storage.Room, rules roomversion.Rules, userID string) error { entry, err := r.store.Membership(ctx, room.ID, userID) if err != nil { return err } if entry.Membership == event.MembershipJoin { return nil } summary, err := r.store.RoomSummary(ctx, room.ID) if err != nil { return err } if summary.HistoryVisibility == HistoryVisibilityWorldReadable { return nil } // An invited user can see enough of the room to decide, which the spec // delivers as stripped state on the invite rather than as a state read. if entry.Membership == event.MembershipInvite { return fmt.Errorf("%w: %s has been invited to %s but has not joined it", ErrForbidden, userID, room.ID) } return fmt.Errorf("%w: %s is not in %s", ErrForbidden, userID, room.ID) } // 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 }