Explorar o código

fix crash at login in AIM 1.7.563-1.1.8.924

Fields "LastTime" and "CurrentState" on SNAC(0x01,0x07) cause AIM 1.x
to crash at login. The commit omits those fields when AIM 1.x clients
connect.

AIM 1.7.486 and under still crash for different reasons.
Mike hai 1 ano
pai
achega
9665333ac2

+ 104 - 45
foodgroup/auth.go

@@ -76,8 +76,11 @@ func (s AuthService) RegisterChatSession(authCookie []byte) (*state.Session, err
 	return s.chatSessionRegistry.AddSession(c.ChatCookie, c.ScreenName), nil
 }
 
+// bosCookie represents a token containing client metadata passed to the BOS
+// service upon connection.
 type bosCookie struct {
 	ScreenName state.DisplayScreenName `oscar:"len_prefix=uint8"`
+	ClientID   string                  `oscar:"len_prefix=uint8"`
 }
 
 // RegisterBOSSession adds a new session to the session registry.
@@ -108,6 +111,9 @@ func (s AuthService) RegisterBOSSession(authCookie []byte) (*state.Session, erro
 		sess.SetUserInfoFlag(wire.OServiceUserFlagUnconfirmed)
 	}
 
+	// set string containing OSCAR client name and version
+	sess.SetClientID(c.ClientID)
+
 	if u.DisplayScreenName.IsUIN() {
 		sess.SetUserInfoFlag(wire.OServiceUserFlagICQ)
 
@@ -229,7 +235,10 @@ func (s AuthService) BUCPChallenge(
 // (wire.LoginTLVTagsReconnectHere) and an authorization cookie
 // (wire.LoginTLVTagsAuthorizationCookie). Else, an error code is set
 // (wire.LoginTLVTagsErrorSubcode).
-func (s AuthService) BUCPLogin(bodyIn wire.SNAC_0x17_0x02_BUCPLoginRequest, newUserFn func(screenName state.DisplayScreenName) (state.User, error)) (wire.SNACMessage, error) {
+func (s AuthService) BUCPLogin(
+	bodyIn wire.SNAC_0x17_0x02_BUCPLoginRequest,
+	newUserFn func(screenName state.DisplayScreenName) (state.User, error),
+) (wire.SNACMessage, error) {
 
 	block, err := s.login(bodyIn.TLVList, newUserFn)
 	if err != nil {
@@ -257,82 +266,132 @@ func (s AuthService) BUCPLogin(bodyIn wire.SNAC_0x17_0x02_BUCPLoginRequest, newU
 // (wire.LoginTLVTagsReconnectHere) and an authorization cookie
 // (wire.LoginTLVTagsAuthorizationCookie). Else, an error code is set
 // (wire.LoginTLVTagsErrorSubcode).
-func (s AuthService) FLAPLogin(frame wire.FLAPSignonFrame, newUserFn func(screenName state.DisplayScreenName) (state.User, error)) (wire.TLVRestBlock, error) {
+func (s AuthService) FLAPLogin(
+	frame wire.FLAPSignonFrame,
+	newUserFn func(screenName state.DisplayScreenName) (state.User, error),
+) (wire.TLVRestBlock, error) {
 	return s.login(frame.TLVList, newUserFn)
 }
 
+// loginProperties represents the properties sent by the client at login.
+type loginProperties struct {
+	screenName   state.DisplayScreenName
+	clientID     string
+	isBUCPAuth   bool
+	passwordHash []byte
+	roastedPass  []byte
+}
+
+// fromTLV creates an instance of loginProperties from a TLV list.
+func (l *loginProperties) fromTLV(list wire.TLVList) error {
+	// extract screen name
+	if screenName, found := list.String(wire.LoginTLVTagsScreenName); found {
+		l.screenName = state.DisplayScreenName(screenName)
+	} else {
+		return errors.New("screen name doesn't exist in tlv")
+	}
+
+	// extract client name and version
+	if clientID, found := list.String(wire.LoginTLVTagsClientIdentity); found {
+		l.clientID = clientID
+	}
+
+	// get the password from the appropriate TLV. older clients have a
+	// roasted password, newer clients have a hashed password. ICQ may omit
+	// the password TLV when logging in without saved password.
+
+	// extract password hash for BUCP login
+	if passwordHash, found := list.Bytes(wire.LoginTLVTagsPasswordHash); found {
+		l.passwordHash = passwordHash
+		l.isBUCPAuth = true
+	}
+
+	// extract roasted password for FLAP login
+	if roastedPass, found := list.Bytes(wire.LoginTLVTagsRoastedPassword); found {
+		l.roastedPass = roastedPass
+	}
+
+	return nil
+}
+
 // login validates a user's credentials and creates their session. it returns
 // metadata used in both BUCP and FLAP authentication responses.
 func (s AuthService) login(
-	TLVList wire.TLVList,
+	tlv wire.TLVList,
 	newUserFn func(screenName state.DisplayScreenName) (state.User, error),
 ) (wire.TLVRestBlock, error) {
 
-	screenName, found := TLVList.String(wire.LoginTLVTagsScreenName)
-	if !found {
-		return wire.TLVRestBlock{}, errors.New("screen name doesn't exist in tlv")
+	props := loginProperties{}
+	if err := props.fromTLV(tlv); err != nil {
+		return wire.TLVRestBlock{}, err
 	}
 
-	sn := state.DisplayScreenName(screenName)
-
-	user, err := s.userManager.User(sn.IdentScreenName())
+	user, err := s.userManager.User(props.screenName.IdentScreenName())
 	if err != nil {
 		return wire.TLVRestBlock{}, err
 	}
 
 	if user == nil {
+		// user not found
 		if s.config.DisableAuth {
-			handleValid := false
-			if sn.IsUIN() {
-				handleValid = sn.ValidateUIN() == nil
-			} else {
-				handleValid = sn.ValidateAIMHandle() == nil
-			}
-			if !handleValid {
-				return loginFailureResponse(sn, wire.LoginErrInvalidUsernameOrPassword), nil
-			}
-
-			newUser, err := newUserFn(sn)
-			if err != nil {
+			// auth disabled, create the user and return success
+			if err := s.createUser(props, newUserFn); err != nil {
 				return wire.TLVRestBlock{}, err
 			}
-			if err := s.userManager.InsertUser(newUser); err != nil {
-				return wire.TLVRestBlock{}, err
-			}
-
-			return s.loginSuccessResponse(sn, err)
+			return s.loginSuccessResponse(props)
 		}
-
+		// auth enabled, return separate login errors for ICQ and AIM
 		loginErr := wire.LoginErrInvalidUsernameOrPassword
-		if sn.IsUIN() {
+		if props.screenName.IsUIN() {
 			loginErr = wire.LoginErrICQUserErr
 		}
-		return loginFailureResponse(sn, loginErr), nil
+		return loginFailureResponse(props, loginErr), nil
 	}
 
 	if s.config.DisableAuth {
-		return s.loginSuccessResponse(sn, err)
+		// user exists, but don't validate
+		return s.loginSuccessResponse(props)
 	}
 
 	var loginOK bool
-	// get the password from the appropriate TLV. older clients have a
-	// roasted password, newer clients have a hashed password. ICQ may omit
-	// the password TLV when logging in without saved password.
-	if md5Hash, hasMD5 := TLVList.Bytes(wire.LoginTLVTagsPasswordHash); hasMD5 {
-		loginOK = user.ValidateHash(md5Hash)
-	} else if roastedPass, hasRoasted := TLVList.Bytes(wire.LoginTLVTagsRoastedPassword); hasRoasted {
-		loginOK = user.ValidateRoastedPass(roastedPass)
+	if props.isBUCPAuth {
+		loginOK = user.ValidateHash(props.passwordHash)
+	} else {
+		loginOK = user.ValidateRoastedPass(props.roastedPass)
 	}
 	if !loginOK {
-		return loginFailureResponse(sn, wire.LoginErrInvalidPassword), nil
+		return loginFailureResponse(props, wire.LoginErrInvalidPassword), nil
 	}
 
-	return s.loginSuccessResponse(sn, err)
+	return s.loginSuccessResponse(props)
+}
+
+func (s AuthService) createUser(
+	props loginProperties,
+	newUserFn func(screenName state.DisplayScreenName) (state.User, error),
+) error {
+
+	handleValid := false
+	if props.screenName.IsUIN() {
+		handleValid = props.screenName.ValidateUIN() == nil
+	} else {
+		handleValid = props.screenName.ValidateAIMHandle() == nil
+	}
+	if !handleValid {
+		return nil
+	}
+
+	newUser, err := newUserFn(props.screenName)
+	if err != nil {
+		return err
+	}
+	return s.userManager.InsertUser(newUser)
 }
 
-func (s AuthService) loginSuccessResponse(screenName state.DisplayScreenName, err error) (wire.TLVRestBlock, error) {
+func (s AuthService) loginSuccessResponse(props loginProperties) (wire.TLVRestBlock, error) {
 	loginCookie := bosCookie{
-		ScreenName: screenName,
+		ScreenName: props.screenName,
+		ClientID:   props.clientID,
 	}
 
 	buf := &bytes.Buffer{}
@@ -346,18 +405,18 @@ func (s AuthService) loginSuccessResponse(screenName state.DisplayScreenName, er
 
 	return wire.TLVRestBlock{
 		TLVList: []wire.TLV{
-			wire.NewTLVBE(wire.LoginTLVTagsScreenName, screenName),
+			wire.NewTLVBE(wire.LoginTLVTagsScreenName, props.screenName),
 			wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, net.JoinHostPort(s.config.OSCARHost, s.config.BOSPort)),
 			wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, cookie),
 		},
 	}, nil
 }
 
-func loginFailureResponse(screenName state.DisplayScreenName, code uint16) wire.TLVRestBlock {
+func loginFailureResponse(props loginProperties, errCode uint16) wire.TLVRestBlock {
 	return wire.TLVRestBlock{
 		TLVList: []wire.TLV{
-			wire.NewTLVBE(wire.LoginTLVTagsScreenName, screenName),
-			wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, code),
+			wire.NewTLVBE(wire.LoginTLVTagsScreenName, props.screenName),
+			wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, errCode),
 		},
 	}
 }

+ 2 - 0
foodgroup/auth_test.go

@@ -125,6 +125,7 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 							dataIn: func() []byte {
 								loginCookie := bosCookie{
 									ScreenName: user.DisplayScreenName,
+									ClientID:   "ICQ 2000b",
 								}
 								buf := &bytes.Buffer{}
 								assert.NoError(t, wire.MarshalBE(loginCookie, buf))
@@ -554,6 +555,7 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 							dataIn: func() []byte {
 								loginCookie := bosCookie{
 									ScreenName: user.DisplayScreenName,
+									ClientID:   "ICQ 2000b",
 								}
 								buf := &bytes.Buffer{}
 								assert.NoError(t, wire.MarshalBE(loginCookie, buf))

+ 76 - 8
foodgroup/oservice.go

@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"log/slog"
 	"net"
+	"strings"
 	"time"
 
 	"github.com/mk6i/retro-aim-server/config"
@@ -44,7 +45,9 @@ func (s OServiceService) ClientVersions(_ context.Context, frame wire.SNACFrame,
 	}
 }
 
-var rateLimitSNAC = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
+// rateLimitSNACV1 is the rate params reply sent to AIM 1.x clients that does
+// not contain LastTime and CurrentState fields.
+var rateLimitSNACV1 = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
 	RateClasses: []struct {
 		ID              uint16
 		WindowSize      uint32
@@ -54,8 +57,10 @@ var rateLimitSNAC = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
 		DisconnectLevel uint32
 		CurrentLevel    uint32
 		MaxLevel        uint32
-		LastTime        uint32
-		CurrentState    uint8
+		V2Params        *struct {
+			LastTime     uint32
+			CurrentState uint8
+		} `oscar:"optional"`
 	}{
 		{
 			ID:              0x01,
@@ -66,8 +71,58 @@ var rateLimitSNAC = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
 			DisconnectLevel: 0x0320,
 			CurrentLevel:    0x0D69,
 			MaxLevel:        0x1770,
-			LastTime:        0x0000,
-			CurrentState:    0x0,
+			V2Params:        nil,
+		},
+	},
+	RateGroups: []struct {
+		ID    uint16
+		Pairs []struct {
+			FoodGroup uint16
+			SubGroup  uint16
+		} `oscar:"count_prefix=uint16"`
+	}{
+		{
+			ID: 1,
+			Pairs: []struct {
+				FoodGroup uint16
+				SubGroup  uint16
+			}{},
+		},
+	},
+}
+
+// rateLimitSNACV2 is the rate params reply sent to non-AIM 1.x clients.
+var rateLimitSNACV2 = wire.SNAC_0x01_0x07_OServiceRateParamsReply{
+	RateClasses: []struct {
+		ID              uint16
+		WindowSize      uint32
+		ClearLevel      uint32
+		AlertLevel      uint32
+		LimitLevel      uint32
+		DisconnectLevel uint32
+		CurrentLevel    uint32
+		MaxLevel        uint32
+		V2Params        *struct {
+			LastTime     uint32
+			CurrentState uint8
+		} `oscar:"optional"`
+	}{
+		{
+			ID:              0x01,
+			WindowSize:      0x0050,
+			ClearLevel:      0x09C4,
+			AlertLevel:      0x07D0,
+			LimitLevel:      0x05DC,
+			DisconnectLevel: 0x0320,
+			CurrentLevel:    0x0D69,
+			MaxLevel:        0x1770,
+			V2Params: &struct {
+				LastTime     uint32
+				CurrentState uint8
+			}{
+				LastTime:     0x0000,
+				CurrentState: 0x0,
+			},
 		},
 	},
 	RateGroups: []struct {
@@ -374,7 +429,16 @@ func init() {
 	} {
 		subGroups := foodGroupToSubgroup[foodGroup]
 		for _, subGroup := range subGroups {
-			rateLimitSNAC.RateGroups[0].Pairs = append(rateLimitSNAC.RateGroups[0].Pairs, struct {
+			// build response for AIM 1.x clients
+			rateLimitSNACV1.RateGroups[0].Pairs = append(rateLimitSNACV1.RateGroups[0].Pairs, struct {
+				FoodGroup uint16
+				SubGroup  uint16
+			}{
+				FoodGroup: foodGroup,
+				SubGroup:  subGroup,
+			})
+			// build response for all other clients
+			rateLimitSNACV2.RateGroups[0].Pairs = append(rateLimitSNACV2.RateGroups[0].Pairs, struct {
 				FoodGroup uint16
 				SubGroup  uint16
 			}{
@@ -404,14 +468,18 @@ func init() {
 // AIM clients silently fail when they expect a rate limit rule that does not
 // exist in this response. When support for a new food group is added to the
 // server, update this function accordingly.
-func (s OServiceService) RateParamsQuery(_ context.Context, inFrame wire.SNACFrame) wire.SNACMessage {
+func (s OServiceService) RateParamsQuery(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame) wire.SNACMessage {
+	limits := rateLimitSNACV2
+	if strings.Contains(sess.ClientID(), "AOL Instant Messenger (TM), version 1.") {
+		limits = rateLimitSNACV1
+	}
 	return wire.SNACMessage{
 		Frame: wire.SNACFrame{
 			FoodGroup: wire.OService,
 			SubGroup:  wire.OServiceRateParamsReply,
 			RequestID: inFrame.RequestID,
 		},
-		Body: rateLimitSNAC,
+		Body: limits,
 	}
 }
 

+ 1071 - 990
foodgroup/oservice_test.go

@@ -99,6 +99,7 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 						{
 							dataIn: []byte{
 								0x10, 'u', 's', 'e', 'r', '_', 's', 'c', 'r', 'e', 'e', 'n', '_', 'n', 'a', 'm', 'e',
+								0x0, // no client ID
 							},
 							cookieOut: []byte("the-cookie"),
 						},
@@ -145,6 +146,7 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 						{
 							dataIn: []byte{
 								0x10, 'u', 's', 'e', 'r', '_', 's', 'c', 'r', 'e', 'e', 'n', '_', 'n', 'a', 'm', 'e',
+								0x0, // no client ID
 							},
 							cookieOut: []byte("the-cookie"),
 						},
@@ -191,6 +193,7 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 						{
 							dataIn: []byte{
 								0x10, 'u', 's', 'e', 'r', '_', 's', 'c', 'r', 'e', 'e', 'n', '_', 'n', 'a', 'm', 'e',
+								0x0, // no client ID
 							},
 							cookieOut: []byte("the-cookie"),
 						},
@@ -471,1011 +474,1089 @@ func TestSetUserInfoFields(t *testing.T) {
 }
 
 func TestOServiceService_RateParamsQuery(t *testing.T) {
-	svc := OServiceService{
-		cfg:    config.Config{},
-		logger: slog.Default(),
-	}
-
-	have := svc.RateParamsQuery(nil, wire.SNACFrame{RequestID: 1234})
-	want := wire.SNACMessage{
-		Frame: wire.SNACFrame{
-			FoodGroup: wire.OService,
-			SubGroup:  wire.OServiceRateParamsReply,
-			RequestID: 1234,
-		},
-		Body: wire.SNAC_0x01_0x07_OServiceRateParamsReply{
-			RateClasses: []struct {
-				ID              uint16
-				WindowSize      uint32
-				ClearLevel      uint32
-				AlertLevel      uint32
-				LimitLevel      uint32
-				DisconnectLevel uint32
-				CurrentLevel    uint32
-				MaxLevel        uint32
-				LastTime        uint32
-				CurrentState    uint8
+	expectRateGroups := []struct {
+		ID    uint16
+		Pairs []struct {
+			FoodGroup uint16
+			SubGroup  uint16
+		} `oscar:"count_prefix=uint16"`
+	}{
+		{
+			ID: 1,
+			Pairs: []struct {
+				FoodGroup uint16
+				SubGroup  uint16
 			}{
 				{
-					ID:              0x0001,
-					WindowSize:      0x00000050,
-					ClearLevel:      0x000009C4,
-					AlertLevel:      0x000007D0,
-					LimitLevel:      0x000005DC,
-					DisconnectLevel: 0x00000320,
-					CurrentLevel:    0x00000D69,
-					MaxLevel:        0x00001770,
-					LastTime:        0x00000000,
-					CurrentState:    0x00,
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceErr,
 				},
-			},
-			RateGroups: []struct {
-				ID    uint16
-				Pairs []struct {
-					FoodGroup uint16
-					SubGroup  uint16
-				} `oscar:"count_prefix=uint16"`
-			}{
 				{
-					ID: 1,
-					Pairs: []struct {
-						FoodGroup uint16
-						SubGroup  uint16
-					}{
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceErr,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceClientOnline,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceHostOnline,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceServiceRequest,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceServiceResponse,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceRateParamsQuery,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceRateParamsReply,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceRateParamsSubAdd,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceRateDelParamSub,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceRateParamChange,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServicePauseReq,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServicePauseAck,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceResume,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceUserInfoQuery,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceUserInfoUpdate,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceEvilNotification,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceIdleNotification,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceMigrateGroups,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceMotd,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceSetPrivacyFlags,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceWellKnownUrls,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceNoop,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceClientVersions,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceHostVersions,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceMaxConfigQuery,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceMaxConfigReply,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceStoreConfig,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceConfigQuery,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceConfigReply,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceSetUserInfoFields,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceProbeReq,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceProbeAck,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceBartReply,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceBartQuery2,
-						},
-						{
-							FoodGroup: wire.OService,
-							SubGroup:  wire.OServiceBartReply2,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateErr,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateRightsQuery,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateRightsReply,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateSetInfo,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateUserInfoQuery,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateUserInfoReply,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateWatcherSubRequest,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateWatcherNotification,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateSetDirInfo,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateSetDirReply,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateGetDirInfo,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateGetDirReply,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateGroupCapabilityQuery,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateGroupCapabilityReply,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateSetKeywordInfo,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateSetKeywordReply,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateGetKeywordInfo,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateGetKeywordReply,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateFindListByEmail,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateFindListReply,
-						},
-						{
-							FoodGroup: wire.Locate,
-							SubGroup:  wire.LocateUserInfoQuery2,
-						},
-
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyErr,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyRightsQuery,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyRightsReply,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyAddBuddies,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyDelBuddies,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyWatcherListQuery,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyWatcherListResponse,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyWatcherSubRequest,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyWatcherNotification,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyRejectNotification,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyArrived,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyDeparted,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyAddTempBuddies,
-						},
-						{
-							FoodGroup: wire.Buddy,
-							SubGroup:  wire.BuddyDelTempBuddies,
-						},
-
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMErr,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMAddParameters,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMDelParameters,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMParameterQuery,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMParameterReply,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMChannelMsgToHost,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMChannelMsgToClient,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMEvilRequest,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMEvilReply,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMMissedCalls,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMClientErr,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMHostAck,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMSinStored,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMSinListQuery,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMSinListReply,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMSinRetrieve,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMSinDelete,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMNotifyRequest,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMNotifyReply,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMClientEvent,
-						},
-						{
-							FoodGroup: wire.ICBM,
-							SubGroup:  wire.ICBMSinReply,
-						},
-						{
-							FoodGroup: wire.ChatNav,
-							SubGroup:  wire.ChatNavErr,
-						},
-						{
-							FoodGroup: wire.ChatNav,
-							SubGroup:  wire.ChatNavRequestChatRights,
-						},
-						{
-							FoodGroup: wire.ChatNav,
-							SubGroup:  wire.ChatNavRequestExchangeInfo,
-						},
-						{
-							FoodGroup: wire.ChatNav,
-							SubGroup:  wire.ChatNavRequestRoomInfo,
-						},
-						{
-							FoodGroup: wire.ChatNav,
-							SubGroup:  wire.ChatNavRequestMoreRoomInfo,
-						},
-						{
-							FoodGroup: wire.ChatNav,
-							SubGroup:  wire.ChatNavRequestOccupantList,
-						},
-						{
-							FoodGroup: wire.ChatNav,
-							SubGroup:  wire.ChatNavSearchForRoom,
-						},
-						{
-							FoodGroup: wire.ChatNav,
-							SubGroup:  wire.ChatNavCreateRoom,
-						},
-						{
-							FoodGroup: wire.ChatNav,
-							SubGroup:  wire.ChatNavNavInfo,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatErr,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatRoomInfoUpdate,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatUsersJoined,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatUsersLeft,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatChannelMsgToHost,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatChannelMsgToClient,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatEvilRequest,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatEvilReply,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatClientErr,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatPauseRoomReq,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatPauseRoomAck,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatResumeRoom,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatShowMyRow,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatShowRowByUsername,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatShowRowByNumber,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatShowRowByName,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatRowInfo,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatListRows,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatRowListInfo,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatMoreRows,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatMoveToRow,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatToggleChat,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatSendQuestion,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatSendComment,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatTallyVote,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatAcceptBid,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatSendInvite,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatDeclineInvite,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatAcceptInvite,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatNotifyMessage,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatGotoRow,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatStageUserJoin,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatStageUserLeft,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatUnnamedSnac22,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatClose,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatUserBan,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatUserUnban,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatJoined,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatUnnamedSnac27,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatUnnamedSnac28,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatUnnamedSnac29,
-						},
-						{
-							FoodGroup: wire.Chat,
-							SubGroup:  wire.ChatRoomInfoOwner,
-						},
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceClientOnline,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceHostOnline,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceServiceRequest,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceServiceResponse,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceRateParamsQuery,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceRateParamsReply,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceRateParamsSubAdd,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceRateDelParamSub,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceRateParamChange,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServicePauseReq,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServicePauseAck,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceResume,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceUserInfoQuery,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceUserInfoUpdate,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceEvilNotification,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceIdleNotification,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceMigrateGroups,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceMotd,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceSetPrivacyFlags,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceWellKnownUrls,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceNoop,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceClientVersions,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceHostVersions,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceMaxConfigQuery,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceMaxConfigReply,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceStoreConfig,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceConfigQuery,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceConfigReply,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceSetUserInfoFields,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceProbeReq,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceProbeAck,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceBartReply,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceBartQuery2,
+				},
+				{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceBartReply2,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateErr,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateRightsQuery,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateRightsReply,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateSetInfo,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateUserInfoQuery,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateUserInfoReply,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateWatcherSubRequest,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateWatcherNotification,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateSetDirInfo,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateSetDirReply,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateGetDirInfo,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateGetDirReply,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateGroupCapabilityQuery,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateGroupCapabilityReply,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateSetKeywordInfo,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateSetKeywordReply,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateGetKeywordInfo,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateGetKeywordReply,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateFindListByEmail,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateFindListReply,
+				},
+				{
+					FoodGroup: wire.Locate,
+					SubGroup:  wire.LocateUserInfoQuery2,
+				},
+
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyErr,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyRightsQuery,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyRightsReply,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyAddBuddies,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyDelBuddies,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyWatcherListQuery,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyWatcherListResponse,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyWatcherSubRequest,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyWatcherNotification,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyRejectNotification,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyArrived,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyDeparted,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyAddTempBuddies,
+				},
+				{
+					FoodGroup: wire.Buddy,
+					SubGroup:  wire.BuddyDelTempBuddies,
+				},
+
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMErr,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMAddParameters,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMDelParameters,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMParameterQuery,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMParameterReply,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMChannelMsgToHost,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMChannelMsgToClient,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMEvilRequest,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMEvilReply,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMMissedCalls,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMClientErr,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMHostAck,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMSinStored,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMSinListQuery,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMSinListReply,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMSinRetrieve,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMSinDelete,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMNotifyRequest,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMNotifyReply,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMClientEvent,
+				},
+				{
+					FoodGroup: wire.ICBM,
+					SubGroup:  wire.ICBMSinReply,
+				},
+				{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavErr,
+				},
+				{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavRequestChatRights,
+				},
+				{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavRequestExchangeInfo,
+				},
+				{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavRequestRoomInfo,
+				},
+				{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavRequestMoreRoomInfo,
+				},
+				{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavRequestOccupantList,
+				},
+				{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavSearchForRoom,
+				},
+				{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavCreateRoom,
+				},
+				{
+					FoodGroup: wire.ChatNav,
+					SubGroup:  wire.ChatNavNavInfo,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatErr,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatRoomInfoUpdate,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatUsersJoined,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatUsersLeft,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatChannelMsgToHost,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatChannelMsgToClient,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatEvilRequest,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatEvilReply,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatClientErr,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatPauseRoomReq,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatPauseRoomAck,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatResumeRoom,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatShowMyRow,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatShowRowByUsername,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatShowRowByNumber,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatShowRowByName,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatRowInfo,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatListRows,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatRowListInfo,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatMoreRows,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatMoveToRow,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatToggleChat,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatSendQuestion,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatSendComment,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatTallyVote,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatAcceptBid,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatSendInvite,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatDeclineInvite,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatAcceptInvite,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatNotifyMessage,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatGotoRow,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatStageUserJoin,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatStageUserLeft,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatUnnamedSnac22,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatClose,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatUserBan,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatUserUnban,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatJoined,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatUnnamedSnac27,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatUnnamedSnac28,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatUnnamedSnac29,
+				},
+				{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatRoomInfoOwner,
+				},
+
+				{
+					FoodGroup: wire.BART,
+					SubGroup:  wire.BARTErr,
+				},
+				{
+					FoodGroup: wire.BART,
+					SubGroup:  wire.BARTUploadQuery,
+				},
+				{
+					FoodGroup: wire.BART,
+					SubGroup:  wire.BARTUploadReply,
+				},
+				{
+					FoodGroup: wire.BART,
+					SubGroup:  wire.BARTDownloadQuery,
+				},
+				{
+					FoodGroup: wire.BART,
+					SubGroup:  wire.BARTDownloadReply,
+				},
+				{
+					FoodGroup: wire.BART,
+					SubGroup:  wire.BARTDownload2Query,
+				},
+				{
+					FoodGroup: wire.BART,
+					SubGroup:  wire.BARTDownload2Reply,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagErr,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRightsQuery,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRightsReply,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagQuery,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagQueryIfModified,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagReply,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagUse,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagInsertItem,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagUpdateItem,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagDeleteItem,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagInsertClass,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagUpdateClass,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagDeleteClass,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagStatus,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagReplyNotModified,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagDeleteUser,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagStartCluster,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagEndCluster,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagAuthorizeBuddy,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagPreAuthorizeBuddy,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagPreAuthorizedBuddy,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRemoveMe,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRemoveMe2,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRequestAuthorizeToHost,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRequestAuthorizeToClient,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRespondAuthorizeToHost,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRespondAuthorizeToClient,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagBuddyAdded,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRequestAuthorizeToBadog,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRespondAuthorizeToBadog,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagBuddyAddedToBadog,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagTestSnac,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagForwardMsg,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagIsAuthRequiredQuery,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagIsAuthRequiredReply,
+				},
+				{
+					FoodGroup: wire.Feedbag,
+					SubGroup:  wire.FeedbagRecentBuddyUpdate,
+				},
+				{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPErr,
+				},
+				{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPLoginRequest,
+				},
+				{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPLoginResponse,
+				},
+				{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPRegisterRequest,
+				},
+				{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPChallengeRequest,
+				},
+				{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPChallengeResponse,
+				},
+				{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPAsasnRequest,
+				},
+				{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPSecuridRequest,
+				},
+				{
+					FoodGroup: wire.BUCP,
+					SubGroup:  wire.BUCPRegistrationImageRequest,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertErr,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertSetAlertRequest,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertSetAlertReply,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertGetSubsRequest,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertGetSubsResponse,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertNotifyCapabilities,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertNotify,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertGetRuleRequest,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertGetRuleReply,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertGetFeedRequest,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertGetFeedReply,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertRefreshFeed,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertEvent,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertQogSnac,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertRefreshFeedStock,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertNotifyTransport,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertSetAlertRequestV2,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertSetAlertReplyV2,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertTransitReply,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertNotifyAck,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertNotifyDisplayCapabilities,
+				},
+				{
+					FoodGroup: wire.Alert,
+					SubGroup:  wire.AlertUserOnline,
+				},
+				{
+					FoodGroup: wire.ICQ,
+					SubGroup:  wire.ICQErr,
+				},
+				{
+					FoodGroup: wire.ICQ,
+					SubGroup:  wire.ICQDBQuery,
+				},
+				{
+					FoodGroup: wire.ICQ,
+					SubGroup:  wire.ICQDBReply,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenyErr,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenyRightsQuery,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenyRightsReply,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenySetGroupPermitMask,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenyAddPermListEntries,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenyDelPermListEntries,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenyAddDenyListEntries,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenyDelDenyListEntries,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenyBosErr,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenyAddTempPermitListEntries,
+				},
+				{
+					FoodGroup: wire.PermitDeny,
+					SubGroup:  wire.PermitDenyDelTempPermitListEntries,
+				},
+				{
+					FoodGroup: wire.ODir,
+					SubGroup:  wire.ODirErr,
+				},
+				{
+					FoodGroup: wire.ODir,
+					SubGroup:  wire.ODirInfoQuery,
+				},
+				{
+					FoodGroup: wire.ODir,
+					SubGroup:  wire.ODirInfoReply,
+				},
+				{
+					FoodGroup: wire.ODir,
+					SubGroup:  wire.ODirKeywordListQuery,
+				},
+				{
+					FoodGroup: wire.ODir,
+					SubGroup:  wire.ODirKeywordListReply,
+				},
+				{
+					FoodGroup: wire.UserLookup,
+					SubGroup:  wire.UserLookupFindByEmail,
+				},
+			},
+		},
+	}
 
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// config is the application config
+		cfg config.Config
+		// userSession is the session of the user requesting the chat service
+		// info
+		userSession *state.Session
+		// inputSNAC is the SNAC sent by the sender client
+		inputSNAC wire.SNACMessage
+		// expectSNACFrame is the SNAC frame sent from the server to the recipient
+		// client
+		expectOutput wire.SNACMessage
+		// expectErr is the expected error returned by the router
+		expectErr error
+	}{
+		{
+			name:        "get rate limits for non-AIM 1.x client",
+			userSession: newTestSession("user_screen_name"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{RequestID: 1234},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceRateParamsReply,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x01_0x07_OServiceRateParamsReply{
+					RateClasses: []struct {
+						ID              uint16
+						WindowSize      uint32
+						ClearLevel      uint32
+						AlertLevel      uint32
+						LimitLevel      uint32
+						DisconnectLevel uint32
+						CurrentLevel    uint32
+						MaxLevel        uint32
+						V2Params        *struct {
+							LastTime     uint32
+							CurrentState uint8
+						} `oscar:"optional"`
+					}{
 						{
-							FoodGroup: wire.BART,
-							SubGroup:  wire.BARTErr,
-						},
-						{
-							FoodGroup: wire.BART,
-							SubGroup:  wire.BARTUploadQuery,
-						},
-						{
-							FoodGroup: wire.BART,
-							SubGroup:  wire.BARTUploadReply,
-						},
-						{
-							FoodGroup: wire.BART,
-							SubGroup:  wire.BARTDownloadQuery,
-						},
-						{
-							FoodGroup: wire.BART,
-							SubGroup:  wire.BARTDownloadReply,
-						},
-						{
-							FoodGroup: wire.BART,
-							SubGroup:  wire.BARTDownload2Query,
-						},
-						{
-							FoodGroup: wire.BART,
-							SubGroup:  wire.BARTDownload2Reply,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagErr,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRightsQuery,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRightsReply,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagQuery,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagQueryIfModified,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagReply,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagUse,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagInsertItem,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagUpdateItem,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagDeleteItem,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagInsertClass,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagUpdateClass,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagDeleteClass,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagStatus,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagReplyNotModified,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagDeleteUser,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagStartCluster,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagEndCluster,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagAuthorizeBuddy,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagPreAuthorizeBuddy,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagPreAuthorizedBuddy,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRemoveMe,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRemoveMe2,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRequestAuthorizeToHost,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRequestAuthorizeToClient,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRespondAuthorizeToHost,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRespondAuthorizeToClient,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagBuddyAdded,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRequestAuthorizeToBadog,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRespondAuthorizeToBadog,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagBuddyAddedToBadog,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagTestSnac,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagForwardMsg,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagIsAuthRequiredQuery,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagIsAuthRequiredReply,
-						},
-						{
-							FoodGroup: wire.Feedbag,
-							SubGroup:  wire.FeedbagRecentBuddyUpdate,
-						},
-						{
-							FoodGroup: wire.BUCP,
-							SubGroup:  wire.BUCPErr,
-						},
-						{
-							FoodGroup: wire.BUCP,
-							SubGroup:  wire.BUCPLoginRequest,
-						},
-						{
-							FoodGroup: wire.BUCP,
-							SubGroup:  wire.BUCPLoginResponse,
-						},
-						{
-							FoodGroup: wire.BUCP,
-							SubGroup:  wire.BUCPRegisterRequest,
-						},
-						{
-							FoodGroup: wire.BUCP,
-							SubGroup:  wire.BUCPChallengeRequest,
-						},
-						{
-							FoodGroup: wire.BUCP,
-							SubGroup:  wire.BUCPChallengeResponse,
-						},
-						{
-							FoodGroup: wire.BUCP,
-							SubGroup:  wire.BUCPAsasnRequest,
-						},
-						{
-							FoodGroup: wire.BUCP,
-							SubGroup:  wire.BUCPSecuridRequest,
-						},
-						{
-							FoodGroup: wire.BUCP,
-							SubGroup:  wire.BUCPRegistrationImageRequest,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertErr,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertSetAlertRequest,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertSetAlertReply,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertGetSubsRequest,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertGetSubsResponse,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertNotifyCapabilities,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertNotify,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertGetRuleRequest,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertGetRuleReply,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertGetFeedRequest,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertGetFeedReply,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertRefreshFeed,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertEvent,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertQogSnac,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertRefreshFeedStock,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertNotifyTransport,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertSetAlertRequestV2,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertSetAlertReplyV2,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertTransitReply,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertNotifyAck,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertNotifyDisplayCapabilities,
-						},
-						{
-							FoodGroup: wire.Alert,
-							SubGroup:  wire.AlertUserOnline,
-						},
-						{
-							FoodGroup: wire.ICQ,
-							SubGroup:  wire.ICQErr,
-						},
-						{
-							FoodGroup: wire.ICQ,
-							SubGroup:  wire.ICQDBQuery,
-						},
-						{
-							FoodGroup: wire.ICQ,
-							SubGroup:  wire.ICQDBReply,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenyErr,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenyRightsQuery,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenyRightsReply,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenySetGroupPermitMask,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenyAddPermListEntries,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenyDelPermListEntries,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenyAddDenyListEntries,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenyDelDenyListEntries,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenyBosErr,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenyAddTempPermitListEntries,
-						},
-						{
-							FoodGroup: wire.PermitDeny,
-							SubGroup:  wire.PermitDenyDelTempPermitListEntries,
-						},
-						{
-							FoodGroup: wire.ODir,
-							SubGroup:  wire.ODirErr,
-						},
-						{
-							FoodGroup: wire.ODir,
-							SubGroup:  wire.ODirInfoQuery,
-						},
-						{
-							FoodGroup: wire.ODir,
-							SubGroup:  wire.ODirInfoReply,
-						},
-						{
-							FoodGroup: wire.ODir,
-							SubGroup:  wire.ODirKeywordListQuery,
-						},
-						{
-							FoodGroup: wire.ODir,
-							SubGroup:  wire.ODirKeywordListReply,
+							ID:              0x0001,
+							WindowSize:      0x00000050,
+							ClearLevel:      0x000009C4,
+							AlertLevel:      0x000007D0,
+							LimitLevel:      0x000005DC,
+							DisconnectLevel: 0x00000320,
+							CurrentLevel:    0x00000D69,
+							MaxLevel:        0x00001770,
+							V2Params: &struct {
+								LastTime     uint32
+								CurrentState uint8
+							}{
+								LastTime:     0x00000000,
+								CurrentState: 0x00,
+							},
 						},
+					},
+					RateGroups: expectRateGroups,
+				},
+			},
+		},
+		{
+			name:        "get rate limits for AIM 1.x client",
+			userSession: newTestSession("user_screen_name", sessClientID("AOL Instant Messenger (TM), version 1.")),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{RequestID: 1234},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceRateParamsReply,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x01_0x07_OServiceRateParamsReply{
+					RateClasses: []struct {
+						ID              uint16
+						WindowSize      uint32
+						ClearLevel      uint32
+						AlertLevel      uint32
+						LimitLevel      uint32
+						DisconnectLevel uint32
+						CurrentLevel    uint32
+						MaxLevel        uint32
+						V2Params        *struct {
+							LastTime     uint32
+							CurrentState uint8
+						} `oscar:"optional"`
+					}{
 						{
-							FoodGroup: wire.UserLookup,
-							SubGroup:  wire.UserLookupFindByEmail,
+							ID:              0x0001,
+							WindowSize:      0x00000050,
+							ClearLevel:      0x000009C4,
+							AlertLevel:      0x000007D0,
+							LimitLevel:      0x000005DC,
+							DisconnectLevel: 0x00000320,
+							CurrentLevel:    0x00000D69,
+							MaxLevel:        0x00001770,
 						},
 					},
+					RateGroups: expectRateGroups,
 				},
 			},
 		},
 	}
 
-	assert.Equal(t, want, have)
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			svc := OServiceService{
+				cfg:    config.Config{},
+				logger: slog.Default(),
+			}
+			have := svc.RateParamsQuery(nil, tc.userSession, tc.inputSNAC.Frame)
+			assert.Equal(t, tc.expectOutput, have)
+		})
+	}
 }
 
 func TestOServiceServiceForBOS_OServiceHostOnline(t *testing.T) {

+ 7 - 0
foodgroup/test_helpers.go

@@ -738,6 +738,13 @@ func sessOptUIN(UIN uint32) func(session *state.Session) {
 	}
 }
 
+// sessClientID sets the client ID
+func sessClientID(clientID string) func(session *state.Session) {
+	return func(session *state.Session) {
+		session.SetClientID(clientID)
+	}
+}
+
 // newTestSession creates a session object with 0 or more functional options
 // applied
 func newTestSession(screenName state.DisplayScreenName, options ...func(session *state.Session)) *state.Session {

+ 11 - 10
server/oscar/handler/mock_oservice_test.go

@@ -213,17 +213,17 @@ func (_c *mockOServiceService_IdleNotification_Call) RunAndReturn(run func(conte
 	return _c
 }
 
-// RateParamsQuery provides a mock function with given fields: ctx, frame
-func (_m *mockOServiceService) RateParamsQuery(ctx context.Context, frame wire.SNACFrame) wire.SNACMessage {
-	ret := _m.Called(ctx, frame)
+// RateParamsQuery provides a mock function with given fields: ctx, sess, frame
+func (_m *mockOServiceService) RateParamsQuery(ctx context.Context, sess *state.Session, frame wire.SNACFrame) wire.SNACMessage {
+	ret := _m.Called(ctx, sess, frame)
 
 	if len(ret) == 0 {
 		panic("no return value specified for RateParamsQuery")
 	}
 
 	var r0 wire.SNACMessage
-	if rf, ok := ret.Get(0).(func(context.Context, wire.SNACFrame) wire.SNACMessage); ok {
-		r0 = rf(ctx, frame)
+	if rf, ok := ret.Get(0).(func(context.Context, *state.Session, wire.SNACFrame) wire.SNACMessage); ok {
+		r0 = rf(ctx, sess, frame)
 	} else {
 		r0 = ret.Get(0).(wire.SNACMessage)
 	}
@@ -238,14 +238,15 @@ type mockOServiceService_RateParamsQuery_Call struct {
 
 // RateParamsQuery is a helper method to define mock.On call
 //   - ctx context.Context
+//   - sess *state.Session
 //   - frame wire.SNACFrame
-func (_e *mockOServiceService_Expecter) RateParamsQuery(ctx interface{}, frame interface{}) *mockOServiceService_RateParamsQuery_Call {
-	return &mockOServiceService_RateParamsQuery_Call{Call: _e.mock.On("RateParamsQuery", ctx, frame)}
+func (_e *mockOServiceService_Expecter) RateParamsQuery(ctx interface{}, sess interface{}, frame interface{}) *mockOServiceService_RateParamsQuery_Call {
+	return &mockOServiceService_RateParamsQuery_Call{Call: _e.mock.On("RateParamsQuery", ctx, sess, frame)}
 }
 
-func (_c *mockOServiceService_RateParamsQuery_Call) Run(run func(ctx context.Context, frame wire.SNACFrame)) *mockOServiceService_RateParamsQuery_Call {
+func (_c *mockOServiceService_RateParamsQuery_Call) Run(run func(ctx context.Context, sess *state.Session, frame wire.SNACFrame)) *mockOServiceService_RateParamsQuery_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(wire.SNACFrame))
+		run(args[0].(context.Context), args[1].(*state.Session), args[2].(wire.SNACFrame))
 	})
 	return _c
 }
@@ -255,7 +256,7 @@ func (_c *mockOServiceService_RateParamsQuery_Call) Return(_a0 wire.SNACMessage)
 	return _c
 }
 
-func (_c *mockOServiceService_RateParamsQuery_Call) RunAndReturn(run func(context.Context, wire.SNACFrame) wire.SNACMessage) *mockOServiceService_RateParamsQuery_Call {
+func (_c *mockOServiceService_RateParamsQuery_Call) RunAndReturn(run func(context.Context, *state.Session, wire.SNACFrame) wire.SNACMessage) *mockOServiceService_RateParamsQuery_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 3 - 3
server/oscar/handler/oservice.go

@@ -17,7 +17,7 @@ type OServiceService interface {
 	ClientVersions(ctx context.Context, frame wire.SNACFrame, bodyIn wire.SNAC_0x01_0x17_OServiceClientVersions) wire.SNACMessage
 	HostOnline() wire.SNACMessage
 	IdleNotification(ctx context.Context, sess *state.Session, bodyIn wire.SNAC_0x01_0x11_OServiceIdleNotification) error
-	RateParamsQuery(ctx context.Context, frame wire.SNACFrame) wire.SNACMessage
+	RateParamsQuery(ctx context.Context, sess *state.Session, frame wire.SNACFrame) wire.SNACMessage
 	RateParamsSubAdd(context.Context, wire.SNAC_0x01_0x08_OServiceRateParamsSubAdd)
 	ServiceRequest(ctx context.Context, sess *state.Session, frame wire.SNACFrame, bodyIn wire.SNAC_0x01_0x04_OServiceServiceRequest) (wire.SNACMessage, error)
 	SetPrivacyFlags(ctx context.Context, bodyIn wire.SNAC_0x01_0x14_OServiceSetPrivacyFlags)
@@ -39,8 +39,8 @@ type OServiceHandler struct {
 	middleware.RouteLogger
 }
 
-func (h OServiceHandler) RateParamsQuery(ctx context.Context, _ *state.Session, inFrame wire.SNACFrame, _ io.Reader, rw oscar.ResponseWriter) error {
-	outSNAC := h.OServiceService.RateParamsQuery(ctx, inFrame)
+func (h OServiceHandler) RateParamsQuery(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, _ io.Reader, rw oscar.ResponseWriter) error {
+	outSNAC := h.OServiceService.RateParamsQuery(ctx, sess, inFrame)
 	h.LogRequestAndResponse(ctx, inFrame, nil, outSNAC.Frame, outSNAC.Body)
 	return rw.SendSNAC(outSNAC.Frame, outSNAC.Body)
 }

+ 1 - 1
server/oscar/handler/oservice_test.go

@@ -232,7 +232,7 @@ func TestOServiceHandler_RateParamsQuery(t *testing.T) {
 
 	svc := newMockOServiceService(t)
 	svc.EXPECT().
-		RateParamsQuery(mock.Anything, input.Frame).
+		RateParamsQuery(mock.Anything, mock.Anything, input.Frame).
 		Return(output)
 
 	h := OServiceHandler{

+ 15 - 0
state/session.go

@@ -42,6 +42,7 @@ type Session struct {
 	warning           uint16
 	userInfoBitmask   uint16
 	userStatusBitmask uint32
+	clientID          string
 }
 
 // NewSession returns a new instance of Session. By default, the user may have
@@ -348,3 +349,17 @@ func (s *Session) Close() {
 func (s *Session) Closed() <-chan struct{} {
 	return s.stopCh
 }
+
+// SetClientID sets the client ID.
+func (s *Session) SetClientID(clientID string) {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+	s.clientID = clientID
+}
+
+// ClientID retrieves the client ID.
+func (s *Session) ClientID() string {
+	s.mutex.RLock()
+	defer s.mutex.RUnlock()
+	return s.clientID
+}

+ 8 - 0
state/session_test.go

@@ -58,6 +58,14 @@ func TestSession_SetAndGetUIN(t *testing.T) {
 	assert.Equal(t, uin, s.UIN())
 }
 
+func TestSession_SetAndGetClientID(t *testing.T) {
+	s := NewSession()
+	assert.Empty(t, s.ClientID())
+	clientID := "AIM Client ID"
+	s.SetClientID(clientID)
+	assert.Equal(t, clientID, s.ClientID())
+}
+
 func TestSession_TLVUserInfo(t *testing.T) {
 	tests := []struct {
 		name           string

+ 4 - 2
wire/snacs.go

@@ -250,8 +250,10 @@ type SNAC_0x01_0x07_OServiceRateParamsReply struct {
 		DisconnectLevel uint32
 		CurrentLevel    uint32
 		MaxLevel        uint32
-		LastTime        uint32
-		CurrentState    uint8
+		V2Params        *struct {
+			LastTime     uint32
+			CurrentState uint8
+		} `oscar:"optional"`
 	} `oscar:"count_prefix=uint16"`
 	RateGroups []struct {
 		ID    uint16