فهرست منبع

conditionally send "wants typing events" ICBM TLV

Mike 1 سال پیش
والد
کامیت
b1a9e03332
8فایلهای تغییر یافته به همراه346 افزوده شده و 36 حذف شده
  1. 18 0
      foodgroup/feedbag.go
  2. 188 8
      foodgroup/feedbag_test.go
  3. 12 0
      foodgroup/helpers_test.go
  4. 2 3
      foodgroup/icbm.go
  5. 75 2
      foodgroup/icbm_test.go
  6. 40 23
      state/session.go
  7. 9 0
      state/session_test.go
  8. 2 0
      wire/snacs.go

+ 18 - 0
foodgroup/feedbag.go

@@ -196,6 +196,8 @@ func (s FeedbagService) UpsertItem(ctx context.Context, sess *state.Session, inF
 		return wire.SNACMessage{}, err
 	}
 
+	setSessionBuddyPrefs(items, sess)
+
 	var filter []state.IdentScreenName
 	var alertAll bool
 	for _, item := range items {
@@ -337,6 +339,11 @@ func (s FeedbagService) Use(ctx context.Context, sess *state.Session) error {
 	if err := s.feedbagManager.UseFeedbag(ctx, sess.IdentScreenName()); err != nil {
 		return fmt.Errorf("could not use feedbag: %w", err)
 	}
+	items, err := s.feedbagManager.Feedbag(ctx, sess.IdentScreenName())
+	if err != nil {
+		return fmt.Errorf("feedbagManager.Feedbag: %w", err)
+	}
+	setSessionBuddyPrefs(items, sess)
 	return nil
 }
 
@@ -380,3 +387,14 @@ func (s FeedbagService) RespondAuthorizeToHost(ctx context.Context, sess *state.
 
 	return nil
 }
+
+// setSessionBuddyPrefs sets session preferences based on the feedbag buddy prefs item, if present.
+func setSessionBuddyPrefs(items []wire.FeedbagItem, sess *state.Session) {
+	for _, item := range items {
+		if item.ClassID == wire.FeedbagClassIdBuddyPrefs && item.HasTag(wire.FeedbagAttributesBuddyPrefs) {
+			buddyPrefs, _ := item.Uint32BE(wire.FeedbagAttributesBuddyPrefs)
+			sess.SetTypingEventsEnabled(buddyPrefs&wire.FeedbagBuddyPrefsWantsTypingEvents == wire.FeedbagBuddyPrefsWantsTypingEvents)
+			break
+		}
+	}
+}

+ 188 - 8
foodgroup/feedbag_test.go

@@ -382,6 +382,8 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 		mockParams mockParams
 		// expectOutput is the SNAC sent from the server to client
 		expectOutput wire.SNACMessage
+		// wantTypingEventsEnabled indicates that the session should have typing events enabled
+		wantTypingEventsEnabled bool
 	}{
 		{
 			name:        "add buddies",
@@ -444,6 +446,108 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 				},
 			},
 		},
+		{
+			name:        "disable typing events",
+			userSession: newTestSession("me"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x13_0x08_FeedbagInsertItem{
+					Items: []wire.FeedbagItem{
+						{
+							ClassID: wire.FeedbagClassIdBuddyPrefs,
+							TLVLBlock: wire.TLVLBlock{
+								TLVList: wire.TLVList{
+									wire.NewTLVBE(wire.FeedbagAttributesBuddyPrefs, uint32(0x8000)),
+								},
+							},
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				feedbagManagerParams: feedbagManagerParams{
+					feedbagUpsertParams: feedbagUpsertParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+							items: []wire.FeedbagItem{
+								{
+									ClassID: wire.FeedbagClassIdBuddyPrefs,
+									TLVLBlock: wire.TLVLBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.FeedbagAttributesBuddyPrefs, uint32(0x8000)),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagStatus,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x13_0x0E_FeedbagStatus{
+					Results: []uint16{0x0000},
+				},
+			},
+			wantTypingEventsEnabled: false,
+		},
+		{
+			name:        "enable typing events",
+			userSession: newTestSession("me"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x13_0x08_FeedbagInsertItem{
+					Items: []wire.FeedbagItem{
+						{
+							ClassID: wire.FeedbagClassIdBuddyPrefs,
+							TLVLBlock: wire.TLVLBlock{
+								TLVList: wire.TLVList{
+									wire.NewTLVBE(wire.FeedbagAttributesBuddyPrefs, uint32(wire.FeedbagBuddyPrefsWantsTypingEvents)),
+								},
+							},
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				feedbagManagerParams: feedbagManagerParams{
+					feedbagUpsertParams: feedbagUpsertParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+							items: []wire.FeedbagItem{
+								{
+									ClassID: wire.FeedbagClassIdBuddyPrefs,
+									TLVLBlock: wire.TLVLBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.FeedbagAttributesBuddyPrefs, uint32(wire.FeedbagBuddyPrefsWantsTypingEvents)),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagStatus,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x13_0x0E_FeedbagStatus{
+					Results: []uint16{0x0000},
+				},
+			},
+			wantTypingEventsEnabled: true,
+		},
 		{
 			name:        "block buddies",
 			userSession: newTestSession("me"),
@@ -917,6 +1021,8 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 				tc.inputSNAC.Body.(wire.SNAC_0x13_0x08_FeedbagInsertItem).Items)
 			assert.NoError(t, err)
 			assert.Equal(t, output, tc.expectOutput)
+
+			assert.Equal(t, tc.wantTypingEventsEnabled, tc.userSession.TypingEventsEnabled())
 		})
 	}
 }
@@ -1038,7 +1144,7 @@ func TestFeedbagService_Use(t *testing.T) {
 	tests := []struct {
 		// name is the name of the test
 		name string
-		// joiningChatter is the session of the arriving user
+		// sess is the user's session
 		sess *state.Session
 		// bodyIn is the SNAC body sent from the arriving user's client to the
 		// server
@@ -1046,43 +1152,117 @@ func TestFeedbagService_Use(t *testing.T) {
 		// mockParams is the list of params sent to mocks that satisfy this
 		// method's dependencies
 		mockParams mockParams
-		wantErr    error
+		// wantTypingEventsEnabled indicates that the session should have
+		// typing events enabled
+		wantTypingEventsEnabled bool
+		// wantErr indicates an error is expected
+		wantErr error
 	}{
 		{
-			name:   "notify arriving user's buddies of its arrival and populate the arriving user's buddy list",
+			name:   "enable user's feedbag, no feedbag buddy params item",
 			sess:   newTestSession("me"),
 			bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
 			mockParams: mockParams{
 				feedbagManagerParams: feedbagManagerParams{
-					buddiesParams: buddiesParams{
+					useParams: useParams{
 						{
 							screenName: state.NewIdentScreenName("me"),
-							results: []state.IdentScreenName{
-								state.NewIdentScreenName("buddy1"),
-								state.NewIdentScreenName("buddy3"),
+						},
+					},
+					feedbagParams: feedbagParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+						},
+					},
+				},
+			},
+			wantTypingEventsEnabled: false,
+		},
+		{
+			name:   "enable user's feedbag and set typing events disabled",
+			sess:   newTestSession("me"),
+			bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
+			mockParams: mockParams{
+				feedbagManagerParams: feedbagManagerParams{
+					useParams: useParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+						},
+					},
+					feedbagParams: feedbagParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+							results: []wire.FeedbagItem{
+								{
+									ClassID: wire.FeedbagClassIdBuddyPrefs,
+									TLVLBlock: wire.TLVLBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.FeedbagAttributesBuddyPrefs, uint32(0x8000)),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			wantTypingEventsEnabled: false,
+		},
+		{
+			name:   "enable user's feedbag and set typing events enabled",
+			sess:   newTestSession("me"),
+			bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
+			mockParams: mockParams{
+				feedbagManagerParams: feedbagManagerParams{
+					useParams: useParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+						},
+					},
+					feedbagParams: feedbagParams{
+						{
+							screenName: state.NewIdentScreenName("me"),
+							results: []wire.FeedbagItem{
+								{
+									ClassID: wire.FeedbagClassIdBuddyPrefs,
+									TLVLBlock: wire.TLVLBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.FeedbagAttributesBuddyPrefs, uint32(wire.FeedbagBuddyPrefsWantsTypingEvents)),
+										},
+									},
+								},
 							},
 						},
 					},
 				},
 			},
+			wantTypingEventsEnabled: true,
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			feedbagManager := newMockFeedbagManager(t)
-			for _, params := range tt.mockParams.buddiesParams {
+			for _, params := range tt.mockParams.useParams {
 				feedbagManager.EXPECT().
 					UseFeedbag(matchContext(), params.screenName).
 					Return(nil)
 			}
+			for _, params := range tt.mockParams.feedbagParams {
+				feedbagManager.EXPECT().
+					Feedbag(matchContext(), params.screenName).
+					Return(params.results, nil)
+			}
 
 			svc := NewFeedbagService(slog.Default(), nil, feedbagManager, nil, nil, nil)
 
 			haveErr := svc.Use(context.Background(), tt.sess)
 			assert.ErrorIs(t, tt.wantErr, haveErr)
+
+			assert.Equal(t, tt.wantTypingEventsEnabled, tt.sess.TypingEventsEnabled())
 		})
 	}
 }
+
 func TestFeedbagService_RespondAuthorizeToHost(t *testing.T) {
 	tests := []struct {
 		name       string

+ 12 - 0
foodgroup/helpers_test.go

@@ -296,6 +296,7 @@ type feedbagManagerParams struct {
 	feedbagParams
 	feedbagLastModifiedParams
 	feedbagDeleteParams
+	useParams
 }
 
 // adjacentUsersParams is the list of parameters passed at the mock
@@ -320,6 +321,12 @@ type buddiesParams []struct {
 	results    []state.IdentScreenName
 }
 
+// useParams is the list of parameters passed at the mock
+// FeedbagManager.Use call site
+type useParams []struct {
+	screenName state.IdentScreenName
+}
+
 // feedbagParams is the list of parameters passed at the mock
 // FeedbagManager.Feedbag call site
 type feedbagParams []struct {
@@ -762,6 +769,11 @@ func sessOptUIN(UIN uint32) func(session *state.Session) {
 	}
 }
 
+// sessOptCaps sets caps
+func sessOptWantTypingEvents(session *state.Session) {
+	session.SetTypingEventsEnabled(true)
+}
+
 func sessOptSetFoodGroupVersion(foodGroup uint16, version uint16) func(session *state.Session) {
 	return func(session *state.Session) {
 		var versions [wire.MDir + 1]uint16

+ 2 - 3
foodgroup/icbm.go

@@ -146,9 +146,8 @@ func (s ICBMService) ChannelMsgToHost(ctx context.Context, sess *state.Session,
 		clientIM.Append(tlv)
 	}
 
-	if inBody.ChannelID == wire.ICBMChannelIM || inBody.ChannelID == wire.ICBMChannelMIME {
-		// tell the receiver that we want to receive their typing events.
-		// todo: only send this TLV if the sender opts to disclose typing events
+	if sess.TypingEventsEnabled() && (inBody.ChannelID == wire.ICBMChannelIM || inBody.ChannelID == wire.ICBMChannelMIME) {
+		// tell the receiver that we want to receive their typing events
 		clientIM.Append(wire.NewTLVBE(wire.ICBMTLVWantEvents, []byte{}))
 	}
 

+ 75 - 2
foodgroup/icbm_test.go

@@ -33,7 +33,7 @@ func TestICBMService_ChannelMsgToHost(t *testing.T) {
 	}{
 		{
 			name:          "transmit message from sender to recipient, ack message back to sender",
-			senderSession: newTestSession("sender-screen-name", sessOptWarning(10)),
+			senderSession: newTestSession("sender-screen-name", sessOptWarning(10), sessOptWantTypingEvents),
 			mockParams: mockParams{
 				relationshipFetcherParams: relationshipFetcherParams{
 					relationshipParams: relationshipParams{
@@ -124,7 +124,7 @@ func TestICBMService_ChannelMsgToHost(t *testing.T) {
 		},
 		{
 			name:          "transmit message from sender to recipient, don't ack message back to sender",
-			senderSession: newTestSession("sender-screen-name", sessOptWarning(10)),
+			senderSession: newTestSession("sender-screen-name", sessOptWarning(10), sessOptWantTypingEvents),
 			mockParams: mockParams{
 				relationshipFetcherParams: relationshipFetcherParams{
 					relationshipParams: relationshipParams{
@@ -199,6 +199,79 @@ func TestICBMService_ChannelMsgToHost(t *testing.T) {
 			},
 			expectOutput: nil,
 		},
+		{
+			name:          "transmit message from sender to recipient, don't ack message back to sender, don't want typing events",
+			senderSession: newTestSession("sender-screen-name", sessOptWarning(10)),
+			mockParams: mockParams{
+				relationshipFetcherParams: relationshipFetcherParams{
+					relationshipParams: relationshipParams{
+						{
+							me:   state.NewIdentScreenName("sender-screen-name"),
+							them: state.NewIdentScreenName("recipient-screen-name"),
+							result: state.Relationship{
+								User:          state.NewIdentScreenName("recipient-screen-name"),
+								BlocksYou:     false,
+								YouBlock:      false,
+								IsOnTheirList: false,
+								IsOnYourList:  false,
+							},
+						},
+					},
+				},
+				sessionRetrieverParams: sessionRetrieverParams{
+					retrieveSessionParams{
+						{
+							screenName: state.NewIdentScreenName("recipient-screen-name"),
+							result:     newTestSession("recipient-screen-name", sessOptWarning(20)),
+						},
+					},
+				},
+				messageRelayerParams: messageRelayerParams{
+					relayToScreenNameParams: relayToScreenNameParams{
+						{
+							screenName: state.NewIdentScreenName("recipient-screen-name"),
+							message: wire.SNACMessage{
+								Frame: wire.SNACFrame{
+									FoodGroup: wire.ICBM,
+									SubGroup:  wire.ICBMChannelMsgToClient,
+									RequestID: wire.ReqIDFromServer,
+								},
+								Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
+									ChannelID:   wire.ICBMChannelIM,
+									TLVUserInfo: newTestSession("sender-screen-name", sessOptWarning(10)).TLVUserInfo(),
+									TLVRestBlock: wire.TLVRestBlock{
+										TLVList: wire.TLVList{
+											{
+												Tag:   wire.ICBMTLVData,
+												Value: []byte{1, 2, 3, 4},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
+					ChannelID:  wire.ICBMChannelIM,
+					ScreenName: "recipient-screen-name",
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							{
+								Tag:   wire.ICBMTLVData,
+								Value: []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+			expectOutput: nil,
+		},
 		{
 			name:          "don't transmit message from sender to recipient because sender has blocked recipient",
 			senderSession: newTestSession("sender-screen-name", sessOptWarning(10)),

+ 40 - 23
state/session.go

@@ -49,29 +49,30 @@ const (
 // Session represents a user's current session. Unless stated otherwise, all
 // methods may be safely accessed by multiple goroutines.
 type Session struct {
-	awayMessage        string
-	caps               [][16]byte
-	chatRoomCookie     string
-	closed             bool
-	displayScreenName  DisplayScreenName
-	identScreenName    IdentScreenName
-	idle               bool
-	idleTime           time.Time
-	msgCh              chan wire.SNACMessage
-	mutex              sync.RWMutex
-	nowFn              func() time.Time
-	signonComplete     bool
-	signonTime         time.Time
-	stopCh             chan struct{}
-	uin                uint32
-	warning            uint16
-	userInfoBitmask    uint16
-	userStatusBitmask  uint32
-	clientID           string
-	remoteAddr         *netip.AddrPort
-	lastObservedStates [5]RateClassState
-	rateByClassID      [5]RateClassState
-	foodGroupVersions  [wire.MDir + 1]uint16
+	awayMessage         string
+	caps                [][16]byte
+	chatRoomCookie      string
+	closed              bool
+	displayScreenName   DisplayScreenName
+	identScreenName     IdentScreenName
+	idle                bool
+	idleTime            time.Time
+	msgCh               chan wire.SNACMessage
+	mutex               sync.RWMutex
+	nowFn               func() time.Time
+	signonComplete      bool
+	signonTime          time.Time
+	stopCh              chan struct{}
+	uin                 uint32
+	warning             uint16
+	userInfoBitmask     uint16
+	userStatusBitmask   uint32
+	clientID            string
+	remoteAddr          *netip.AddrPort
+	lastObservedStates  [5]RateClassState
+	rateByClassID       [5]RateClassState
+	foodGroupVersions   [wire.MDir + 1]uint16
+	typingEventsEnabled bool
 }
 
 // NewSession returns a new instance of Session. By default, the user may have
@@ -550,3 +551,19 @@ func (s *Session) FoodGroupVersions() [wire.MDir + 1]uint16 {
 	defer s.mutex.RUnlock()
 	return s.foodGroupVersions
 }
+
+// TypingEventsEnabled indicates whether the client wants to send and receive
+// typing events.
+func (s *Session) TypingEventsEnabled() bool {
+	s.mutex.RLock()
+	defer s.mutex.RUnlock()
+	return s.typingEventsEnabled
+}
+
+// SetTypingEventsEnabled sets whether the client wants to send and receive
+// typing events.
+func (s *Session) SetTypingEventsEnabled(enabled bool) {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+	s.typingEventsEnabled = enabled
+}

+ 9 - 0
state/session_test.go

@@ -585,3 +585,12 @@ func TestSession_SetAndGetFoodGroupVersions(t *testing.T) {
 
 	assert.Equal(t, versions, s.FoodGroupVersions())
 }
+
+func TestSession_SetAndGetTypingEventsEnabled(t *testing.T) {
+	s := NewSession()
+	assert.False(t, s.TypingEventsEnabled())
+	s.SetTypingEventsEnabled(true)
+	assert.True(t, s.TypingEventsEnabled())
+	s.SetTypingEventsEnabled(false)
+	assert.False(t, s.TypingEventsEnabled())
+}

+ 2 - 0
wire/snacs.go

@@ -1496,6 +1496,8 @@ const (
 	FeedbagAttributesFirstCreationTimeXc     uint16 = 0x0167
 	FeedbagAttributesPdModeXc                uint16 = 0x016E
 
+	FeedbagBuddyPrefsWantsTypingEvents uint32 = 0x400000 // user wants to send and receive typing events
+
 	FeedbagRightsMaxClassAttrs       uint16 = 0x02
 	FeedbagRightsMaxItemAttrs        uint16 = 0x03
 	FeedbagRightsMaxItemsByClass     uint16 = 0x04