Jelajahi Sumber

feedbag: enforce the ignore list as a block (#226)

Items of class 0x000E were stored but never read, so an ignored user
could still send IMs and offline messages and see presence. The
relationship query now treats an ignore item as a block in either
direction, regardless of PD mode, and adding or removing one updates
visibility right away, as deny items do. Ignore list names are
normalized like buddy, permit and deny names.
Dmitrij Gorbachov 19 jam lalu
induk
melakukan
0b40ca4e79

+ 2 - 2
foodgroup/feedbag.go

@@ -332,7 +332,7 @@ func (s *FeedbagService) UpsertItem(ctx context.Context, instance *state.Session
 	var alertAll bool
 	for _, item := range toUpsert {
 		switch item.ClassID {
-		case wire.FeedbagClassIdBuddy, wire.FeedbagClassIDPermit, wire.FeedbagClassIDDeny:
+		case wire.FeedbagClassIdBuddy, wire.FeedbagClassIDPermit, wire.FeedbagClassIDDeny, wire.FeedbagClassIdIgnoreList:
 			filter = append(filter, state.NewIdentScreenName(item.Name))
 		case wire.FeedbagClassIdBart:
 			if err := s.setBARTItem(ctx, instance, item); err != nil {
@@ -513,7 +513,7 @@ func (s *FeedbagService) DeleteItem(ctx context.Context, instance *state.Session
 
 	for _, item := range inBody.Items {
 		switch item.ClassID {
-		case wire.FeedbagClassIdBuddy, wire.FeedbagClassIDDeny, wire.FeedbagClassIDPermit:
+		case wire.FeedbagClassIdBuddy, wire.FeedbagClassIDDeny, wire.FeedbagClassIDPermit, wire.FeedbagClassIdIgnoreList:
 			filter = append(filter, state.NewIdentScreenName(item.Name))
 		}
 	}

+ 202 - 0
foodgroup/feedbag_test.go

@@ -1138,6 +1138,101 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 			},
 			expectOutput: nil,
 		},
+		{
+			name:     "ignore buddies",
+			instance: newTestInstance("me"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagInsertItem,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x13_0x08_FeedbagInsertItem{
+					Items: []wire.FeedbagItem{
+						{
+							ClassID: wire.FeedbagClassIdIgnoreList,
+							Name:    "buddy1",
+						},
+						{
+							ClassID: wire.FeedbagClassIdIgnoreList,
+							Name:    "buddy2",
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				feedbagManagerParams: feedbagManagerParams{
+					feedbagUpsertParams: feedbagUpsertParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+							items: []wire.FeedbagItem{
+								{
+									ClassID: wire.FeedbagClassIdIgnoreList,
+									Name:    "buddy1",
+								},
+								{
+									ClassID: wire.FeedbagClassIdIgnoreList,
+									Name:    "buddy2",
+								},
+							},
+						},
+					},
+				},
+				buddyBroadcasterParams: buddyBroadcasterParams{
+					broadcastVisibilityParams: broadcastVisibilityParams{
+						{
+							from: state.NewIdentScreenName("me"),
+							filter: []state.IdentScreenName{
+								state.NewIdentScreenName("buddy1"),
+								state.NewIdentScreenName("buddy2"),
+							},
+						},
+					},
+				},
+				messageRelayerParams: messageRelayerParams{
+					relayToOtherInstancesParams: relayToOtherInstancesParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+							message: wire.SNACMessage{
+								Frame: wire.SNACFrame{
+									FoodGroup: wire.Feedbag,
+									SubGroup:  wire.FeedbagInsertItem,
+									RequestID: wire.ReqIDFromServer,
+								},
+								Body: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
+									Items: []wire.FeedbagItem{
+										{
+											ClassID: wire.FeedbagClassIdIgnoreList,
+											Name:    "buddy1",
+										},
+										{
+											ClassID: wire.FeedbagClassIdIgnoreList,
+											Name:    "buddy2",
+										},
+									},
+								},
+							},
+						},
+					},
+					relayToSelfParams: relayToSelfParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+							message: wire.SNACMessage{
+								Frame: wire.SNACFrame{
+									FoodGroup: wire.Feedbag,
+									SubGroup:  wire.FeedbagStatus,
+									RequestID: 1234,
+								},
+								Body: wire.SNAC_0x13_0x0E_FeedbagStatus{
+									Results: []uint16{0x0000, 0x0000},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectOutput: nil,
+		},
 		{
 			name:     "permit buddies",
 			instance: newTestInstance("me"),
@@ -2846,6 +2941,113 @@ func TestFeedbagService_DeleteItem(t *testing.T) {
 			},
 			expectOutput: nil,
 		},
+		{
+			name:     "delete ignored users",
+			instance: newTestInstance("me"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagDeleteItem,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
+					Items: []wire.FeedbagItem{
+						{
+							ClassID: wire.FeedbagClassIdIgnoreList,
+							Name:    "buddy1",
+						},
+						{
+							ClassID: wire.FeedbagClassIdIgnoreList,
+							Name:    "buddy2",
+						},
+						{
+							ClassID: wire.FeedbagClassIdGroup,
+							Name:    "group",
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				feedbagManagerParams: feedbagManagerParams{
+					feedbagDeleteParams: feedbagDeleteParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+							items: []wire.FeedbagItem{
+								{
+									ClassID: wire.FeedbagClassIdIgnoreList,
+									Name:    "buddy1",
+								},
+								{
+									ClassID: wire.FeedbagClassIdIgnoreList,
+									Name:    "buddy2",
+								},
+								{
+									ClassID: wire.FeedbagClassIdGroup,
+									Name:    "group",
+								},
+							},
+						},
+					},
+				},
+				buddyBroadcasterParams: buddyBroadcasterParams{
+					broadcastVisibilityParams: broadcastVisibilityParams{
+						{
+							from: state.NewIdentScreenName("me"),
+							filter: []state.IdentScreenName{
+								state.NewIdentScreenName("buddy1"),
+								state.NewIdentScreenName("buddy2"),
+							},
+						},
+					},
+				},
+				messageRelayerParams: messageRelayerParams{
+					relayToOtherInstancesParams: relayToOtherInstancesParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+							message: wire.SNACMessage{
+								Frame: wire.SNACFrame{
+									FoodGroup: wire.Feedbag,
+									SubGroup:  wire.FeedbagDeleteItem,
+									RequestID: wire.ReqIDFromServer,
+								},
+								Body: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
+									Items: []wire.FeedbagItem{
+										{
+											ClassID: wire.FeedbagClassIdIgnoreList,
+											Name:    "buddy1",
+										},
+										{
+											ClassID: wire.FeedbagClassIdIgnoreList,
+											Name:    "buddy2",
+										},
+										{
+											ClassID: wire.FeedbagClassIdGroup,
+											Name:    "group",
+										},
+									},
+								},
+							},
+						},
+					},
+					relayToSelfParams: relayToSelfParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+							message: wire.SNACMessage{
+								Frame: wire.SNACFrame{
+									FoodGroup: wire.Feedbag,
+									SubGroup:  wire.FeedbagStatus,
+									RequestID: 1234,
+								},
+								Body: wire.SNAC_0x13_0x0E_FeedbagStatus{
+									Results: []uint16{0x0000, 0x0000, 0x0000},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectOutput: nil,
+		},
 		{
 			name: "defer visibility during notify txn",
 			instance: func() *state.SessionInstance {

+ 3 - 2
state/feedbag_list.go

@@ -716,10 +716,11 @@ func (f *FeedbagList) genID() uint16 {
 }
 
 // hasScreenName reports whether the feedbag class stores a screen name that
-// should be normalized (buddy, permit, deny).
+// should be normalized (buddy, permit, deny, ignore).
 func hasScreenName(classID uint16) bool {
 	return classID == wire.FeedbagClassIdBuddy ||
 		classID == wire.FeedbagClassIDPermit ||
 		classID == wire.FeedbagClassIdAlInfo ||
-		classID == wire.FeedbagClassIDDeny
+		classID == wire.FeedbagClassIDDeny ||
+		classID == wire.FeedbagClassIdIgnoreList
 }

+ 14 - 6
state/relationship.go

@@ -27,15 +27,17 @@ WITH myScreenName AS (SELECT ?),
      theirBuddyLists AS (SELECT COALESCE(clientSide._screenName, feedbag._screenName) AS _screenName,
                               COALESCE(clientSide.isBuddy OR feedbag.isBuddy, FALSE) AS isBuddy,
                               COALESCE(clientSide.isPermit OR feedbag.isPermit, FALSE) AS isPermit,
-                              COALESCE(clientSide.isDeny OR feedbag.isDeny, FALSE) AS isDeny
+                              COALESCE(clientSide.isDeny OR feedbag.isDeny, FALSE) AS isDeny,
+                              COALESCE(feedbag.isIgnore, FALSE) AS isIgnore
                        FROM (SELECT feedbag.screenName                                   AS _screenName,
                                     MAX(CASE WHEN feedbag.classId = 0 AND NOT feedbag.authPending THEN 1 ELSE 0 END) AS isBuddy,
                                     MAX(CASE WHEN feedbag.classId = 2 THEN 1 ELSE 0 END) AS isPermit,
-                                    MAX(CASE WHEN feedbag.classId = 3 THEN 1 ELSE 0 END) AS isDeny
+                                    MAX(CASE WHEN feedbag.classId = 3 THEN 1 ELSE 0 END) AS isDeny,
+                                    MAX(CASE WHEN feedbag.classId = 14 THEN 1 ELSE 0 END) AS isIgnore
                              FROM feedbag
                              WHERE feedbag.name = (SELECT * FROM myScreenName)
                              {{ if .DoFilter }}AND feedbag.screenName IN (SELECT * FROM filter){{ end }}
-                               AND feedbag.classId IN (0, 2, 3)
+                               AND feedbag.classId IN (0, 2, 3, 14)
                                AND EXISTS(SELECT 1
                                           FROM buddyListMode
                                           WHERE buddyListMode.screenName = feedbag.screenName
@@ -54,15 +56,17 @@ WITH myScreenName AS (SELECT ?),
      yourBuddyList AS (SELECT COALESCE(clientSide._screenName, feedbag._screenName) AS _screenName,
                               COALESCE(clientSide.isBuddy OR feedbag.isBuddy, FALSE) AS isBuddy,
                               COALESCE(clientSide.isPermit OR feedbag.isPermit, FALSE) AS isPermit,
-                              COALESCE(clientSide.isDeny OR feedbag.isDeny, FALSE) AS isDeny
+                              COALESCE(clientSide.isDeny OR feedbag.isDeny, FALSE) AS isDeny,
+                              COALESCE(feedbag.isIgnore, FALSE) AS isIgnore
                        FROM (SELECT feedbag.name                                         AS _screenName,
                                     MAX(CASE WHEN feedbag.classId = 0 AND NOT feedbag.authPending THEN 1 ELSE 0 END) AS isBuddy,
                                     MAX(CASE WHEN feedbag.classId = 2 THEN 1 ELSE 0 END) AS isPermit,
-                                    MAX(CASE WHEN feedbag.classId = 3 THEN 1 ELSE 0 END) AS isDeny
+                                    MAX(CASE WHEN feedbag.classId = 3 THEN 1 ELSE 0 END) AS isDeny,
+                                    MAX(CASE WHEN feedbag.classId = 14 THEN 1 ELSE 0 END) AS isIgnore
                              FROM feedbag
                              WHERE feedbag.screenName = (SELECT * FROM myScreenName)
                              {{ if .DoFilter }}AND feedbag.name IN (SELECT * FROM filter){{ end }}
-                               AND feedbag.classId IN (0, 2, 3)
+                               AND feedbag.classId IN (0, 2, 3, 14)
                                AND EXISTS(SELECT 1
                                           FROM buddyListMode
                                           WHERE buddyListMode.screenName = feedbag.screenName
@@ -107,6 +111,7 @@ WITH myScreenName AS (SELECT ?),
 -- create relationships between you and all combined users
 SELECT COALESCE(yourBuddyList._screenName, theirBuddyLists._screenName) AS screenName,
        CASE
+           WHEN IFNULL(yourBuddyList.isIgnore, false) THEN true
            WHEN yourPrivacyPrefs.pdMode = 1 THEN false
            WHEN yourPrivacyPrefs.pdMode = 2 THEN true
            WHEN yourPrivacyPrefs.pdMode = 3 THEN IFNULL(yourBuddyList.isPermit, false) = false
@@ -115,6 +120,7 @@ SELECT COALESCE(yourBuddyList._screenName, theirBuddyLists._screenName) AS scree
            ELSE false
            END                                                        AS youBlock,
        CASE
+           WHEN IFNULL(theirBuddyLists.isIgnore, false) THEN true
            WHEN theirPrivacyPrefs.pdMode = 1 THEN false
            WHEN theirPrivacyPrefs.pdMode = 2 THEN true
            WHEN theirPrivacyPrefs.pdMode = 3 THEN IFNULL(theirBuddyLists.isPermit, false) = false
@@ -151,6 +157,7 @@ type Relationship struct {
 	// 	- PermitSome (and you are not on permit list)
 	// 	- DenySome (and you are on deny list)
 	// 	- PermitOnList (and you are not on their buddy list)
+	// It is also true when you are on user's ignore list, regardless of mode.
 	BlocksYou bool
 	// YouBlock indicates whether you block user. This is true when user has
 	// the following permit/deny modes set:
@@ -158,6 +165,7 @@ type Relationship struct {
 	// 	- PermitSome (and they are not on your permit list)
 	// 	- DenySome (and they are on your deny list)
 	// 	- PermitOnList (and they are not on your buddy list)
+	// It is also true when user is on your ignore list, regardless of mode.
 	YouBlock bool
 	// IsOnTheirList indicates whether you are on user's buddy list.
 	IsOnTheirList bool

+ 104 - 0
state/relationship_test.go

@@ -25,6 +25,8 @@ func TestSQLiteUserStore_AllRelationships(t *testing.T) {
 		// buddyPendingAuth lists buddies (subset of buddyList) whose server-side feedbag buddy row
 		// is written with TLV FeedbagAttributesPending / authPending.
 		buddyPendingAuth []IdentScreenName
+		// ignoreList is the list of users on the server-side ignore list. always active.
+		ignoreList []IdentScreenName
 	}
 
 	tests := []struct {
@@ -3194,6 +3196,78 @@ func TestSQLiteUserStore_AllRelationships(t *testing.T) {
 				},
 			},
 		},
+		{
+			name:            "feedbag server-side: I ignore them while allowing all, block them",
+			me:              NewIdentScreenName("me"),
+			clientSideLists: map[IdentScreenName]buddyList{},
+			serverSideLists: map[IdentScreenName]buddyList{
+				NewIdentScreenName("me"): {
+					privacyMode: wire.FeedbagPDModePermitAll,
+					ignoreList:  []IdentScreenName{NewIdentScreenName("them")},
+				},
+				NewIdentScreenName("them"): {
+					privacyMode: wire.FeedbagPDModePermitAll,
+				},
+			},
+			expect: []Relationship{
+				{
+					User:          NewIdentScreenName("them"),
+					BlocksYou:     false,
+					YouBlock:      true,
+					IsOnTheirList: false,
+					IsOnYourList:  false,
+				},
+			},
+		},
+		{
+			name:            "feedbag server-side: they ignore me while allowing all, they block me",
+			me:              NewIdentScreenName("me"),
+			clientSideLists: map[IdentScreenName]buddyList{},
+			serverSideLists: map[IdentScreenName]buddyList{
+				NewIdentScreenName("me"): {
+					privacyMode: wire.FeedbagPDModePermitAll,
+					buddyList:   []IdentScreenName{NewIdentScreenName("them")},
+				},
+				NewIdentScreenName("them"): {
+					privacyMode: wire.FeedbagPDModePermitAll,
+					ignoreList:  []IdentScreenName{NewIdentScreenName("me")},
+				},
+			},
+			expect: []Relationship{
+				{
+					User:          NewIdentScreenName("them"),
+					BlocksYou:     true,
+					YouBlock:      false,
+					IsOnTheirList: false,
+					IsOnYourList:  true,
+				},
+			},
+		},
+		{
+			name:            "feedbag server-side: I have them on buddy and ignore lists, block them",
+			me:              NewIdentScreenName("me"),
+			clientSideLists: map[IdentScreenName]buddyList{},
+			serverSideLists: map[IdentScreenName]buddyList{
+				NewIdentScreenName("me"): {
+					privacyMode: wire.FeedbagPDModePermitOnList,
+					buddyList:   []IdentScreenName{NewIdentScreenName("them")},
+					ignoreList:  []IdentScreenName{NewIdentScreenName("them")},
+				},
+				NewIdentScreenName("them"): {
+					privacyMode: wire.FeedbagPDModePermitAll,
+					buddyList:   []IdentScreenName{NewIdentScreenName("me")},
+				},
+			},
+			expect: []Relationship{
+				{
+					User:          NewIdentScreenName("them"),
+					BlocksYou:     false,
+					YouBlock:      true,
+					IsOnTheirList: true,
+					IsOnYourList:  true,
+				},
+			},
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -3242,6 +3316,10 @@ func TestSQLiteUserStore_AllRelationships(t *testing.T) {
 					items = append(items, newFeedbagItem(wire.FeedbagClassIDDeny, itemID, buddy.String(), false))
 					itemID++
 				}
+				for _, buddy := range list.ignoreList {
+					items = append(items, newFeedbagItem(wire.FeedbagClassIdIgnoreList, itemID, buddy.String(), false))
+					itemID++
+				}
 				assert.NoError(t, feedbagStore.FeedbagUpsert(context.Background(), sn, items))
 			}
 
@@ -3257,3 +3335,29 @@ func TestSQLiteUserStore_AllRelationships(t *testing.T) {
 		})
 	}
 }
+
+func TestSQLiteUserStore_AllRelationships_IgnoreListNormalizesScreenName(t *testing.T) {
+	defer func() {
+		_ = os.Remove(testFile)
+	}()
+
+	store, err := NewSQLiteUserStore(testFile)
+	assert.NoError(t, err)
+
+	me := NewIdentScreenName("me")
+	them := NewIdentScreenName("them")
+	for _, sn := range []IdentScreenName{me, them} {
+		insertTestUsers(t, store, sn)
+		assert.NoError(t, store.UseFeedbag(context.Background(), sn))
+		assert.NoError(t, store.FeedbagUpsert(context.Background(), sn, []wire.FeedbagItem{
+			pdInfoItem(1, wire.FeedbagPDModePermitAll),
+		}))
+	}
+	assert.NoError(t, store.FeedbagUpsert(context.Background(), me, []wire.FeedbagItem{
+		newFeedbagItem(wire.FeedbagClassIdIgnoreList, 2, "The M", false),
+	}))
+
+	have, err := store.AllRelationships(context.Background(), me, []IdentScreenName{them})
+	assert.NoError(t, err)
+	assert.Equal(t, []Relationship{{User: them, YouBlock: true}}, have)
+}

+ 2 - 1
state/user_store.go

@@ -1138,7 +1138,8 @@ func (f SQLiteUserStore) FeedbagUpsert(ctx context.Context, screenName IdentScre
 		if item.ClassID == wire.FeedbagClassIdBuddy ||
 			item.ClassID == wire.FeedbagClassIDPermit ||
 			item.ClassID == wire.FeedbagClassIdAlInfo ||
-			item.ClassID == wire.FeedbagClassIDDeny {
+			item.ClassID == wire.FeedbagClassIDDeny ||
+			item.ClassID == wire.FeedbagClassIdIgnoreList {
 			// insert screen name identifier
 			item.Name = NewIdentScreenName(item.Name).String()
 		}