Membership viewing API when user left the room (#194)

* Implement case where user left the room

* Filter by membership event

* Move the logic from the storage to the query API

* Fix check on state entries iteration

* Remove aliases methods from query API

* Use structure for response to match with the spec

* Remove filtering on /members and implement /joined_members
This commit is contained in:
Brendan Abolivier 2017-08-24 16:00:14 +01:00 committed by Mark Haines
parent fceb027ecc
commit 685e056ab3
6 changed files with 126 additions and 43 deletions

View file

@ -23,16 +23,22 @@ import (
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
) )
type response struct {
Chunk []gomatrixserverlib.ClientEvent `json:"chunk"`
}
// GetMemberships implements GET /rooms/{roomId}/members // GetMemberships implements GET /rooms/{roomId}/members
func GetMemberships( func GetMemberships(
req *http.Request, device *authtypes.Device, roomID string, req *http.Request, device *authtypes.Device, roomID string, joinedOnly bool,
accountDB *accounts.Database, cfg config.Dendrite, accountDB *accounts.Database, cfg config.Dendrite,
queryAPI api.RoomserverQueryAPI, queryAPI api.RoomserverQueryAPI,
) util.JSONResponse { ) util.JSONResponse {
queryReq := api.QueryMembershipsForRoomRequest{ queryReq := api.QueryMembershipsForRoomRequest{
JoinedOnly: joinedOnly,
RoomID: roomID, RoomID: roomID,
Sender: device.UserID, Sender: device.UserID,
} }
@ -50,6 +56,6 @@ func GetMemberships(
return util.JSONResponse{ return util.JSONResponse{
Code: 200, Code: 200,
JSON: queryRes.JoinEvents, JSON: response{queryRes.JoinEvents},
} }
} }

View file

@ -302,7 +302,14 @@ func Setup(
r0mux.Handle("/rooms/{roomID}/members", r0mux.Handle("/rooms/{roomID}/members",
common.MakeAuthAPI("rooms_members", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { common.MakeAuthAPI("rooms_members", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars := mux.Vars(req) vars := mux.Vars(req)
return readers.GetMemberships(req, device, vars["roomID"], accountDB, cfg, queryAPI) return readers.GetMemberships(req, device, vars["roomID"], false, accountDB, cfg, queryAPI)
}),
)
r0mux.Handle("/rooms/{roomID}/joined_members",
common.MakeAuthAPI("rooms_members", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
vars := mux.Vars(req)
return readers.GetMemberships(req, device, vars["roomID"], true, accountDB, cfg, queryAPI)
}), }),
) )

View file

@ -102,6 +102,8 @@ type QueryEventsByIDResponse struct {
// QueryMembershipsForRoomRequest is a request to QueryMembershipsForRoom // QueryMembershipsForRoomRequest is a request to QueryMembershipsForRoom
type QueryMembershipsForRoomRequest struct { type QueryMembershipsForRoomRequest struct {
// If true, only returns the membership events of "join" membership
JoinedOnly bool `json:"joined_only"`
// ID of the room to fetch memberships from // ID of the room to fetch memberships from
RoomID string `json:"room_id"` RoomID string `json:"room_id"`
// ID of the user sending the request // ID of the user sending the request

View file

@ -40,13 +40,20 @@ type RoomserverQueryAPIDatabase interface {
// Look up the numeric IDs for a list of events. // Look up the numeric IDs for a list of events.
// Returns an error if there was a problem talking to the database. // Returns an error if there was a problem talking to the database.
EventNIDs(eventIDs []string) (map[string]types.EventNID, error) EventNIDs(eventIDs []string) (map[string]types.EventNID, error)
// Look up the join events for all members in a room as requested by a given // Lookup the event IDs for a batch of event numeric IDs.
// user. If the user is currently in the room, returns the room's current // Returns an error if the retrieval went wrong.
// members, if not returns an empty array (TODO: Fix it) EventIDs(eventNIDs []types.EventNID) (map[types.EventNID]string, error)
// If the user requesting the list of members has never been in the room, // Lookup the membership of a given user in a given room.
// returns nil. // Returns the numeric ID of the latest membership event sent from this user
// If there was an issue retrieving the events, returns an error. // in this room, along a boolean set to true if the user is still in this room,
GetMembershipEvents(roomNID types.RoomNID, requestSenderUserID string) (events []types.Event, err error) // false if not.
// Returns an error if there was a problem talking to the database.
GetMembership(roomNID types.RoomNID, requestSenderUserID string) (membershipEventNID types.EventNID, stillInRoom bool, err error)
// Lookup the membership event numeric IDs for all user that are or have
// been members of a given room. Only lookup events of "join" membership if
// joinOnly is set to true.
// Returns an error if there was a problem talking to the database.
GetMembershipEventNIDsForRoom(roomNID types.RoomNID, joinOnly bool) ([]types.EventNID, error)
// Look up the active invites targeting a user in a room and return the // Look up the active invites targeting a user in a room and return the
// numeric state key IDs for the user IDs who sent them. // numeric state key IDs for the user IDs who sent them.
// Returns an error if there was a problem talking to the database. // Returns an error if there was a problem talking to the database.
@ -194,12 +201,12 @@ func (r *RoomserverQueryAPI) QueryMembershipsForRoom(
return err return err
} }
events, err := r.DB.GetMembershipEvents(roomNID, request.Sender) membershipEventNID, stillInRoom, err := r.DB.GetMembership(roomNID, request.Sender)
if err != nil { if err != nil {
return nil return nil
} }
if events == nil { if membershipEventNID == 0 {
response.HasBeenInRoom = false response.HasBeenInRoom = false
response.JoinEvents = nil response.JoinEvents = nil
return nil return nil
@ -207,6 +214,24 @@ func (r *RoomserverQueryAPI) QueryMembershipsForRoom(
response.HasBeenInRoom = true response.HasBeenInRoom = true
response.JoinEvents = []gomatrixserverlib.ClientEvent{} response.JoinEvents = []gomatrixserverlib.ClientEvent{}
var events []types.Event
if stillInRoom {
var eventNIDs []types.EventNID
eventNIDs, err = r.DB.GetMembershipEventNIDsForRoom(roomNID, request.JoinedOnly)
if err != nil {
return err
}
events, err = r.DB.Events(eventNIDs)
} else {
events, err = r.getMembershipsBeforeEventNID(membershipEventNID, request.JoinedOnly)
}
if err != nil {
return err
}
for _, event := range events { for _, event := range events {
clientEvent := gomatrixserverlib.ToClientEvent(event.Event, gomatrixserverlib.FormatAll) clientEvent := gomatrixserverlib.ToClientEvent(event.Event, gomatrixserverlib.FormatAll)
response.JoinEvents = append(response.JoinEvents, clientEvent) response.JoinEvents = append(response.JoinEvents, clientEvent)
@ -215,6 +240,63 @@ func (r *RoomserverQueryAPI) QueryMembershipsForRoom(
return nil return nil
} }
// getMembershipsBeforeEventNID takes the numeric ID of an event and fetches the state
// of the event's room as it was when this event was fired, then filters the state events to
// only keep the "m.room.member" events with a "join" membership. These events are returned.
// Returns an error if there was an issue fetching the events.
func (r *RoomserverQueryAPI) getMembershipsBeforeEventNID(eventNID types.EventNID, joinedOnly bool) ([]types.Event, error) {
events := []types.Event{}
// Lookup the event NID
eIDs, err := r.DB.EventIDs([]types.EventNID{eventNID})
if err != nil {
return nil, err
}
eventIDs := []string{eIDs[eventNID]}
prevState, err := r.DB.StateAtEventIDs(eventIDs)
if err != nil {
return nil, err
}
// Fetch the state as it was when this event was fired
stateEntries, err := state.LoadCombinedStateAfterEvents(r.DB, prevState)
if err != nil {
return nil, err
}
var eventNIDs []types.EventNID
for _, entry := range stateEntries {
// Filter the events to retrieve to only keep the membership events
if entry.EventTypeNID == types.MRoomMemberNID {
eventNIDs = append(eventNIDs, entry.EventNID)
}
}
// Get all of the events in this state
stateEvents, err := r.DB.Events(eventNIDs)
if err != nil {
return nil, err
}
if !joinedOnly {
return stateEvents, nil
}
// Filter the events to only keep the "join" membership events
for _, event := range stateEvents {
membership, err := event.Membership()
if err != nil {
return nil, err
}
if membership == "join" {
events = append(events, event)
}
}
return events, nil
}
// QueryInvitesForUser implements api.RoomserverQueryAPI // QueryInvitesForUser implements api.RoomserverQueryAPI
func (r *RoomserverQueryAPI) QueryInvitesForUser( func (r *RoomserverQueryAPI) QueryInvitesForUser(
request *api.QueryInvitesForUserRequest, request *api.QueryInvitesForUserRequest,

View file

@ -77,7 +77,7 @@ const selectMembershipsFromRoomAndMembershipSQL = "" +
" WHERE room_nid = $1 AND membership_nid = $2" " WHERE room_nid = $1 AND membership_nid = $2"
const selectMembershipsFromRoomSQL = "" + const selectMembershipsFromRoomSQL = "" +
"SELECT membership_nid, event_nid FROM roomserver_membership" + "SELECT event_nid FROM roomserver_membership" +
" WHERE room_nid = $1" " WHERE room_nid = $1"
const selectMembershipForUpdateSQL = "" + const selectMembershipForUpdateSQL = "" +
@ -140,20 +140,18 @@ func (s *membershipStatements) selectMembershipFromRoomAndTarget(
func (s *membershipStatements) selectMembershipsFromRoom( func (s *membershipStatements) selectMembershipsFromRoom(
roomNID types.RoomNID, roomNID types.RoomNID,
) (eventNIDs map[types.EventNID]membershipState, err error) { ) (eventNIDs []types.EventNID, err error) {
rows, err := s.selectMembershipsFromRoomStmt.Query(roomNID) rows, err := s.selectMembershipsFromRoomStmt.Query(roomNID)
if err != nil { if err != nil {
return return
} }
eventNIDs = make(map[types.EventNID]membershipState)
for rows.Next() { for rows.Next() {
var eNID types.EventNID var eNID types.EventNID
var membership membershipState if err = rows.Scan(&eNID); err != nil {
if err = rows.Scan(&membership, &eNID); err != nil {
return return
} }
eventNIDs[eNID] = membership eventNIDs = append(eventNIDs, eNID)
} }
return return
} }

View file

@ -552,8 +552,8 @@ func (u *membershipUpdater) SetToLeave(senderUserID string, eventID string) ([]s
return inviteEventIDs, nil return inviteEventIDs, nil
} }
// GetMembershipEvents implements query.RoomserverQueryAPIDB // GetMembership implements query.RoomserverQueryAPIDB
func (d *Database) GetMembershipEvents(roomNID types.RoomNID, requestSenderUserID string) (events []types.Event, err error) { func (d *Database) GetMembership(roomNID types.RoomNID, requestSenderUserID string) (membershipEventNID types.EventNID, stillInRoom bool, err error) {
txn, err := d.db.Begin() txn, err := d.db.Begin()
if err != nil { if err != nil {
return return
@ -565,36 +565,24 @@ func (d *Database) GetMembershipEvents(roomNID types.RoomNID, requestSenderUserI
return return
} }
_, senderMembership, err := d.statements.selectMembershipFromRoomAndTarget(roomNID, requestSenderUserNID) senderMembershipEventNID, senderMembership, err := d.statements.selectMembershipFromRoomAndTarget(roomNID, requestSenderUserNID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
// The user has never been a member of that room // The user has never been a member of that room
return nil, nil return 0, false, nil
} else if err != nil { } else if err != nil {
return return
} }
if senderMembership == membershipStateJoin { return senderMembershipEventNID, senderMembership == membershipStateJoin, nil
// The user is still in the room: Send the current list of joined members }
var joinEventNIDs []types.EventNID
joinEventNIDs, err = d.statements.selectMembershipsFromRoomAndMembership(roomNID, membershipStateJoin) // GetMembershipEventNIDsForRoom implements query.RoomserverQueryAPIDB
if err != nil { func (d *Database) GetMembershipEventNIDsForRoom(roomNID types.RoomNID, joinOnly bool) ([]types.EventNID, error) {
return nil, err if joinOnly {
return d.statements.selectMembershipsFromRoomAndMembership(roomNID, membershipStateJoin)
} }
events, err = d.Events(joinEventNIDs) return d.statements.selectMembershipsFromRoom(roomNID)
} else {
// The user isn't in the room anymore
// TODO: Send the list of joined member as it was when the user left
// We cannot do this using only the memberships database, as it
// only stores the latest join event NID for a given target user.
// The solution would be to build the state of a room after before
// the leave event and extract a members list from it.
// For now, we return an empty slice so we know the user has been
// in the room before.
events = []types.Event{}
}
return
} }
type transaction struct { type transaction struct {