From 9b9da2a8e9187f4f6f7936343d9ed19d51503287 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 10 Oct 2023 18:17:58 +0100 Subject: [PATCH] New storage func for removing redundant invites --- state/invites_table.go | 38 +++++++++ state/invites_table_test.go | 151 ++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) diff --git a/state/invites_table.go b/state/invites_table.go index 2511afa..874491f 100644 --- a/state/invites_table.go +++ b/state/invites_table.go @@ -3,6 +3,7 @@ package state import ( "database/sql" "encoding/json" + "github.com/lib/pq" "github.com/jmoiron/sqlx" ) @@ -14,14 +15,17 @@ import ( // correctly when the user joined the room. // - The user could read room data in the room without being joined to the room e.g could pull // `required_state` and `timeline` as they would be authorised by the invite to see this data. +// // Instead, we now completely split out invites from the normal event flow. This fixes the issues // outlined above but introduce more problems: // - How do you sort the invite with rooms? // - How do you calculate the room name when you lack heroes? +// // For now, we say that invites: // - are treated as a highlightable event for the purposes of sorting by highlight count. // - are given the timestamp of when the invite arrived. // - calculate the room name on a best-effort basis given the lack of heroes (same as element-web). +// // When an invite is rejected, it appears in the `leave` section which then causes the invite to be // removed from this table. type InvitesTable struct { @@ -47,6 +51,40 @@ func (t *InvitesTable) RemoveInvite(userID, roomID string) error { return err } +// RemoveSupersededInvites accepts a list of events in the given room. The events should +// either +// - contain at most one membership event per user, or else +// - be in timeline order (most recent last) +// +// (corresponding to an Accumulate and an Initialise call, respectively). +// +// The events are scanned in order for membership changes, to determine the "final" +// memberships. Users who final membership is not "invite" have their outstanding +// invites to this room deleted. +func (t *InvitesTable) RemoveSupersededInvites(txn *sqlx.Tx, roomID string, newEvents []Event) error { + memberships := map[string]string{} // user ID -> memberships + for _, ev := range newEvents { + if ev.Type != "m.room.member" { + continue + } + memberships[ev.StateKey] = ev.Membership + } + + var usersToRemove []string + for userID, membership := range memberships { + if membership != "invite" && membership != "_invite" { + usersToRemove = append(usersToRemove, userID) + } + } + + _, err := txn.Exec(` + DELETE FROM syncv3_invites + WHERE user_id = ANY($1) AND room_id = $2 + `, pq.StringArray(usersToRemove), roomID) + + return err +} + func (t *InvitesTable) InsertInvite(userID, roomID string, inviteRoomState []json.RawMessage) error { blob, err := json.Marshal(inviteRoomState) if err != nil { diff --git a/state/invites_table_test.go b/state/invites_table_test.go index 92c0077..cf37fbd 100644 --- a/state/invites_table_test.go +++ b/state/invites_table_test.go @@ -2,6 +2,8 @@ package state import ( "encoding/json" + "github.com/jmoiron/sqlx" + "github.com/matrix-org/sliding-sync/sqlutil" "reflect" "testing" ) @@ -128,6 +130,155 @@ func TestInviteTable(t *testing.T) { } } +func TestInviteTable_RemoveSupersededInvites(t *testing.T) { + db, close := connectToDB(t) + defer close() + + alice := "@alice:localhost" + bob := "@bob:localhost" + roomA := "!a:localhost" + roomB := "!b:localhost" + inviteState := []json.RawMessage{[]byte(`{"foo":"bar"}`)} + + table := NewInvitesTable(db) + t.Log("Invite Alice and Bob to both rooms.") + + // Add some invites + if err := table.InsertInvite(alice, roomA, inviteState); err != nil { + t.Fatalf("failed to InsertInvite: %s", err) + } + if err := table.InsertInvite(bob, roomA, inviteState); err != nil { + t.Fatalf("failed to InsertInvite: %s", err) + } + if err := table.InsertInvite(alice, roomB, inviteState); err != nil { + t.Fatalf("failed to InsertInvite: %s", err) + } + if err := table.InsertInvite(bob, roomB, inviteState); err != nil { + t.Fatalf("failed to InsertInvite: %s", err) + } + + t.Log("Alice joins room A. Remove her superseded invite.") + newEvents := []Event{ + { + Type: "m.room.member", + StateKey: alice, + Membership: "join", + RoomID: roomA, + }, + } + err := sqlutil.WithTransaction(db, func(txn *sqlx.Tx) error { + return table.RemoveSupersededInvites(txn, roomA, newEvents) + }) + if err != nil { + t.Fatalf("failed to RemoveSupersededInvites: %s", err) + } + + t.Log("Alice should still be invited to room B.") + assertInvites(t, table, alice, map[string][]json.RawMessage{roomB: inviteState}) + t.Log("Bob should still be invited to rooms A and B.") + assertInvites(t, table, bob, map[string][]json.RawMessage{roomA: inviteState, roomB: inviteState}) + + t.Log("Bob declines his invitation to room B.") + newEvents = []Event{ + { + Type: "m.room.member", + StateKey: bob, + Membership: "leave", + RoomID: roomB, + }, + } + err = sqlutil.WithTransaction(db, func(txn *sqlx.Tx) error { + return table.RemoveSupersededInvites(txn, roomB, newEvents) + }) + if err != nil { + t.Fatalf("failed to RemoveSupersededInvites: %s", err) + } + + t.Log("Alice should still be invited to room B.") + assertInvites(t, table, alice, map[string][]json.RawMessage{roomB: inviteState}) + t.Log("Bob should still be invited to room A.") + assertInvites(t, table, bob, map[string][]json.RawMessage{roomA: inviteState}) + + // Now try multiple membership changes in one call. + t.Log("Alice joins, changes profile, leaves and is re-invited to room B.") + newEvents = []Event{ + { + Type: "m.room.member", + StateKey: alice, + Membership: "join", + RoomID: roomB, + }, + { + Type: "m.room.member", + StateKey: alice, + Membership: "_join", + RoomID: roomB, + }, + { + Type: "m.room.member", + StateKey: alice, + Membership: "leave", + RoomID: roomB, + }, + { + Type: "m.room.member", + StateKey: alice, + Membership: "invite", + RoomID: roomB, + }, + } + + err = sqlutil.WithTransaction(db, func(txn *sqlx.Tx) error { + return table.RemoveSupersededInvites(txn, roomB, newEvents) + }) + if err != nil { + t.Fatalf("failed to RemoveSupersededInvites: %s", err) + } + + t.Log("Alice should still be invited to room B.") + assertInvites(t, table, alice, map[string][]json.RawMessage{roomB: inviteState}) + + t.Log("Bob declines, is reinvited to and joins room A.") + newEvents = []Event{ + { + Type: "m.room.member", + StateKey: bob, + Membership: "leave", + RoomID: roomA, + }, + { + Type: "m.room.member", + StateKey: bob, + Membership: "invite", + RoomID: roomA, + }, + { + Type: "m.room.member", + StateKey: bob, + Membership: "join", + RoomID: roomA, + }, + } + + err = sqlutil.WithTransaction(db, func(txn *sqlx.Tx) error { + return table.RemoveSupersededInvites(txn, roomA, newEvents) + }) + if err != nil { + t.Fatalf("failed to RemoveSupersededInvites: %s", err) + } + assertInvites(t, table, bob, map[string][]json.RawMessage{}) +} + +func assertInvites(t *testing.T, table *InvitesTable, user string, expected map[string][]json.RawMessage) { + invites, err := table.SelectAllInvitesForUser(user) + if err != nil { + t.Fatalf("failed to SelectAllInvitesForUser: %s", err) + } + if !reflect.DeepEqual(invites, expected) { + t.Fatalf("got %v invites, want %v", invites, expected) + } +} + func jsonArrStr(a []json.RawMessage) (result string) { for _, e := range a { result += string(e) + "\n"