Преглед изворни кода

integrate ICQ into session mangagement

Mike пре 2 година
родитељ
комит
f5202c9555
7 измењених фајлова са 401 додато и 154 уклоњено
  1. 13 2
      foodgroup/auth.go
  2. 266 122
      foodgroup/auth_test.go
  3. 29 21
      foodgroup/oservice_test.go
  4. 24 9
      foodgroup/test_helpers.go
  5. 21 0
      state/session.go
  6. 32 0
      state/session_test.go
  7. 16 0
      wire/snacs.go

+ 13 - 2
foodgroup/auth.go

@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"strconv"
 	"strings"
 
 	"github.com/mk6i/retro-aim-server/config"
@@ -81,7 +82,7 @@ type bosCookie struct {
 	ScreenName state.DisplayScreenName `oscar:"len_prefix=uint8"`
 }
 
-// RegisterBOSSession creates and returns a user's session.
+// RegisterBOSSession adds a new session to the session registry.
 func (s AuthService) RegisterBOSSession(authCookie []byte) (*state.Session, error) {
 	buf, err := s.cookieBaker.Crack(authCookie)
 	if err != nil {
@@ -109,6 +110,16 @@ func (s AuthService) RegisterBOSSession(authCookie []byte) (*state.Session, erro
 		sess.SetUserInfoFlag(wire.OServiceUserFlagUnconfirmed)
 	}
 
+	if c.ICQ == 1 {
+		sess.SetUserInfoFlag(wire.OServiceUserFlagICQ)
+
+		uin, err := strconv.Atoi(u.IdentScreenName.String())
+		if err != nil {
+			return nil, fmt.Errorf("error converting username to UIN: %w", err)
+		}
+		sess.SetUIN(uint32(uin))
+	}
+
 	return sess, nil
 }
 
@@ -322,7 +333,7 @@ func (s AuthService) loginSuccessResponse(screenName string, isICQ bool, err err
 	}
 	cookie, err := s.cookieBaker.Issue(buf.Bytes())
 	if err != nil {
-		return wire.TLVRestBlock{}, fmt.Errorf("failed to make auth cookie: %w", err)
+		return wire.TLVRestBlock{}, fmt.Errorf("failed to issue auth cookie: %w", err)
 	}
 
 	return wire.TLVRestBlock{

+ 266 - 122
foodgroup/auth_test.go

@@ -2,6 +2,7 @@ package foodgroup
 
 import (
 	"bytes"
+	"fmt"
 	"io"
 	"testing"
 
@@ -62,17 +63,19 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 						},
 					},
 				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data: func() []byte {
-							loginCookie := bosCookie{
-								ScreenName: user.DisplayScreenName,
-							}
-							buf := &bytes.Buffer{}
-							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
-							return buf.Bytes()
-						}(),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn: func() []byte {
+								loginCookie := bosCookie{
+									ScreenName: user.DisplayScreenName,
+								}
+								buf := &bytes.Buffer{}
+								assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+								return buf.Bytes()
+							}(),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -116,18 +119,20 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 						},
 					},
 				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data: func() []byte {
-							loginCookie := bosCookie{
-								ScreenName: user.DisplayScreenName,
-								ICQ:        1,
-							}
-							buf := &bytes.Buffer{}
-							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
-							return buf.Bytes()
-						}(),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn: func() []byte {
+								loginCookie := bosCookie{
+									ScreenName: user.DisplayScreenName,
+									ICQ:        1,
+								}
+								buf := &bytes.Buffer{}
+								assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+								return buf.Bytes()
+							}(),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -294,17 +299,19 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 						},
 					},
 				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data: func() []byte {
-							loginCookie := bosCookie{
-								ScreenName: user.DisplayScreenName,
-							}
-							buf := &bytes.Buffer{}
-							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
-							return buf.Bytes()
-						}(),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn: func() []byte {
+								loginCookie := bosCookie{
+									ScreenName: user.DisplayScreenName,
+								}
+								buf := &bytes.Buffer{}
+								assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+								return buf.Bytes()
+							}(),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -351,17 +358,19 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 						},
 					},
 				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data: func() []byte {
-							loginCookie := bosCookie{
-								ScreenName: user.DisplayScreenName,
-							}
-							buf := &bytes.Buffer{}
-							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
-							return buf.Bytes()
-						}(),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn: func() []byte {
+								loginCookie := bosCookie{
+									ScreenName: user.DisplayScreenName,
+								}
+								buf := &bytes.Buffer{}
+								assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+								return buf.Bytes()
+							}(),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -423,10 +432,10 @@ func TestAuthService_BUCPLoginRequest(t *testing.T) {
 			}
 			sessionManager := newMockSessionManager(t)
 			cookieBaker := newMockCookieBaker(t)
-			for _, params := range tc.mockParams.cookieIssuerParams {
+			for _, params := range tc.mockParams.cookieIssueParams {
 				cookieBaker.EXPECT().
-					Issue(params.data).
-					Return(params.cookie, params.err)
+					Issue(params.dataIn).
+					Return(params.cookieOut, params.err)
 			}
 
 			svc := AuthService{
@@ -493,17 +502,19 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 						},
 					},
 				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data: func() []byte {
-							loginCookie := bosCookie{
-								ScreenName: user.DisplayScreenName,
-							}
-							buf := &bytes.Buffer{}
-							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
-							return buf.Bytes()
-						}(),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn: func() []byte {
+								loginCookie := bosCookie{
+									ScreenName: user.DisplayScreenName,
+								}
+								buf := &bytes.Buffer{}
+								assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+								return buf.Bytes()
+							}(),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -539,18 +550,20 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 						},
 					},
 				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data: func() []byte {
-							loginCookie := bosCookie{
-								ScreenName: user.DisplayScreenName,
-								ICQ:        1,
-							}
-							buf := &bytes.Buffer{}
-							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
-							return buf.Bytes()
-						}(),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn: func() []byte {
+								loginCookie := bosCookie{
+									ScreenName: user.DisplayScreenName,
+									ICQ:        1,
+								}
+								buf := &bytes.Buffer{}
+								assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+								return buf.Bytes()
+							}(),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -685,17 +698,19 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 						},
 					},
 				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data: func() []byte {
-							loginCookie := bosCookie{
-								ScreenName: user.DisplayScreenName,
-							}
-							buf := &bytes.Buffer{}
-							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
-							return buf.Bytes()
-						}(),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn: func() []byte {
+								loginCookie := bosCookie{
+									ScreenName: user.DisplayScreenName,
+								}
+								buf := &bytes.Buffer{}
+								assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+								return buf.Bytes()
+							}(),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -734,17 +749,19 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 						},
 					},
 				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data: func() []byte {
-							loginCookie := bosCookie{
-								ScreenName: user.DisplayScreenName,
-							}
-							buf := &bytes.Buffer{}
-							assert.NoError(t, wire.MarshalBE(loginCookie, buf))
-							return buf.Bytes()
-						}(),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn: func() []byte {
+								loginCookie := bosCookie{
+									ScreenName: user.DisplayScreenName,
+								}
+								buf := &bytes.Buffer{}
+								assert.NoError(t, wire.MarshalBE(loginCookie, buf))
+								return buf.Bytes()
+							}(),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -798,10 +815,10 @@ func TestAuthService_FLAPLoginResponse(t *testing.T) {
 			}
 			sessionManager := newMockSessionManager(t)
 			cookieBaker := newMockCookieBaker(t)
-			for _, params := range tc.mockParams.cookieIssuerParams {
+			for _, params := range tc.mockParams.cookieIssueParams {
 				cookieBaker.EXPECT().
-					Issue(params.data).
-					Return(params.cookie, params.err)
+					Issue(params.dataIn).
+					Return(params.cookieOut, params.err)
 			}
 			svc := AuthService{
 				config:         tc.cfg,
@@ -1014,40 +1031,167 @@ func TestAuthService_RegisterChatSession_HappyPath(t *testing.T) {
 	assert.Equal(t, sess, have)
 }
 
-func TestAuthService_RegisterBOSSession_HappyPath(t *testing.T) {
-	sess := newTestSession("screen-name")
+func TestAuthService_RegisterBOSSession(t *testing.T) {
+	screenName := state.DisplayScreenName("UserScreenName")
+	aimAuthCookie := bosCookie{
+		ScreenName: screenName,
+	}
+	buf := &bytes.Buffer{}
+	assert.NoError(t, wire.MarshalBE(aimAuthCookie, buf))
+	aimCookie := buf.Bytes()
 
-	sessionManager := newMockSessionManager(t)
-	sessionManager.EXPECT().
-		AddSession(sess.DisplayScreenName()).
-		Return(sess)
+	uin := state.DisplayScreenName("100003")
+	icqAuthCookie := bosCookie{
+		ICQ:        1,
+		ScreenName: uin,
+	}
+	buf = &bytes.Buffer{}
+	assert.NoError(t, wire.MarshalBE(icqAuthCookie, buf))
+	icqCookie := buf.Bytes()
 
-	authCookie := bosCookie{
-		ScreenName: sess.DisplayScreenName(),
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// cookieOut is the auth cookieOut that contains session information
+		cookie []byte
+		// mockParams is the list of params sent to mocks that satisfy this
+		// method's dependencies
+		mockParams mockParams
+		// wantSess asserts the values of one or more session properties
+		wantSess func(*state.Session) bool
+		// wantErr is the error we expect from the method
+		wantErr error
+	}{
+		{
+			name:   "successfully register an AIM session",
+			cookie: aimCookie,
+			mockParams: mockParams{
+				cookieBakerParams: cookieBakerParams{
+					cookieCrackParams: cookieCrackParams{
+						{
+							dataOut:  aimCookie,
+							cookieIn: aimCookie,
+						},
+					},
+				},
+				sessionManagerParams: sessionManagerParams{
+					addSessionParams: addSessionParams{
+						{
+							screenName: screenName,
+							result:     newTestSession(screenName),
+						},
+					},
+				},
+				userManagerParams: userManagerParams{
+					getUserParams: getUserParams{
+						{
+							screenName: screenName.IdentScreenName(),
+							result: &state.User{
+								IdentScreenName:   screenName.IdentScreenName(),
+								DisplayScreenName: screenName,
+							},
+						},
+					},
+				},
+				accountManagerParams: accountManagerParams{
+					accountManagerConfirmStatusByNameParams: accountManagerConfirmStatusByNameParams{
+						{
+							screenName:    screenName.IdentScreenName(),
+							confirmStatus: true,
+						},
+					},
+				},
+			},
+			wantSess: func(session *state.Session) bool {
+				return true
+			},
+		},
+		{
+			name:   "successfully register an ICQ session",
+			cookie: icqCookie,
+			mockParams: mockParams{
+				cookieBakerParams: cookieBakerParams{
+					cookieCrackParams: cookieCrackParams{
+						{
+							dataOut:  icqCookie,
+							cookieIn: icqCookie,
+						},
+					},
+				},
+				sessionManagerParams: sessionManagerParams{
+					addSessionParams: addSessionParams{
+						{
+							screenName: uin,
+							result:     newTestSession(uin),
+						},
+					},
+				},
+				userManagerParams: userManagerParams{
+					getUserParams: getUserParams{
+						{
+							screenName: uin.IdentScreenName(),
+							result: &state.User{
+								IdentScreenName:   uin.IdentScreenName(),
+								DisplayScreenName: uin,
+							},
+						},
+					},
+				},
+				accountManagerParams: accountManagerParams{
+					accountManagerConfirmStatusByNameParams: accountManagerConfirmStatusByNameParams{
+						{
+							screenName:    uin.IdentScreenName(),
+							confirmStatus: true,
+						},
+					},
+				},
+			},
+			wantSess: func(sess *state.Session) bool {
+				uinMatches := fmt.Sprintf("%d", sess.UIN()) == uin.String()
+				flagsMatch := sess.UserInfoFlags()&wire.OServiceUserFlagICQ == wire.OServiceUserFlagICQ
+				return uinMatches && flagsMatch
+			},
+		},
 	}
-	cookieBuf := &bytes.Buffer{}
-	assert.NoError(t, wire.MarshalBE(authCookie, cookieBuf))
 
-	cookieBaker := newMockCookieBaker(t)
-	cookieBaker.EXPECT().
-		Crack(cookieBuf.Bytes()).
-		Return(cookieBuf.Bytes(), nil)
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			sessionManager := newMockSessionManager(t)
+			for _, params := range tc.mockParams.addSessionParams {
+				sessionManager.EXPECT().
+					AddSession(params.screenName).
+					Return(params.result)
+			}
+			cookieBaker := newMockCookieBaker(t)
+			for _, params := range tc.mockParams.cookieCrackParams {
+				cookieBaker.EXPECT().
+					Crack(params.cookieIn).
+					Return(params.dataOut, nil)
+			}
+			userManager := newMockUserManager(t)
+			for _, params := range tc.mockParams.getUserParams {
+				userManager.EXPECT().
+					User(params.screenName).
+					Return(params.result, nil)
+			}
+			accountManager := newMockAccountManager(t)
+			for _, params := range tc.mockParams.accountManagerConfirmStatusByNameParams {
+				accountManager.EXPECT().
+					ConfirmStatusByName(params.screenName).
+					Return(params.confirmStatus, nil)
+			}
 
-	userManager := newMockUserManager(t)
-	userManager.EXPECT().
-		User(sess.IdentScreenName()).
-		Return(&state.User{DisplayScreenName: sess.DisplayScreenName()}, nil)
+			svc := NewAuthService(config.Config{}, sessionManager, nil, userManager, nil, cookieBaker, nil, nil, nil, accountManager)
 
-	accountManager := newMockAccountManager(t)
-	accountManager.EXPECT().
-		ConfirmStatusByName(sess.IdentScreenName()).
-		Return(true, nil)
+			have, err := svc.RegisterBOSSession(tc.cookie)
+			assert.NoError(t, err)
 
-	svc := NewAuthService(config.Config{}, sessionManager, nil, userManager, nil, cookieBaker, nil, nil, nil, accountManager)
+			if tc.wantSess != nil {
+				assert.True(t, tc.wantSess(have))
+			}
+		})
+	}
 
-	have, err := svc.RegisterBOSSession(cookieBuf.Bytes())
-	assert.NoError(t, err)
-	assert.Equal(t, sess, have)
 }
 
 func TestAuthService_RetrieveBOSSession_HappyPath(t *testing.T) {

+ 29 - 21
foodgroup/oservice_test.go

@@ -94,10 +94,12 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 				},
 			},
 			mockParams: mockParams{
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data:   []byte("user_screen_name"),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn:    []byte("user_screen_name"),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -136,10 +138,12 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 				},
 			},
 			mockParams: mockParams{
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data:   []byte("user_screen_name"),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn:    []byte("user_screen_name"),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -178,10 +182,12 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 				},
 			},
 			mockParams: mockParams{
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data:   []byte("user_screen_name"),
-						cookie: []byte("the-cookie"),
+				cookieBakerParams: cookieBakerParams{
+					cookieIssueParams: cookieIssueParams{
+						{
+							dataIn:    []byte("user_screen_name"),
+							cookieOut: []byte("the-cookie"),
+						},
 					},
 				},
 			},
@@ -238,13 +244,15 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 							},
 						},
 					},
-					cookieIssuerParams: cookieIssuerParams{
-						{
-							data: []byte{
-								0x11, '4', '-', '0', '-', 't', 'h', 'e', '-', 'c', 'h', 'a', 't', '-', 'r', 'o', 'o', 'm',
-								0x10, 'u', 's', 'e', 'r', '_', 's', 'c', 'r', 'e', 'e', 'n', '_', 'n', 'a', 'm', 'e',
+					cookieBakerParams: cookieBakerParams{
+						cookieIssueParams: cookieIssueParams{
+							{
+								dataIn: []byte{
+									0x11, '4', '-', '0', '-', 't', 'h', 'e', '-', 'c', 'h', 'a', 't', '-', 'r', 'o', 'o', 'm',
+									0x10, 'u', 's', 'e', 'r', '_', 's', 'c', 'r', 'e', 'e', 'n', '_', 'n', 'a', 'm', 'e',
+								},
+								cookieOut: []byte("the-auth-cookie"),
 							},
-							cookie: []byte("the-auth-cookie"),
 						},
 					},
 				}
@@ -300,10 +308,10 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 					Return(params.room, params.err)
 			}
 			cookieIssuer := newMockCookieBaker(t)
-			for _, params := range tc.mockParams.cookieIssuerParams {
+			for _, params := range tc.mockParams.cookieIssueParams {
 				cookieIssuer.EXPECT().
-					Issue(params.data).
-					Return(params.cookie, params.err)
+					Issue(params.dataIn).
+					Return(params.cookieOut, params.err)
 			}
 			//
 			// send input SNAC

+ 24 - 9
foodgroup/test_helpers.go

@@ -13,18 +13,18 @@ import (
 // mockParams is a helper struct that centralizes mock function call parameters
 // in one place for a table test
 type mockParams struct {
+	accountManagerParams
 	bartManagerParams
+	buddyBroadcasterParams
 	chatMessageRelayerParams
+	chatRoomRegistryParams
+	cookieBakerParams
 	feedbagManagerParams
 	legacyBuddyListManagerParams
 	messageRelayerParams
 	profileManagerParams
 	sessionManagerParams
 	userManagerParams
-	cookieIssuerParams
-	buddyBroadcasterParams
-	chatRoomRegistryParams
-	accountManagerParams
 }
 
 // bartManagerParams is a helper struct that contains mock parameters for
@@ -291,12 +291,27 @@ type whoAddedUserParams []struct {
 	result         []state.IdentScreenName
 }
 
-// cookieIssuerParams is the list of parameters passed at the mock
+// cookieBakerParams is a helper struct that contains mock parameters for
+// CookieBaker methods
+type cookieBakerParams struct {
+	cookieCrackParams
+	cookieIssueParams
+}
+
+// cookieCrackParams is the list of parameters passed at the mock
+// CookieBaker.Crack call site
+type cookieCrackParams []struct {
+	cookieIn []byte
+	dataOut  []byte
+	err      error
+}
+
+// cookieIssueParams is the list of parameters passed at the mock
 // CookieBaker.Issue call site
-type cookieIssuerParams []struct {
-	data   []byte
-	cookie []byte
-	err    error
+type cookieIssueParams []struct {
+	dataIn    []byte
+	cookieOut []byte
+	err       error
 }
 
 // accountManagerParams is a helper struct that contains mock parameters for

+ 21 - 0
state/session.go

@@ -38,6 +38,7 @@ type Session struct {
 	signonComplete    bool
 	signonTime        time.Time
 	stopCh            chan struct{}
+	uin               uint32
 	warning           uint16
 	userInfoFlags     uint16
 }
@@ -200,6 +201,20 @@ func (s *Session) SetSignonComplete() {
 	s.signonComplete = true
 }
 
+// UIN returns the user's ICQ number.
+func (s *Session) UIN() uint32 {
+	s.mutex.RLock()
+	defer s.mutex.RUnlock()
+	return s.uin
+}
+
+// SetUIN sets the user's ICQ number.
+func (s *Session) SetUIN(uin uint32) {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+	s.uin = uin
+}
+
 // TLVUserInfo returns a TLV list containing session information required by
 // multiple SNAC message types that convey user information.
 func (s *Session) TLVUserInfo() wire.TLVUserInfo {
@@ -235,6 +250,12 @@ func (s *Session) userInfo() wire.TLVList {
 		tlvs.Append(wire.NewTLV(wire.OServiceUserInfoIdleTime, uint16(s.nowFn().Sub(s.idleTime).Minutes())))
 	}
 
+	// ICQ direct-connect info. The TLV is required for buddy arrival events to
+	// work in ICQ, even if the values are set to default.
+	if s.userInfoFlags&wire.OServiceUserFlagICQ == wire.OServiceUserFlagICQ {
+		tlvs.Append(wire.NewTLV(wire.OServiceUserInfoICQDC, wire.ICQDCInfo{}))
+	}
+
 	// capabilities (buddy icon, chat, etc...)
 	if len(s.caps) > 0 {
 		tlvs.Append(wire.NewTLV(wire.OServiceUserInfoOscarCaps, s.caps))

+ 32 - 0
state/session_test.go

@@ -52,6 +52,14 @@ func TestSession_SetAndGetChatRoomCookie(t *testing.T) {
 	assert.Equal(t, sn, s.ChatRoomCookie())
 }
 
+func TestSession_SetAndGetUIN(t *testing.T) {
+	s := NewSession()
+	assert.Empty(t, s.UIN())
+	uin := uint32(100003)
+	s.SetUIN(uin)
+	assert.Equal(t, uin, s.UIN())
+}
+
 func TestSession_TLVUserInfo(t *testing.T) {
 	tests := []struct {
 		name           string
@@ -66,6 +74,7 @@ func TestSession_TLVUserInfo(t *testing.T) {
 				s.SetIdentScreenName(NewIdentScreenName("xXAIMUSERXx"))
 				s.SetDisplayScreenName("xXAIMUSERXx")
 				s.IncrementWarning(10)
+				s.SetUserInfoFlag(wire.OServiceUserFlagOSCARFree)
 				return s
 			},
 			want: wire.TLVUserInfo{
@@ -80,6 +89,29 @@ func TestSession_TLVUserInfo(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "user is on ICQ",
+			givenSessionFn: func() *Session {
+				s := NewSession()
+				s.SetSignonTime(time.Unix(1, 0))
+				s.SetIdentScreenName(NewIdentScreenName("1000003"))
+				s.SetDisplayScreenName("1000003")
+				s.SetUserInfoFlag(wire.OServiceUserFlagICQ)
+
+				return s
+			},
+			want: wire.TLVUserInfo{
+				ScreenName: "1000003",
+				TLVBlock: wire.TLVBlock{
+					TLVList: wire.TLVList{
+						wire.NewTLV(wire.OServiceUserInfoSignonTOD, uint32(1)),
+						wire.NewTLV(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree|wire.OServiceUserFlagICQ),
+						wire.NewTLV(wire.OServiceUserInfoStatus, uint32(0x0000)),
+						wire.NewTLV(wire.OServiceUserInfoICQDC, wire.ICQDCInfo{}),
+					},
+				},
+			},
+		},
 		{
 			name: "user has away message set",
 			givenSessionFn: func() *Session {

+ 16 - 0
wire/snacs.go

@@ -141,6 +141,7 @@ const (
 	OServiceUserInfoSignonTOD  uint16 = 0x03
 	OServiceUserInfoIdleTime   uint16 = 0x04
 	OServiceUserInfoStatus     uint16 = 0x06
+	OServiceUserInfoICQDC      uint16 = 0x0C
 	OServiceUserInfoOscarCaps  uint16 = 0x0D
 	OServiceUserInfoBARTInfo   uint16 = 0x1D
 	OServiceUserInfoUserFlags2 uint16 = 0x1F
@@ -1304,3 +1305,18 @@ type FeedbagItem struct {
 	ClassID uint16
 	TLVLBlock
 }
+
+// ICQDCInfo represents ICQ direct connect settings.
+type ICQDCInfo struct {
+	IP                      uint32
+	Port                    uint32
+	DCType                  uint8
+	ProtoVersion            uint16
+	AuthCookie              uint32
+	WebPort                 uint32
+	ClientFutures           uint32
+	LastUpdateTime          uint32
+	LastExtInfoUpdateTime   uint32
+	LastExtStatusUpdateTime uint32
+	Unknown                 uint16
+}