Mike 4 месяцев назад
Родитель
Сommit
3f08f40ecb

+ 12 - 7
foodgroup/auth.go

@@ -108,7 +108,8 @@ func (s AuthService) CrackCookie(authCookie []byte) (state.ServerCookie, error)
 }
 
 // RegisterBOSSession adds a new session to the session registry.
-func (s AuthService) RegisterBOSSession(ctx context.Context, serverCookie state.ServerCookie) (*state.SessionInstance, error) {
+func (s AuthService) RegisterBOSSession(ctx context.Context, serverCookie state.ServerCookie, conf func(sess *state.Session)) (*state.SessionInstance, error) {
+
 	u, err := s.userManager.User(ctx, serverCookie.ScreenName.IdentScreenName())
 	if err != nil {
 		return nil, fmt.Errorf("failed to retrieve user: %w", err)
@@ -127,7 +128,13 @@ func (s AuthService) RegisterBOSSession(ctx context.Context, serverCookie state.
 		doMultiSess = true
 	}
 
-	sess, err := s.sessionManager.AddSession(ctx, u.DisplayScreenName, doMultiSess)
+	conf2 := func(sess *state.Session) {
+		sess.SetSignonTime(time.Now())
+		sess.SetRateClasses(time.Now(), s.rateLimitClasses)
+		sess.SetMemberSince(time.Now())
+	}
+
+	sess, err := s.sessionManager.AddSession(ctx, u.DisplayScreenName, doMultiSess, conf, conf2)
 	if err != nil {
 		return nil, fmt.Errorf("AddSession: %w", err)
 	}
@@ -144,11 +151,9 @@ func (s AuthService) RegisterBOSSession(ctx context.Context, serverCookie state.
 	}
 
 	sess.SetKerberosAuth(serverCookie.KerberosAuth == 1)
-	sess.Session().SetSignonTime(time.Now())
-	sess.Session().SetRateClasses(time.Now(), s.rateLimitClasses)
+
 	// set string containing OSCAR client name and version
 	sess.SetClientID(serverCookie.ClientID)
-	sess.Session().SetMemberSince(time.Now())
 	sess.Session().SetOfflineMsgCount(u.OfflineMsgCount)
 
 	if _, alreadySet := sess.Session().BuddyIcon(); !alreadySet {
@@ -196,8 +201,8 @@ func (s AuthService) RetrieveBOSSession(ctx context.Context, serverCookie state.
 }
 
 // Signout removes this user's session.
-func (s AuthService) Signout(ctx context.Context, instance *state.SessionInstance) {
-	s.sessionManager.RemoveSession(instance)
+func (s AuthService) Signout(ctx context.Context, session *state.Session) {
+	s.sessionManager.RemoveSession(session)
 }
 
 // SignoutChat removes user from chat room and notifies remaining participants

+ 1 - 1
foodgroup/auth_test.go

@@ -1963,7 +1963,7 @@ func TestAuthService_RegisterBOSSession(t *testing.T) {
 
 			svc := NewAuthService(config.Config{}, sessionRegistry, nil, nil, userManager, nil, nil, accountManager, bartItemManager, wire.DefaultRateLimitClasses(), nil, slog.Default())
 
-			have, err := svc.RegisterBOSSession(context.Background(), tc.cookie)
+			have, err := svc.RegisterBOSSession(context.Background(), tc.cookie, nil)
 			assert.NoError(t, err)
 
 			if tc.wantSess != nil {

+ 40 - 21
foodgroup/mock_session_registry_test.go

@@ -39,8 +39,16 @@ func (_m *mockSessionRegistry) EXPECT() *mockSessionRegistry_Expecter {
 }
 
 // AddSession provides a mock function for the type mockSessionRegistry
-func (_mock *mockSessionRegistry) AddSession(ctx context.Context, screenName state.DisplayScreenName, doMultiSess bool) (*state.SessionInstance, error) {
-	ret := _mock.Called(ctx, screenName, doMultiSess)
+func (_mock *mockSessionRegistry) AddSession(ctx context.Context, screenName state.DisplayScreenName, doMultiSess bool, cfg ...func(sess *state.Session)) (*state.SessionInstance, error) {
+	// func(sess *state.Session)
+	_va := make([]interface{}, len(cfg))
+	for _i := range cfg {
+		_va[_i] = cfg[_i]
+	}
+	var _ca []interface{}
+	_ca = append(_ca, ctx, screenName, doMultiSess)
+	_ca = append(_ca, _va...)
+	ret := _mock.Called(_ca...)
 
 	if len(ret) == 0 {
 		panic("no return value specified for AddSession")
@@ -48,18 +56,18 @@ func (_mock *mockSessionRegistry) AddSession(ctx context.Context, screenName sta
 
 	var r0 *state.SessionInstance
 	var r1 error
-	if returnFunc, ok := ret.Get(0).(func(context.Context, state.DisplayScreenName, bool) (*state.SessionInstance, error)); ok {
-		return returnFunc(ctx, screenName, doMultiSess)
+	if returnFunc, ok := ret.Get(0).(func(context.Context, state.DisplayScreenName, bool, ...func(sess *state.Session)) (*state.SessionInstance, error)); ok {
+		return returnFunc(ctx, screenName, doMultiSess, cfg...)
 	}
-	if returnFunc, ok := ret.Get(0).(func(context.Context, state.DisplayScreenName, bool) *state.SessionInstance); ok {
-		r0 = returnFunc(ctx, screenName, doMultiSess)
+	if returnFunc, ok := ret.Get(0).(func(context.Context, state.DisplayScreenName, bool, ...func(sess *state.Session)) *state.SessionInstance); ok {
+		r0 = returnFunc(ctx, screenName, doMultiSess, cfg...)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*state.SessionInstance)
 		}
 	}
-	if returnFunc, ok := ret.Get(1).(func(context.Context, state.DisplayScreenName, bool) error); ok {
-		r1 = returnFunc(ctx, screenName, doMultiSess)
+	if returnFunc, ok := ret.Get(1).(func(context.Context, state.DisplayScreenName, bool, ...func(sess *state.Session)) error); ok {
+		r1 = returnFunc(ctx, screenName, doMultiSess, cfg...)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -75,11 +83,13 @@ type mockSessionRegistry_AddSession_Call struct {
 //   - ctx context.Context
 //   - screenName state.DisplayScreenName
 //   - doMultiSess bool
-func (_e *mockSessionRegistry_Expecter) AddSession(ctx interface{}, screenName interface{}, doMultiSess interface{}) *mockSessionRegistry_AddSession_Call {
-	return &mockSessionRegistry_AddSession_Call{Call: _e.mock.On("AddSession", ctx, screenName, doMultiSess)}
+//   - cfg ...func(sess *state.Session)
+func (_e *mockSessionRegistry_Expecter) AddSession(ctx interface{}, screenName interface{}, doMultiSess interface{}, cfg ...interface{}) *mockSessionRegistry_AddSession_Call {
+	return &mockSessionRegistry_AddSession_Call{Call: _e.mock.On("AddSession",
+		append([]interface{}{ctx, screenName, doMultiSess}, cfg...)...)}
 }
 
-func (_c *mockSessionRegistry_AddSession_Call) Run(run func(ctx context.Context, screenName state.DisplayScreenName, doMultiSess bool)) *mockSessionRegistry_AddSession_Call {
+func (_c *mockSessionRegistry_AddSession_Call) Run(run func(ctx context.Context, screenName state.DisplayScreenName, doMultiSess bool, cfg ...func(sess *state.Session))) *mockSessionRegistry_AddSession_Call {
 	_c.Call.Run(func(args mock.Arguments) {
 		var arg0 context.Context
 		if args[0] != nil {
@@ -93,10 +103,19 @@ func (_c *mockSessionRegistry_AddSession_Call) Run(run func(ctx context.Context,
 		if args[2] != nil {
 			arg2 = args[2].(bool)
 		}
+		var arg3 []func(sess *state.Session)
+		variadicArgs := make([]func(sess *state.Session), len(args)-3)
+		for i, a := range args[3:] {
+			if a != nil {
+				variadicArgs[i] = a.(func(sess *state.Session))
+			}
+		}
+		arg3 = variadicArgs
 		run(
 			arg0,
 			arg1,
 			arg2,
+			arg3...,
 		)
 	})
 	return _c
@@ -107,14 +126,14 @@ func (_c *mockSessionRegistry_AddSession_Call) Return(sessionInstance *state.Ses
 	return _c
 }
 
-func (_c *mockSessionRegistry_AddSession_Call) RunAndReturn(run func(ctx context.Context, screenName state.DisplayScreenName, doMultiSess bool) (*state.SessionInstance, error)) *mockSessionRegistry_AddSession_Call {
+func (_c *mockSessionRegistry_AddSession_Call) RunAndReturn(run func(ctx context.Context, screenName state.DisplayScreenName, doMultiSess bool, cfg ...func(sess *state.Session)) (*state.SessionInstance, error)) *mockSessionRegistry_AddSession_Call {
 	_c.Call.Return(run)
 	return _c
 }
 
 // RemoveSession provides a mock function for the type mockSessionRegistry
-func (_mock *mockSessionRegistry) RemoveSession(instance *state.SessionInstance) {
-	_mock.Called(instance)
+func (_mock *mockSessionRegistry) RemoveSession(session *state.Session) {
+	_mock.Called(session)
 	return
 }
 
@@ -124,16 +143,16 @@ type mockSessionRegistry_RemoveSession_Call struct {
 }
 
 // RemoveSession is a helper method to define mock.On call
-//   - instance *state.SessionInstance
-func (_e *mockSessionRegistry_Expecter) RemoveSession(instance interface{}) *mockSessionRegistry_RemoveSession_Call {
-	return &mockSessionRegistry_RemoveSession_Call{Call: _e.mock.On("RemoveSession", instance)}
+//   - session *state.Session
+func (_e *mockSessionRegistry_Expecter) RemoveSession(session interface{}) *mockSessionRegistry_RemoveSession_Call {
+	return &mockSessionRegistry_RemoveSession_Call{Call: _e.mock.On("RemoveSession", session)}
 }
 
-func (_c *mockSessionRegistry_RemoveSession_Call) Run(run func(instance *state.SessionInstance)) *mockSessionRegistry_RemoveSession_Call {
+func (_c *mockSessionRegistry_RemoveSession_Call) Run(run func(session *state.Session)) *mockSessionRegistry_RemoveSession_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		var arg0 *state.SessionInstance
+		var arg0 *state.Session
 		if args[0] != nil {
-			arg0 = args[0].(*state.SessionInstance)
+			arg0 = args[0].(*state.Session)
 		}
 		run(
 			arg0,
@@ -147,7 +166,7 @@ func (_c *mockSessionRegistry_RemoveSession_Call) Return() *mockSessionRegistry_
 	return _c
 }
 
-func (_c *mockSessionRegistry_RemoveSession_Call) RunAndReturn(run func(instance *state.SessionInstance)) *mockSessionRegistry_RemoveSession_Call {
+func (_c *mockSessionRegistry_RemoveSession_Call) RunAndReturn(run func(session *state.Session)) *mockSessionRegistry_RemoveSession_Call {
 	_c.Run(run)
 	return _c
 }

+ 1 - 0
foodgroup/session.go

@@ -0,0 +1 @@
+package foodgroup

+ 2 - 2
foodgroup/types.go

@@ -364,11 +364,11 @@ type SessionRegistry interface {
 	// When multiple concurrent calls are made for the same screen name, only one will succeed;
 	// the others will return an error once the context is done.
 	// If doMultiSess is true, allows multiple sessions for the same screen name.
-	AddSession(ctx context.Context, screenName state.DisplayScreenName, doMultiSess bool) (*state.SessionInstance, error)
+	AddSession(ctx context.Context, screenName state.DisplayScreenName, doMultiSess bool, cfg ...func(sess *state.Session)) (*state.SessionInstance, error)
 
 	// RemoveSession removes the given session from the registry, allowing future sessions
 	// to be created for the same screen name.
-	RemoveSession(instance *state.SessionInstance)
+	RemoveSession(session *state.Session)
 }
 
 // SessionRetriever defines a method for retrieving an active session

+ 27 - 21
server/oscar/mock_auth_test.go

@@ -389,8 +389,8 @@ func (_c *mockAuthService_KerberosLogin_Call) RunAndReturn(run func(ctx context.
 }
 
 // RegisterBOSSession provides a mock function for the type mockAuthService
-func (_mock *mockAuthService) RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error) {
-	ret := _mock.Called(ctx, authCookie)
+func (_mock *mockAuthService) RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, conf func(sess *state.Session)) (*state.SessionInstance, error) {
+	ret := _mock.Called(ctx, authCookie, conf)
 
 	if len(ret) == 0 {
 		panic("no return value specified for RegisterBOSSession")
@@ -398,18 +398,18 @@ func (_mock *mockAuthService) RegisterBOSSession(ctx context.Context, authCookie
 
 	var r0 *state.SessionInstance
 	var r1 error
-	if returnFunc, ok := ret.Get(0).(func(context.Context, state.ServerCookie) (*state.SessionInstance, error)); ok {
-		return returnFunc(ctx, authCookie)
+	if returnFunc, ok := ret.Get(0).(func(context.Context, state.ServerCookie, func(sess *state.Session)) (*state.SessionInstance, error)); ok {
+		return returnFunc(ctx, authCookie, conf)
 	}
-	if returnFunc, ok := ret.Get(0).(func(context.Context, state.ServerCookie) *state.SessionInstance); ok {
-		r0 = returnFunc(ctx, authCookie)
+	if returnFunc, ok := ret.Get(0).(func(context.Context, state.ServerCookie, func(sess *state.Session)) *state.SessionInstance); ok {
+		r0 = returnFunc(ctx, authCookie, conf)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*state.SessionInstance)
 		}
 	}
-	if returnFunc, ok := ret.Get(1).(func(context.Context, state.ServerCookie) error); ok {
-		r1 = returnFunc(ctx, authCookie)
+	if returnFunc, ok := ret.Get(1).(func(context.Context, state.ServerCookie, func(sess *state.Session)) error); ok {
+		r1 = returnFunc(ctx, authCookie, conf)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -424,11 +424,12 @@ type mockAuthService_RegisterBOSSession_Call struct {
 // RegisterBOSSession is a helper method to define mock.On call
 //   - ctx context.Context
 //   - authCookie state.ServerCookie
-func (_e *mockAuthService_Expecter) RegisterBOSSession(ctx interface{}, authCookie interface{}) *mockAuthService_RegisterBOSSession_Call {
-	return &mockAuthService_RegisterBOSSession_Call{Call: _e.mock.On("RegisterBOSSession", ctx, authCookie)}
+//   - conf func(sess *state.Session)
+func (_e *mockAuthService_Expecter) RegisterBOSSession(ctx interface{}, authCookie interface{}, conf interface{}) *mockAuthService_RegisterBOSSession_Call {
+	return &mockAuthService_RegisterBOSSession_Call{Call: _e.mock.On("RegisterBOSSession", ctx, authCookie, conf)}
 }
 
-func (_c *mockAuthService_RegisterBOSSession_Call) Run(run func(ctx context.Context, authCookie state.ServerCookie)) *mockAuthService_RegisterBOSSession_Call {
+func (_c *mockAuthService_RegisterBOSSession_Call) Run(run func(ctx context.Context, authCookie state.ServerCookie, conf func(sess *state.Session))) *mockAuthService_RegisterBOSSession_Call {
 	_c.Call.Run(func(args mock.Arguments) {
 		var arg0 context.Context
 		if args[0] != nil {
@@ -438,9 +439,14 @@ func (_c *mockAuthService_RegisterBOSSession_Call) Run(run func(ctx context.Cont
 		if args[1] != nil {
 			arg1 = args[1].(state.ServerCookie)
 		}
+		var arg2 func(sess *state.Session)
+		if args[2] != nil {
+			arg2 = args[2].(func(sess *state.Session))
+		}
 		run(
 			arg0,
 			arg1,
+			arg2,
 		)
 	})
 	return _c
@@ -451,7 +457,7 @@ func (_c *mockAuthService_RegisterBOSSession_Call) Return(sessionInstance *state
 	return _c
 }
 
-func (_c *mockAuthService_RegisterBOSSession_Call) RunAndReturn(run func(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)) *mockAuthService_RegisterBOSSession_Call {
+func (_c *mockAuthService_RegisterBOSSession_Call) RunAndReturn(run func(ctx context.Context, authCookie state.ServerCookie, conf func(sess *state.Session)) (*state.SessionInstance, error)) *mockAuthService_RegisterBOSSession_Call {
 	_c.Call.Return(run)
 	return _c
 }
@@ -593,8 +599,8 @@ func (_c *mockAuthService_RetrieveBOSSession_Call) RunAndReturn(run func(ctx con
 }
 
 // Signout provides a mock function for the type mockAuthService
-func (_mock *mockAuthService) Signout(ctx context.Context, instance *state.SessionInstance) {
-	_mock.Called(ctx, instance)
+func (_mock *mockAuthService) Signout(ctx context.Context, session *state.Session) {
+	_mock.Called(ctx, session)
 	return
 }
 
@@ -605,20 +611,20 @@ type mockAuthService_Signout_Call struct {
 
 // Signout is a helper method to define mock.On call
 //   - ctx context.Context
-//   - instance *state.SessionInstance
-func (_e *mockAuthService_Expecter) Signout(ctx interface{}, instance interface{}) *mockAuthService_Signout_Call {
-	return &mockAuthService_Signout_Call{Call: _e.mock.On("Signout", ctx, instance)}
+//   - session *state.Session
+func (_e *mockAuthService_Expecter) Signout(ctx interface{}, session interface{}) *mockAuthService_Signout_Call {
+	return &mockAuthService_Signout_Call{Call: _e.mock.On("Signout", ctx, session)}
 }
 
-func (_c *mockAuthService_Signout_Call) Run(run func(ctx context.Context, instance *state.SessionInstance)) *mockAuthService_Signout_Call {
+func (_c *mockAuthService_Signout_Call) Run(run func(ctx context.Context, session *state.Session)) *mockAuthService_Signout_Call {
 	_c.Call.Run(func(args mock.Arguments) {
 		var arg0 context.Context
 		if args[0] != nil {
 			arg0 = args[0].(context.Context)
 		}
-		var arg1 *state.SessionInstance
+		var arg1 *state.Session
 		if args[1] != nil {
-			arg1 = args[1].(*state.SessionInstance)
+			arg1 = args[1].(*state.Session)
 		}
 		run(
 			arg0,
@@ -633,7 +639,7 @@ func (_c *mockAuthService_Signout_Call) Return() *mockAuthService_Signout_Call {
 	return _c
 }
 
-func (_c *mockAuthService_Signout_Call) RunAndReturn(run func(ctx context.Context, instance *state.SessionInstance)) *mockAuthService_Signout_Call {
+func (_c *mockAuthService_Signout_Call) RunAndReturn(run func(ctx context.Context, session *state.Session)) *mockAuthService_Signout_Call {
 	_c.Run(run)
 	return _c
 }

+ 28 - 21
server/oscar/server.go

@@ -249,7 +249,34 @@ func (s oscarServer) connectToOSCARService(
 	var instance *state.SessionInstance
 	switch cookie.Service {
 	case wire.BOS:
-		instance, err = s.AuthService.RegisterBOSSession(ctx, cookie)
+
+		fnCfg := func(sess *state.Session) {
+			sess.OnSessionClose(func() {
+				fmt.Println("CLOSING SESSION")
+				if !shuttingDown(ctx) {
+					instances := sess.Instances()
+					if len(instances) > 0 {
+						if err := s.DepartureNotifier.BroadcastBuddyDeparted(ctx, instances[0]); err != nil {
+							s.Logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
+						}
+					}
+				}
+
+				ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+				defer cancel()
+
+				// buddy list must be cleared before session is closed, otherwise
+				// there will be a race condition that could cause the buddy list
+				// be prematurely deleted.
+				if err := s.BuddyListRegistry.UnregisterBuddyList(ctx, instance.IdentScreenName()); err != nil {
+					s.Logger.ErrorContext(ctx, "error removing buddy list entry", "err", err.Error())
+				}
+				s.ChatSessionManager.RemoveUserFromAllChats(instance.IdentScreenName())
+				s.AuthService.Signout(ctx, sess)
+			})
+		}
+
+		instance, err = s.AuthService.RegisterBOSSession(ctx, cookie, fnCfg)
 		if err != nil {
 			if errors.Is(err, state.ErrMaxConcurrentSessionsReached) {
 				s.Logger.Debug("session registration failed", "err", err.Error())
@@ -305,26 +332,6 @@ func (s oscarServer) connectToOSCARService(
 			}
 		})
 
-		instance.Session().OnSessionClose(func() {
-			if !shuttingDown(ctx) {
-				if err := s.DepartureNotifier.BroadcastBuddyDeparted(ctx, instance); err != nil {
-					s.Logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
-				}
-			}
-
-			ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
-			defer cancel()
-
-			// buddy list must be cleared before session is closed, otherwise
-			// there will be a race condition that could cause the buddy list
-			// be prematurely deleted.
-			if err := s.BuddyListRegistry.UnregisterBuddyList(ctx, instance.IdentScreenName()); err != nil {
-				s.Logger.ErrorContext(ctx, "error removing buddy list entry", "err", err.Error())
-			}
-			s.ChatSessionManager.RemoveUserFromAllChats(instance.IdentScreenName())
-			s.AuthService.Signout(ctx, instance)
-		})
-
 		if remoteAddr, ok := ctx.Value("ip").(string); ok {
 			ip, err := netip.ParseAddrPort(remoteAddr)
 			if err != nil {

+ 2 - 2
server/oscar/types.go

@@ -51,10 +51,10 @@ type AuthService interface {
 	CrackCookie(authCookie []byte) (state.ServerCookie, error)
 	FLAPLogin(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error)
 	KerberosLogin(ctx context.Context, inBody wire.SNAC_0x050C_0x0002_KerberosLoginRequest, advertisedHost string) (wire.SNACMessage, error)
-	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
+	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, conf func(sess *state.Session)) (*state.SessionInstance, error)
 	RegisterChatSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
 	RetrieveBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
-	Signout(ctx context.Context, instance *state.SessionInstance)
+	Signout(ctx context.Context, session *state.Session)
 	SignoutChat(ctx context.Context, instance *state.SessionInstance)
 }
 

+ 28 - 21
server/toc/cmd_client.go

@@ -2349,7 +2349,34 @@ func (s OSCARProxy) Signon(ctx context.Context, args []byte, recalcWarning func(
 		return nil, s.runtimeErr(ctx, fmt.Errorf("AuthService.CrackCookie: %w", err))
 	}
 
-	instance, err := s.AuthService.RegisterBOSSession(ctx, serverCookie)
+	fnCfg := func(sess *state.Session) {
+		sess.OnSessionClose(func() {
+			fmt.Println("closing session!")
+			if !shuttingDown(ctx) {
+				instances := sess.Instances()
+				if len(instances) > 0 {
+					if err := s.BuddyService.BroadcastBuddyDeparted(ctx, instances[0]); err != nil {
+						s.Logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
+					}
+				}
+			}
+
+			ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+			defer cancel()
+
+			// buddy list must be cleared before session is closed, otherwise
+			// there will be a race condition that could cause the buddy list
+			// be prematurely deleted.
+			if err := s.BuddyListRegistry.UnregisterBuddyList(ctx, sess.IdentScreenName()); err != nil {
+				s.Logger.ErrorContext(ctx, "error removing buddy list entry", "err", err.Error())
+			}
+			s.ChatSessionManager.RemoveUserFromAllChats(sess.IdentScreenName())
+			s.AuthService.Signout(ctx, sess)
+		})
+
+	}
+
+	instance, err := s.AuthService.RegisterBOSSession(ctx, serverCookie, fnCfg)
 	if err != nil {
 		return nil, s.runtimeErr(ctx, fmt.Errorf("AuthService.RegisterBOSSession: %w", err))
 	}
@@ -2388,26 +2415,6 @@ func (s OSCARProxy) Signon(ctx context.Context, args []byte, recalcWarning func(
 		}
 	})
 
-	instance.Session().OnSessionClose(func() {
-		if !shuttingDown(ctx) {
-			if err := s.BuddyService.BroadcastBuddyDeparted(ctx, instance); err != nil {
-				s.Logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
-			}
-		}
-
-		ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
-		defer cancel()
-
-		// buddy list must be cleared before session is closed, otherwise
-		// there will be a race condition that could cause the buddy list
-		// be prematurely deleted.
-		if err := s.BuddyListRegistry.UnregisterBuddyList(ctx, instance.IdentScreenName()); err != nil {
-			s.Logger.ErrorContext(ctx, "error removing buddy list entry", "err", err.Error())
-		}
-		s.ChatSessionManager.RemoveUserFromAllChats(instance.IdentScreenName())
-		s.AuthService.Signout(ctx, instance)
-	})
-
 	// set chat capability so that... tk
 	instance.SetCaps([][16]byte{wire.CapChat})
 

+ 27 - 21
server/toc/mock_auth_service_test.go

@@ -317,8 +317,8 @@ func (_c *mockAuthService_FLAPLogin_Call) RunAndReturn(run func(ctx context.Cont
 }
 
 // RegisterBOSSession provides a mock function for the type mockAuthService
-func (_mock *mockAuthService) RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error) {
-	ret := _mock.Called(ctx, authCookie)
+func (_mock *mockAuthService) RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, cfg func(*state.Session)) (*state.SessionInstance, error) {
+	ret := _mock.Called(ctx, authCookie, cfg)
 
 	if len(ret) == 0 {
 		panic("no return value specified for RegisterBOSSession")
@@ -326,18 +326,18 @@ func (_mock *mockAuthService) RegisterBOSSession(ctx context.Context, authCookie
 
 	var r0 *state.SessionInstance
 	var r1 error
-	if returnFunc, ok := ret.Get(0).(func(context.Context, state.ServerCookie) (*state.SessionInstance, error)); ok {
-		return returnFunc(ctx, authCookie)
+	if returnFunc, ok := ret.Get(0).(func(context.Context, state.ServerCookie, func(*state.Session)) (*state.SessionInstance, error)); ok {
+		return returnFunc(ctx, authCookie, cfg)
 	}
-	if returnFunc, ok := ret.Get(0).(func(context.Context, state.ServerCookie) *state.SessionInstance); ok {
-		r0 = returnFunc(ctx, authCookie)
+	if returnFunc, ok := ret.Get(0).(func(context.Context, state.ServerCookie, func(*state.Session)) *state.SessionInstance); ok {
+		r0 = returnFunc(ctx, authCookie, cfg)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*state.SessionInstance)
 		}
 	}
-	if returnFunc, ok := ret.Get(1).(func(context.Context, state.ServerCookie) error); ok {
-		r1 = returnFunc(ctx, authCookie)
+	if returnFunc, ok := ret.Get(1).(func(context.Context, state.ServerCookie, func(*state.Session)) error); ok {
+		r1 = returnFunc(ctx, authCookie, cfg)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -352,11 +352,12 @@ type mockAuthService_RegisterBOSSession_Call struct {
 // RegisterBOSSession is a helper method to define mock.On call
 //   - ctx context.Context
 //   - authCookie state.ServerCookie
-func (_e *mockAuthService_Expecter) RegisterBOSSession(ctx interface{}, authCookie interface{}) *mockAuthService_RegisterBOSSession_Call {
-	return &mockAuthService_RegisterBOSSession_Call{Call: _e.mock.On("RegisterBOSSession", ctx, authCookie)}
+//   - cfg func(*state.Session)
+func (_e *mockAuthService_Expecter) RegisterBOSSession(ctx interface{}, authCookie interface{}, cfg interface{}) *mockAuthService_RegisterBOSSession_Call {
+	return &mockAuthService_RegisterBOSSession_Call{Call: _e.mock.On("RegisterBOSSession", ctx, authCookie, cfg)}
 }
 
-func (_c *mockAuthService_RegisterBOSSession_Call) Run(run func(ctx context.Context, authCookie state.ServerCookie)) *mockAuthService_RegisterBOSSession_Call {
+func (_c *mockAuthService_RegisterBOSSession_Call) Run(run func(ctx context.Context, authCookie state.ServerCookie, cfg func(*state.Session))) *mockAuthService_RegisterBOSSession_Call {
 	_c.Call.Run(func(args mock.Arguments) {
 		var arg0 context.Context
 		if args[0] != nil {
@@ -366,9 +367,14 @@ func (_c *mockAuthService_RegisterBOSSession_Call) Run(run func(ctx context.Cont
 		if args[1] != nil {
 			arg1 = args[1].(state.ServerCookie)
 		}
+		var arg2 func(*state.Session)
+		if args[2] != nil {
+			arg2 = args[2].(func(*state.Session))
+		}
 		run(
 			arg0,
 			arg1,
+			arg2,
 		)
 	})
 	return _c
@@ -379,7 +385,7 @@ func (_c *mockAuthService_RegisterBOSSession_Call) Return(sessionInstance *state
 	return _c
 }
 
-func (_c *mockAuthService_RegisterBOSSession_Call) RunAndReturn(run func(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)) *mockAuthService_RegisterBOSSession_Call {
+func (_c *mockAuthService_RegisterBOSSession_Call) RunAndReturn(run func(ctx context.Context, authCookie state.ServerCookie, cfg func(*state.Session)) (*state.SessionInstance, error)) *mockAuthService_RegisterBOSSession_Call {
 	_c.Call.Return(run)
 	return _c
 }
@@ -521,8 +527,8 @@ func (_c *mockAuthService_RetrieveBOSSession_Call) RunAndReturn(run func(ctx con
 }
 
 // Signout provides a mock function for the type mockAuthService
-func (_mock *mockAuthService) Signout(ctx context.Context, instance *state.SessionInstance) {
-	_mock.Called(ctx, instance)
+func (_mock *mockAuthService) Signout(ctx context.Context, session *state.Session) {
+	_mock.Called(ctx, session)
 	return
 }
 
@@ -533,20 +539,20 @@ type mockAuthService_Signout_Call struct {
 
 // Signout is a helper method to define mock.On call
 //   - ctx context.Context
-//   - instance *state.SessionInstance
-func (_e *mockAuthService_Expecter) Signout(ctx interface{}, instance interface{}) *mockAuthService_Signout_Call {
-	return &mockAuthService_Signout_Call{Call: _e.mock.On("Signout", ctx, instance)}
+//   - session *state.Session
+func (_e *mockAuthService_Expecter) Signout(ctx interface{}, session interface{}) *mockAuthService_Signout_Call {
+	return &mockAuthService_Signout_Call{Call: _e.mock.On("Signout", ctx, session)}
 }
 
-func (_c *mockAuthService_Signout_Call) Run(run func(ctx context.Context, instance *state.SessionInstance)) *mockAuthService_Signout_Call {
+func (_c *mockAuthService_Signout_Call) Run(run func(ctx context.Context, session *state.Session)) *mockAuthService_Signout_Call {
 	_c.Call.Run(func(args mock.Arguments) {
 		var arg0 context.Context
 		if args[0] != nil {
 			arg0 = args[0].(context.Context)
 		}
-		var arg1 *state.SessionInstance
+		var arg1 *state.Session
 		if args[1] != nil {
-			arg1 = args[1].(*state.SessionInstance)
+			arg1 = args[1].(*state.Session)
 		}
 		run(
 			arg0,
@@ -561,7 +567,7 @@ func (_c *mockAuthService_Signout_Call) Return() *mockAuthService_Signout_Call {
 	return _c
 }
 
-func (_c *mockAuthService_Signout_Call) RunAndReturn(run func(ctx context.Context, instance *state.SessionInstance)) *mockAuthService_Signout_Call {
+func (_c *mockAuthService_Signout_Call) RunAndReturn(run func(ctx context.Context, session *state.Session)) *mockAuthService_Signout_Call {
 	_c.Run(run)
 	return _c
 }

+ 2 - 2
server/toc/types.go

@@ -49,10 +49,10 @@ type AuthService interface {
 	BUCPLogin(ctx context.Context, inBody wire.SNAC_0x17_0x02_BUCPLoginRequest, advertisedHost string) (wire.SNACMessage, error)
 	CrackCookie(authCookie []byte) (state.ServerCookie, error)
 	FLAPLogin(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error)
-	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
+	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, cfg func(*state.Session)) (*state.SessionInstance, error)
 	RegisterChatSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
 	RetrieveBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
-	Signout(ctx context.Context, instance *state.SessionInstance)
+	Signout(ctx context.Context, session *state.Session)
 	SignoutChat(ctx context.Context, instance *state.SessionInstance)
 }
 

+ 2 - 2
server/webapi/handlers/oscar_bridge.go

@@ -30,11 +30,11 @@ type OSCARBridgeHandler struct {
 // OSCARAuthService defines methods needed for OSCAR authentication and session management.
 type OSCARAuthService interface {
 	// RegisterBOSSession creates a new BOS (Basic OSCAR Service) session
-	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
+	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, conf func(sess *state.Session)) (*state.SessionInstance, error)
 	// RetrieveBOSSession retrieves an existing BOS session
 	RetrieveBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
 	// Signout ends an OSCAR session
-	Signout(ctx context.Context, instance *state.SessionInstance)
+	Signout(ctx context.Context, session *state.Session)
 }
 
 // CookieBaker issues and validates authentication cookies for OSCAR services.

+ 1 - 1
server/webapi/handlers/session.go

@@ -34,7 +34,7 @@ type SessionHandler struct {
 type AuthService interface {
 	BUCPChallenge(ctx context.Context, bodyIn wire.SNAC_0x17_0x06_BUCPChallengeRequest, newUUID func() uuid.UUID) (wire.SNACMessage, error)
 	BUCPLogin(ctx context.Context, bodyIn wire.SNAC_0x17_0x02_BUCPLoginRequest, advertisedHost string) (wire.SNACMessage, error)
-	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
+	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, conf func(sess *state.Session)) (*state.SessionInstance, error)
 }
 
 // SessionManager defines methods for OSCAR session management.

+ 2 - 2
server/webapi/types.go

@@ -48,10 +48,10 @@ type AuthService interface {
 	BUCPLogin(ctx context.Context, inBody wire.SNAC_0x17_0x02_BUCPLoginRequest, advertisedHost string) (wire.SNACMessage, error)
 	CrackCookie(authCookie []byte) (state.ServerCookie, error)
 	FLAPLogin(ctx context.Context, inFrame wire.FLAPSignonFrame, advertisedHost string) (wire.TLVRestBlock, error)
-	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
+	RegisterBOSSession(ctx context.Context, authCookie state.ServerCookie, conf func(sess *state.Session)) (*state.SessionInstance, error)
 	RegisterChatSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
 	RetrieveBOSSession(ctx context.Context, authCookie state.ServerCookie) (*state.SessionInstance, error)
-	Signout(ctx context.Context, instance *state.SessionInstance)
+	Signout(ctx context.Context, session *state.Session)
 	SignoutChat(ctx context.Context, instance *state.SessionInstance)
 }
 

+ 5 - 4
state/session.go

@@ -1095,17 +1095,18 @@ func (s *SessionInstance) CloseInstance() {
 	onInstanceCloseFn := s.onInstanceCloseFn
 	s.mutex.Unlock()
 
-	s.session.RemoveInstance(s)
-
 	count := s.session.InstanceCount()
-	if count == 0 {
+	if count == 1 {
 		s.session.mutex.RLock()
 		onSessCloseFn := s.session.onSessCloseFn
 		s.session.mutex.RUnlock()
-		onSessCloseFn()
+		onSessCloseFn() // todo move this up so we can send notif
 	} else {
 		onInstanceCloseFn()
 	}
+
+	s.session.RemoveInstance(s)
+
 }
 
 // OnClose registers a function to be called when the instance closes,

+ 12 - 8
state/session_manager.go

@@ -177,7 +177,7 @@ func (s *InMemorySessionManager) maybeRelayMessageActiveOnly(ctx context.Context
 	}
 }
 
-func (s *InMemorySessionManager) AddSession(ctx context.Context, screenName DisplayScreenName, doMultiSess bool) (*SessionInstance, error) {
+func (s *InMemorySessionManager) AddSession(ctx context.Context, screenName DisplayScreenName, doMultiSess bool, cfg ...func(sess *Session)) (*SessionInstance, error) {
 	s.lockUser(screenName.IdentScreenName())
 	defer s.unlockUser(screenName.IdentScreenName())
 
@@ -189,7 +189,7 @@ func (s *InMemorySessionManager) AddSession(ctx context.Context, screenName Disp
 		if doMultiSess {
 			if !active.multiSession {
 				active.session.CloseSession()
-				return s.newSession(screenName, doMultiSess)
+				return s.newSession(screenName, doMultiSess, cfg)
 			}
 
 			// Check if we've reached the maximum number of concurrent sessions
@@ -212,14 +212,18 @@ func (s *InMemorySessionManager) AddSession(ctx context.Context, screenName Disp
 		}
 	}
 
-	return s.newSession(screenName, doMultiSess)
+	return s.newSession(screenName, doMultiSess, cfg)
 }
 
-func (s *InMemorySessionManager) newSession(screenName DisplayScreenName, doMultiSess bool) (*SessionInstance, error) {
+func (s *InMemorySessionManager) newSession(screenName DisplayScreenName, doMultiSess bool, cfg []func(sess *Session)) (*SessionInstance, error) {
 	sess := NewSession()
 	sess.SetIdentScreenName(screenName.IdentScreenName())
 	sess.SetDisplayScreenName(screenName)
 
+	for _, f := range cfg {
+		f(sess)
+	}
+
 	// Create a new instance within the session group
 	instance := sess.AddInstance()
 
@@ -244,11 +248,11 @@ func (s *InMemorySessionManager) findRec(identScreenName IdentScreenName) *sessi
 }
 
 // RemoveSession takes a session out of the session pool.
-func (s *InMemorySessionManager) RemoveSession(instance *SessionInstance) {
+func (s *InMemorySessionManager) RemoveSession(session *Session) {
 	s.mapMutex.Lock()
 	defer s.mapMutex.Unlock()
-	if rec, ok := s.store[instance.IdentScreenName()]; ok && rec.session == instance.Session() {
-		delete(s.store, instance.IdentScreenName())
+	if rec, ok := s.store[session.IdentScreenName()]; ok && rec.session == session {
+		delete(s.store, session.IdentScreenName())
 		close(rec.removed)
 	}
 }
@@ -370,7 +374,7 @@ func (s *InMemoryChatSessionManager) RemoveSession(instance *SessionInstance) {
 	if !ok {
 		panic("attempting to remove a session after its room has been deleted")
 	}
-	sessionManager.RemoveSession(instance)
+	sessionManager.RemoveSession(instance.Session())
 
 	if sessionManager.Empty() {
 		delete(s.store, instance.ChatRoomCookie())

+ 10 - 10
state/session_manager_test.go

@@ -25,7 +25,7 @@ func TestInMemorySessionManager_AddSession(t *testing.T) {
 
 	go func() {
 		<-sess1.Closed()
-		sm.RemoveSession(sess1)
+		sm.RemoveSession(sess1.Session())
 	}()
 
 	sess2, err := sm.AddSession(ctx, "user-screen-name", false)
@@ -66,7 +66,7 @@ func TestInMemorySessionManager_Remove_Existing(t *testing.T) {
 	assert.Equal(t, user1Old.Session(), rec.session)
 
 	// Remove the session
-	sm.RemoveSession(user1Old)
+	sm.RemoveSession(user1Old.Session())
 
 	// Verify the session is no longer in the store
 	_, ok = sm.store[user1Old.IdentScreenName()]
@@ -89,7 +89,7 @@ func TestInMemorySessionManager_Remove_Existing(t *testing.T) {
 	user2.SetSignonComplete()
 
 	// Remove user1New and verify it's gone
-	sm.RemoveSession(user1New)
+	sm.RemoveSession(user1New.Session())
 	_, ok = sm.store[user1New.IdentScreenName()]
 	assert.False(t, ok)
 
@@ -112,7 +112,7 @@ func TestInMemorySessionManager_Remove_MissingSameScreenName(t *testing.T) {
 	assert.Equal(t, user1Old.Session(), recOld.session)
 
 	// Remove the old session
-	sm.RemoveSession(user1Old)
+	sm.RemoveSession(user1Old.Session())
 	_, ok = sm.store[user1Old.IdentScreenName()]
 	assert.False(t, ok)
 
@@ -132,7 +132,7 @@ func TestInMemorySessionManager_Remove_MissingSameScreenName(t *testing.T) {
 	user2.SetSignonComplete()
 
 	// Try to remove the old session again - should do nothing because Session doesn't match
-	sm.RemoveSession(user1Old)
+	sm.RemoveSession(user1Old.Session())
 
 	// Verify the new session is still in the store (not removed)
 	recNewAfter, ok := sm.store[user1New.IdentScreenName()]
@@ -420,7 +420,7 @@ func TestInMemorySessionManager_SessionReplacement_NoMultiSess_NoMultiSess(t *te
 		synctest.Wait()
 
 		// AddSession() is blocked waiting for the lock, now unblock it
-		sm.RemoveSession(sess1)
+		sm.RemoveSession(sess1.Session())
 
 		wg.Wait()
 
@@ -466,7 +466,7 @@ func TestInMemorySessionManager_SessionReplacement_MultiSess_NoMultiSess(t *test
 
 		// AddSession() is blocked waiting for the lock, now unblock it
 		for _, sess := range sessList {
-			sm.RemoveSession(sess)
+			sm.RemoveSession(sess.Session())
 		}
 
 		wg.Wait()
@@ -506,7 +506,7 @@ func TestInMemorySessionManager_SessionReplacement_NoMultiSess_MultiSess(t *test
 		synctest.Wait()
 
 		// AddSession() is blocked waiting for the lock, now unblock it
-		sm.RemoveSession(sess1)
+		sm.RemoveSession(sess1.Session())
 
 		wg.Wait()
 
@@ -532,7 +532,7 @@ func TestInMemorySessionManager_RemoveSession_DoubleLogin_NoMultiSess_Chaos(t *t
 			sess1, err := sm.AddSession(context.Background(), "user-screen-name-1", false)
 			assert.NoError(t, err)
 			time.Sleep(time.Duration(rand.Intn(1000)) * time.Microsecond)
-			sm.RemoveSession(sess1)
+			sm.RemoveSession(sess1.Session())
 		}()
 	}
 
@@ -1089,7 +1089,7 @@ func TestInMemorySessionManager_AddSession_MaxConcurrentSessions(t *testing.T) {
 		// Close and remove the first session to allow a new one
 		go func() {
 			<-sess1.Closed()
-			sm.RemoveSession(sess1)
+			sm.RemoveSession(sess1.Session())
 		}()
 
 		sess2, err := sm.AddSession(context.Background(), "user-screen-name-1", false)