From 5950293e794680ebc45b446894367ff8067de5d2 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 21 Aug 2017 16:34:26 +0100 Subject: [PATCH] Membership viewing API (#174) * Basic memberships retrieval * Change the way the memberships are saved in the client API database * Retrieve single membership * Get memberships only if the user is or has been in the room * Check server name on room ID instead of user ID * Save the join membership event and updates it when necessary * Membership events retrieval + update on leave * Implement the API on the roomserver and client API server * Fix comments * Remove the functions and attributes used before the new query API * Explicitely state what we return in query * Remove tab --- .../auth/storage/accounts/membership_table.go | 3 - .../auth/storage/accounts/storage.go | 21 ++-- .../dendrite/clientapi/readers/memberships.go | 55 ++++++++++ .../dendrite/clientapi/routing/routing.go | 7 ++ .../dendrite/roomserver/api/query.go | 35 ++++++ .../dendrite/roomserver/input/membership.go | 18 ++-- .../dendrite/roomserver/query/query.go | 52 +++++++++ .../storage/event_state_keys_table.go | 47 ++++++++ .../roomserver/storage/membership_table.go | 87 +++++++++++++-- .../dendrite/roomserver/storage/storage.go | 100 +++++++++++++++--- .../dendrite/roomserver/types/types.go | 6 +- 11 files changed, 383 insertions(+), 48 deletions(-) create mode 100644 src/github.com/matrix-org/dendrite/clientapi/readers/memberships.go diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/membership_table.go b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/membership_table.go index 42922743..9652cdac 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/membership_table.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/membership_table.go @@ -44,9 +44,6 @@ const insertMembershipSQL = ` ON CONFLICT (localpart, room_id) DO UPDATE SET event_id = EXCLUDED.event_id ` -const selectMembershipSQL = "" + - "SELECT * from account_memberships WHERE localpart = $1 AND room_id = $2" - const selectMembershipsByLocalpartSQL = "" + "SELECT room_id, event_id FROM account_memberships WHERE localpart = $1" diff --git a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/storage.go b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/storage.go index fb98946a..76b9a4dd 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/storage.go +++ b/src/github.com/matrix-org/dendrite/clientapi/auth/storage/accounts/storage.go @@ -121,7 +121,8 @@ func (d *Database) SetPartitionOffset(topic string, partition int32, offset int6 } // SaveMembership saves the user matching a given localpart as a member of a given -// room. It also stores the ID of the `join` membership event. +// room. It also stores the ID of the membership event and a flag on whether the user +// is still in the room. // If a membership already exists between the user and the room, or of the // insert fails, returns the SQL error func (d *Database) SaveMembership(localpart string, roomID string, eventID string, txn *sql.Tx) error { @@ -156,23 +157,19 @@ func (d *Database) UpdateMemberships(eventsToAdd []gomatrixserverlib.Event, idsT }) } -// GetMembershipsByLocalpart returns an array containing the IDs of all the rooms -// a user matching a given localpart is a member of +// GetMembershipsByLocalpart returns an array containing the memberships for all +// the rooms a user matching a given localpart is a member of // If no membership match the given localpart, returns an empty array // If there was an issue during the retrieval, returns the SQL error func (d *Database) GetMembershipsByLocalpart(localpart string) (memberships []authtypes.Membership, err error) { return d.memberships.selectMembershipsByLocalpart(localpart) } -// UpdateMembership update the "join" membership event ID of a membership. -// This is useful in case of membership upgrade (e.g. profile update) -// If there was an issue during the update, returns the SQL error -func (d *Database) UpdateMembership(oldEventID string, newEventID string) error { - return d.memberships.updateMembershipByEventID(oldEventID, newEventID) -} - -// newMembership will save a new membership in the database if the given state -// event is a "join" membership event +// newMembership will save a new membership in the database, with a flag on whether +// the user is still in the room. This flag is set to true if the given state +// event is a "join" membership event and false if the event is a "leave" or "ban" +// membership. If the event isn't a m.room.member event with one of these three +// values, does nothing. // If the event isn't a "join" membership event, does nothing // If an error occurred, returns it func (d *Database) newMembership(ev gomatrixserverlib.Event, txn *sql.Tx) error { diff --git a/src/github.com/matrix-org/dendrite/clientapi/readers/memberships.go b/src/github.com/matrix-org/dendrite/clientapi/readers/memberships.go new file mode 100644 index 00000000..c734e3b3 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/readers/memberships.go @@ -0,0 +1,55 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package readers + +import ( + "net/http" + + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" + "github.com/matrix-org/dendrite/clientapi/httputil" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/common/config" + "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/util" +) + +// GetMemberships implements GET /rooms/{roomId}/members +func GetMemberships( + req *http.Request, device *authtypes.Device, roomID string, + accountDB *accounts.Database, cfg config.Dendrite, + queryAPI api.RoomserverQueryAPI, +) util.JSONResponse { + queryReq := api.QueryMembershipsForRoomRequest{ + RoomID: roomID, + Sender: device.UserID, + } + var queryRes api.QueryMembershipsForRoomResponse + if err := queryAPI.QueryMembershipsForRoom(&queryReq, &queryRes); err != nil { + return httputil.LogThenError(req, err) + } + + if !queryRes.HasBeenInRoom { + return util.JSONResponse{ + Code: 403, + JSON: jsonerror.Forbidden("You aren't a member of the room and weren't previously a member of the room."), + } + } + + return util.JSONResponse{ + Code: 200, + JSON: queryRes.JoinEvents, + } +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go index eb84b2a9..8a5799c0 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -313,6 +313,13 @@ func Setup( }), ) + r0mux.Handle("/rooms/{roomID}/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"], accountDB, cfg, queryAPI) + }), + ) + r0mux.Handle("/rooms/{roomID}/read_markers", common.MakeAPI("rooms_read_markers", func(req *http.Request) util.JSONResponse { // TODO: return the read_markers. diff --git a/src/github.com/matrix-org/dendrite/roomserver/api/query.go b/src/github.com/matrix-org/dendrite/roomserver/api/query.go index 6e6a838a..f07da59e 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/api/query.go +++ b/src/github.com/matrix-org/dendrite/roomserver/api/query.go @@ -100,6 +100,23 @@ type QueryEventsByIDResponse struct { Events []gomatrixserverlib.Event `json:"events"` } +// QueryMembershipsForRoomRequest is a request to QueryMembershipsForRoom +type QueryMembershipsForRoomRequest struct { + // ID of the room to fetch memberships from + RoomID string `json:"room_id"` + // ID of the user sending the request + Sender string `json:"sender"` +} + +// QueryMembershipsForRoomResponse is a response to QueryMembershipsForRoom +type QueryMembershipsForRoomResponse struct { + // The "m.room.member" events (of "join" membership) in the client format + JoinEvents []gomatrixserverlib.ClientEvent `json:"join_events"` + // True if the user has been in room before and has either stayed in it or + // left it. + HasBeenInRoom bool `json:"has_been_in_room"` +} + // RoomserverQueryAPI is used to query information from the room server. type RoomserverQueryAPI interface { // Query the latest events and state for a room from the room server. @@ -119,6 +136,12 @@ type RoomserverQueryAPI interface { request *QueryEventsByIDRequest, response *QueryEventsByIDResponse, ) error + + // Query a list of membership events for a room + QueryMembershipsForRoom( + request *QueryMembershipsForRoomRequest, + response *QueryMembershipsForRoomResponse, + ) error } // RoomserverQueryLatestEventsAndStatePath is the HTTP path for the QueryLatestEventsAndState API. @@ -130,6 +153,9 @@ const RoomserverQueryStateAfterEventsPath = "/api/roomserver/queryStateAfterEven // RoomserverQueryEventsByIDPath is the HTTP path for the QueryEventsByID API. const RoomserverQueryEventsByIDPath = "/api/roomserver/queryEventsByID" +// RoomserverQueryMembershipsForRoomPath is the HTTP path for the QueryMembershipsForRoom API +const RoomserverQueryMembershipsForRoomPath = "/api/roomserver/queryMembershipsForRoom" + // NewRoomserverQueryAPIHTTP creates a RoomserverQueryAPI implemented by talking to a HTTP POST API. // If httpClient is nil then it uses the http.DefaultClient func NewRoomserverQueryAPIHTTP(roomserverURL string, httpClient *http.Client) RoomserverQueryAPI { @@ -171,6 +197,15 @@ func (h *httpRoomserverQueryAPI) QueryEventsByID( return postJSON(h.httpClient, apiURL, request, response) } +// QueryMembershipsForRoom implements RoomserverQueryAPI +func (h *httpRoomserverQueryAPI) QueryMembershipsForRoom( + request *QueryMembershipsForRoomRequest, + response *QueryMembershipsForRoomResponse, +) error { + apiURL := h.roomserverURL + RoomserverQueryMembershipsForRoomPath + return postJSON(h.httpClient, apiURL, request, response) +} + func postJSON(httpClient *http.Client, apiURL string, request, response interface{}) error { jsonBytes, err := json.Marshal(request) if err != nil { diff --git a/src/github.com/matrix-org/dendrite/roomserver/input/membership.go b/src/github.com/matrix-org/dendrite/roomserver/input/membership.go index f306697f..6eeb0914 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/input/membership.go +++ b/src/github.com/matrix-org/dendrite/roomserver/input/membership.go @@ -95,10 +95,9 @@ func updateMembership( return nil, err } } - if old == new { + if old == new && new != "join" { // If the membership is the same then nothing changed and we can return - // immediately. This should help speed up processing for display name - // changes where the membership is "join" both before and after. + // immediately, unless it's a "join" update (e.g. profile update). return updates, nil } @@ -152,16 +151,21 @@ func updateToInviteMembership( func updateToJoinMembership( mu types.MembershipUpdater, add *gomatrixserverlib.Event, updates []api.OutputEvent, ) ([]api.OutputEvent, error) { - // If the user is already marked as being joined then we can return immediately. - // TODO: Is this code reachable given the "old != new" guard in updateMembership? + // If the user is already marked as being joined, we call SetToJoin to update + // the event ID then we can return immediately. Retired is ignored as there + // is no invite event to retire. if mu.IsJoin() { + _, err := mu.SetToJoin(add.Sender(), add.EventID(), true) + if err != nil { + return nil, err + } return updates, nil } // When we mark a user as being joined we will invalidate any invites that // are active for that user. We notify the consumers that the invites have // been retired using a special event, even though they could infer this // by studying the state changes in the room event stream. - retired, err := mu.SetToJoin(add.Sender()) + retired, err := mu.SetToJoin(add.Sender(), add.EventID(), false) if err != nil { return nil, err } @@ -194,7 +198,7 @@ func updateToLeaveMembership( // are active for that user. We notify the consumers that the invites have // been retired using a special event, even though they could infer this // by studying the state changes in the room event stream. - retired, err := mu.SetToLeave(add.Sender()) + retired, err := mu.SetToLeave(add.Sender(), add.EventID()) if err != nil { return nil, err } diff --git a/src/github.com/matrix-org/dendrite/roomserver/query/query.go b/src/github.com/matrix-org/dendrite/roomserver/query/query.go index 30b695fb..84f5d44c 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/query/query.go +++ b/src/github.com/matrix-org/dendrite/roomserver/query/query.go @@ -52,6 +52,13 @@ type RoomserverQueryAPIDatabase interface { // Remove a given room alias. // Returns an error if there was a problem talking to the database. RemoveRoomAlias(alias string) error + // Lookup the join events for all members in a room as requested by a given + // user. If the user is currently in the room, returns the room's current + // members, if not returns an empty array (TODO: Fix it) + // If the user requesting the list of members has never been in the room, + // returns nil. + // If there was an issue retrieving the events, returns an error. + GetMembershipEvents(roomNID types.RoomNID, requestSenderUserID string) (events []types.Event, err error) } // RoomserverQueryAPI is an implementation of api.RoomserverQueryAPI @@ -182,6 +189,37 @@ func (r *RoomserverQueryAPI) loadEvents(eventNIDs []types.EventNID) ([]gomatrixs return result, nil } +// QueryMembershipsForRoom implements api.RoomserverQueryAPI +func (r *RoomserverQueryAPI) QueryMembershipsForRoom( + request *api.QueryMembershipsForRoomRequest, + response *api.QueryMembershipsForRoomResponse, +) error { + roomNID, err := r.DB.RoomNID(request.RoomID) + if err != nil { + return err + } + + events, err := r.DB.GetMembershipEvents(roomNID, request.Sender) + if err != nil { + return nil + } + + if events == nil { + response.HasBeenInRoom = false + response.JoinEvents = nil + return nil + } + + response.HasBeenInRoom = true + response.JoinEvents = []gomatrixserverlib.ClientEvent{} + for _, event := range events { + clientEvent := gomatrixserverlib.ToClientEvent(event.Event, gomatrixserverlib.FormatAll) + response.JoinEvents = append(response.JoinEvents, clientEvent) + } + + return nil +} + // SetupHTTP adds the RoomserverQueryAPI handlers to the http.ServeMux. func (r *RoomserverQueryAPI) SetupHTTP(servMux *http.ServeMux) { servMux.Handle( @@ -226,4 +264,18 @@ func (r *RoomserverQueryAPI) SetupHTTP(servMux *http.ServeMux) { return util.JSONResponse{Code: 200, JSON: &response} }), ) + servMux.Handle( + api.RoomserverQueryMembershipsForRoomPath, + common.MakeAPI("queryMembershipsForRoom", func(req *http.Request) util.JSONResponse { + var request api.QueryMembershipsForRoomRequest + var response api.QueryMembershipsForRoomResponse + if err := json.NewDecoder(req.Body).Decode(&request); err != nil { + return util.ErrorResponse(err) + } + if err := r.QueryMembershipsForRoom(&request, &response); err != nil { + return util.ErrorResponse(err) + } + return util.JSONResponse{Code: 200, JSON: &response} + }), + ) } diff --git a/src/github.com/matrix-org/dendrite/roomserver/storage/event_state_keys_table.go b/src/github.com/matrix-org/dendrite/roomserver/storage/event_state_keys_table.go index d30e4581..b4dae8f2 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/storage/event_state_keys_table.go +++ b/src/github.com/matrix-org/dendrite/roomserver/storage/event_state_keys_table.go @@ -58,10 +58,22 @@ const bulkSelectEventStateKeyNIDSQL = "" + "SELECT event_state_key, event_state_key_nid FROM roomserver_event_state_keys" + " WHERE event_state_key = ANY($1)" +const selectEventStateKeySQL = "" + + "SELECT event_state_key FROM roomserver_event_state_keys" + + " WHERE event_state_key_nid = $1" + +// Bulk lookup from numeric ID to string state key for that state key. +// Takes an array of strings as the query parameter. +const bulkSelectEventStateKeySQL = "" + + "SELECT event_state_key, event_state_key_nid FROM roomserver_event_state_keys" + + " WHERE event_state_key_nid = ANY($1)" + type eventStateKeyStatements struct { insertEventStateKeyNIDStmt *sql.Stmt selectEventStateKeyNIDStmt *sql.Stmt + selectEventStateKeyStmt *sql.Stmt bulkSelectEventStateKeyNIDStmt *sql.Stmt + bulkSelectEventStateKeyStmt *sql.Stmt } func (s *eventStateKeyStatements) prepare(db *sql.DB) (err error) { @@ -72,7 +84,9 @@ func (s *eventStateKeyStatements) prepare(db *sql.DB) (err error) { return statementList{ {&s.insertEventStateKeyNIDStmt, insertEventStateKeyNIDSQL}, {&s.selectEventStateKeyNIDStmt, selectEventStateKeyNIDSQL}, + {&s.selectEventStateKeyStmt, selectEventStateKeySQL}, {&s.bulkSelectEventStateKeyNIDStmt, bulkSelectEventStateKeyNIDSQL}, + {&s.bulkSelectEventStateKeyStmt, bulkSelectEventStateKeySQL}, }.prepare(db) } @@ -114,3 +128,36 @@ func (s *eventStateKeyStatements) bulkSelectEventStateKeyNID(eventStateKeys []st } return result, nil } + +func (s *eventStateKeyStatements) selectEventStateKey(txn *sql.Tx, eventStateKeyNID types.EventStateKeyNID) (string, error) { + var eventStateKey string + stmt := s.selectEventStateKeyStmt + if txn != nil { + stmt = txn.Stmt(stmt) + } + err := stmt.QueryRow(eventStateKeyNID).Scan(&eventStateKey) + return eventStateKey, err +} + +func (s *eventStateKeyStatements) bulkSelectEventStateKey(eventStateKeyNIDs []types.EventStateKeyNID) (map[types.EventStateKeyNID]string, error) { + var nIDs pq.Int64Array + for i := range eventStateKeyNIDs { + nIDs[i] = int64(eventStateKeyNIDs[i]) + } + rows, err := s.bulkSelectEventStateKeyStmt.Query(nIDs) + if err != nil { + return nil, err + } + defer rows.Close() + + result := make(map[types.EventStateKeyNID]string, len(eventStateKeyNIDs)) + for rows.Next() { + var stateKey string + var stateKeyNID int64 + if err := rows.Scan(&stateKey, &stateKeyNID); err != nil { + return nil, err + } + result[types.EventStateKeyNID(stateKeyNID)] = stateKey + } + return result, nil +} diff --git a/src/github.com/matrix-org/dendrite/roomserver/storage/membership_table.go b/src/github.com/matrix-org/dendrite/roomserver/storage/membership_table.go index 725e5b8d..52051af5 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/storage/membership_table.go +++ b/src/github.com/matrix-org/dendrite/roomserver/storage/membership_table.go @@ -33,7 +33,7 @@ const membershipSchema = ` -- and the room state tables. -- This table is updated in one of 3 ways: -- 1) The membership of a user changes within the current state of the room. --- 2) An invite is received outside of a room over federation. +-- 2) An invite is received outside of a room over federation. -- 3) An invite is rejected outside of a room over federation. CREATE TABLE IF NOT EXISTS roomserver_membership ( room_nid BIGINT NOT NULL, @@ -46,6 +46,16 @@ CREATE TABLE IF NOT EXISTS roomserver_membership ( -- The state the user is in within this room. -- Default value is "membershipStateLeaveOrBan" membership_nid BIGINT NOT NULL DEFAULT 1, + -- The numeric ID of the membership event. + -- It refers to the join membership event if the membership_nid is join (3), + -- and to the leave/ban membership event if the membership_nid is leave or + -- ban (1). + -- If the membership_nid is invite (2) and the user has been in the room + -- before, it will refer to the previous leave/ban membership event, and will + -- be equals to 0 (its default) if the user never joined the room before. + -- This NID is updated if the join event gets updated (e.g. profile update), + -- or if the user leaves/joins the room. + event_nid BIGINT NOT NULL DEFAULT 0, UNIQUE (room_nid, target_nid) ); ` @@ -57,18 +67,33 @@ const insertMembershipSQL = "" + " VALUES ($1, $2)" + " ON CONFLICT DO NOTHING" +const selectMembershipFromRoomAndTargetSQL = "" + + "SELECT membership_nid, event_nid FROM roomserver_membership" + + " WHERE room_nid = $1 AND target_nid = $2" + +const selectMembershipsFromRoomAndMembershipSQL = "" + + "SELECT event_nid FROM roomserver_membership" + + " WHERE room_nid = $1 AND membership_nid = $2" + +const selectMembershipsFromRoomSQL = "" + + "SELECT membership_nid, event_nid FROM roomserver_membership" + + " WHERE room_nid = $1" + const selectMembershipForUpdateSQL = "" + "SELECT membership_nid FROM roomserver_membership" + " WHERE room_nid = $1 AND target_nid = $2 FOR UPDATE" const updateMembershipSQL = "" + - "UPDATE roomserver_membership SET sender_nid = $3, membership_nid = $4" + + "UPDATE roomserver_membership SET sender_nid = $3, membership_nid = $4, event_nid = $5" + " WHERE room_nid = $1 AND target_nid = $2" type membershipStatements struct { - insertMembershipStmt *sql.Stmt - selectMembershipForUpdateStmt *sql.Stmt - updateMembershipStmt *sql.Stmt + insertMembershipStmt *sql.Stmt + selectMembershipForUpdateStmt *sql.Stmt + selectMembershipFromRoomAndTargetStmt *sql.Stmt + selectMembershipsFromRoomAndMembershipStmt *sql.Stmt + selectMembershipsFromRoomStmt *sql.Stmt + updateMembershipStmt *sql.Stmt } func (s *membershipStatements) prepare(db *sql.DB) (err error) { @@ -80,6 +105,9 @@ func (s *membershipStatements) prepare(db *sql.DB) (err error) { return statementList{ {&s.insertMembershipStmt, insertMembershipSQL}, {&s.selectMembershipForUpdateStmt, selectMembershipForUpdateSQL}, + {&s.selectMembershipFromRoomAndTargetStmt, selectMembershipFromRoomAndTargetSQL}, + {&s.selectMembershipsFromRoomAndMembershipStmt, selectMembershipsFromRoomAndMembershipSQL}, + {&s.selectMembershipsFromRoomStmt, selectMembershipsFromRoomSQL}, {&s.updateMembershipStmt, updateMembershipSQL}, }.prepare(db) } @@ -100,12 +128,59 @@ func (s *membershipStatements) selectMembershipForUpdate( return } +func (s *membershipStatements) selectMembershipFromRoomAndTarget( + roomNID types.RoomNID, targetUserNID types.EventStateKeyNID, +) (eventNID types.EventNID, membership membershipState, err error) { + err = s.selectMembershipFromRoomAndTargetStmt.QueryRow( + roomNID, targetUserNID, + ).Scan(&membership, &eventNID) + return +} + +func (s *membershipStatements) selectMembershipsFromRoom( + roomNID types.RoomNID, +) (eventNIDs map[types.EventNID]membershipState, err error) { + rows, err := s.selectMembershipsFromRoomStmt.Query(roomNID) + if err != nil { + return + } + + eventNIDs = make(map[types.EventNID]membershipState) + for rows.Next() { + var eNID types.EventNID + var membership membershipState + if err = rows.Scan(&membership, &eNID); err != nil { + return + } + eventNIDs[eNID] = membership + } + return +} +func (s *membershipStatements) selectMembershipsFromRoomAndMembership( + roomNID types.RoomNID, membership membershipState, +) (eventNIDs []types.EventNID, err error) { + rows, err := s.selectMembershipsFromRoomAndMembershipStmt.Query(roomNID, membership) + if err != nil { + return + } + + for rows.Next() { + var eNID types.EventNID + if err = rows.Scan(&eNID); err != nil { + return + } + eventNIDs = append(eventNIDs, eNID) + } + return +} + func (s *membershipStatements) updateMembership( txn *sql.Tx, roomNID types.RoomNID, targetUserNID types.EventStateKeyNID, senderUserNID types.EventStateKeyNID, membership membershipState, + eventNID types.EventNID, ) error { _, err := txn.Stmt(s.updateMembershipStmt).Exec( - roomNID, targetUserNID, senderUserNID, membership, + roomNID, targetUserNID, senderUserNID, membership, eventNID, ) return err } diff --git a/src/github.com/matrix-org/dendrite/roomserver/storage/storage.go b/src/github.com/matrix-org/dendrite/roomserver/storage/storage.go index d323fd13..17b30860 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/storage/storage.go +++ b/src/github.com/matrix-org/dendrite/roomserver/storage/storage.go @@ -435,7 +435,7 @@ func (u *membershipUpdater) SetToInvite(event gomatrixserverlib.Event) (bool, er } if u.membership != membershipStateInvite { if err = u.d.statements.updateMembership( - u.txn, u.roomNID, u.targetUserNID, senderUserNID, membershipStateInvite, + u.txn, u.roomNID, u.targetUserNID, senderUserNID, membershipStateInvite, 0, ); err != nil { return false, err } @@ -444,7 +444,43 @@ func (u *membershipUpdater) SetToInvite(event gomatrixserverlib.Event) (bool, er } // SetToJoin implements types.MembershipUpdater -func (u *membershipUpdater) SetToJoin(senderUserID string) ([]string, error) { +func (u *membershipUpdater) SetToJoin(senderUserID string, eventID string, isUpdate bool) ([]string, error) { + var inviteEventIDs []string + + senderUserNID, err := u.d.assignStateKeyNID(u.txn, senderUserID) + if err != nil { + return nil, err + } + + // If this is a join event update, there is no invite to update + if !isUpdate { + inviteEventIDs, err = u.d.statements.updateInviteRetired( + u.txn, u.roomNID, u.targetUserNID, + ) + if err != nil { + return nil, err + } + } + + // Lookup the NID of the new join event + nIDs, err := u.d.EventNIDs([]string{eventID}) + if err != nil { + return nil, err + } + + if u.membership != membershipStateJoin || isUpdate { + if err = u.d.statements.updateMembership( + u.txn, u.roomNID, u.targetUserNID, senderUserNID, membershipStateJoin, nIDs[eventID], + ); err != nil { + return nil, err + } + } + + return inviteEventIDs, nil +} + +// SetToLeave implements types.MembershipUpdater +func (u *membershipUpdater) SetToLeave(senderUserID string, eventID string) ([]string, error) { senderUserNID, err := u.d.assignStateKeyNID(u.txn, senderUserID) if err != nil { return nil, err @@ -455,9 +491,16 @@ func (u *membershipUpdater) SetToJoin(senderUserID string) ([]string, error) { if err != nil { return nil, err } - if u.membership != membershipStateJoin { + + // Lookup the NID of the new leave event + nIDs, err := u.d.EventNIDs([]string{eventID}) + if err != nil { + return nil, err + } + + if u.membership != membershipStateLeaveOrBan { if err = u.d.statements.updateMembership( - u.txn, u.roomNID, u.targetUserNID, senderUserNID, membershipStateJoin, + u.txn, u.roomNID, u.targetUserNID, senderUserNID, membershipStateLeaveOrBan, nIDs[eventID], ); err != nil { return nil, err } @@ -465,26 +508,49 @@ func (u *membershipUpdater) SetToJoin(senderUserID string) ([]string, error) { return inviteEventIDs, nil } -// SetToLeave implements types.MembershipUpdater -func (u *membershipUpdater) SetToLeave(senderUserID string) ([]string, error) { - senderUserNID, err := u.d.assignStateKeyNID(u.txn, senderUserID) +// GetMembershipEvents implements query.RoomserverQueryAPIDB +func (d *Database) GetMembershipEvents(roomNID types.RoomNID, requestSenderUserID string) (events []types.Event, err error) { + txn, err := d.db.Begin() if err != nil { - return nil, err + return } - inviteEventIDs, err := u.d.statements.updateInviteRetired( - u.txn, u.roomNID, u.targetUserNID, - ) + defer txn.Commit() + + requestSenderUserNID, err := d.assignStateKeyNID(txn, requestSenderUserID) if err != nil { - return nil, err + return } - if u.membership != membershipStateLeaveOrBan { - if err = u.d.statements.updateMembership( - u.txn, u.roomNID, u.targetUserNID, senderUserNID, membershipStateLeaveOrBan, - ); err != nil { + + _, senderMembership, err := d.statements.selectMembershipFromRoomAndTarget(roomNID, requestSenderUserNID) + if err == sql.ErrNoRows { + // The user has never been a member of that room + return nil, nil + } else if err != nil { + return + } + + if senderMembership == membershipStateJoin { + // 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) + if err != nil { return nil, err } + + events, err = d.Events(joinEventNIDs) + } 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 inviteEventIDs, nil + + return } type transaction struct { diff --git a/src/github.com/matrix-org/dendrite/roomserver/types/types.go b/src/github.com/matrix-org/dendrite/roomserver/types/types.go index 809b6e57..e8bc99fc 100644 --- a/src/github.com/matrix-org/dendrite/roomserver/types/types.go +++ b/src/github.com/matrix-org/dendrite/roomserver/types/types.go @@ -193,12 +193,12 @@ type MembershipUpdater interface { // Set the state to invite. // Returns whether this invite needs to be sent SetToInvite(event gomatrixserverlib.Event) (needsSending bool, err error) - // Set the state to join. + // Set the state to join or updates the event ID in the database. // Returns a list of invite event IDs that this state change retired. - SetToJoin(senderUserID string) (inviteEventIDs []string, err error) + SetToJoin(senderUserID string, eventID string, isUpdate bool) (inviteEventIDs []string, err error) // Set the state to leave. // Returns a list of invite event IDs that this state change retired. - SetToLeave(senderUserID string) (inviteEventIDs []string, err error) + SetToLeave(senderUserID string, eventID string) (inviteEventIDs []string, err error) // Implements Transaction so it can be committed or rolledback. Transaction }