Quellcode durchsuchen

implement auth flow btwn aim and icq

Mike vor 2 Monaten
Ursprung
Commit
5bf363b19f

+ 10 - 0
foodgroup/buddy.go

@@ -64,6 +64,16 @@ func (s BuddyService) AddBuddies(ctx context.Context, instance *state.SessionIns
 	for _, entry := range inBody.Buddies {
 	for _, entry := range inBody.Buddies {
 		them := state.NewIdentScreenName(entry.ScreenName)
 		them := state.NewIdentScreenName(entry.ScreenName)
 		if them.UIN() != 0 {
 		if them.UIN() != 0 {
+			if instance.IdentScreenName().UIN() == 0 {
+				// AIM may not add ICQ UIN buddies on the client-side buddy path; only the
+				// feedbag route supports ICQ interop for AIM with proper authorization.
+				// Otherwise an AIM session could subscribe to buddy presence ("snoop" on ICQ
+				// status) without completing the authorization flow.
+				rejected = append(rejected, struct {
+					ScreenName string `oscar:"len_prefix=uint8"`
+				}{ScreenName: entry.ScreenName})
+				continue
+			}
 			blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, them, instance.IdentScreenName())
 			blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, them, instance.IdentScreenName())
 			if err != nil {
 			if err != nil {
 				return nil, err
 				return nil, err

+ 69 - 0
foodgroup/buddy_test.go

@@ -159,6 +159,75 @@ func TestBuddyService_AddBuddies(t *testing.T) {
 				},
 				},
 			},
 			},
 		},
 		},
+		{
+			name:     "AIM session rejects ICQ UIN on client-side buddy path",
+			instance: newTestInstance("aimuser", sessOptSignonComplete),
+			bodyIn: wire.SNAC_0x03_0x04_BuddyAddBuddies{
+				Buddies: []struct {
+					ScreenName string `oscar:"len_prefix=uint8"`
+				}{
+					{ScreenName: "100001"},
+				},
+			},
+			wantSNAC: &wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyRejectNotification,
+					RequestID: 42,
+				},
+				Body: wire.SNAC_0x03_0x0A_BuddyRejectNotification{
+					Buddies: []struct {
+						ScreenName string `oscar:"len_prefix=uint8"`
+					}{
+						{ScreenName: "100001"},
+					},
+				},
+			},
+		},
+		{
+			name:     "AIM session rejects ICQ UIN but adds non-UIN buddy in same batch",
+			instance: newTestInstance("aimuser", sessOptSignonComplete),
+			bodyIn: wire.SNAC_0x03_0x04_BuddyAddBuddies{
+				Buddies: []struct {
+					ScreenName string `oscar:"len_prefix=uint8"`
+				}{
+					{ScreenName: "100001"},
+					{ScreenName: "buddyalice"},
+				},
+			},
+			mockParams: mockParams{
+				clientSideBuddyListManagerParams: clientSideBuddyListManagerParams{
+					addBuddyParams: addBuddyParams{
+						{
+							me:   state.NewIdentScreenName("aimuser"),
+							them: state.NewIdentScreenName("buddyalice"),
+						},
+					},
+				},
+				buddyBroadcasterParams: buddyBroadcasterParams{
+					broadcastVisibilityParams: broadcastVisibilityParams{
+						{
+							from:   state.NewIdentScreenName("aimuser"),
+							filter: []state.IdentScreenName{state.NewIdentScreenName("buddyalice")},
+						},
+					},
+				},
+			},
+			wantSNAC: &wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyRejectNotification,
+					RequestID: 42,
+				},
+				Body: wire.SNAC_0x03_0x0A_BuddyRejectNotification{
+					Buddies: []struct {
+						ScreenName string `oscar:"len_prefix=uint8"`
+					}{
+						{ScreenName: "100001"},
+					},
+				},
+			},
+		},
 		{
 		{
 			name:     "non-UIN skips pre-auth check",
 			name:     "non-UIN skips pre-auth check",
 			instance: newTestInstance("100002", sessOptSignonComplete),
 			instance: newTestInstance("100002", sessOptSignonComplete),

+ 43 - 17
foodgroup/feedbag.go

@@ -224,7 +224,17 @@ func (s *FeedbagService) UpsertItem(ctx context.Context, instance *state.Session
 	for _, item := range items {
 	for _, item := range items {
 		if item.ClassID == wire.FeedbagClassIdBuddy && !item.HasTag(wire.FeedbagAttributesPending) {
 		if item.ClassID == wire.FeedbagClassIdBuddy && !item.HasTag(wire.FeedbagAttributesPending) {
 			sn := state.NewIdentScreenName(item.Name)
 			sn := state.NewIdentScreenName(item.Name)
-			if sn.UIN() != 0 {
+			switch {
+			case instance.UIN() != 0 && sn.UIN() == 0:
+				// sender: icq, recipient:aim
+				// Automatically authorize AIM users when adding to ICQ buddy
+				// list since AIM does not support the ICQ authorization flow.
+				if err := s.contactPreAuthorizer.RecordPreAuth(ctx, instance.IdentScreenName(), sn); err != nil {
+					return nil, fmt.Errorf("contactPreAuthorizer.RecordPreAuth: %w", err)
+				}
+			case instance.UIN() != 0 && sn.UIN() != 0:
+				// sender: icq, recipient:icq
+				// Perform ICQ authorization flow.
 				blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, sn, instance.IdentScreenName())
 				blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, sn, instance.IdentScreenName())
 				if err != nil {
 				if err != nil {
 					return nil, fmt.Errorf("contactPreAuthorizer.RequiresAuthorization: %w", err)
 					return nil, fmt.Errorf("contactPreAuthorizer.RequiresAuthorization: %w", err)
@@ -233,6 +243,21 @@ func (s *FeedbagService) UpsertItem(ctx context.Context, instance *state.Session
 					authRequired[item.Name] = true
 					authRequired[item.Name] = true
 					continue
 					continue
 				}
 				}
+			case instance.UIN() == 0 && sn.UIN() != 0:
+				// sender:aim, recipient: icq
+				// Automatically request authorization on behalf of AIM user since
+				// AIM does not support the ICQ authorization flow. The ICQ user
+				// will appear offline until authorization is granted.
+				blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, sn, instance.IdentScreenName())
+				if err != nil {
+					return nil, fmt.Errorf("contactPreAuthorizer.RequiresAuthorization: %w", err)
+				}
+				if blocked {
+					item.Append(wire.NewTLVBE(wire.FeedbagAttributesPending, []byte{}))
+					if err := s.sendLegacyAuthReq(ctx, instance, sn, "", state.ICQBasicInfo{}, 1); err != nil {
+						return nil, fmt.Errorf("sendLegacyAuthReq: %w", err)
+					}
+				}
 			}
 			}
 		}
 		}
 		toUpsert = append(toUpsert, item)
 		toUpsert = append(toUpsert, item)
@@ -510,13 +535,6 @@ func (s *FeedbagService) RequestAuthorizeToHost(ctx context.Context, instance *s
 		return nil
 		return nil
 	}
 	}
 
 
-	// send an offline authorization or request to legacy client
-	frame := wire.SNACFrame{
-		FoodGroup: wire.ICBM,
-		SubGroup:  wire.ICBMChannelMsgToHost,
-		RequestID: inFrame.RequestID,
-	}
-
 	// if authorized, the recipient can reciprocate without requesting authorization
 	// if authorized, the recipient can reciprocate without requesting authorization
 	authorized := 1
 	authorized := 1
 	blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, instance.IdentScreenName(), recipient)
 	blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, instance.IdentScreenName(), recipient)
@@ -536,31 +554,39 @@ func (s *FeedbagService) RequestAuthorizeToHost(ctx context.Context, instance *s
 		s.logger.ErrorContext(ctx, "user not found", "screen_name", instance.IdentScreenName())
 		s.logger.ErrorContext(ctx, "user not found", "screen_name", instance.IdentScreenName())
 	}
 	}
 
 
+	return s.sendLegacyAuthReq(ctx, instance, recipient, inBody.Reason, userInfo.ICQBasicInfo, authorized)
+}
+
+// sendLegacyAuthReq sends an offline authorization request to ICQ client
+func (s *FeedbagService) sendLegacyAuthReq(ctx context.Context, from *state.SessionInstance, to state.IdentScreenName, reason string, userInfo state.ICQBasicInfo, authorized int) error {
+	frame := wire.SNACFrame{
+		FoodGroup: wire.ICBM,
+		SubGroup:  wire.ICBMChannelMsgToHost,
+	}
 	snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 	snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 		ChannelID:  wire.ICBMChannelICQ,
 		ChannelID:  wire.ICBMChannelICQ,
-		ScreenName: inBody.ScreenName,
+		ScreenName: to.String(),
 		TLVRestBlock: wire.TLVRestBlock{
 		TLVRestBlock: wire.TLVRestBlock{
 			TLVList: wire.TLVList{
 			TLVList: wire.TLVList{
 				wire.NewTLVLE(wire.ICBMTLVData, wire.ICBMCh4Message{
 				wire.NewTLVLE(wire.ICBMTLVData, wire.ICBMCh4Message{
-					UIN:         instance.UIN(),
+					UIN:         from.UIN(),
 					MessageType: wire.ICBMMsgTypeAuthReq,
 					MessageType: wire.ICBMMsgTypeAuthReq,
 					Message: fmt.Sprintf("%s\xFE%s\xFE%s\xFE%s\xFE%d\xFE%s",
 					Message: fmt.Sprintf("%s\xFE%s\xFE%s\xFE%s\xFE%d\xFE%s",
-						userInfo.ICQBasicInfo.Nickname,
-						userInfo.ICQBasicInfo.FirstName,
-						userInfo.ICQBasicInfo.LastName,
-						userInfo.ICQBasicInfo.EmailAddress,
+						userInfo.Nickname,
+						userInfo.FirstName,
+						userInfo.LastName,
+						userInfo.EmailAddress,
 						authorized,
 						authorized,
-						utf8ToLatin1(inBody.Reason)),
+						utf8ToLatin1(reason)),
 				}),
 				}),
 				wire.NewTLVBE(wire.ICBMTLVStore, []byte{}),
 				wire.NewTLVBE(wire.ICBMTLVStore, []byte{}),
 			},
 			},
 		},
 		},
 	}
 	}
 
 
-	if _, err := s.icbmSender(ctx, instance, frame, snac); err != nil {
+	if _, err := s.icbmSender(ctx, from, frame, snac); err != nil {
 		return fmt.Errorf("icbmSender: %w", err)
 		return fmt.Errorf("icbmSender: %w", err)
 	}
 	}
-
 	return nil
 	return nil
 }
 }
 
 

+ 219 - 14
foodgroup/feedbag_test.go

@@ -498,7 +498,7 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 		},
 		},
 		{
 		{
 			name:     "add 2 ICQ buddies one that requires authorization",
 			name:     "add 2 ICQ buddies one that requires authorization",
-			instance: newTestInstance("me"),
+			instance: newTestInstance("100001", sessOptUIN(100001)),
 			inputSNAC: wire.SNACMessage{
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 				Frame: wire.SNACFrame{
 					FoodGroup: wire.Feedbag,
 					FoodGroup: wire.Feedbag,
@@ -522,7 +522,7 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 				feedbagManagerParams: feedbagManagerParams{
 				feedbagManagerParams: feedbagManagerParams{
 					feedbagUpsertParams: feedbagUpsertParams{
 					feedbagUpsertParams: feedbagUpsertParams{
 						{
 						{
-							screenName: state.NewIdentScreenName("me"),
+							screenName: state.NewIdentScreenName("100001"),
 							items: []wire.FeedbagItem{
 							items: []wire.FeedbagItem{
 								{
 								{
 									ClassID: wire.FeedbagClassIdBuddy,
 									ClassID: wire.FeedbagClassIdBuddy,
@@ -543,7 +543,7 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 				buddyBroadcasterParams: buddyBroadcasterParams{
 				buddyBroadcasterParams: buddyBroadcasterParams{
 					broadcastVisibilityParams: broadcastVisibilityParams{
 					broadcastVisibilityParams: broadcastVisibilityParams{
 						{
 						{
-							from: state.NewIdentScreenName("me"),
+							from: state.NewIdentScreenName("100001"),
 							filter: []state.IdentScreenName{
 							filter: []state.IdentScreenName{
 								state.NewIdentScreenName("123401"),
 								state.NewIdentScreenName("123401"),
 							},
 							},
@@ -553,7 +553,7 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 				messageRelayerParams: messageRelayerParams{
 				messageRelayerParams: messageRelayerParams{
 					relayToOtherInstancesParams: relayToOtherInstancesParams{
 					relayToOtherInstancesParams: relayToOtherInstancesParams{
 						{
 						{
-							screenName: state.NewIdentScreenName("me"),
+							screenName: state.NewIdentScreenName("100001"),
 							message: wire.SNACMessage{
 							message: wire.SNACMessage{
 								Frame: wire.SNACFrame{
 								Frame: wire.SNACFrame{
 									FoodGroup: wire.Feedbag,
 									FoodGroup: wire.Feedbag,
@@ -573,7 +573,7 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 					},
 					},
 					relayToSelfParams: relayToSelfParams{
 					relayToSelfParams: relayToSelfParams{
 						{
 						{
-							screenName: state.NewIdentScreenName("me"),
+							screenName: state.NewIdentScreenName("100001"),
 							message: wire.SNACMessage{
 							message: wire.SNACMessage{
 								Frame: wire.SNACFrame{
 								Frame: wire.SNACFrame{
 									FoodGroup: wire.Feedbag,
 									FoodGroup: wire.Feedbag,
@@ -589,8 +589,8 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 				},
 				},
 				contactPreAuthorizerParams: contactPreAuthorizerParams{
 				contactPreAuthorizerParams: contactPreAuthorizerParams{
 					requiresAuthorizationParams: requiresAuthorizationParams{
 					requiresAuthorizationParams: requiresAuthorizationParams{
-						{owner: state.NewIdentScreenName("123400"), requester: state.NewIdentScreenName("me"), result: true},
-						{owner: state.NewIdentScreenName("123401"), requester: state.NewIdentScreenName("me"), result: false},
+						{owner: state.NewIdentScreenName("123400"), requester: state.NewIdentScreenName("100001"), result: true},
+						{owner: state.NewIdentScreenName("123401"), requester: state.NewIdentScreenName("100001"), result: false},
 					},
 					},
 				},
 				},
 			},
 			},
@@ -602,7 +602,7 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 				TLVRestBlock: wire.TLVRestBlock{
 				TLVRestBlock: wire.TLVRestBlock{
 					TLVList: wire.TLVList{
 					TLVList: wire.TLVList{
 						wire.NewTLVLE(wire.ICBMTLVData, wire.ICBMCh4Message{
 						wire.NewTLVLE(wire.ICBMTLVData, wire.ICBMCh4Message{
-							UIN:         0,
+							UIN:         100001,
 							MessageType: wire.ICBMMsgTypeAdded,
 							MessageType: wire.ICBMMsgTypeAdded,
 						}),
 						}),
 						wire.NewTLVBE(wire.ICBMTLVStore, []byte{}),
 						wire.NewTLVBE(wire.ICBMTLVStore, []byte{}),
@@ -2007,6 +2007,212 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 				},
 				},
 			},
 			},
 		},
 		},
+		{
+			name:     "ICQ user adds AIM buddy: expect ICQ user to preauthorize AIM user",
+			instance: newTestInstance("100777", sessOptUIN(100777)),
+			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.FeedbagClassIdBuddy,
+							Name:    "buddyaim",
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				contactPreAuthorizerParams: contactPreAuthorizerParams{
+					recordPreAuthParams: recordPreAuthParams{
+						{
+							owner: state.NewIdentScreenName("100777"),
+							buddy: state.NewIdentScreenName("buddyaim"),
+						},
+					},
+				},
+				feedbagManagerParams: feedbagManagerParams{
+					feedbagUpsertParams: feedbagUpsertParams{
+						{
+							screenName: state.NewIdentScreenName("100777"),
+							items: []wire.FeedbagItem{
+								{
+									ClassID: wire.FeedbagClassIdBuddy,
+									Name:    "buddyaim",
+								},
+							},
+						},
+					},
+				},
+				messageRelayerParams: messageRelayerParams{
+					relayToOtherInstancesParams: relayToOtherInstancesParams{
+						{
+							screenName: state.NewIdentScreenName("100777"),
+							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.FeedbagClassIdBuddy,
+											Name:    "buddyaim",
+										},
+									},
+								},
+							},
+						},
+					},
+					relayToSelfParams: relayToSelfParams{
+						{
+							screenName: state.NewIdentScreenName("100777"),
+							message: wire.SNACMessage{
+								Frame: wire.SNACFrame{
+									FoodGroup: wire.Feedbag,
+									SubGroup:  wire.FeedbagStatus,
+									RequestID: 1234,
+								},
+								Body: wire.SNAC_0x13_0x0E_FeedbagStatus{
+									Results: []uint16{0x0000},
+								},
+							},
+						},
+					},
+				},
+				buddyBroadcasterParams: buddyBroadcasterParams{
+					broadcastVisibilityParams: broadcastVisibilityParams{
+						{
+							from: state.NewIdentScreenName("100777"),
+							filter: []state.IdentScreenName{
+								state.NewIdentScreenName("buddyaim"),
+							},
+						},
+					},
+				},
+			},
+			expectOutput: nil,
+		},
+		{
+			name:     "AIM user adds ICQ buddy pending auth: expect AIM user to send auth request to ICQ user",
+			instance: newTestInstance("myaim"),
+			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.FeedbagClassIdBuddy,
+							Name:    "990011",
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				contactPreAuthorizerParams: contactPreAuthorizerParams{
+					requiresAuthorizationParams: requiresAuthorizationParams{
+						{
+							owner:     state.NewIdentScreenName("990011"),
+							requester: state.NewIdentScreenName("myaim"),
+							result:    true,
+						},
+					},
+				},
+				feedbagManagerParams: feedbagManagerParams{
+					feedbagUpsertParams: feedbagUpsertParams{
+						{
+							screenName: state.NewIdentScreenName("myaim"),
+							items: []wire.FeedbagItem{
+								{
+									ClassID: wire.FeedbagClassIdBuddy,
+									Name:    "990011",
+									TLVLBlock: wire.TLVLBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.FeedbagAttributesPending, []byte{}),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				messageRelayerParams: messageRelayerParams{
+					relayToOtherInstancesParams: relayToOtherInstancesParams{
+						{
+							screenName: state.NewIdentScreenName("myaim"),
+							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.FeedbagClassIdBuddy,
+											Name:    "990011",
+											TLVLBlock: wire.TLVLBlock{
+												TLVList: wire.TLVList{
+													wire.NewTLVBE(wire.FeedbagAttributesPending, []byte{}),
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+					relayToSelfParams: relayToSelfParams{
+						{
+							screenName: state.NewIdentScreenName("myaim"),
+							message: wire.SNACMessage{
+								Frame: wire.SNACFrame{
+									FoodGroup: wire.Feedbag,
+									SubGroup:  wire.FeedbagStatus,
+									RequestID: 1234,
+								},
+								Body: wire.SNAC_0x13_0x0E_FeedbagStatus{
+									Results: []uint16{0x0000},
+								},
+							},
+						},
+					},
+				},
+				buddyBroadcasterParams: buddyBroadcasterParams{
+					broadcastVisibilityParams: broadcastVisibilityParams{
+						{
+							from: state.NewIdentScreenName("myaim"),
+							filter: []state.IdentScreenName{
+								state.NewIdentScreenName("990011"),
+							},
+						},
+					},
+				},
+			},
+			expectOutput: nil,
+			expectICBM:   true,
+			wantICBMBody: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
+				ChannelID:  wire.ICBMChannelICQ,
+				ScreenName: "990011",
+				TLVRestBlock: wire.TLVRestBlock{
+					TLVList: wire.TLVList{
+						wire.NewTLVLE(wire.ICBMTLVData, wire.ICBMCh4Message{
+							UIN:         0,
+							MessageType: wire.ICBMMsgTypeAuthReq,
+							Message:     "\xFE\xFE\xFE\xFE1\xFE",
+						}),
+						wire.NewTLVBE(wire.ICBMTLVStore, []byte{}),
+					},
+				},
+			},
+		},
 	}
 	}
 
 
 	for _, tc := range cases {
 	for _, tc := range cases {
@@ -2058,6 +2264,9 @@ func TestFeedbagService_UpsertItem(t *testing.T) {
 					Return(params.err)
 					Return(params.err)
 			}
 			}
 			contactPreAuth := newMockContactPreAuthorizer(t)
 			contactPreAuth := newMockContactPreAuthorizer(t)
+			for _, params := range tc.mockParams.recordPreAuthParams {
+				contactPreAuth.EXPECT().RecordPreAuth(matchContext(), params.owner, params.buddy).Return(params.err)
+			}
 			for _, params := range tc.mockParams.requiresAuthorizationParams {
 			for _, params := range tc.mockParams.requiresAuthorizationParams {
 				contactPreAuth.EXPECT().RequiresAuthorization(matchContext(), params.owner, params.requester).Return(params.result, params.err)
 				contactPreAuth.EXPECT().RequiresAuthorization(matchContext(), params.owner, params.requester).Return(params.result, params.err)
 			}
 			}
@@ -2494,7 +2703,6 @@ func TestFeedbagService_RequestAuthorizeToHost(t *testing.T) {
 				Frame: wire.SNACFrame{
 				Frame: wire.SNACFrame{
 					FoodGroup: wire.ICBM,
 					FoodGroup: wire.ICBM,
 					SubGroup:  wire.ICBMChannelMsgToHost,
 					SubGroup:  wire.ICBMChannelMsgToHost,
-					RequestID: 1234,
 				},
 				},
 				Body: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 				Body: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 					ChannelID:  wire.ICBMChannelICQ,
 					ChannelID:  wire.ICBMChannelICQ,
@@ -2560,7 +2768,6 @@ func TestFeedbagService_RequestAuthorizeToHost(t *testing.T) {
 				Frame: wire.SNACFrame{
 				Frame: wire.SNACFrame{
 					FoodGroup: wire.ICBM,
 					FoodGroup: wire.ICBM,
 					SubGroup:  wire.ICBMChannelMsgToHost,
 					SubGroup:  wire.ICBMChannelMsgToHost,
-					RequestID: 1234,
 				},
 				},
 				Body: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 				Body: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 					ChannelID:  wire.ICBMChannelICQ,
 					ChannelID:  wire.ICBMChannelICQ,
@@ -2622,7 +2829,6 @@ func TestFeedbagService_RequestAuthorizeToHost(t *testing.T) {
 				Frame: wire.SNACFrame{
 				Frame: wire.SNACFrame{
 					FoodGroup: wire.ICBM,
 					FoodGroup: wire.ICBM,
 					SubGroup:  wire.ICBMChannelMsgToHost,
 					SubGroup:  wire.ICBMChannelMsgToHost,
-					RequestID: 1234,
 				},
 				},
 				Body: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 				Body: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 					ChannelID:  wire.ICBMChannelICQ,
 					ChannelID:  wire.ICBMChannelICQ,
@@ -2684,7 +2890,6 @@ func TestFeedbagService_RequestAuthorizeToHost(t *testing.T) {
 				Frame: wire.SNACFrame{
 				Frame: wire.SNACFrame{
 					FoodGroup: wire.ICBM,
 					FoodGroup: wire.ICBM,
 					SubGroup:  wire.ICBMChannelMsgToHost,
 					SubGroup:  wire.ICBMChannelMsgToHost,
-					RequestID: 1234,
 				},
 				},
 				Body: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 				Body: wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
 					ChannelID:  wire.ICBMChannelICQ,
 					ChannelID:  wire.ICBMChannelICQ,
@@ -2918,7 +3123,7 @@ func TestFeedbagService_RespondAuthorizeToHost(t *testing.T) {
 									Name:    "100001",
 									Name:    "100001",
 									TLVLBlock: wire.TLVLBlock{
 									TLVLBlock: wire.TLVLBlock{
 										TLVList: wire.TLVList{
 										TLVList: wire.TLVList{
-											wire.NewTLVBE(wire.FeedbagAttributesPending, uint16(0)),
+											wire.NewTLVBE(wire.FeedbagAttributesPending, []byte{}),
 										},
 										},
 									},
 									},
 								},
 								},
@@ -3037,7 +3242,7 @@ func TestFeedbagService_RespondAuthorizeToHost(t *testing.T) {
 									Name:    "100001",
 									Name:    "100001",
 									TLVLBlock: wire.TLVLBlock{
 									TLVLBlock: wire.TLVLBlock{
 										TLVList: wire.TLVList{
 										TLVList: wire.TLVList{
-											wire.NewTLVBE(wire.FeedbagAttributesPending, uint16(0)),
+											wire.NewTLVBE(wire.FeedbagAttributesPending, []byte{}),
 										},
 										},
 									},
 									},
 								},
 								},

+ 14 - 0
foodgroup/icq.go

@@ -644,6 +644,20 @@ func (s *ICQService) SetWorkInfo(ctx context.Context, instance *state.SessionIns
 func (s *ICQService) ShortUserInfo(ctx context.Context, instance *state.SessionInstance, inBody wire.ICQ_0x07D0_0x04BA_DBQueryMetaReqShortInfo, seq uint16) error {
 func (s *ICQService) ShortUserInfo(ctx context.Context, instance *state.SessionInstance, inBody wire.ICQ_0x07D0_0x04BA_DBQueryMetaReqShortInfo, seq uint16) error {
 	user, err := s.userFinder.FindByUIN(ctx, inBody.UIN)
 	user, err := s.userFinder.FindByUIN(ctx, inBody.UIN)
 	if err != nil {
 	if err != nil {
+		if errors.Is(err, state.ErrNoUser) {
+			msg := wire.ICQMessageReplyEnvelope{
+				Message: wire.ICQ_0x07DA_0x0104_DBQueryMetaReplyShortInfo{
+					ICQMetadata: wire.ICQMetadata{
+						UIN:     instance.UIN(),
+						ReqType: wire.ICQDBQueryMetaReply,
+						Seq:     seq,
+					},
+					ReqSubType: wire.ICQDBQueryMetaReplyShortInfo,
+					Success:    wire.ICQStatusCodeErr,
+				},
+			}
+			return s.reply(ctx, instance, msg)
+		}
 		return err
 		return err
 	}
 	}
 
 

+ 2 - 0
state/migrations/0031_feedbag_auth_pending.down.sql

@@ -0,0 +1,2 @@
+ALTER TABLE feedbag
+	DROP COLUMN authPending;

+ 4 - 0
state/migrations/0031_feedbag_auth_pending.up.sql

@@ -0,0 +1,4 @@
+-- Denormalized buddy authorization-pending flag (TLV 0x0066 on buddy items).
+-- Maintained by FeedbagUpsert; no backfill of historical rows.
+ALTER TABLE feedbag
+	ADD COLUMN authPending BOOLEAN NOT NULL DEFAULT FALSE;

+ 1 - 1
state/relationship.go

@@ -56,7 +56,7 @@ WITH myScreenName AS (SELECT ?),
                               COALESCE(clientSide.isPermit OR feedbag.isPermit, FALSE) AS isPermit,
                               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
                        FROM (SELECT feedbag.name                                         AS _screenName,
                        FROM (SELECT feedbag.name                                         AS _screenName,
-                                    MAX(CASE WHEN feedbag.classId = 0 THEN 1 ELSE 0 END) AS isBuddy,
+                                    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 = 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
                              FROM feedbag
                              FROM feedbag

+ 41 - 3
state/relationship_test.go

@@ -3,6 +3,7 @@ package state
 import (
 import (
 	"context"
 	"context"
 	"os"
 	"os"
+	"slices"
 	"testing"
 	"testing"
 
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
@@ -21,6 +22,9 @@ func TestSQLiteUserStore_AllRelationships(t *testing.T) {
 		permitList []IdentScreenName
 		permitList []IdentScreenName
 		// buddyList is the list of users on the deny list. only active when wire.FeedbagPDModeDenySome is set.
 		// buddyList is the list of users on the deny list. only active when wire.FeedbagPDModeDenySome is set.
 		denyList []IdentScreenName
 		denyList []IdentScreenName
+		// buddyPendingAuth lists buddies (subset of buddyList) whose server-side feedbag buddy row
+		// is written with TLV FeedbagAttributesPending / authPending.
+		buddyPendingAuth []IdentScreenName
 	}
 	}
 
 
 	tests := []struct {
 	tests := []struct {
@@ -3131,6 +3135,35 @@ func TestSQLiteUserStore_AllRelationships(t *testing.T) {
 				},
 				},
 			},
 			},
 		},
 		},
+		{
+			name:            "feedbag server-side: buddy with auth pending does not count on your list",
+			me:              NewIdentScreenName("me"),
+			clientSideLists: map[IdentScreenName]buddyList{},
+			serverSideLists: map[IdentScreenName]buddyList{
+				NewIdentScreenName("me"): {
+					privacyMode:      wire.FeedbagPDModePermitAll,
+					buddyList:        []IdentScreenName{NewIdentScreenName("them")},
+					buddyPendingAuth: []IdentScreenName{NewIdentScreenName("them")},
+					permitList:       []IdentScreenName{},
+					denyList:         []IdentScreenName{},
+				},
+				NewIdentScreenName("them"): {
+					privacyMode: wire.FeedbagPDModePermitAll,
+					buddyList:   []IdentScreenName{},
+					permitList:  []IdentScreenName{},
+					denyList:    []IdentScreenName{},
+				},
+			},
+			expect: []Relationship{
+				{
+					User:          NewIdentScreenName("them"),
+					BlocksYou:     false,
+					YouBlock:      false,
+					IsOnTheirList: false,
+					IsOnYourList:  false,
+				},
+			},
+		},
 	}
 	}
 	for _, tt := range tests {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
@@ -3162,15 +3195,20 @@ func TestSQLiteUserStore_AllRelationships(t *testing.T) {
 				}
 				}
 				itemID++
 				itemID++
 				for _, buddy := range list.buddyList {
 				for _, buddy := range list.buddyList {
-					items = append(items, newFeedbagItem(wire.FeedbagClassIdBuddy, itemID, buddy.String()))
+					items = append(items, newFeedbagItem(
+						wire.FeedbagClassIdBuddy,
+						itemID,
+						buddy.String(),
+						slices.Contains(list.buddyPendingAuth, buddy),
+					))
 					itemID++
 					itemID++
 				}
 				}
 				for _, buddy := range list.permitList {
 				for _, buddy := range list.permitList {
-					items = append(items, newFeedbagItem(wire.FeedbagClassIDPermit, itemID, buddy.String()))
+					items = append(items, newFeedbagItem(wire.FeedbagClassIDPermit, itemID, buddy.String(), false))
 					itemID++
 					itemID++
 				}
 				}
 				for _, buddy := range list.denyList {
 				for _, buddy := range list.denyList {
-					items = append(items, newFeedbagItem(wire.FeedbagClassIDDeny, itemID, buddy.String()))
+					items = append(items, newFeedbagItem(wire.FeedbagClassIDDeny, itemID, buddy.String(), false))
 					itemID++
 					itemID++
 				}
 				}
 				assert.NoError(t, feedbagStore.FeedbagUpsert(context.Background(), sn, items))
 				assert.NoError(t, feedbagStore.FeedbagUpsert(context.Background(), sn, items))

+ 8 - 4
state/user_store.go

@@ -769,13 +769,14 @@ func (f SQLiteUserStore) FeedbagDelete(ctx context.Context, screenName IdentScre
 
 
 func (f SQLiteUserStore) FeedbagUpsert(ctx context.Context, screenName IdentScreenName, items []wire.FeedbagItem) error {
 func (f SQLiteUserStore) FeedbagUpsert(ctx context.Context, screenName IdentScreenName, items []wire.FeedbagItem) error {
 	q := `
 	q := `
-		INSERT INTO feedbag (screenName, groupID, itemID, classID, name, attributes, pdMode, lastModified)
-		VALUES (?, ?, ?, ?, ?, ?, ?, UNIXEPOCH())
+		INSERT INTO feedbag (screenName, groupID, itemID, classID, name, attributes, pdMode, authPending, lastModified)
+		VALUES (?, ?, ?, ?, ?, ?, ?, ?, UNIXEPOCH())
 		ON CONFLICT (screenName, groupID, itemID)
 		ON CONFLICT (screenName, groupID, itemID)
 			DO UPDATE SET classID      = excluded.classID,
 			DO UPDATE SET classID      = excluded.classID,
 						  name         = excluded.name,
 						  name         = excluded.name,
 						  attributes   = excluded.attributes,
 						  attributes   = excluded.attributes,
-						  pdMode       = excluded.pdMode, 
+						  pdMode       = excluded.pdMode,
+						  authPending  = excluded.authPending,
 						  lastModified = UNIXEPOCH()
 						  lastModified = UNIXEPOCH()
 	`
 	`
 
 
@@ -800,6 +801,8 @@ func (f SQLiteUserStore) FeedbagUpsert(ctx context.Context, screenName IdentScre
 				pdMode = uint8(wire.FeedbagPDModePermitAll)
 				pdMode = uint8(wire.FeedbagPDModePermitAll)
 			}
 			}
 		}
 		}
+		authPending := item.ClassID == wire.FeedbagClassIdBuddy &&
+			item.HasTag(wire.FeedbagAttributesPending)
 		_, err := f.db.ExecContext(ctx,
 		_, err := f.db.ExecContext(ctx,
 			q,
 			q,
 			screenName.String(),
 			screenName.String(),
@@ -808,7 +811,8 @@ func (f SQLiteUserStore) FeedbagUpsert(ctx context.Context, screenName IdentScre
 			item.ClassID,
 			item.ClassID,
 			item.Name,
 			item.Name,
 			buf.Bytes(),
 			buf.Bytes(),
-			pdMode)
+			pdMode,
+			authPending)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}

+ 68 - 2
state/user_store_test.go

@@ -127,6 +127,68 @@ func TestSQLiteUserStore_FeedbagUpsert(t *testing.T) {
 		assert.NoError(t, err)
 		assert.NoError(t, err)
 		assert.Equal(t, wire.FeedbagPDMode(pdMode), wire.FeedbagPDModePermitAll)
 		assert.Equal(t, wire.FeedbagPDMode(pdMode), wire.FeedbagPDModePermitAll)
 	})
 	})
+
+	t.Run("authPending true for buddy with FeedbagAttributesPending TLV", func(t *testing.T) {
+		defer func() {
+			assert.NoError(t, os.Remove(testFile))
+		}()
+
+		f, err := NewSQLiteUserStore(testFile)
+		require.NoError(t, err)
+
+		me := NewIdentScreenName("me")
+		err = f.FeedbagUpsert(context.Background(), me, []wire.FeedbagItem{
+			{
+				GroupID: 0,
+				ItemID:  1,
+				ClassID: wire.FeedbagClassIdBuddy,
+				Name:    "buddy1",
+				TLVLBlock: wire.TLVLBlock{
+					TLVList: wire.TLVList{
+						wire.NewTLVBE(wire.FeedbagAttributesPending, uint16(0)),
+					},
+				},
+			},
+		})
+		require.NoError(t, err)
+
+		var authPending bool
+		err = f.db.QueryRow(
+			`SELECT authPending FROM feedbag WHERE screenName = ? AND itemID = 1`,
+			me.String(),
+		).Scan(&authPending)
+		require.NoError(t, err)
+		assert.True(t, authPending)
+	})
+
+	t.Run("authPending false for buddy without pending TLV", func(t *testing.T) {
+		defer func() {
+			assert.NoError(t, os.Remove(testFile))
+		}()
+
+		f, err := NewSQLiteUserStore(testFile)
+		require.NoError(t, err)
+
+		me := NewIdentScreenName("me")
+		err = f.FeedbagUpsert(context.Background(), me, []wire.FeedbagItem{
+			{
+				GroupID:   0,
+				ItemID:    1,
+				ClassID:   wire.FeedbagClassIdBuddy,
+				Name:      "buddy1",
+				TLVLBlock: wire.TLVLBlock{},
+			},
+		})
+		require.NoError(t, err)
+
+		var authPending bool
+		err = f.db.QueryRow(
+			`SELECT authPending FROM feedbag WHERE screenName = ? AND itemID = 1`,
+			me.String(),
+		).Scan(&authPending)
+		require.NoError(t, err)
+		assert.False(t, authPending)
+	})
 }
 }
 
 
 func TestFeedbagDelete(t *testing.T) {
 func TestFeedbagDelete(t *testing.T) {
@@ -626,12 +688,16 @@ func TestSQLiteUserStore_DeleteUser_DeleteNonExistentUser(t *testing.T) {
 	assert.ErrorIs(t, ErrNoUser, err)
 	assert.ErrorIs(t, ErrNoUser, err)
 }
 }
 
 
-func newFeedbagItem(classID uint16, itemID uint16, name string) wire.FeedbagItem {
-	return wire.FeedbagItem{
+func newFeedbagItem(classID uint16, itemID uint16, name string, feedbagPending bool) wire.FeedbagItem {
+	item := wire.FeedbagItem{
 		ClassID: classID,
 		ClassID: classID,
 		ItemID:  itemID,
 		ItemID:  itemID,
 		Name:    name,
 		Name:    name,
 	}
 	}
+	if feedbagPending && classID == wire.FeedbagClassIdBuddy {
+		item.Append(wire.NewTLVBE(wire.FeedbagAttributesPending, uint16(0)))
+	}
+	return item
 }
 }
 
 
 func pdInfoItem(itemID uint16, pdMode wire.FeedbagPDMode) wire.FeedbagItem {
 func pdInfoItem(itemID uint16, pdMode wire.FeedbagPDMode) wire.FeedbagItem {