New storage func for removing redundant invites

This commit is contained in:
David Robertson 2023-10-10 18:17:58 +01:00
parent cd8390fe2f
commit 9b9da2a8e9
No known key found for this signature in database
GPG Key ID: 903ECE108A39DEDD
2 changed files with 189 additions and 0 deletions

View File

@ -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 {

View File

@ -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"