Просмотр исходного кода

split OService services into separate BOS/CHAT services

Mike 2 лет назад
Родитель
Сommit
1d808d9d6f

+ 6 - 0
.mockery.yaml

@@ -29,6 +29,12 @@ packages:
       OServiceHandler:
         config:
           filename: "oservice_mock.go"
+      OServiceBOSHandler:
+        config:
+          filename: "oservice_bos_mock.go"
+      OServiceChatHandler:
+        config:
+          filename: "oservice_chat_mock.go"
       ProfileManager:
         config:
           filename: "profile_manager_mock.go"

+ 4 - 2
cmd/main.go

@@ -35,11 +35,13 @@ func main() {
 		wg.Done()
 	}()
 	go func() {
-		server.ListenBOS(cfg, sm, fm, cr, logger.With("svc", "BOS"))
+		router := server.NewBOSServiceRouter(logger, cfg, fm, sm, cr, fm)
+		server.ListenBOS(cfg, router, logger.With("svc", "BOS"))
 		wg.Done()
 	}()
 	go func() {
-		server.ListenChat(cfg, fm, cr, logger.With("svc", "CHAT"))
+		router := server.NewChatServiceRouter(logger, cfg, fm, sm)
+		server.ListenChat(cfg, router, cr, logger.With("svc", "CHAT"))
 		wg.Done()
 	}()
 	go func() {

+ 1 - 1
server/buddy.go

@@ -88,7 +88,7 @@ func BroadcastArrival(ctx context.Context, sess *Session, sm SessionManager, fm
 	return nil
 }
 
-func BroadcastDeparture(ctx context.Context, sess *Session, sm SessionManager, fm *FeedbagStore) error {
+func BroadcastDeparture(ctx context.Context, sess *Session, sm SessionManager, fm FeedbagManager) error {
 	screenNames, err := fm.InterestedUsers(sess.ScreenName)
 	if err != nil {
 		return err

+ 7 - 7
server/chat.go

@@ -9,7 +9,7 @@ import (
 )
 
 type ChatHandler interface {
-	ChannelMsgToHostHandler(ctx context.Context, sess *Session, sm SessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error)
+	ChannelMsgToHostHandler(ctx context.Context, sess *Session, room ChatRoom, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error)
 }
 
 func NewChatRouter(logger *slog.Logger) ChatRouter {
@@ -26,14 +26,14 @@ type ChatRouter struct {
 	RouteLogger
 }
 
-func (rt *ChatRouter) RouteChat(ctx context.Context, sess *Session, sm SessionManager, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func (rt *ChatRouter) RouteChat(ctx context.Context, sess *Session, room ChatRoom, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch SNACFrame.SubGroup {
 	case oscar.ChatChannelMsgToHost:
 		inSNAC := oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.ChannelMsgToHostHandler(ctx, sess, sm, inSNAC)
+		outSNAC, err := rt.ChannelMsgToHostHandler(ctx, sess, room, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -51,7 +51,7 @@ func (rt *ChatRouter) RouteChat(ctx context.Context, sess *Session, sm SessionMa
 type ChatService struct {
 }
 
-func (s ChatService) ChannelMsgToHostHandler(ctx context.Context, sess *Session, sm SessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error) {
+func (s ChatService) ChannelMsgToHostHandler(ctx context.Context, sess *Session, room ChatRoom, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error) {
 	snacFrameOut := oscar.SnacFrame{
 		FoodGroup: oscar.CHAT,
 		SubGroup:  oscar.ChatChannelMsgToClient,
@@ -74,7 +74,7 @@ func (s ChatService) ChannelMsgToHostHandler(ctx context.Context, sess *Session,
 	)
 
 	// send message to all the participants except sender
-	sm.BroadcastExcept(ctx, sess, XMessage{
+	room.BroadcastExcept(ctx, sess, XMessage{
 		snacFrame: snacFrameOut,
 		snacOut:   snacPayloadOut,
 	})
@@ -154,8 +154,8 @@ func AlertUserLeft(ctx context.Context, sess *Session, sm SessionManager) {
 	})
 }
 
-func SendChatRoomInfoUpdate(ctx context.Context, sess *Session, sm SessionManager, room ChatRoom) {
-	sm.SendToScreenName(ctx, sess.ScreenName, XMessage{
+func SendChatRoomInfoUpdate(ctx context.Context, sess *Session, room ChatRoom) {
+	room.SendToScreenName(ctx, sess.ScreenName, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.CHAT,
 			SubGroup:  oscar.ChatRoomInfoUpdate,

+ 15 - 15
server/chat_mock.go

@@ -22,25 +22,25 @@ func (_m *MockChatHandler) EXPECT() *MockChatHandler_Expecter {
 	return &MockChatHandler_Expecter{mock: &_m.Mock}
 }
 
-// ChannelMsgToHostHandler provides a mock function with given fields: ctx, sess, sm, snacPayloadIn
-func (_m *MockChatHandler) ChannelMsgToHostHandler(ctx context.Context, sess *Session, sm SessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error) {
-	ret := _m.Called(ctx, sess, sm, snacPayloadIn)
+// ChannelMsgToHostHandler provides a mock function with given fields: ctx, sess, room, snacPayloadIn
+func (_m *MockChatHandler) ChannelMsgToHostHandler(ctx context.Context, sess *Session, room ChatRoom, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error) {
+	ret := _m.Called(ctx, sess, room, snacPayloadIn)
 
 	var r0 *XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, SessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error)); ok {
-		return rf(ctx, sess, sm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, ChatRoom, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error)); ok {
+		return rf(ctx, sess, room, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, SessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) *XMessage); ok {
-		r0 = rf(ctx, sess, sm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, ChatRoom, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) *XMessage); ok {
+		r0 = rf(ctx, sess, room, snacPayloadIn)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*XMessage)
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *Session, SessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) error); ok {
-		r1 = rf(ctx, sess, sm, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, ChatRoom, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) error); ok {
+		r1 = rf(ctx, sess, room, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -56,15 +56,15 @@ type MockChatHandler_ChannelMsgToHostHandler_Call struct {
 // ChannelMsgToHostHandler is a helper method to define mock.On call
 //   - ctx context.Context
 //   - sess *Session
-//   - sm SessionManager
+//   - room ChatRoom
 //   - snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost
-func (_e *MockChatHandler_Expecter) ChannelMsgToHostHandler(ctx interface{}, sess interface{}, sm interface{}, snacPayloadIn interface{}) *MockChatHandler_ChannelMsgToHostHandler_Call {
-	return &MockChatHandler_ChannelMsgToHostHandler_Call{Call: _e.mock.On("ChannelMsgToHostHandler", ctx, sess, sm, snacPayloadIn)}
+func (_e *MockChatHandler_Expecter) ChannelMsgToHostHandler(ctx interface{}, sess interface{}, room interface{}, snacPayloadIn interface{}) *MockChatHandler_ChannelMsgToHostHandler_Call {
+	return &MockChatHandler_ChannelMsgToHostHandler_Call{Call: _e.mock.On("ChannelMsgToHostHandler", ctx, sess, room, snacPayloadIn)}
 }
 
-func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) Run(run func(ctx context.Context, sess *Session, sm SessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost)) *MockChatHandler_ChannelMsgToHostHandler_Call {
+func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) Run(run func(ctx context.Context, sess *Session, room ChatRoom, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost)) *MockChatHandler_ChannelMsgToHostHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*Session), args[2].(SessionManager), args[3].(oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(ChatRoom), args[3].(oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost))
 	})
 	return _c
 }
@@ -74,7 +74,7 @@ func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) Return(_a0 *XMessage, _a
 	return _c
 }
 
-func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) RunAndReturn(run func(context.Context, *Session, SessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error)) *MockChatHandler_ChannelMsgToHostHandler_Call {
+func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) RunAndReturn(run func(context.Context, *Session, ChatRoom, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error)) *MockChatHandler_ChannelMsgToHostHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 12 - 10
server/chat_nav.go

@@ -11,15 +11,16 @@ import (
 )
 
 type ChatNavHandler interface {
-	CreateRoomHandler(ctx context.Context, sess *Session, cr *ChatRegistry, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error)
+	CreateRoomHandler(ctx context.Context, sess *Session, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error)
 	RequestChatRightsHandler(ctx context.Context) XMessage
-	RequestRoomInfoHandler(ctx context.Context, cr *ChatRegistry, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error)
+	RequestRoomInfoHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error)
 }
 
-func NewChatNavRouter(logger *slog.Logger) ChatNavRouter {
+func NewChatNavRouter(logger *slog.Logger, cr *ChatRegistry) ChatNavRouter {
 	return ChatNavRouter{
 		ChatNavHandler: ChatNavService{
 			Logger: logger,
+			cr:     cr,
 		},
 		RouteLogger: RouteLogger{
 			Logger: logger,
@@ -32,7 +33,7 @@ type ChatNavRouter struct {
 	RouteLogger
 }
 
-func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *Session, cr *ChatRegistry, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *Session, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch SNACFrame.SubGroup {
 	case oscar.ChatNavRequestChatRights:
 		outSNAC := rt.RequestChatRightsHandler(ctx)
@@ -43,7 +44,7 @@ func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *Session, cr *Ch
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.RequestRoomInfoHandler(ctx, cr, inSNAC)
+		outSNAC, err := rt.RequestRoomInfoHandler(ctx, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -54,7 +55,7 @@ func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *Session, cr *Ch
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.CreateRoomHandler(ctx, sess, cr, NewChatRoom, inSNAC)
+		outSNAC, err := rt.CreateRoomHandler(ctx, sess, NewChatRoom, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -69,6 +70,7 @@ func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *Session, cr *Ch
 
 type ChatNavService struct {
 	Logger *slog.Logger
+	cr     *ChatRegistry
 }
 
 type ChatCookie struct {
@@ -117,7 +119,7 @@ func NewChatRoom(logger *slog.Logger) ChatRoom {
 
 type ChatRoomFactory func(logger *slog.Logger) ChatRoom
 
-func (s ChatNavService) CreateRoomHandler(_ context.Context, sess *Session, cr *ChatRegistry, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error) {
+func (s ChatNavService) CreateRoomHandler(_ context.Context, sess *Session, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error) {
 	name, hasName := snacPayloadIn.GetString(oscar.ChatTLVRoomName)
 	if !hasName {
 		return XMessage{}, errors.New("unable to find chat name")
@@ -128,7 +130,7 @@ func (s ChatNavService) CreateRoomHandler(_ context.Context, sess *Session, cr *
 	room.Exchange = snacPayloadIn.Exchange
 	room.InstanceNumber = snacPayloadIn.InstanceNumber
 	room.Name = name
-	cr.Register(room)
+	s.cr.Register(room)
 
 	// add user to chat room
 	room.NewSessionWithSN(sess.ID, sess.ScreenName)
@@ -156,8 +158,8 @@ func (s ChatNavService) CreateRoomHandler(_ context.Context, sess *Session, cr *
 	}, nil
 }
 
-func (s ChatNavService) RequestRoomInfoHandler(_ context.Context, cr *ChatRegistry, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error) {
-	room, err := cr.Retrieve(string(snacPayloadIn.Cookie))
+func (s ChatNavService) RequestRoomInfoHandler(_ context.Context, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error) {
+	room, err := s.cr.Retrieve(string(snacPayloadIn.Cookie))
 	if err != nil {
 		return XMessage{}, err
 	}

+ 28 - 30
server/chat_nav_mock.go

@@ -22,23 +22,23 @@ func (_m *MockChatNavHandler) EXPECT() *MockChatNavHandler_Expecter {
 	return &MockChatNavHandler_Expecter{mock: &_m.Mock}
 }
 
-// CreateRoomHandler provides a mock function with given fields: ctx, sess, cr, newChatRoom, snacPayloadIn
-func (_m *MockChatNavHandler) CreateRoomHandler(ctx context.Context, sess *Session, cr *ChatRegistry, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error) {
-	ret := _m.Called(ctx, sess, cr, newChatRoom, snacPayloadIn)
+// CreateRoomHandler provides a mock function with given fields: ctx, sess, newChatRoom, snacPayloadIn
+func (_m *MockChatNavHandler) CreateRoomHandler(ctx context.Context, sess *Session, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error) {
+	ret := _m.Called(ctx, sess, newChatRoom, snacPayloadIn)
 
 	var r0 XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, *ChatRegistry, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error)); ok {
-		return rf(ctx, sess, cr, newChatRoom, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error)); ok {
+		return rf(ctx, sess, newChatRoom, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, *ChatRegistry, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) XMessage); ok {
-		r0 = rf(ctx, sess, cr, newChatRoom, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) XMessage); ok {
+		r0 = rf(ctx, sess, newChatRoom, snacPayloadIn)
 	} else {
 		r0 = ret.Get(0).(XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *Session, *ChatRegistry, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) error); ok {
-		r1 = rf(ctx, sess, cr, newChatRoom, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) error); ok {
+		r1 = rf(ctx, sess, newChatRoom, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -54,16 +54,15 @@ type MockChatNavHandler_CreateRoomHandler_Call struct {
 // CreateRoomHandler is a helper method to define mock.On call
 //   - ctx context.Context
 //   - sess *Session
-//   - cr *ChatRegistry
 //   - newChatRoom ChatRoomFactory
 //   - snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate
-func (_e *MockChatNavHandler_Expecter) CreateRoomHandler(ctx interface{}, sess interface{}, cr interface{}, newChatRoom interface{}, snacPayloadIn interface{}) *MockChatNavHandler_CreateRoomHandler_Call {
-	return &MockChatNavHandler_CreateRoomHandler_Call{Call: _e.mock.On("CreateRoomHandler", ctx, sess, cr, newChatRoom, snacPayloadIn)}
+func (_e *MockChatNavHandler_Expecter) CreateRoomHandler(ctx interface{}, sess interface{}, newChatRoom interface{}, snacPayloadIn interface{}) *MockChatNavHandler_CreateRoomHandler_Call {
+	return &MockChatNavHandler_CreateRoomHandler_Call{Call: _e.mock.On("CreateRoomHandler", ctx, sess, newChatRoom, snacPayloadIn)}
 }
 
-func (_c *MockChatNavHandler_CreateRoomHandler_Call) Run(run func(ctx context.Context, sess *Session, cr *ChatRegistry, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate)) *MockChatNavHandler_CreateRoomHandler_Call {
+func (_c *MockChatNavHandler_CreateRoomHandler_Call) Run(run func(ctx context.Context, sess *Session, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate)) *MockChatNavHandler_CreateRoomHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*Session), args[2].(*ChatRegistry), args[3].(ChatRoomFactory), args[4].(oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(ChatRoomFactory), args[3].(oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate))
 	})
 	return _c
 }
@@ -73,7 +72,7 @@ func (_c *MockChatNavHandler_CreateRoomHandler_Call) Return(_a0 XMessage, _a1 er
 	return _c
 }
 
-func (_c *MockChatNavHandler_CreateRoomHandler_Call) RunAndReturn(run func(context.Context, *Session, *ChatRegistry, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error)) *MockChatNavHandler_CreateRoomHandler_Call {
+func (_c *MockChatNavHandler_CreateRoomHandler_Call) RunAndReturn(run func(context.Context, *Session, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error)) *MockChatNavHandler_CreateRoomHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }
@@ -120,23 +119,23 @@ func (_c *MockChatNavHandler_RequestChatRightsHandler_Call) RunAndReturn(run fun
 	return _c
 }
 
-// RequestRoomInfoHandler provides a mock function with given fields: ctx, cr, snacPayloadIn
-func (_m *MockChatNavHandler) RequestRoomInfoHandler(ctx context.Context, cr *ChatRegistry, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error) {
-	ret := _m.Called(ctx, cr, snacPayloadIn)
+// RequestRoomInfoHandler provides a mock function with given fields: ctx, snacPayloadIn
+func (_m *MockChatNavHandler) RequestRoomInfoHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error) {
+	ret := _m.Called(ctx, snacPayloadIn)
 
 	var r0 XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *ChatRegistry, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error)); ok {
-		return rf(ctx, cr, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error)); ok {
+		return rf(ctx, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *ChatRegistry, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) XMessage); ok {
-		r0 = rf(ctx, cr, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) XMessage); ok {
+		r0 = rf(ctx, snacPayloadIn)
 	} else {
 		r0 = ret.Get(0).(XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *ChatRegistry, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) error); ok {
-		r1 = rf(ctx, cr, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) error); ok {
+		r1 = rf(ctx, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -151,15 +150,14 @@ type MockChatNavHandler_RequestRoomInfoHandler_Call struct {
 
 // RequestRoomInfoHandler is a helper method to define mock.On call
 //   - ctx context.Context
-//   - cr *ChatRegistry
 //   - snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo
-func (_e *MockChatNavHandler_Expecter) RequestRoomInfoHandler(ctx interface{}, cr interface{}, snacPayloadIn interface{}) *MockChatNavHandler_RequestRoomInfoHandler_Call {
-	return &MockChatNavHandler_RequestRoomInfoHandler_Call{Call: _e.mock.On("RequestRoomInfoHandler", ctx, cr, snacPayloadIn)}
+func (_e *MockChatNavHandler_Expecter) RequestRoomInfoHandler(ctx interface{}, snacPayloadIn interface{}) *MockChatNavHandler_RequestRoomInfoHandler_Call {
+	return &MockChatNavHandler_RequestRoomInfoHandler_Call{Call: _e.mock.On("RequestRoomInfoHandler", ctx, snacPayloadIn)}
 }
 
-func (_c *MockChatNavHandler_RequestRoomInfoHandler_Call) Run(run func(ctx context.Context, cr *ChatRegistry, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo)) *MockChatNavHandler_RequestRoomInfoHandler_Call {
+func (_c *MockChatNavHandler_RequestRoomInfoHandler_Call) Run(run func(ctx context.Context, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo)) *MockChatNavHandler_RequestRoomInfoHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*ChatRegistry), args[2].(oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo))
+		run(args[0].(context.Context), args[1].(oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo))
 	})
 	return _c
 }
@@ -169,7 +167,7 @@ func (_c *MockChatNavHandler_RequestRoomInfoHandler_Call) Return(_a0 XMessage, _
 	return _c
 }
 
-func (_c *MockChatNavHandler_RequestRoomInfoHandler_Call) RunAndReturn(run func(context.Context, *ChatRegistry, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error)) *MockChatNavHandler_RequestRoomInfoHandler_Call {
+func (_c *MockChatNavHandler_RequestRoomInfoHandler_Call) RunAndReturn(run func(context.Context, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error)) *MockChatNavHandler_RequestRoomInfoHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 7 - 5
server/chat_nav_test.go

@@ -49,8 +49,10 @@ func TestSendAndReceiveCreateRoom(t *testing.T) {
 			},
 		},
 	}
-	svc := ChatNavService{}
-	outputSNAC, err := svc.CreateRoomHandler(context.Background(), userSess, cr, crf, inputSNAC)
+	svc := ChatNavService{
+		cr: cr,
+	}
+	outputSNAC, err := svc.CreateRoomHandler(context.Background(), userSess, crf, inputSNAC)
 	assert.NoError(t, err)
 
 	//
@@ -208,11 +210,11 @@ func TestChatNavRouter_RouteChatNavRouter(t *testing.T) {
 				Return(tc.output).
 				Maybe()
 			svc.EXPECT().
-				RequestRoomInfoHandler(mock.Anything, mock.Anything, tc.input.snacOut).
+				RequestRoomInfoHandler(mock.Anything, tc.input.snacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
-				CreateRoomHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				CreateRoomHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 
@@ -229,7 +231,7 @@ func TestChatNavRouter_RouteChatNavRouter(t *testing.T) {
 			bufOut := &bytes.Buffer{}
 			seq := uint32(0)
 
-			err := router.RouteChatNav(nil, nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			err := router.RouteChatNav(nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
 			if tc.expectErr != nil {
 				return

+ 3 - 2
server/chat_test.go

@@ -134,7 +134,8 @@ func TestSendAndReceiveChatChannelMsgToHost(t *testing.T) {
 			// send input SNAC
 			//
 			svc := ChatService{}
-			outputSNAC, err := svc.ChannelMsgToHostHandler(context.Background(), tc.userSession, crm, tc.inputSNAC)
+			room := ChatRoom{SessionManager: crm}
+			outputSNAC, err := svc.ChannelMsgToHostHandler(context.Background(), tc.userSession, room, tc.inputSNAC)
 			assert.NoError(t, err)
 
 			if tc.expectOutput.snacFrame == (oscar.SnacFrame{}) {
@@ -228,7 +229,7 @@ func TestChatRouter_RouteChat(t *testing.T) {
 			bufOut := &bytes.Buffer{}
 			seq := uint32(0)
 
-			err := router.RouteChat(nil, nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			err := router.RouteChat(nil, nil, ChatRoom{}, tc.input.snacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
 			if tc.expectErr != nil {
 				return

+ 27 - 17
server/connection.go

@@ -91,11 +91,9 @@ func Signout(ctx context.Context, logger *slog.Logger, sess *Session, sm Session
 	sm.Remove(sess)
 }
 
-func ReadBos(ctx context.Context, cfg Config, sess *Session, seq uint32, sm SessionManager, fm *FeedbagStore, cr *ChatRegistry, rwc io.ReadWriter, room ChatRoom, router Router, logger *slog.Logger) {
-	if err := router.WriteOServiceHostOnline(rwc, &seq); err != nil {
-		logger.ErrorContext(ctx, "error WriteOServiceHostOnline")
-	}
+type RouteSig func(ctx context.Context, rwc io.ReadWriter, u *uint32, snac oscar.SnacFrame, buf io.Reader) error
 
+func ReadBos(ctx context.Context, cfg Config, sess *Session, seq uint32, rwc io.ReadWriter, logger *slog.Logger, fn RouteSig) {
 	// buffered so that the go routine has room to exit
 	msgCh := make(chan IncomingMessage, 1)
 	errCh := make(chan error, 1)
@@ -108,7 +106,7 @@ func ReadBos(ctx context.Context, cfg Config, sess *Session, seq uint32, sm Sess
 	for {
 		select {
 		case m := <-msgCh:
-			if err := router.routeIncomingRequests(ctx, cfg, sm, sess, fm, cr, rwc, &seq, m.snac, m.buf, room); err != nil {
+			if err := fn(ctx, rwc, &seq, m.snac, m.buf); err != nil {
 				if errors.Is(err, ErrUnsupportedSubGroup) || errors.Is(err, ErrUnsupportedFoodGroup) {
 					if err1 := sendInvalidSNACErr(m.snac, rwc, &seq); err1 != nil {
 						err = errors.Join(err1, err)
@@ -163,7 +161,7 @@ func gracefulDisconnect(seq uint32, rwc io.ReadWriter) error {
 	}, rwc)
 }
 
-func HandleChatConnection(ctx context.Context, cfg Config, fm *FeedbagStore, cr *ChatRegistry, conn net.Conn, router Router, logger *slog.Logger) {
+func HandleChatConnection(ctx context.Context, cfg Config, cr *ChatRegistry, conn net.Conn, router ChatServiceRouter, logger *slog.Logger) {
 	cookie, seq, err := VerifyChatLogin(conn)
 	if err != nil {
 		logger.ErrorContext(ctx, "user disconnected with error", "err", err.Error())
@@ -193,7 +191,14 @@ func HandleChatConnection(ctx context.Context, cfg Config, fm *FeedbagStore, cr
 
 	ctx = context.WithValue(ctx, "screenName", chatSess.ScreenName)
 
-	ReadBos(ctx, cfg, chatSess, seq, room.SessionManager, fm, cr, conn, room, router, logger)
+	if err := router.WriteOServiceHostOnline(conn, &seq); err != nil {
+		logger.ErrorContext(ctx, "error WriteOServiceHostOnline")
+	}
+
+	fn := func(ctx context.Context, rwc io.ReadWriter, u *uint32, snac oscar.SnacFrame, buf io.Reader) error {
+		return router.Route(ctx, chatSess, rwc, u, snac, buf, room)
+	}
+	ReadBos(ctx, cfg, chatSess, seq, conn, logger, fn)
 }
 
 func HandleAuthConnection(cfg Config, sm *InMemorySessionManager, fm *FeedbagStore, conn net.Conn) {
@@ -218,8 +223,8 @@ func HandleAuthConnection(cfg Config, sm *InMemorySessionManager, fm *FeedbagSto
 	}
 }
 
-func HandleBOSConnection(ctx context.Context, cfg Config, sm *InMemorySessionManager, fm *FeedbagStore, cr *ChatRegistry, conn net.Conn, router Router, logger *slog.Logger) {
-	sess, seq, err := VerifyLogin(sm, conn)
+func HandleBOSConnection(ctx context.Context, cfg Config, conn net.Conn, router BOSServiceRouter, logger *slog.Logger) {
+	sess, seq, err := router.VerifyLogin(conn)
 	if err != nil {
 		logger.ErrorContext(ctx, "user disconnected with error", "err", err.Error())
 		return
@@ -230,15 +235,22 @@ func HandleBOSConnection(ctx context.Context, cfg Config, sm *InMemorySessionMan
 
 	go func() {
 		<-sess.Closed()
-		Signout(ctx, logger, sess, sm, fm)
+		router.Signout(ctx, logger, sess)
 	}()
 
 	ctx = context.WithValue(ctx, "screenName", sess.ScreenName)
 
-	ReadBos(ctx, cfg, sess, seq, sm, fm, cr, conn, ChatRoom{}, router, logger)
+	if err := router.WriteOServiceHostOnline(conn, &seq); err != nil {
+		logger.ErrorContext(ctx, "error WriteOServiceHostOnline")
+	}
+
+	fn := func(ctx context.Context, rwc io.ReadWriter, u *uint32, snac oscar.SnacFrame, buf io.Reader) error {
+		return router.Route(ctx, sess, rwc, u, snac, buf)
+	}
+	ReadBos(ctx, cfg, sess, seq, conn, logger, fn)
 }
 
-func ListenChat(cfg Config, fm *FeedbagStore, cr *ChatRegistry, logger *slog.Logger) {
+func ListenChat(cfg Config, router ChatServiceRouter, cr *ChatRegistry, logger *slog.Logger) {
 	addr := Address("", cfg.ChatPort)
 	listener, err := net.Listen("tcp", addr)
 	if err != nil {
@@ -249,7 +261,6 @@ func ListenChat(cfg Config, fm *FeedbagStore, cr *ChatRegistry, logger *slog.Log
 
 	logger.Info("starting service", "addr", addr)
 
-	router := NewRouterForChat(logger)
 	for {
 		conn, err := listener.Accept()
 		if err != nil {
@@ -259,11 +270,11 @@ func ListenChat(cfg Config, fm *FeedbagStore, cr *ChatRegistry, logger *slog.Log
 		ctx := context.Background()
 		ctx = context.WithValue(ctx, "ip", conn.RemoteAddr().String())
 		logger.DebugContext(ctx, "accepted connection")
-		go HandleChatConnection(ctx, cfg, fm, cr, conn, router, logger)
+		go HandleChatConnection(ctx, cfg, cr, conn, router, logger)
 	}
 }
 
-func ListenBOS(cfg Config, sm *InMemorySessionManager, fm *FeedbagStore, cr *ChatRegistry, logger *slog.Logger) {
+func ListenBOS(cfg Config, router BOSServiceRouter, logger *slog.Logger) {
 	addr := Address("", cfg.BOSPort)
 	listener, err := net.Listen("tcp", addr)
 	if err != nil {
@@ -274,7 +285,6 @@ func ListenBOS(cfg Config, sm *InMemorySessionManager, fm *FeedbagStore, cr *Cha
 
 	logger.Info("starting service", "addr", addr)
 
-	router := NewRouter(logger)
 	for {
 		conn, err := listener.Accept()
 		if err != nil {
@@ -284,7 +294,7 @@ func ListenBOS(cfg Config, sm *InMemorySessionManager, fm *FeedbagStore, cr *Cha
 		ctx := context.Background()
 		ctx = context.WithValue(ctx, "ip", conn.RemoteAddr().String())
 		logger.DebugContext(ctx, "accepted connection")
-		go HandleBOSConnection(ctx, cfg, sm, fm, cr, conn, router, logger)
+		go HandleBOSConnection(ctx, cfg, conn, router, logger)
 	}
 }
 

+ 36 - 31
server/feedbag.go

@@ -11,18 +11,21 @@ import (
 )
 
 type FeedbagHandler interface {
-	DeleteItemHandler(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error)
-	InsertItemHandler(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error)
-	QueryHandler(ctx context.Context, sess *Session, fm FeedbagManager) (XMessage, error)
-	QueryIfModifiedHandler(ctx context.Context, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error)
+	DeleteItemHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error)
+	InsertItemHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error)
+	QueryHandler(ctx context.Context, sess *Session) (XMessage, error)
+	QueryIfModifiedHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error)
 	RightsQueryHandler(context.Context) XMessage
 	StartClusterHandler(context.Context, oscar.SNAC_0x13_0x11_FeedbagStartCluster)
-	UpdateItemHandler(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error)
+	UpdateItemHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error)
 }
 
-func NewFeedbagRouter(logger *slog.Logger) FeedbagRouter {
+func NewFeedbagRouter(logger *slog.Logger, sm SessionManager, fm FeedbagManager) FeedbagRouter {
 	return FeedbagRouter{
-		FeedbagHandler: FeedbagService{},
+		FeedbagHandler: FeedbagService{
+			sm: sm,
+			fm: fm,
+		},
 		RouteLogger: RouteLogger{
 			Logger: logger,
 		},
@@ -34,7 +37,7 @@ type FeedbagRouter struct {
 	RouteLogger
 }
 
-func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sess *Session, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch SNACFrame.SubGroup {
 	case oscar.FeedbagRightsQuery:
 		inSNAC := oscar.SNAC_0x13_0x02_FeedbagRightsQuery{}
@@ -45,7 +48,7 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sm SessionManager, ses
 		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.snacFrame, outSNAC.snacOut)
 		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
 	case oscar.FeedbagQuery:
-		inSNAC, err := rt.QueryHandler(ctx, sess, fm)
+		inSNAC, err := rt.QueryHandler(ctx, sess)
 		if err != nil {
 			return err
 		}
@@ -56,7 +59,7 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sm SessionManager, ses
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.QueryIfModifiedHandler(ctx, sess, fm, inSNAC)
+		outSNAC, err := rt.QueryIfModifiedHandler(ctx, sess, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -70,7 +73,7 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sm SessionManager, ses
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.InsertItemHandler(ctx, sm, sess, fm, inSNAC)
+		outSNAC, err := rt.InsertItemHandler(ctx, sess, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -81,7 +84,7 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sm SessionManager, ses
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.UpdateItemHandler(ctx, sm, sess, fm, inSNAC)
+		outSNAC, err := rt.UpdateItemHandler(ctx, sess, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -92,7 +95,7 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sm SessionManager, ses
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.DeleteItemHandler(ctx, sm, sess, fm, inSNAC)
+		outSNAC, err := rt.DeleteItemHandler(ctx, sess, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -115,6 +118,8 @@ func (rt FeedbagRouter) RouteFeedbag(ctx context.Context, sm SessionManager, ses
 }
 
 type FeedbagService struct {
+	sm SessionManager
+	fm FeedbagManager
 }
 
 func (s FeedbagService) RightsQueryHandler(context.Context) XMessage {
@@ -165,8 +170,8 @@ func (s FeedbagService) RightsQueryHandler(context.Context) XMessage {
 	}
 }
 
-func (s FeedbagService) QueryHandler(ctx context.Context, sess *Session, fm FeedbagManager) (XMessage, error) {
-	fb, err := fm.Retrieve(sess.ScreenName)
+func (s FeedbagService) QueryHandler(_ context.Context, sess *Session) (XMessage, error) {
+	fb, err := s.fm.Retrieve(sess.ScreenName)
 	if err != nil {
 		return XMessage{}, err
 	}
@@ -174,7 +179,7 @@ func (s FeedbagService) QueryHandler(ctx context.Context, sess *Session, fm Feed
 	lm := time.UnixMilli(0)
 
 	if len(fb) > 0 {
-		lm, err = fm.LastModified(sess.ScreenName)
+		lm, err = s.fm.LastModified(sess.ScreenName)
 		if err != nil {
 			return XMessage{}, err
 		}
@@ -193,8 +198,8 @@ func (s FeedbagService) QueryHandler(ctx context.Context, sess *Session, fm Feed
 	}, nil
 }
 
-func (s FeedbagService) QueryIfModifiedHandler(ctx context.Context, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error) {
-	fb, err := fm.Retrieve(sess.ScreenName)
+func (s FeedbagService) QueryIfModifiedHandler(_ context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error) {
+	fb, err := s.fm.Retrieve(sess.ScreenName)
 	if err != nil {
 		return XMessage{}, err
 	}
@@ -202,7 +207,7 @@ func (s FeedbagService) QueryIfModifiedHandler(ctx context.Context, sess *Sessio
 	lm := time.UnixMilli(0)
 
 	if len(fb) > 0 {
-		lm, err = fm.LastModified(sess.ScreenName)
+		lm, err = s.fm.LastModified(sess.ScreenName)
 		if err != nil {
 			return XMessage{}, err
 		}
@@ -233,7 +238,7 @@ func (s FeedbagService) QueryIfModifiedHandler(ctx context.Context, sess *Sessio
 	}, nil
 }
 
-func (s FeedbagService) InsertItemHandler(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error) {
+func (s FeedbagService) InsertItemHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error) {
 	for _, item := range snacPayloadIn.Items {
 		// don't let users block themselves, it causes the AIM client to go
 		// into a weird state.
@@ -250,14 +255,14 @@ func (s FeedbagService) InsertItemHandler(ctx context.Context, sm SessionManager
 		}
 	}
 
-	if err := fm.Upsert(sess.ScreenName, snacPayloadIn.Items); err != nil {
+	if err := s.fm.Upsert(sess.ScreenName, snacPayloadIn.Items); err != nil {
 		return XMessage{}, nil
 	}
 
 	for _, item := range snacPayloadIn.Items {
 		switch item.ClassID {
 		case oscar.FeedbagClassIdBuddy, oscar.FeedbagClassIDPermit: // add new buddy
-			err := UnicastArrival(ctx, item.Name, sess.ScreenName, sm)
+			err := UnicastArrival(ctx, item.Name, sess.ScreenName, s.sm)
 			switch {
 			case errors.Is(err, ErrSessNotFound):
 				continue
@@ -266,7 +271,7 @@ func (s FeedbagService) InsertItemHandler(ctx context.Context, sm SessionManager
 			}
 		case oscar.FeedbagClassIDDeny: // block buddy
 			// notify this user that buddy is offline
-			err := UnicastDeparture(ctx, item.Name, sess.ScreenName, sm)
+			err := UnicastDeparture(ctx, item.Name, sess.ScreenName, s.sm)
 			switch {
 			case errors.Is(err, ErrSessNotFound):
 				continue
@@ -274,7 +279,7 @@ func (s FeedbagService) InsertItemHandler(ctx context.Context, sm SessionManager
 				return XMessage{}, err
 			}
 			// notify former buddy that this user is offline
-			if err := UnicastDeparture(ctx, sess.ScreenName, item.Name, sm); err != nil {
+			if err := UnicastDeparture(ctx, sess.ScreenName, item.Name, s.sm); err != nil {
 				return XMessage{}, err
 			}
 		}
@@ -294,15 +299,15 @@ func (s FeedbagService) InsertItemHandler(ctx context.Context, sm SessionManager
 	}, nil
 }
 
-func (s FeedbagService) UpdateItemHandler(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error) {
-	if err := fm.Upsert(sess.ScreenName, snacPayloadIn.Items); err != nil {
+func (s FeedbagService) UpdateItemHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error) {
+	if err := s.fm.Upsert(sess.ScreenName, snacPayloadIn.Items); err != nil {
 		return XMessage{}, nil
 	}
 
 	for _, item := range snacPayloadIn.Items {
 		switch item.ClassID {
 		case oscar.FeedbagClassIdBuddy, oscar.FeedbagClassIDPermit:
-			err := UnicastArrival(ctx, item.Name, sess.ScreenName, sm)
+			err := UnicastArrival(ctx, item.Name, sess.ScreenName, s.sm)
 			switch {
 			case errors.Is(err, ErrSessNotFound):
 				continue
@@ -326,21 +331,21 @@ func (s FeedbagService) UpdateItemHandler(ctx context.Context, sm SessionManager
 	}, nil
 }
 
-func (s FeedbagService) DeleteItemHandler(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error) {
-	if err := fm.Delete(sess.ScreenName, snacPayloadIn.Items); err != nil {
+func (s FeedbagService) DeleteItemHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error) {
+	if err := s.fm.Delete(sess.ScreenName, snacPayloadIn.Items); err != nil {
 		return XMessage{}, err
 	}
 
 	for _, item := range snacPayloadIn.Items {
 		if item.ClassID == oscar.FeedbagClassIDDeny {
-			err := UnicastArrival(ctx, item.Name, sess.ScreenName, sm)
+			err := UnicastArrival(ctx, item.Name, sess.ScreenName, s.sm)
 			switch {
 			case errors.Is(err, ErrSessNotFound):
 				continue
 			case err != nil:
 				return XMessage{}, err
 			}
-			err = UnicastArrival(ctx, sess.ScreenName, item.Name, sm)
+			err = UnicastArrival(ctx, sess.ScreenName, item.Name, s.sm)
 			switch {
 			case errors.Is(err, ErrSessNotFound):
 				continue

+ 70 - 78
server/feedbag_mock.go

@@ -22,23 +22,23 @@ func (_m *MockFeedbagHandler) EXPECT() *MockFeedbagHandler_Expecter {
 	return &MockFeedbagHandler_Expecter{mock: &_m.Mock}
 }
 
-// DeleteItemHandler provides a mock function with given fields: ctx, sm, sess, fm, snacPayloadIn
-func (_m *MockFeedbagHandler) DeleteItemHandler(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error) {
-	ret := _m.Called(ctx, sm, sess, fm, snacPayloadIn)
+// DeleteItemHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockFeedbagHandler) DeleteItemHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error)); ok {
-		return rf(ctx, sm, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) XMessage); ok {
-		r0 = rf(ctx, sm, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r0 = ret.Get(0).(XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) error); ok {
-		r1 = rf(ctx, sm, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -53,17 +53,15 @@ type MockFeedbagHandler_DeleteItemHandler_Call struct {
 
 // DeleteItemHandler is a helper method to define mock.On call
 //   - ctx context.Context
-//   - sm SessionManager
 //   - sess *Session
-//   - fm FeedbagManager
 //   - snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem
-func (_e *MockFeedbagHandler_Expecter) DeleteItemHandler(ctx interface{}, sm interface{}, sess interface{}, fm interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_DeleteItemHandler_Call {
-	return &MockFeedbagHandler_DeleteItemHandler_Call{Call: _e.mock.On("DeleteItemHandler", ctx, sm, sess, fm, snacPayloadIn)}
+func (_e *MockFeedbagHandler_Expecter) DeleteItemHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_DeleteItemHandler_Call {
+	return &MockFeedbagHandler_DeleteItemHandler_Call{Call: _e.mock.On("DeleteItemHandler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockFeedbagHandler_DeleteItemHandler_Call) Run(run func(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem)) *MockFeedbagHandler_DeleteItemHandler_Call {
+func (_c *MockFeedbagHandler_DeleteItemHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem)) *MockFeedbagHandler_DeleteItemHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(SessionManager), args[2].(*Session), args[3].(FeedbagManager), args[4].(oscar.SNAC_0x13_0x0A_FeedbagDeleteItem))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x13_0x0A_FeedbagDeleteItem))
 	})
 	return _c
 }
@@ -73,28 +71,28 @@ func (_c *MockFeedbagHandler_DeleteItemHandler_Call) Return(_a0 XMessage, _a1 er
 	return _c
 }
 
-func (_c *MockFeedbagHandler_DeleteItemHandler_Call) RunAndReturn(run func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error)) *MockFeedbagHandler_DeleteItemHandler_Call {
+func (_c *MockFeedbagHandler_DeleteItemHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error)) *MockFeedbagHandler_DeleteItemHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }
 
-// InsertItemHandler provides a mock function with given fields: ctx, sm, sess, fm, snacPayloadIn
-func (_m *MockFeedbagHandler) InsertItemHandler(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error) {
-	ret := _m.Called(ctx, sm, sess, fm, snacPayloadIn)
+// InsertItemHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockFeedbagHandler) InsertItemHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error)); ok {
-		return rf(ctx, sm, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x08_FeedbagInsertItem) XMessage); ok {
-		r0 = rf(ctx, sm, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x13_0x08_FeedbagInsertItem) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r0 = ret.Get(0).(XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x08_FeedbagInsertItem) error); ok {
-		r1 = rf(ctx, sm, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x13_0x08_FeedbagInsertItem) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -109,17 +107,15 @@ type MockFeedbagHandler_InsertItemHandler_Call struct {
 
 // InsertItemHandler is a helper method to define mock.On call
 //   - ctx context.Context
-//   - sm SessionManager
 //   - sess *Session
-//   - fm FeedbagManager
 //   - snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem
-func (_e *MockFeedbagHandler_Expecter) InsertItemHandler(ctx interface{}, sm interface{}, sess interface{}, fm interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_InsertItemHandler_Call {
-	return &MockFeedbagHandler_InsertItemHandler_Call{Call: _e.mock.On("InsertItemHandler", ctx, sm, sess, fm, snacPayloadIn)}
+func (_e *MockFeedbagHandler_Expecter) InsertItemHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_InsertItemHandler_Call {
+	return &MockFeedbagHandler_InsertItemHandler_Call{Call: _e.mock.On("InsertItemHandler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockFeedbagHandler_InsertItemHandler_Call) Run(run func(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem)) *MockFeedbagHandler_InsertItemHandler_Call {
+func (_c *MockFeedbagHandler_InsertItemHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem)) *MockFeedbagHandler_InsertItemHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(SessionManager), args[2].(*Session), args[3].(FeedbagManager), args[4].(oscar.SNAC_0x13_0x08_FeedbagInsertItem))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x13_0x08_FeedbagInsertItem))
 	})
 	return _c
 }
@@ -129,28 +125,28 @@ func (_c *MockFeedbagHandler_InsertItemHandler_Call) Return(_a0 XMessage, _a1 er
 	return _c
 }
 
-func (_c *MockFeedbagHandler_InsertItemHandler_Call) RunAndReturn(run func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error)) *MockFeedbagHandler_InsertItemHandler_Call {
+func (_c *MockFeedbagHandler_InsertItemHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error)) *MockFeedbagHandler_InsertItemHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }
 
-// QueryHandler provides a mock function with given fields: ctx, sess, fm
-func (_m *MockFeedbagHandler) QueryHandler(ctx context.Context, sess *Session, fm FeedbagManager) (XMessage, error) {
-	ret := _m.Called(ctx, sess, fm)
+// QueryHandler provides a mock function with given fields: ctx, sess
+func (_m *MockFeedbagHandler) QueryHandler(ctx context.Context, sess *Session) (XMessage, error) {
+	ret := _m.Called(ctx, sess)
 
 	var r0 XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, FeedbagManager) (XMessage, error)); ok {
-		return rf(ctx, sess, fm)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session) (XMessage, error)); ok {
+		return rf(ctx, sess)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, FeedbagManager) XMessage); ok {
-		r0 = rf(ctx, sess, fm)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session) XMessage); ok {
+		r0 = rf(ctx, sess)
 	} else {
 		r0 = ret.Get(0).(XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *Session, FeedbagManager) error); ok {
-		r1 = rf(ctx, sess, fm)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session) error); ok {
+		r1 = rf(ctx, sess)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -166,14 +162,13 @@ type MockFeedbagHandler_QueryHandler_Call struct {
 // QueryHandler is a helper method to define mock.On call
 //   - ctx context.Context
 //   - sess *Session
-//   - fm FeedbagManager
-func (_e *MockFeedbagHandler_Expecter) QueryHandler(ctx interface{}, sess interface{}, fm interface{}) *MockFeedbagHandler_QueryHandler_Call {
-	return &MockFeedbagHandler_QueryHandler_Call{Call: _e.mock.On("QueryHandler", ctx, sess, fm)}
+func (_e *MockFeedbagHandler_Expecter) QueryHandler(ctx interface{}, sess interface{}) *MockFeedbagHandler_QueryHandler_Call {
+	return &MockFeedbagHandler_QueryHandler_Call{Call: _e.mock.On("QueryHandler", ctx, sess)}
 }
 
-func (_c *MockFeedbagHandler_QueryHandler_Call) Run(run func(ctx context.Context, sess *Session, fm FeedbagManager)) *MockFeedbagHandler_QueryHandler_Call {
+func (_c *MockFeedbagHandler_QueryHandler_Call) Run(run func(ctx context.Context, sess *Session)) *MockFeedbagHandler_QueryHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*Session), args[2].(FeedbagManager))
+		run(args[0].(context.Context), args[1].(*Session))
 	})
 	return _c
 }
@@ -183,28 +178,28 @@ func (_c *MockFeedbagHandler_QueryHandler_Call) Return(_a0 XMessage, _a1 error)
 	return _c
 }
 
-func (_c *MockFeedbagHandler_QueryHandler_Call) RunAndReturn(run func(context.Context, *Session, FeedbagManager) (XMessage, error)) *MockFeedbagHandler_QueryHandler_Call {
+func (_c *MockFeedbagHandler_QueryHandler_Call) RunAndReturn(run func(context.Context, *Session) (XMessage, error)) *MockFeedbagHandler_QueryHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }
 
-// QueryIfModifiedHandler provides a mock function with given fields: ctx, sess, fm, snacPayloadIn
-func (_m *MockFeedbagHandler) QueryIfModifiedHandler(ctx context.Context, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error) {
-	ret := _m.Called(ctx, sess, fm, snacPayloadIn)
+// QueryIfModifiedHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockFeedbagHandler) QueryIfModifiedHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, FeedbagManager, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error)); ok {
-		return rf(ctx, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, FeedbagManager, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) XMessage); ok {
-		r0 = rf(ctx, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r0 = ret.Get(0).(XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *Session, FeedbagManager, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) error); ok {
-		r1 = rf(ctx, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -220,15 +215,14 @@ type MockFeedbagHandler_QueryIfModifiedHandler_Call struct {
 // QueryIfModifiedHandler is a helper method to define mock.On call
 //   - ctx context.Context
 //   - sess *Session
-//   - fm FeedbagManager
 //   - snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified
-func (_e *MockFeedbagHandler_Expecter) QueryIfModifiedHandler(ctx interface{}, sess interface{}, fm interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_QueryIfModifiedHandler_Call {
-	return &MockFeedbagHandler_QueryIfModifiedHandler_Call{Call: _e.mock.On("QueryIfModifiedHandler", ctx, sess, fm, snacPayloadIn)}
+func (_e *MockFeedbagHandler_Expecter) QueryIfModifiedHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_QueryIfModifiedHandler_Call {
+	return &MockFeedbagHandler_QueryIfModifiedHandler_Call{Call: _e.mock.On("QueryIfModifiedHandler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockFeedbagHandler_QueryIfModifiedHandler_Call) Run(run func(ctx context.Context, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified)) *MockFeedbagHandler_QueryIfModifiedHandler_Call {
+func (_c *MockFeedbagHandler_QueryIfModifiedHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified)) *MockFeedbagHandler_QueryIfModifiedHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*Session), args[2].(FeedbagManager), args[3].(oscar.SNAC_0x13_0x05_FeedbagQueryIfModified))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x13_0x05_FeedbagQueryIfModified))
 	})
 	return _c
 }
@@ -238,7 +232,7 @@ func (_c *MockFeedbagHandler_QueryIfModifiedHandler_Call) Return(_a0 XMessage, _
 	return _c
 }
 
-func (_c *MockFeedbagHandler_QueryIfModifiedHandler_Call) RunAndReturn(run func(context.Context, *Session, FeedbagManager, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error)) *MockFeedbagHandler_QueryIfModifiedHandler_Call {
+func (_c *MockFeedbagHandler_QueryIfModifiedHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error)) *MockFeedbagHandler_QueryIfModifiedHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }
@@ -319,23 +313,23 @@ func (_c *MockFeedbagHandler_StartClusterHandler_Call) RunAndReturn(run func(con
 	return _c
 }
 
-// UpdateItemHandler provides a mock function with given fields: ctx, sm, sess, fm, snacPayloadIn
-func (_m *MockFeedbagHandler) UpdateItemHandler(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error) {
-	ret := _m.Called(ctx, sm, sess, fm, snacPayloadIn)
+// UpdateItemHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockFeedbagHandler) UpdateItemHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error)); ok {
-		return rf(ctx, sm, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) XMessage); ok {
-		r0 = rf(ctx, sm, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r0 = ret.Get(0).(XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) error); ok {
-		r1 = rf(ctx, sm, sess, fm, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -350,17 +344,15 @@ type MockFeedbagHandler_UpdateItemHandler_Call struct {
 
 // UpdateItemHandler is a helper method to define mock.On call
 //   - ctx context.Context
-//   - sm SessionManager
 //   - sess *Session
-//   - fm FeedbagManager
 //   - snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem
-func (_e *MockFeedbagHandler_Expecter) UpdateItemHandler(ctx interface{}, sm interface{}, sess interface{}, fm interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_UpdateItemHandler_Call {
-	return &MockFeedbagHandler_UpdateItemHandler_Call{Call: _e.mock.On("UpdateItemHandler", ctx, sm, sess, fm, snacPayloadIn)}
+func (_e *MockFeedbagHandler_Expecter) UpdateItemHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_UpdateItemHandler_Call {
+	return &MockFeedbagHandler_UpdateItemHandler_Call{Call: _e.mock.On("UpdateItemHandler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockFeedbagHandler_UpdateItemHandler_Call) Run(run func(ctx context.Context, sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem)) *MockFeedbagHandler_UpdateItemHandler_Call {
+func (_c *MockFeedbagHandler_UpdateItemHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem)) *MockFeedbagHandler_UpdateItemHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(SessionManager), args[2].(*Session), args[3].(FeedbagManager), args[4].(oscar.SNAC_0x13_0x09_FeedbagUpdateItem))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x13_0x09_FeedbagUpdateItem))
 	})
 	return _c
 }
@@ -370,7 +362,7 @@ func (_c *MockFeedbagHandler_UpdateItemHandler_Call) Return(_a0 XMessage, _a1 er
 	return _c
 }
 
-func (_c *MockFeedbagHandler_UpdateItemHandler_Call) RunAndReturn(run func(context.Context, SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error)) *MockFeedbagHandler_UpdateItemHandler_Call {
+func (_c *MockFeedbagHandler_UpdateItemHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error)) *MockFeedbagHandler_UpdateItemHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 19 - 12
server/feedbag_test.go

@@ -91,8 +91,10 @@ func TestQueryHandler(t *testing.T) {
 			senderSession := &Session{
 				ScreenName: tc.screenName,
 			}
-			svc := FeedbagService{}
-			outputSNAC, err := svc.QueryHandler(nil, senderSession, fm)
+			svc := FeedbagService{
+				fm: fm,
+			}
+			outputSNAC, err := svc.QueryHandler(nil, senderSession)
 			assert.NoError(t, err)
 			assert.Equal(t, tc.expectOutput, outputSNAC)
 		})
@@ -215,8 +217,10 @@ func TestQueryIfModifiedHandler(t *testing.T) {
 			senderSession := &Session{
 				ScreenName: tc.screenName,
 			}
-			svc := FeedbagService{}
-			outputSNAC, err := svc.QueryIfModifiedHandler(nil, senderSession, fm, tc.inputSNAC)
+			svc := FeedbagService{
+				fm: fm,
+			}
+			outputSNAC, err := svc.QueryIfModifiedHandler(nil, senderSession, tc.inputSNAC)
 			assert.NoError(t, err)
 			//
 			// verify output
@@ -553,8 +557,11 @@ func TestInsertItemHandler(t *testing.T) {
 			//
 			// send input SNAC
 			//
-			svc := FeedbagService{}
-			output, err := svc.InsertItemHandler(nil, sm, tc.userSession, fm, tc.inputSNAC)
+			svc := FeedbagService{
+				fm: fm,
+				sm: sm,
+			}
+			output, err := svc.InsertItemHandler(nil, tc.userSession, tc.inputSNAC)
 			assert.NoError(t, err)
 			//
 			// verify response
@@ -796,15 +803,15 @@ func TestFeedbagRouter_RouteFeedbag(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			svc := NewMockFeedbagHandler(t)
 			svc.EXPECT().
-				DeleteItemHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				DeleteItemHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
-				QueryHandler(mock.Anything, mock.Anything, mock.Anything).
+				QueryHandler(mock.Anything, mock.Anything).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
-				QueryIfModifiedHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				QueryIfModifiedHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
@@ -812,11 +819,11 @@ func TestFeedbagRouter_RouteFeedbag(t *testing.T) {
 				Return(tc.output).
 				Maybe()
 			svc.EXPECT().
-				InsertItemHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				InsertItemHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
-				UpdateItemHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				UpdateItemHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
@@ -836,7 +843,7 @@ func TestFeedbagRouter_RouteFeedbag(t *testing.T) {
 			bufOut := &bytes.Buffer{}
 			seq := uint32(0)
 
-			err := router.RouteFeedbag(nil, nil, nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			err := router.RouteFeedbag(nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
 			if tc.expectErr != nil {
 				return

+ 26 - 21
server/icbm.go

@@ -15,15 +15,18 @@ const (
 )
 
 type ICBMHandler interface {
-	ChannelMsgToHostHandler(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error)
-	ClientEventHandler(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent) error
-	EvilRequestHandler(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error)
+	ChannelMsgToHostHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error)
+	ClientEventHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent) error
+	EvilRequestHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error)
 	ParameterQueryHandler(context.Context) XMessage
 }
 
-func NewICBMRouter(logger *slog.Logger) ICBMRouter {
+func NewICBMRouter(logger *slog.Logger, sm SessionManager, fm FeedbagManager) ICBMRouter {
 	return ICBMRouter{
-		ICBMHandler: ICBMService{},
+		ICBMHandler: ICBMService{
+			sm: sm,
+			fm: fm,
+		},
 		RouteLogger: RouteLogger{
 			Logger: logger,
 		},
@@ -35,7 +38,7 @@ type ICBMRouter struct {
 	RouteLogger
 }
 
-func (rt *ICBMRouter) RouteICBM(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func (rt *ICBMRouter) RouteICBM(ctx context.Context, sess *Session, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch SNACFrame.SubGroup {
 	case oscar.ICBMAddParameters:
 		inSNAC := oscar.SNAC_0x04_0x02_ICBMAddParameters{}
@@ -50,7 +53,7 @@ func (rt *ICBMRouter) RouteICBM(ctx context.Context, sm SessionManager, fm Feedb
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.ChannelMsgToHostHandler(ctx, sm, fm, sess, inSNAC)
+		outSNAC, err := rt.ChannelMsgToHostHandler(ctx, sess, inSNAC)
 		if err != nil || outSNAC == nil {
 			return err
 		}
@@ -62,7 +65,7 @@ func (rt *ICBMRouter) RouteICBM(ctx context.Context, sm SessionManager, fm Feedb
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.EvilRequestHandler(ctx, sm, fm, sess, inSNAC)
+		outSNAC, err := rt.EvilRequestHandler(ctx, sess, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -78,13 +81,15 @@ func (rt *ICBMRouter) RouteICBM(ctx context.Context, sm SessionManager, fm Feedb
 			return err
 		}
 		rt.logRequest(ctx, SNACFrame, inSNAC)
-		return rt.ClientEventHandler(ctx, sm, fm, sess, inSNAC)
+		return rt.ClientEventHandler(ctx, sess, inSNAC)
 	default:
 		return ErrUnsupportedSubGroup
 	}
 }
 
 type ICBMService struct {
+	sm SessionManager
+	fm FeedbagManager
 }
 
 func (s ICBMService) ParameterQueryHandler(context.Context) XMessage {
@@ -104,8 +109,8 @@ func (s ICBMService) ParameterQueryHandler(context.Context) XMessage {
 	}
 }
 
-func (s ICBMService) ChannelMsgToHostHandler(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error) {
-	blocked, err := fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
+func (s ICBMService) ChannelMsgToHostHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error) {
+	blocked, err := s.fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
 	if err != nil {
 		return nil, err
 	}
@@ -126,7 +131,7 @@ func (s ICBMService) ChannelMsgToHostHandler(ctx context.Context, sm SessionMana
 		}, nil
 	}
 
-	recipSess, err := sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
+	recipSess, err := s.sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
 	switch {
 	case errors.Is(err, ErrSessNotFound):
 		return &XMessage{
@@ -163,7 +168,7 @@ func (s ICBMService) ChannelMsgToHostHandler(ctx context.Context, sm SessionMana
 	// far as I can tell.
 	clientIM.AddTLVList(snacPayloadIn.TLVRestBlock.TLVList)
 
-	sm.SendToScreenName(ctx, recipSess.ScreenName, XMessage{
+	s.sm.SendToScreenName(ctx, recipSess.ScreenName, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.ICBM,
 			SubGroup:  oscar.ICBMChannelMsgToclient,
@@ -190,8 +195,8 @@ func (s ICBMService) ChannelMsgToHostHandler(ctx context.Context, sm SessionMana
 	}, nil
 }
 
-func (s ICBMService) ClientEventHandler(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent) error {
-	blocked, err := fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
+func (s ICBMService) ClientEventHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent) error {
+	blocked, err := s.fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
 
 	switch {
 	case err != nil:
@@ -199,7 +204,7 @@ func (s ICBMService) ClientEventHandler(ctx context.Context, sm SessionManager,
 	case blocked != BlockedNo:
 		return nil
 	default:
-		sm.SendToScreenName(ctx, snacPayloadIn.ScreenName, XMessage{
+		s.sm.SendToScreenName(ctx, snacPayloadIn.ScreenName, XMessage{
 			snacFrame: oscar.SnacFrame{
 				FoodGroup: oscar.ICBM,
 				SubGroup:  oscar.ICBMClientEvent,
@@ -215,7 +220,7 @@ func (s ICBMService) ClientEventHandler(ctx context.Context, sm SessionManager,
 	}
 }
 
-func (s ICBMService) EvilRequestHandler(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error) {
+func (s ICBMService) EvilRequestHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error) {
 	// don't let users warn themselves, it causes the AIM client to go into a
 	// weird state.
 	if snacPayloadIn.ScreenName == sess.ScreenName {
@@ -230,7 +235,7 @@ func (s ICBMService) EvilRequestHandler(ctx context.Context, sm SessionManager,
 		}, nil
 	}
 
-	blocked, err := fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
+	blocked, err := s.fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
 	if err != nil {
 		return XMessage{}, nil
 	}
@@ -246,7 +251,7 @@ func (s ICBMService) EvilRequestHandler(ctx context.Context, sm SessionManager,
 		}, nil
 	}
 
-	recipSess, err := sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
+	recipSess, err := s.sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
 	if err != nil {
 		return XMessage{}, nil
 	}
@@ -272,7 +277,7 @@ func (s ICBMService) EvilRequestHandler(ctx context.Context, sm SessionManager,
 		}
 	}
 
-	sm.SendToScreenName(ctx, recipSess.ScreenName, XMessage{
+	s.sm.SendToScreenName(ctx, recipSess.ScreenName, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.OSERVICE,
 			SubGroup:  oscar.OServiceEvilNotification,
@@ -280,7 +285,7 @@ func (s ICBMService) EvilRequestHandler(ctx context.Context, sm SessionManager,
 		snacOut: notif,
 	})
 
-	if err := BroadcastArrival(ctx, recipSess, sm, fm); err != nil {
+	if err := BroadcastArrival(ctx, recipSess, s.sm, s.fm); err != nil {
 		return XMessage{}, nil
 	}
 

+ 38 - 44
server/icbm_mock.go

@@ -22,25 +22,25 @@ func (_m *MockICBMHandler) EXPECT() *MockICBMHandler_Expecter {
 	return &MockICBMHandler_Expecter{mock: &_m.Mock}
 }
 
-// ChannelMsgToHostHandler provides a mock function with given fields: ctx, sm, fm, sess, snacPayloadIn
-func (_m *MockICBMHandler) ChannelMsgToHostHandler(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error) {
-	ret := _m.Called(ctx, sm, fm, sess, snacPayloadIn)
+// ChannelMsgToHostHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockICBMHandler) ChannelMsgToHostHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 *XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error)); ok {
-		return rf(ctx, sm, fm, sess, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) *XMessage); ok {
-		r0 = rf(ctx, sm, fm, sess, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) *XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		if ret.Get(0) != nil {
 			r0 = ret.Get(0).(*XMessage)
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) error); ok {
-		r1 = rf(ctx, sm, fm, sess, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -55,17 +55,15 @@ type MockICBMHandler_ChannelMsgToHostHandler_Call struct {
 
 // ChannelMsgToHostHandler is a helper method to define mock.On call
 //   - ctx context.Context
-//   - sm SessionManager
-//   - fm FeedbagManager
 //   - sess *Session
 //   - snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost
-func (_e *MockICBMHandler_Expecter) ChannelMsgToHostHandler(ctx interface{}, sm interface{}, fm interface{}, sess interface{}, snacPayloadIn interface{}) *MockICBMHandler_ChannelMsgToHostHandler_Call {
-	return &MockICBMHandler_ChannelMsgToHostHandler_Call{Call: _e.mock.On("ChannelMsgToHostHandler", ctx, sm, fm, sess, snacPayloadIn)}
+func (_e *MockICBMHandler_Expecter) ChannelMsgToHostHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockICBMHandler_ChannelMsgToHostHandler_Call {
+	return &MockICBMHandler_ChannelMsgToHostHandler_Call{Call: _e.mock.On("ChannelMsgToHostHandler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockICBMHandler_ChannelMsgToHostHandler_Call) Run(run func(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost)) *MockICBMHandler_ChannelMsgToHostHandler_Call {
+func (_c *MockICBMHandler_ChannelMsgToHostHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost)) *MockICBMHandler_ChannelMsgToHostHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(SessionManager), args[2].(FeedbagManager), args[3].(*Session), args[4].(oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost))
 	})
 	return _c
 }
@@ -75,18 +73,18 @@ func (_c *MockICBMHandler_ChannelMsgToHostHandler_Call) Return(_a0 *XMessage, _a
 	return _c
 }
 
-func (_c *MockICBMHandler_ChannelMsgToHostHandler_Call) RunAndReturn(run func(context.Context, SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error)) *MockICBMHandler_ChannelMsgToHostHandler_Call {
+func (_c *MockICBMHandler_ChannelMsgToHostHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*XMessage, error)) *MockICBMHandler_ChannelMsgToHostHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }
 
-// ClientEventHandler provides a mock function with given fields: ctx, sm, fm, sess, snacPayloadIn
-func (_m *MockICBMHandler) ClientEventHandler(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent) error {
-	ret := _m.Called(ctx, sm, fm, sess, snacPayloadIn)
+// ClientEventHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockICBMHandler) ClientEventHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent) error {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x14_ICBMClientEvent) error); ok {
-		r0 = rf(ctx, sm, fm, sess, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x04_0x14_ICBMClientEvent) error); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -101,17 +99,15 @@ type MockICBMHandler_ClientEventHandler_Call struct {
 
 // ClientEventHandler is a helper method to define mock.On call
 //   - ctx context.Context
-//   - sm SessionManager
-//   - fm FeedbagManager
 //   - sess *Session
 //   - snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent
-func (_e *MockICBMHandler_Expecter) ClientEventHandler(ctx interface{}, sm interface{}, fm interface{}, sess interface{}, snacPayloadIn interface{}) *MockICBMHandler_ClientEventHandler_Call {
-	return &MockICBMHandler_ClientEventHandler_Call{Call: _e.mock.On("ClientEventHandler", ctx, sm, fm, sess, snacPayloadIn)}
+func (_e *MockICBMHandler_Expecter) ClientEventHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockICBMHandler_ClientEventHandler_Call {
+	return &MockICBMHandler_ClientEventHandler_Call{Call: _e.mock.On("ClientEventHandler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockICBMHandler_ClientEventHandler_Call) Run(run func(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent)) *MockICBMHandler_ClientEventHandler_Call {
+func (_c *MockICBMHandler_ClientEventHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x14_ICBMClientEvent)) *MockICBMHandler_ClientEventHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(SessionManager), args[2].(FeedbagManager), args[3].(*Session), args[4].(oscar.SNAC_0x04_0x14_ICBMClientEvent))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x04_0x14_ICBMClientEvent))
 	})
 	return _c
 }
@@ -121,28 +117,28 @@ func (_c *MockICBMHandler_ClientEventHandler_Call) Return(_a0 error) *MockICBMHa
 	return _c
 }
 
-func (_c *MockICBMHandler_ClientEventHandler_Call) RunAndReturn(run func(context.Context, SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x14_ICBMClientEvent) error) *MockICBMHandler_ClientEventHandler_Call {
+func (_c *MockICBMHandler_ClientEventHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x04_0x14_ICBMClientEvent) error) *MockICBMHandler_ClientEventHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }
 
-// EvilRequestHandler provides a mock function with given fields: ctx, sm, fm, sess, snacPayloadIn
-func (_m *MockICBMHandler) EvilRequestHandler(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error) {
-	ret := _m.Called(ctx, sm, fm, sess, snacPayloadIn)
+// EvilRequestHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockICBMHandler) EvilRequestHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error)); ok {
-		return rf(ctx, sm, fm, sess, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) XMessage); ok {
-		r0 = rf(ctx, sm, fm, sess, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r0 = ret.Get(0).(XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) error); ok {
-		r1 = rf(ctx, sm, fm, sess, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -157,17 +153,15 @@ type MockICBMHandler_EvilRequestHandler_Call struct {
 
 // EvilRequestHandler is a helper method to define mock.On call
 //   - ctx context.Context
-//   - sm SessionManager
-//   - fm FeedbagManager
 //   - sess *Session
 //   - snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest
-func (_e *MockICBMHandler_Expecter) EvilRequestHandler(ctx interface{}, sm interface{}, fm interface{}, sess interface{}, snacPayloadIn interface{}) *MockICBMHandler_EvilRequestHandler_Call {
-	return &MockICBMHandler_EvilRequestHandler_Call{Call: _e.mock.On("EvilRequestHandler", ctx, sm, fm, sess, snacPayloadIn)}
+func (_e *MockICBMHandler_Expecter) EvilRequestHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockICBMHandler_EvilRequestHandler_Call {
+	return &MockICBMHandler_EvilRequestHandler_Call{Call: _e.mock.On("EvilRequestHandler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockICBMHandler_EvilRequestHandler_Call) Run(run func(ctx context.Context, sm SessionManager, fm FeedbagManager, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest)) *MockICBMHandler_EvilRequestHandler_Call {
+func (_c *MockICBMHandler_EvilRequestHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x04_0x08_ICBMEvilRequest)) *MockICBMHandler_EvilRequestHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(SessionManager), args[2].(FeedbagManager), args[3].(*Session), args[4].(oscar.SNAC_0x04_0x08_ICBMEvilRequest))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x04_0x08_ICBMEvilRequest))
 	})
 	return _c
 }
@@ -177,7 +171,7 @@ func (_c *MockICBMHandler_EvilRequestHandler_Call) Return(_a0 XMessage, _a1 erro
 	return _c
 }
 
-func (_c *MockICBMHandler_EvilRequestHandler_Call) RunAndReturn(run func(context.Context, SessionManager, FeedbagManager, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error)) *MockICBMHandler_EvilRequestHandler_Call {
+func (_c *MockICBMHandler_EvilRequestHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x04_0x08_ICBMEvilRequest) (XMessage, error)) *MockICBMHandler_EvilRequestHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 19 - 10
server/icbm_test.go

@@ -243,8 +243,11 @@ func TestSendAndReceiveChannelMsgTohost(t *testing.T) {
 			//
 			// send input SNAC
 			//
-			svc := ICBMService{}
-			outputSNAC, err := svc.ChannelMsgToHostHandler(nil, sm, fm, tc.senderSession, tc.inputSNAC)
+			svc := ICBMService{
+				sm: sm,
+				fm: fm,
+			}
+			outputSNAC, err := svc.ChannelMsgToHostHandler(nil, tc.senderSession, tc.inputSNAC)
 			assert.NoError(t, err)
 			//
 			// verify output
@@ -322,8 +325,11 @@ func TestSendAndReceiveClientEvent(t *testing.T) {
 			senderSession := &Session{
 				ScreenName: tc.senderScreenName,
 			}
-			svc := ICBMService{}
-			assert.NoError(t, svc.ClientEventHandler(nil, sm, fm, senderSession, tc.inputSNAC))
+			svc := ICBMService{
+				sm: sm,
+				fm: fm,
+			}
+			assert.NoError(t, svc.ClientEventHandler(nil, senderSession, tc.inputSNAC))
 		})
 	}
 }
@@ -554,8 +560,11 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 			senderSession := &Session{
 				ScreenName: tc.senderSession.ScreenName,
 			}
-			svc := ICBMService{}
-			outputSNAC, err := svc.EvilRequestHandler(nil, sm, fm, senderSession, tc.inputSNAC)
+			svc := ICBMService{
+				sm: sm,
+				fm: fm,
+			}
+			outputSNAC, err := svc.EvilRequestHandler(nil, senderSession, tc.inputSNAC)
 			assert.NoError(t, err)
 			assert.Equal(t, tc.expectOutput, outputSNAC)
 		})
@@ -706,16 +715,16 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			svc := NewMockICBMHandler(t)
 			svc.EXPECT().
-				ChannelMsgToHostHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				ChannelMsgToHostHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
-				ClientEventHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				ClientEventHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.handlerErr).
 				Maybe()
 			if tc.output != nil {
 				svc.EXPECT().
-					EvilRequestHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+					EvilRequestHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 					Return(*tc.output, tc.handlerErr).
 					Maybe()
 				svc.EXPECT().
@@ -737,7 +746,7 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 			bufOut := &bytes.Buffer{}
 			seq := uint32(1)
 
-			err := router.RouteICBM(nil, nil, nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			err := router.RouteICBM(nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
 			if tc.expectErr != nil {
 				return

+ 23 - 16
server/locate.go

@@ -12,14 +12,18 @@ import (
 type LocateHandler interface {
 	RightsQueryHandler(ctx context.Context) XMessage
 	SetDirInfoHandler(ctx context.Context) XMessage
-	SetInfoHandler(ctx context.Context, sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo) error
+	SetInfoHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo) error
 	SetKeywordInfoHandler(ctx context.Context) XMessage
-	UserInfoQuery2Handler(ctx context.Context, sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error)
+	UserInfoQuery2Handler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error)
 }
 
-func NewLocateRouter(logger *slog.Logger) LocateRouter {
+func NewLocateRouter(logger *slog.Logger, sm SessionManager, fm FeedbagManager, pm ProfileManager) LocateRouter {
 	return LocateRouter{
-		LocateHandler: LocateService{},
+		LocateHandler: LocateService{
+			sm: sm,
+			fm: fm,
+			pm: pm,
+		},
 		RouteLogger: RouteLogger{
 			Logger: logger,
 		},
@@ -31,7 +35,7 @@ type LocateRouter struct {
 	RouteLogger
 }
 
-func (rt LocateRouter) RouteLocate(ctx context.Context, sess *Session, sm SessionManager, fm *FeedbagStore, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func (rt LocateRouter) RouteLocate(ctx context.Context, sess *Session, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch SNACFrame.SubGroup {
 	case oscar.LocateRightsQuery:
 		outSNAC := rt.RightsQueryHandler(ctx)
@@ -43,7 +47,7 @@ func (rt LocateRouter) RouteLocate(ctx context.Context, sess *Session, sm Sessio
 			return err
 		}
 		rt.logRequest(ctx, SNACFrame, inSNAC)
-		return rt.SetInfoHandler(ctx, sess, sm, fm, fm, inSNAC)
+		return rt.SetInfoHandler(ctx, sess, inSNAC)
 	case oscar.LocateSetDirInfo:
 		inSNAC := oscar.SNAC_0x02_0x09_LocateSetDirInfo{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -69,7 +73,7 @@ func (rt LocateRouter) RouteLocate(ctx context.Context, sess *Session, sm Sessio
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.UserInfoQuery2Handler(ctx, sess, sm, fm, fm, inSNAC)
+		outSNAC, err := rt.UserInfoQuery2Handler(ctx, sess, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -81,6 +85,9 @@ func (rt LocateRouter) RouteLocate(ctx context.Context, sess *Session, sm Sessio
 }
 
 type LocateService struct {
+	sm SessionManager
+	fm FeedbagManager
+	pm ProfileManager
 }
 
 func (s LocateService) RightsQueryHandler(context.Context) XMessage {
@@ -103,10 +110,10 @@ func (s LocateService) RightsQueryHandler(context.Context) XMessage {
 	}
 }
 
-func (s LocateService) SetInfoHandler(ctx context.Context, sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo) error {
+func (s LocateService) SetInfoHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo) error {
 	// update profile
 	if profile, hasProfile := snacPayloadIn.GetString(oscar.LocateTLVTagsInfoSigData); hasProfile {
-		if err := pm.UpsertProfile(sess.ScreenName, profile); err != nil {
+		if err := s.pm.UpsertProfile(sess.ScreenName, profile); err != nil {
 			return err
 		}
 	}
@@ -114,15 +121,15 @@ func (s LocateService) SetInfoHandler(ctx context.Context, sess *Session, sm Ses
 	// broadcast away message change to buddies
 	if awayMsg, hasAwayMsg := snacPayloadIn.GetString(oscar.LocateTLVTagsInfoUnavailableData); hasAwayMsg {
 		sess.SetAwayMessage(awayMsg)
-		if err := BroadcastArrival(ctx, sess, sm, fm); err != nil {
+		if err := BroadcastArrival(ctx, sess, s.sm, s.fm); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func (s LocateService) UserInfoQuery2Handler(ctx context.Context, sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error) {
-	blocked, err := fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
+func (s LocateService) UserInfoQuery2Handler(_ context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error) {
+	blocked, err := s.fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
 	switch {
 	case err != nil:
 		return XMessage{}, err
@@ -138,7 +145,7 @@ func (s LocateService) UserInfoQuery2Handler(ctx context.Context, sess *Session,
 		}, nil
 	}
 
-	buddySess, err := sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
+	buddySess, err := s.sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
 	switch {
 	case errors.Is(err, ErrSessNotFound):
 		return XMessage{
@@ -157,7 +164,7 @@ func (s LocateService) UserInfoQuery2Handler(ctx context.Context, sess *Session,
 	var list oscar.TLVList
 
 	if snacPayloadIn.RequestProfile() {
-		profile, err := pm.RetrieveProfile(snacPayloadIn.ScreenName)
+		profile, err := s.pm.RetrieveProfile(snacPayloadIn.ScreenName)
 		if err != nil {
 			return XMessage{}, err
 		}
@@ -188,7 +195,7 @@ func (s LocateService) UserInfoQuery2Handler(ctx context.Context, sess *Session,
 	}, nil
 }
 
-func (s LocateService) SetDirInfoHandler(ctx context.Context) XMessage {
+func (s LocateService) SetDirInfoHandler(_ context.Context) XMessage {
 	return XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.LOCATE,
@@ -200,7 +207,7 @@ func (s LocateService) SetDirInfoHandler(ctx context.Context) XMessage {
 	}
 }
 
-func (s LocateService) SetKeywordInfoHandler(ctx context.Context) XMessage {
+func (s LocateService) SetKeywordInfoHandler(_ context.Context) XMessage {
 	return XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.LOCATE,

+ 24 - 30
server/locate_mock.go

@@ -106,13 +106,13 @@ func (_c *MockLocateHandler_SetDirInfoHandler_Call) RunAndReturn(run func(contex
 	return _c
 }
 
-// SetInfoHandler provides a mock function with given fields: ctx, sess, sm, fm, pm, snacPayloadIn
-func (_m *MockLocateHandler) SetInfoHandler(ctx context.Context, sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo) error {
-	ret := _m.Called(ctx, sess, sm, fm, pm, snacPayloadIn)
+// SetInfoHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockLocateHandler) SetInfoHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo) error {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x04_LocateSetInfo) error); ok {
-		r0 = rf(ctx, sess, sm, fm, pm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x02_0x04_LocateSetInfo) error); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -128,17 +128,14 @@ type MockLocateHandler_SetInfoHandler_Call struct {
 // SetInfoHandler is a helper method to define mock.On call
 //   - ctx context.Context
 //   - sess *Session
-//   - sm SessionManager
-//   - fm FeedbagManager
-//   - pm ProfileManager
 //   - snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo
-func (_e *MockLocateHandler_Expecter) SetInfoHandler(ctx interface{}, sess interface{}, sm interface{}, fm interface{}, pm interface{}, snacPayloadIn interface{}) *MockLocateHandler_SetInfoHandler_Call {
-	return &MockLocateHandler_SetInfoHandler_Call{Call: _e.mock.On("SetInfoHandler", ctx, sess, sm, fm, pm, snacPayloadIn)}
+func (_e *MockLocateHandler_Expecter) SetInfoHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockLocateHandler_SetInfoHandler_Call {
+	return &MockLocateHandler_SetInfoHandler_Call{Call: _e.mock.On("SetInfoHandler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockLocateHandler_SetInfoHandler_Call) Run(run func(ctx context.Context, sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo)) *MockLocateHandler_SetInfoHandler_Call {
+func (_c *MockLocateHandler_SetInfoHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo)) *MockLocateHandler_SetInfoHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*Session), args[2].(SessionManager), args[3].(FeedbagManager), args[4].(ProfileManager), args[5].(oscar.SNAC_0x02_0x04_LocateSetInfo))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x02_0x04_LocateSetInfo))
 	})
 	return _c
 }
@@ -148,7 +145,7 @@ func (_c *MockLocateHandler_SetInfoHandler_Call) Return(_a0 error) *MockLocateHa
 	return _c
 }
 
-func (_c *MockLocateHandler_SetInfoHandler_Call) RunAndReturn(run func(context.Context, *Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x04_LocateSetInfo) error) *MockLocateHandler_SetInfoHandler_Call {
+func (_c *MockLocateHandler_SetInfoHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x02_0x04_LocateSetInfo) error) *MockLocateHandler_SetInfoHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }
@@ -195,23 +192,23 @@ func (_c *MockLocateHandler_SetKeywordInfoHandler_Call) RunAndReturn(run func(co
 	return _c
 }
 
-// UserInfoQuery2Handler provides a mock function with given fields: ctx, sess, sm, fm, pm, snacPayloadIn
-func (_m *MockLocateHandler) UserInfoQuery2Handler(ctx context.Context, sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error) {
-	ret := _m.Called(ctx, sess, sm, fm, pm, snacPayloadIn)
+// UserInfoQuery2Handler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockLocateHandler) UserInfoQuery2Handler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error)); ok {
-		return rf(ctx, sess, sm, fm, pm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) XMessage); ok {
-		r0 = rf(ctx, sess, sm, fm, pm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r0 = ret.Get(0).(XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) error); ok {
-		r1 = rf(ctx, sess, sm, fm, pm, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -227,17 +224,14 @@ type MockLocateHandler_UserInfoQuery2Handler_Call struct {
 // UserInfoQuery2Handler is a helper method to define mock.On call
 //   - ctx context.Context
 //   - sess *Session
-//   - sm SessionManager
-//   - fm FeedbagManager
-//   - pm ProfileManager
 //   - snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2
-func (_e *MockLocateHandler_Expecter) UserInfoQuery2Handler(ctx interface{}, sess interface{}, sm interface{}, fm interface{}, pm interface{}, snacPayloadIn interface{}) *MockLocateHandler_UserInfoQuery2Handler_Call {
-	return &MockLocateHandler_UserInfoQuery2Handler_Call{Call: _e.mock.On("UserInfoQuery2Handler", ctx, sess, sm, fm, pm, snacPayloadIn)}
+func (_e *MockLocateHandler_Expecter) UserInfoQuery2Handler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockLocateHandler_UserInfoQuery2Handler_Call {
+	return &MockLocateHandler_UserInfoQuery2Handler_Call{Call: _e.mock.On("UserInfoQuery2Handler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockLocateHandler_UserInfoQuery2Handler_Call) Run(run func(ctx context.Context, sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2)) *MockLocateHandler_UserInfoQuery2Handler_Call {
+func (_c *MockLocateHandler_UserInfoQuery2Handler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2)) *MockLocateHandler_UserInfoQuery2Handler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*Session), args[2].(SessionManager), args[3].(FeedbagManager), args[4].(ProfileManager), args[5].(oscar.SNAC_0x02_0x15_LocateUserInfoQuery2))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x02_0x15_LocateUserInfoQuery2))
 	})
 	return _c
 }
@@ -247,7 +241,7 @@ func (_c *MockLocateHandler_UserInfoQuery2Handler_Call) Return(_a0 XMessage, _a1
 	return _c
 }
 
-func (_c *MockLocateHandler_UserInfoQuery2Handler_Call) RunAndReturn(run func(context.Context, *Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error)) *MockLocateHandler_UserInfoQuery2Handler_Call {
+func (_c *MockLocateHandler_UserInfoQuery2Handler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error)) *MockLocateHandler_UserInfoQuery2Handler_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 9 - 5
server/locate_test.go

@@ -284,8 +284,12 @@ func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
 					Maybe()
 			}
 
-			svc := LocateService{}
-			outputSNAC, err := svc.UserInfoQuery2Handler(context.Background(), tc.userSession, sm, fm, pm, tc.inputSNAC)
+			svc := LocateService{
+				sm: sm,
+				fm: fm,
+				pm: pm,
+			}
+			outputSNAC, err := svc.UserInfoQuery2Handler(context.Background(), tc.userSession, tc.inputSNAC)
 			assert.NoError(t, err)
 			assert.Equal(t, tc.expectOutput, outputSNAC)
 		})
@@ -478,7 +482,7 @@ func TestLocateRouter_RouteLocate(t *testing.T) {
 				Return(tc.output).
 				Maybe()
 			svc.EXPECT().
-				SetInfoHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				SetInfoHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
@@ -486,7 +490,7 @@ func TestLocateRouter_RouteLocate(t *testing.T) {
 				Return(tc.output).
 				Maybe()
 			svc.EXPECT().
-				UserInfoQuery2Handler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				UserInfoQuery2Handler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 
@@ -503,7 +507,7 @@ func TestLocateRouter_RouteLocate(t *testing.T) {
 			bufOut := &bytes.Buffer{}
 			seq := uint32(1)
 
-			err := router.RouteLocate(nil, nil, nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			err := router.RouteLocate(nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
 			if tc.expectErr != nil {
 				return

+ 186 - 96
server/oservice.go

@@ -12,24 +12,26 @@ import (
 )
 
 type OServiceHandler interface {
-	WriteOServiceHostOnline(w io.Writer, sequence *uint32) error
-	ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session, sm SessionManager, fm FeedbagManager, room ChatRoom) error
 	ClientVersionsHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage
-	IdleNotificationHandler(ctx context.Context, sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error
+	IdleNotificationHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error
 	RateParamsQueryHandler(ctx context.Context) XMessage
 	RateParamsSubAddHandler(context.Context, oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd)
-	ServiceRequestHandler(ctx context.Context, cfg Config, cr *ChatRegistry, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)
-	SetUserInfoFieldsHandler(ctx context.Context, sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)
+	SetUserInfoFieldsHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)
 	UserInfoQueryHandler(ctx context.Context, sess *Session) XMessage
 }
 
-func NewOServiceRouter(logger *slog.Logger) OServiceRouter {
-	return OServiceRouter{
-		OServiceHandler: OServiceService{},
-		RouteLogger: RouteLogger{
-			Logger: logger,
-		},
-	}
+type OServiceBOSHandler interface {
+	OServiceHandler
+	WriteOServiceHostOnline(w io.Writer, sequence *uint32) error
+	ServiceRequestHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)
+	ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session) error
+}
+
+type OServiceChatHandler interface {
+	OServiceHandler
+	WriteOServiceHostOnline(w io.Writer, sequence *uint32) error
+	ServiceRequestHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)
+	ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session, room ChatRoom) error
 }
 
 type OServiceRouter struct {
@@ -37,30 +39,8 @@ type OServiceRouter struct {
 	RouteLogger
 }
 
-func (rt OServiceRouter) RouteOService(ctx context.Context, cfg Config, cr *ChatRegistry, sm SessionManager, fm *FeedbagStore, sess *Session, room ChatRoom, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func (rt OServiceRouter) RouteOService(ctx context.Context, sess *Session, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch SNACFrame.SubGroup {
-	case oscar.OServiceClientOnline:
-		inSNAC := oscar.SNAC_0x01_0x02_OServiceClientOnline{}
-		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
-			return err
-		}
-		rt.Logger.InfoContext(ctx, "user signed on")
-		rt.logRequest(ctx, SNACFrame, inSNAC)
-		return rt.ClientOnlineHandler(ctx, inSNAC, sess, sm, fm, room)
-	case oscar.OServiceServiceRequest:
-		inSNAC := oscar.SNAC_0x01_0x04_OServiceServiceRequest{}
-		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
-			return err
-		}
-		outSNAC, err := rt.ServiceRequestHandler(ctx, cfg, cr, sess, inSNAC)
-		switch {
-		case errors.Is(err, ErrUnsupportedSubGroup):
-			return sendInvalidSNACErr(SNACFrame, w, sequence)
-		case err != nil:
-			return err
-		}
-		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.snacFrame, outSNAC.snacOut)
-		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
 	case oscar.OServiceRateParamsQuery:
 		outSNAC := rt.RateParamsQueryHandler(ctx)
 		rt.logRequestAndResponse(ctx, SNACFrame, nil, outSNAC.snacFrame, outSNAC.snacOut)
@@ -83,7 +63,7 @@ func (rt OServiceRouter) RouteOService(ctx context.Context, cfg Config, cr *Chat
 			return err
 		}
 		rt.logRequest(ctx, SNACFrame, inSNAC)
-		return rt.IdleNotificationHandler(ctx, sess, sm, fm, inSNAC)
+		return rt.IdleNotificationHandler(ctx, sess, inSNAC)
 	case oscar.OServiceClientVersions:
 		inSNAC := oscar.SNAC_0x01_0x17_OServiceClientVersions{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -97,7 +77,7 @@ func (rt OServiceRouter) RouteOService(ctx context.Context, cfg Config, cr *Chat
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.SetUserInfoFieldsHandler(ctx, sess, sm, fm, inSNAC)
+		outSNAC, err := rt.SetUserInfoFieldsHandler(ctx, sess, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -108,29 +88,101 @@ func (rt OServiceRouter) RouteOService(ctx context.Context, cfg Config, cr *Chat
 	}
 }
 
-type OServiceService struct {
+func NewOServiceRouterForBOS(logger *slog.Logger, cfg Config, fm FeedbagManager, sm SessionManager, cr *ChatRegistry) OServiceBOSRouter {
+	oss := OServiceService{
+		cfg: cfg,
+		fm:  fm,
+		sm:  sm,
+	}
+	return OServiceBOSRouter{
+		OServiceRouter: OServiceRouter{
+			OServiceHandler: oss,
+			RouteLogger: RouteLogger{
+				Logger: logger,
+			},
+		},
+		OServiceBOSHandler: OServiceServiceForBOS{
+			OServiceService: oss,
+			cr:              cr,
+		},
+	}
 }
 
-func (s OServiceService) WriteOServiceHostOnline(w io.Writer, sequence *uint32) error {
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: oscar.OSERVICE,
-		SubGroup:  oscar.OServiceHostOnline,
+type OServiceBOSRouter struct {
+	OServiceRouter
+	OServiceBOSHandler
+}
+
+func (rt OServiceBOSRouter) RouteOService(ctx context.Context, sess *Session, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	switch SNACFrame.SubGroup {
+	case oscar.OServiceServiceRequest:
+		inSNAC := oscar.SNAC_0x01_0x04_OServiceServiceRequest{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.ServiceRequestHandler(ctx, sess, inSNAC)
+		switch {
+		case errors.Is(err, ErrUnsupportedSubGroup):
+			return sendInvalidSNACErr(SNACFrame, w, sequence)
+		case err != nil:
+			return err
+		}
+		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.snacFrame, outSNAC.snacOut)
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.OServiceClientOnline:
+		inSNAC := oscar.SNAC_0x01_0x02_OServiceClientOnline{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		rt.Logger.InfoContext(ctx, "user signed on")
+		rt.logRequest(ctx, SNACFrame, inSNAC)
+		return rt.OServiceBOSHandler.ClientOnlineHandler(ctx, inSNAC, sess)
+	default:
+		return rt.OServiceRouter.RouteOService(ctx, sess, SNACFrame, r, w, sequence)
 	}
-	snacPayloadOut := oscar.SNAC_0x01_0x03_OServiceHostOnline{
-		FoodGroups: []uint16{
-			oscar.ALERT,
-			oscar.BUDDY,
-			oscar.CHAT_NAV,
-			oscar.FEEDBAG,
-			oscar.ICBM,
-			oscar.LOCATE,
-			oscar.OSERVICE,
-		},
+}
+
+type OServiceChatRouter struct {
+	OServiceRouter
+	OServiceChatHandler
+}
+
+func (rt OServiceChatRouter) RouteOService(ctx context.Context, sess *Session, room ChatRoom, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	switch SNACFrame.SubGroup {
+	case oscar.OServiceServiceRequest:
+		inSNAC := oscar.SNAC_0x01_0x04_OServiceServiceRequest{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.ServiceRequestHandler(ctx, sess, inSNAC)
+		switch {
+		case errors.Is(err, ErrUnsupportedSubGroup):
+			return sendInvalidSNACErr(SNACFrame, w, sequence)
+		case err != nil:
+			return err
+		}
+		rt.logRequestAndResponse(ctx, SNACFrame, inSNAC, outSNAC.snacFrame, outSNAC.snacOut)
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.OServiceClientOnline:
+		inSNAC := oscar.SNAC_0x01_0x02_OServiceClientOnline{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		rt.Logger.InfoContext(ctx, "user signed on")
+		rt.logRequest(ctx, SNACFrame, inSNAC)
+		return rt.OServiceChatHandler.ClientOnlineHandler(ctx, inSNAC, sess, room)
+	default:
+		return rt.OServiceRouter.RouteOService(ctx, sess, SNACFrame, r, w, sequence)
 	}
-	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func (s OServiceService) ClientVersionsHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage {
+type OServiceService struct {
+	cfg Config
+	fm  FeedbagManager
+	sm  SessionManager
+}
+
+func (s OServiceService) ClientVersionsHandler(_ context.Context, snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage {
 	return XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.OSERVICE,
@@ -142,7 +194,7 @@ func (s OServiceService) ClientVersionsHandler(ctx context.Context, snacPayloadI
 	}
 }
 
-func (s OServiceService) RateParamsQueryHandler(ctx context.Context) XMessage {
+func (s OServiceService) RateParamsQueryHandler(_ context.Context) XMessage {
 	snacFrameOut := oscar.SnacFrame{
 		FoodGroup: oscar.OSERVICE,
 		SubGroup:  oscar.OServiceRateParamsReply,
@@ -209,7 +261,7 @@ func (s OServiceService) RateParamsQueryHandler(ctx context.Context) XMessage {
 	}
 }
 
-func (s OServiceService) UserInfoQueryHandler(ctx context.Context, sess *Session) XMessage {
+func (s OServiceService) UserInfoQueryHandler(_ context.Context, sess *Session) XMessage {
 	return XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.OSERVICE,
@@ -221,37 +273,17 @@ func (s OServiceService) UserInfoQueryHandler(ctx context.Context, sess *Session
 	}
 }
 
-func (s OServiceService) ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session, sm SessionManager, fm FeedbagManager, room ChatRoom) error {
-	if err := BroadcastArrival(ctx, sess, sm, fm); err != nil {
-		return err
-	}
-	buddies, err := fm.Buddies(sess.ScreenName)
-	if err != nil {
-		return err
-	}
-	for _, buddy := range buddies {
-		err := UnicastArrival(ctx, buddy, sess.ScreenName, sm)
-		switch {
-		case errors.Is(err, ErrSessNotFound):
-			continue
-		case err != nil:
-			return err
-		}
-	}
-	return nil
-}
-
-func (s OServiceService) SetUserInfoFieldsHandler(ctx context.Context, sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error) {
+func (s OServiceService) SetUserInfoFieldsHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error) {
 	if status, hasStatus := snacPayloadIn.GetUint32(0x06); hasStatus {
 		switch status {
 		case 0x000:
 			sess.SetInvisible(false)
-			if err := BroadcastArrival(ctx, sess, sm, fm); err != nil {
+			if err := BroadcastArrival(ctx, sess, s.sm, s.fm); err != nil {
 				return XMessage{}, err
 			}
 		case 0x100:
 			sess.SetInvisible(true)
-			if err := BroadcastDeparture(ctx, sess, sm, fm); err != nil {
+			if err := BroadcastDeparture(ctx, sess, s.sm, s.fm); err != nil {
 				return XMessage{}, err
 			}
 		default:
@@ -269,16 +301,26 @@ func (s OServiceService) SetUserInfoFieldsHandler(ctx context.Context, sess *Ses
 	}, nil
 }
 
-func (s OServiceService) IdleNotificationHandler(ctx context.Context, sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
+func (s OServiceService) IdleNotificationHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
 	if snacPayloadIn.IdleTime == 0 {
 		sess.SetActive()
 	} else {
 		sess.SetIdle(time.Duration(snacPayloadIn.IdleTime) * time.Second)
 	}
-	return BroadcastArrival(ctx, sess, sm, fm)
+	return BroadcastArrival(ctx, sess, s.sm, s.fm)
+}
+
+// RateParamsSubAddHandler exists to capture the SNAC input in unit tests to
+// verify it's correctly unmarshalled.
+func (s OServiceService) RateParamsSubAddHandler(context.Context, oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd) {
 }
 
-func (s OServiceService) ServiceRequestHandler(ctx context.Context, cfg Config, cr *ChatRegistry, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error) {
+type OServiceServiceForBOS struct {
+	OServiceService
+	cr *ChatRegistry
+}
+
+func (s OServiceServiceForBOS) ServiceRequestHandler(_ context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error) {
 	if snacPayloadIn.FoodGroup != oscar.CHAT {
 		return XMessage{}, ErrUnsupportedSubGroup
 	}
@@ -293,7 +335,7 @@ func (s OServiceService) ServiceRequestHandler(ctx context.Context, cfg Config,
 		return XMessage{}, err
 	}
 
-	room, err := cr.Retrieve(string(roomSnac.Cookie))
+	room, err := s.cr.Retrieve(string(roomSnac.Cookie))
 	if err != nil {
 		return XMessage{}, ErrUnsupportedSubGroup
 	}
@@ -307,7 +349,7 @@ func (s OServiceService) ServiceRequestHandler(ctx context.Context, cfg Config,
 		snacOut: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
 			TLVRestBlock: oscar.TLVRestBlock{
 				TLVList: oscar.TLVList{
-					oscar.NewTLV(oscar.OServiceTLVTagsReconnectHere, Address(cfg.OSCARHost, cfg.ChatPort)),
+					oscar.NewTLV(oscar.OServiceTLVTagsReconnectHere, Address(s.cfg.OSCARHost, s.cfg.ChatPort)),
 					oscar.NewTLV(oscar.OServiceTLVTagsLoginCookie, ChatCookie{
 						Cookie: []byte(room.Cookie),
 						SessID: sess.ID,
@@ -321,16 +363,60 @@ func (s OServiceService) ServiceRequestHandler(ctx context.Context, cfg Config,
 	}, nil
 }
 
-// RateParamsSubAddHandler exists to capture the SNAC input in unit tests to
-// verify it's correctly unmarshalled.
-func (s OServiceService) RateParamsSubAddHandler(context.Context, oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd) {
+func (s OServiceServiceForBOS) WriteOServiceHostOnline(w io.Writer, sequence *uint32) error {
+	snacFrameOut := oscar.SnacFrame{
+		FoodGroup: oscar.OSERVICE,
+		SubGroup:  oscar.OServiceHostOnline,
+	}
+	snacPayloadOut := oscar.SNAC_0x01_0x03_OServiceHostOnline{
+		FoodGroups: []uint16{
+			oscar.ALERT,
+			oscar.BUDDY,
+			oscar.CHAT_NAV,
+			oscar.FEEDBAG,
+			oscar.ICBM,
+			oscar.LOCATE,
+			oscar.OSERVICE,
+		},
+	}
+	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func NewOServiceRouterForChat(logger *slog.Logger) OServiceRouter {
-	return OServiceRouter{
-		OServiceHandler: OServiceServiceForChat{},
-		RouteLogger: RouteLogger{
-			Logger: logger,
+func (s OServiceServiceForBOS) ClientOnlineHandler(ctx context.Context, _ oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session) error {
+	if err := BroadcastArrival(ctx, sess, s.sm, s.fm); err != nil {
+		return err
+	}
+	buddies, err := s.fm.Buddies(sess.ScreenName)
+	if err != nil {
+		return err
+	}
+	for _, buddy := range buddies {
+		err := UnicastArrival(ctx, buddy, sess.ScreenName, s.sm)
+		switch {
+		case errors.Is(err, ErrSessNotFound):
+			continue
+		case err != nil:
+			return err
+		}
+	}
+	return nil
+}
+
+func NewOServiceRouterForChat(logger *slog.Logger, cfg Config, fm FeedbagManager, sm SessionManager) OServiceChatRouter {
+	oss := OServiceService{
+		cfg: cfg,
+		fm:  fm,
+		sm:  sm,
+	}
+	return OServiceChatRouter{
+		OServiceRouter: OServiceRouter{
+			OServiceHandler: oss,
+			RouteLogger: RouteLogger{
+				Logger: logger,
+			},
+		},
+		OServiceChatHandler: OServiceServiceForChat{
+			OServiceService: oss,
 		},
 	}
 }
@@ -340,6 +426,10 @@ type OServiceServiceForChat struct {
 	RouteLogger
 }
 
+func (s OServiceServiceForChat) ServiceRequestHandler(_ context.Context, _ *Session, _ oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error) {
+	return XMessage{}, ErrUnsupportedSubGroup
+}
+
 func (s OServiceServiceForChat) WriteOServiceHostOnline(w io.Writer, sequence *uint32) error {
 	snacFrameOut := oscar.SnacFrame{
 		FoodGroup: oscar.OSERVICE,
@@ -351,9 +441,9 @@ func (s OServiceServiceForChat) WriteOServiceHostOnline(w io.Writer, sequence *u
 	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func (s OServiceServiceForChat) ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session, sm SessionManager, fm FeedbagManager, room ChatRoom) error {
-	SendChatRoomInfoUpdate(ctx, sess, sm, room)
-	AlertUserJoined(ctx, sess, sm)
-	SetOnlineChatUsers(ctx, sess, sm)
+func (s OServiceServiceForChat) ClientOnlineHandler(ctx context.Context, _ oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session, room ChatRoom) error {
+	SendChatRoomInfoUpdate(ctx, sess, room)
+	AlertUserJoined(ctx, sess, room)
+	SetOnlineChatUsers(ctx, sess, room)
 	return nil
 }

+ 440 - 0
server/oservice_bos_mock.go

@@ -0,0 +1,440 @@
+// Code generated by mockery v2.35.2. DO NOT EDIT.
+
+package server
+
+import (
+	context "context"
+	io "io"
+
+	mock "github.com/stretchr/testify/mock"
+
+	oscar "github.com/mkaminski/goaim/oscar"
+)
+
+// MockOServiceBOSHandler is an autogenerated mock type for the OServiceBOSHandler type
+type MockOServiceBOSHandler struct {
+	mock.Mock
+}
+
+type MockOServiceBOSHandler_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockOServiceBOSHandler) EXPECT() *MockOServiceBOSHandler_Expecter {
+	return &MockOServiceBOSHandler_Expecter{mock: &_m.Mock}
+}
+
+// ClientOnlineHandler provides a mock function with given fields: ctx, snacPayloadIn, sess
+func (_m *MockOServiceBOSHandler) ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session) error {
+	ret := _m.Called(ctx, snacPayloadIn, sess)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, oscar.SNAC_0x01_0x02_OServiceClientOnline, *Session) error); ok {
+		r0 = rf(ctx, snacPayloadIn, sess)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockOServiceBOSHandler_ClientOnlineHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClientOnlineHandler'
+type MockOServiceBOSHandler_ClientOnlineHandler_Call struct {
+	*mock.Call
+}
+
+// ClientOnlineHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline
+//   - sess *Session
+func (_e *MockOServiceBOSHandler_Expecter) ClientOnlineHandler(ctx interface{}, snacPayloadIn interface{}, sess interface{}) *MockOServiceBOSHandler_ClientOnlineHandler_Call {
+	return &MockOServiceBOSHandler_ClientOnlineHandler_Call{Call: _e.mock.On("ClientOnlineHandler", ctx, snacPayloadIn, sess)}
+}
+
+func (_c *MockOServiceBOSHandler_ClientOnlineHandler_Call) Run(run func(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session)) *MockOServiceBOSHandler_ClientOnlineHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(oscar.SNAC_0x01_0x02_OServiceClientOnline), args[2].(*Session))
+	})
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_ClientOnlineHandler_Call) Return(_a0 error) *MockOServiceBOSHandler_ClientOnlineHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_ClientOnlineHandler_Call) RunAndReturn(run func(context.Context, oscar.SNAC_0x01_0x02_OServiceClientOnline, *Session) error) *MockOServiceBOSHandler_ClientOnlineHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// ClientVersionsHandler provides a mock function with given fields: ctx, snacPayloadIn
+func (_m *MockOServiceBOSHandler) ClientVersionsHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage {
+	ret := _m.Called(ctx, snacPayloadIn)
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func(context.Context, oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage); ok {
+		r0 = rf(ctx, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockOServiceBOSHandler_ClientVersionsHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClientVersionsHandler'
+type MockOServiceBOSHandler_ClientVersionsHandler_Call struct {
+	*mock.Call
+}
+
+// ClientVersionsHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions
+func (_e *MockOServiceBOSHandler_Expecter) ClientVersionsHandler(ctx interface{}, snacPayloadIn interface{}) *MockOServiceBOSHandler_ClientVersionsHandler_Call {
+	return &MockOServiceBOSHandler_ClientVersionsHandler_Call{Call: _e.mock.On("ClientVersionsHandler", ctx, snacPayloadIn)}
+}
+
+func (_c *MockOServiceBOSHandler_ClientVersionsHandler_Call) Run(run func(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions)) *MockOServiceBOSHandler_ClientVersionsHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(oscar.SNAC_0x01_0x17_OServiceClientVersions))
+	})
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_ClientVersionsHandler_Call) Return(_a0 XMessage) *MockOServiceBOSHandler_ClientVersionsHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_ClientVersionsHandler_Call) RunAndReturn(run func(context.Context, oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage) *MockOServiceBOSHandler_ClientVersionsHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// IdleNotificationHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockOServiceBOSHandler) IdleNotificationHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x11_OServiceIdleNotification) error); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockOServiceBOSHandler_IdleNotificationHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IdleNotificationHandler'
+type MockOServiceBOSHandler_IdleNotificationHandler_Call struct {
+	*mock.Call
+}
+
+// IdleNotificationHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *Session
+//   - snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification
+func (_e *MockOServiceBOSHandler_Expecter) IdleNotificationHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockOServiceBOSHandler_IdleNotificationHandler_Call {
+	return &MockOServiceBOSHandler_IdleNotificationHandler_Call{Call: _e.mock.On("IdleNotificationHandler", ctx, sess, snacPayloadIn)}
+}
+
+func (_c *MockOServiceBOSHandler_IdleNotificationHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification)) *MockOServiceBOSHandler_IdleNotificationHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x01_0x11_OServiceIdleNotification))
+	})
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_IdleNotificationHandler_Call) Return(_a0 error) *MockOServiceBOSHandler_IdleNotificationHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_IdleNotificationHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x01_0x11_OServiceIdleNotification) error) *MockOServiceBOSHandler_IdleNotificationHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RateParamsQueryHandler provides a mock function with given fields: ctx
+func (_m *MockOServiceBOSHandler) RateParamsQueryHandler(ctx context.Context) XMessage {
+	ret := _m.Called(ctx)
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func(context.Context) XMessage); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockOServiceBOSHandler_RateParamsQueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RateParamsQueryHandler'
+type MockOServiceBOSHandler_RateParamsQueryHandler_Call struct {
+	*mock.Call
+}
+
+// RateParamsQueryHandler is a helper method to define mock.On call
+//   - ctx context.Context
+func (_e *MockOServiceBOSHandler_Expecter) RateParamsQueryHandler(ctx interface{}) *MockOServiceBOSHandler_RateParamsQueryHandler_Call {
+	return &MockOServiceBOSHandler_RateParamsQueryHandler_Call{Call: _e.mock.On("RateParamsQueryHandler", ctx)}
+}
+
+func (_c *MockOServiceBOSHandler_RateParamsQueryHandler_Call) Run(run func(ctx context.Context)) *MockOServiceBOSHandler_RateParamsQueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context))
+	})
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_RateParamsQueryHandler_Call) Return(_a0 XMessage) *MockOServiceBOSHandler_RateParamsQueryHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_RateParamsQueryHandler_Call) RunAndReturn(run func(context.Context) XMessage) *MockOServiceBOSHandler_RateParamsQueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RateParamsSubAddHandler provides a mock function with given fields: _a0, _a1
+func (_m *MockOServiceBOSHandler) RateParamsSubAddHandler(_a0 context.Context, _a1 oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd) {
+	_m.Called(_a0, _a1)
+}
+
+// MockOServiceBOSHandler_RateParamsSubAddHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RateParamsSubAddHandler'
+type MockOServiceBOSHandler_RateParamsSubAddHandler_Call struct {
+	*mock.Call
+}
+
+// RateParamsSubAddHandler is a helper method to define mock.On call
+//   - _a0 context.Context
+//   - _a1 oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd
+func (_e *MockOServiceBOSHandler_Expecter) RateParamsSubAddHandler(_a0 interface{}, _a1 interface{}) *MockOServiceBOSHandler_RateParamsSubAddHandler_Call {
+	return &MockOServiceBOSHandler_RateParamsSubAddHandler_Call{Call: _e.mock.On("RateParamsSubAddHandler", _a0, _a1)}
+}
+
+func (_c *MockOServiceBOSHandler_RateParamsSubAddHandler_Call) Run(run func(_a0 context.Context, _a1 oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd)) *MockOServiceBOSHandler_RateParamsSubAddHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd))
+	})
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_RateParamsSubAddHandler_Call) Return() *MockOServiceBOSHandler_RateParamsSubAddHandler_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_RateParamsSubAddHandler_Call) RunAndReturn(run func(context.Context, oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd)) *MockOServiceBOSHandler_RateParamsSubAddHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// ServiceRequestHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockOServiceBOSHandler) ServiceRequestHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockOServiceBOSHandler_ServiceRequestHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServiceRequestHandler'
+type MockOServiceBOSHandler_ServiceRequestHandler_Call struct {
+	*mock.Call
+}
+
+// ServiceRequestHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *Session
+//   - snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest
+func (_e *MockOServiceBOSHandler_Expecter) ServiceRequestHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockOServiceBOSHandler_ServiceRequestHandler_Call {
+	return &MockOServiceBOSHandler_ServiceRequestHandler_Call{Call: _e.mock.On("ServiceRequestHandler", ctx, sess, snacPayloadIn)}
+}
+
+func (_c *MockOServiceBOSHandler_ServiceRequestHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest)) *MockOServiceBOSHandler_ServiceRequestHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x01_0x04_OServiceServiceRequest))
+	})
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_ServiceRequestHandler_Call) Return(_a0 XMessage, _a1 error) *MockOServiceBOSHandler_ServiceRequestHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_ServiceRequestHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)) *MockOServiceBOSHandler_ServiceRequestHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// SetUserInfoFieldsHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockOServiceBOSHandler) SetUserInfoFieldsHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockOServiceBOSHandler_SetUserInfoFieldsHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetUserInfoFieldsHandler'
+type MockOServiceBOSHandler_SetUserInfoFieldsHandler_Call struct {
+	*mock.Call
+}
+
+// SetUserInfoFieldsHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *Session
+//   - snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields
+func (_e *MockOServiceBOSHandler_Expecter) SetUserInfoFieldsHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockOServiceBOSHandler_SetUserInfoFieldsHandler_Call {
+	return &MockOServiceBOSHandler_SetUserInfoFieldsHandler_Call{Call: _e.mock.On("SetUserInfoFieldsHandler", ctx, sess, snacPayloadIn)}
+}
+
+func (_c *MockOServiceBOSHandler_SetUserInfoFieldsHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields)) *MockOServiceBOSHandler_SetUserInfoFieldsHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields))
+	})
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_SetUserInfoFieldsHandler_Call) Return(_a0 XMessage, _a1 error) *MockOServiceBOSHandler_SetUserInfoFieldsHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_SetUserInfoFieldsHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)) *MockOServiceBOSHandler_SetUserInfoFieldsHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// UserInfoQueryHandler provides a mock function with given fields: ctx, sess
+func (_m *MockOServiceBOSHandler) UserInfoQueryHandler(ctx context.Context, sess *Session) XMessage {
+	ret := _m.Called(ctx, sess)
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func(context.Context, *Session) XMessage); ok {
+		r0 = rf(ctx, sess)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockOServiceBOSHandler_UserInfoQueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UserInfoQueryHandler'
+type MockOServiceBOSHandler_UserInfoQueryHandler_Call struct {
+	*mock.Call
+}
+
+// UserInfoQueryHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *Session
+func (_e *MockOServiceBOSHandler_Expecter) UserInfoQueryHandler(ctx interface{}, sess interface{}) *MockOServiceBOSHandler_UserInfoQueryHandler_Call {
+	return &MockOServiceBOSHandler_UserInfoQueryHandler_Call{Call: _e.mock.On("UserInfoQueryHandler", ctx, sess)}
+}
+
+func (_c *MockOServiceBOSHandler_UserInfoQueryHandler_Call) Run(run func(ctx context.Context, sess *Session)) *MockOServiceBOSHandler_UserInfoQueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*Session))
+	})
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_UserInfoQueryHandler_Call) Return(_a0 XMessage) *MockOServiceBOSHandler_UserInfoQueryHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_UserInfoQueryHandler_Call) RunAndReturn(run func(context.Context, *Session) XMessage) *MockOServiceBOSHandler_UserInfoQueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// WriteOServiceHostOnline provides a mock function with given fields: w, sequence
+func (_m *MockOServiceBOSHandler) WriteOServiceHostOnline(w io.Writer, sequence *uint32) error {
+	ret := _m.Called(w, sequence)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(io.Writer, *uint32) error); ok {
+		r0 = rf(w, sequence)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockOServiceBOSHandler_WriteOServiceHostOnline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteOServiceHostOnline'
+type MockOServiceBOSHandler_WriteOServiceHostOnline_Call struct {
+	*mock.Call
+}
+
+// WriteOServiceHostOnline is a helper method to define mock.On call
+//   - w io.Writer
+//   - sequence *uint32
+func (_e *MockOServiceBOSHandler_Expecter) WriteOServiceHostOnline(w interface{}, sequence interface{}) *MockOServiceBOSHandler_WriteOServiceHostOnline_Call {
+	return &MockOServiceBOSHandler_WriteOServiceHostOnline_Call{Call: _e.mock.On("WriteOServiceHostOnline", w, sequence)}
+}
+
+func (_c *MockOServiceBOSHandler_WriteOServiceHostOnline_Call) Run(run func(w io.Writer, sequence *uint32)) *MockOServiceBOSHandler_WriteOServiceHostOnline_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(io.Writer), args[1].(*uint32))
+	})
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_WriteOServiceHostOnline_Call) Return(_a0 error) *MockOServiceBOSHandler_WriteOServiceHostOnline_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceBOSHandler_WriteOServiceHostOnline_Call) RunAndReturn(run func(io.Writer, *uint32) error) *MockOServiceBOSHandler_WriteOServiceHostOnline_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockOServiceBOSHandler creates a new instance of MockOServiceBOSHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewMockOServiceBOSHandler(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockOServiceBOSHandler {
+	mock := &MockOServiceBOSHandler{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 441 - 0
server/oservice_chat_mock.go

@@ -0,0 +1,441 @@
+// Code generated by mockery v2.35.2. DO NOT EDIT.
+
+package server
+
+import (
+	context "context"
+	io "io"
+
+	mock "github.com/stretchr/testify/mock"
+
+	oscar "github.com/mkaminski/goaim/oscar"
+)
+
+// MockOServiceChatHandler is an autogenerated mock type for the OServiceChatHandler type
+type MockOServiceChatHandler struct {
+	mock.Mock
+}
+
+type MockOServiceChatHandler_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockOServiceChatHandler) EXPECT() *MockOServiceChatHandler_Expecter {
+	return &MockOServiceChatHandler_Expecter{mock: &_m.Mock}
+}
+
+// ClientOnlineHandler provides a mock function with given fields: ctx, snacPayloadIn, sess, room
+func (_m *MockOServiceChatHandler) ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session, room ChatRoom) error {
+	ret := _m.Called(ctx, snacPayloadIn, sess, room)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, oscar.SNAC_0x01_0x02_OServiceClientOnline, *Session, ChatRoom) error); ok {
+		r0 = rf(ctx, snacPayloadIn, sess, room)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockOServiceChatHandler_ClientOnlineHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClientOnlineHandler'
+type MockOServiceChatHandler_ClientOnlineHandler_Call struct {
+	*mock.Call
+}
+
+// ClientOnlineHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline
+//   - sess *Session
+//   - room ChatRoom
+func (_e *MockOServiceChatHandler_Expecter) ClientOnlineHandler(ctx interface{}, snacPayloadIn interface{}, sess interface{}, room interface{}) *MockOServiceChatHandler_ClientOnlineHandler_Call {
+	return &MockOServiceChatHandler_ClientOnlineHandler_Call{Call: _e.mock.On("ClientOnlineHandler", ctx, snacPayloadIn, sess, room)}
+}
+
+func (_c *MockOServiceChatHandler_ClientOnlineHandler_Call) Run(run func(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session, room ChatRoom)) *MockOServiceChatHandler_ClientOnlineHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(oscar.SNAC_0x01_0x02_OServiceClientOnline), args[2].(*Session), args[3].(ChatRoom))
+	})
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_ClientOnlineHandler_Call) Return(_a0 error) *MockOServiceChatHandler_ClientOnlineHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_ClientOnlineHandler_Call) RunAndReturn(run func(context.Context, oscar.SNAC_0x01_0x02_OServiceClientOnline, *Session, ChatRoom) error) *MockOServiceChatHandler_ClientOnlineHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// ClientVersionsHandler provides a mock function with given fields: ctx, snacPayloadIn
+func (_m *MockOServiceChatHandler) ClientVersionsHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage {
+	ret := _m.Called(ctx, snacPayloadIn)
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func(context.Context, oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage); ok {
+		r0 = rf(ctx, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockOServiceChatHandler_ClientVersionsHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClientVersionsHandler'
+type MockOServiceChatHandler_ClientVersionsHandler_Call struct {
+	*mock.Call
+}
+
+// ClientVersionsHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions
+func (_e *MockOServiceChatHandler_Expecter) ClientVersionsHandler(ctx interface{}, snacPayloadIn interface{}) *MockOServiceChatHandler_ClientVersionsHandler_Call {
+	return &MockOServiceChatHandler_ClientVersionsHandler_Call{Call: _e.mock.On("ClientVersionsHandler", ctx, snacPayloadIn)}
+}
+
+func (_c *MockOServiceChatHandler_ClientVersionsHandler_Call) Run(run func(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions)) *MockOServiceChatHandler_ClientVersionsHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(oscar.SNAC_0x01_0x17_OServiceClientVersions))
+	})
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_ClientVersionsHandler_Call) Return(_a0 XMessage) *MockOServiceChatHandler_ClientVersionsHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_ClientVersionsHandler_Call) RunAndReturn(run func(context.Context, oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage) *MockOServiceChatHandler_ClientVersionsHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// IdleNotificationHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockOServiceChatHandler) IdleNotificationHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x11_OServiceIdleNotification) error); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockOServiceChatHandler_IdleNotificationHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IdleNotificationHandler'
+type MockOServiceChatHandler_IdleNotificationHandler_Call struct {
+	*mock.Call
+}
+
+// IdleNotificationHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *Session
+//   - snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification
+func (_e *MockOServiceChatHandler_Expecter) IdleNotificationHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockOServiceChatHandler_IdleNotificationHandler_Call {
+	return &MockOServiceChatHandler_IdleNotificationHandler_Call{Call: _e.mock.On("IdleNotificationHandler", ctx, sess, snacPayloadIn)}
+}
+
+func (_c *MockOServiceChatHandler_IdleNotificationHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification)) *MockOServiceChatHandler_IdleNotificationHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x01_0x11_OServiceIdleNotification))
+	})
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_IdleNotificationHandler_Call) Return(_a0 error) *MockOServiceChatHandler_IdleNotificationHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_IdleNotificationHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x01_0x11_OServiceIdleNotification) error) *MockOServiceChatHandler_IdleNotificationHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RateParamsQueryHandler provides a mock function with given fields: ctx
+func (_m *MockOServiceChatHandler) RateParamsQueryHandler(ctx context.Context) XMessage {
+	ret := _m.Called(ctx)
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func(context.Context) XMessage); ok {
+		r0 = rf(ctx)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockOServiceChatHandler_RateParamsQueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RateParamsQueryHandler'
+type MockOServiceChatHandler_RateParamsQueryHandler_Call struct {
+	*mock.Call
+}
+
+// RateParamsQueryHandler is a helper method to define mock.On call
+//   - ctx context.Context
+func (_e *MockOServiceChatHandler_Expecter) RateParamsQueryHandler(ctx interface{}) *MockOServiceChatHandler_RateParamsQueryHandler_Call {
+	return &MockOServiceChatHandler_RateParamsQueryHandler_Call{Call: _e.mock.On("RateParamsQueryHandler", ctx)}
+}
+
+func (_c *MockOServiceChatHandler_RateParamsQueryHandler_Call) Run(run func(ctx context.Context)) *MockOServiceChatHandler_RateParamsQueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context))
+	})
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_RateParamsQueryHandler_Call) Return(_a0 XMessage) *MockOServiceChatHandler_RateParamsQueryHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_RateParamsQueryHandler_Call) RunAndReturn(run func(context.Context) XMessage) *MockOServiceChatHandler_RateParamsQueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RateParamsSubAddHandler provides a mock function with given fields: _a0, _a1
+func (_m *MockOServiceChatHandler) RateParamsSubAddHandler(_a0 context.Context, _a1 oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd) {
+	_m.Called(_a0, _a1)
+}
+
+// MockOServiceChatHandler_RateParamsSubAddHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RateParamsSubAddHandler'
+type MockOServiceChatHandler_RateParamsSubAddHandler_Call struct {
+	*mock.Call
+}
+
+// RateParamsSubAddHandler is a helper method to define mock.On call
+//   - _a0 context.Context
+//   - _a1 oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd
+func (_e *MockOServiceChatHandler_Expecter) RateParamsSubAddHandler(_a0 interface{}, _a1 interface{}) *MockOServiceChatHandler_RateParamsSubAddHandler_Call {
+	return &MockOServiceChatHandler_RateParamsSubAddHandler_Call{Call: _e.mock.On("RateParamsSubAddHandler", _a0, _a1)}
+}
+
+func (_c *MockOServiceChatHandler_RateParamsSubAddHandler_Call) Run(run func(_a0 context.Context, _a1 oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd)) *MockOServiceChatHandler_RateParamsSubAddHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd))
+	})
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_RateParamsSubAddHandler_Call) Return() *MockOServiceChatHandler_RateParamsSubAddHandler_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_RateParamsSubAddHandler_Call) RunAndReturn(run func(context.Context, oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd)) *MockOServiceChatHandler_RateParamsSubAddHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// ServiceRequestHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockOServiceChatHandler) ServiceRequestHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockOServiceChatHandler_ServiceRequestHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServiceRequestHandler'
+type MockOServiceChatHandler_ServiceRequestHandler_Call struct {
+	*mock.Call
+}
+
+// ServiceRequestHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *Session
+//   - snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest
+func (_e *MockOServiceChatHandler_Expecter) ServiceRequestHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockOServiceChatHandler_ServiceRequestHandler_Call {
+	return &MockOServiceChatHandler_ServiceRequestHandler_Call{Call: _e.mock.On("ServiceRequestHandler", ctx, sess, snacPayloadIn)}
+}
+
+func (_c *MockOServiceChatHandler_ServiceRequestHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest)) *MockOServiceChatHandler_ServiceRequestHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x01_0x04_OServiceServiceRequest))
+	})
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_ServiceRequestHandler_Call) Return(_a0 XMessage, _a1 error) *MockOServiceChatHandler_ServiceRequestHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_ServiceRequestHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)) *MockOServiceChatHandler_ServiceRequestHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// SetUserInfoFieldsHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockOServiceChatHandler) SetUserInfoFieldsHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockOServiceChatHandler_SetUserInfoFieldsHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetUserInfoFieldsHandler'
+type MockOServiceChatHandler_SetUserInfoFieldsHandler_Call struct {
+	*mock.Call
+}
+
+// SetUserInfoFieldsHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *Session
+//   - snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields
+func (_e *MockOServiceChatHandler_Expecter) SetUserInfoFieldsHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockOServiceChatHandler_SetUserInfoFieldsHandler_Call {
+	return &MockOServiceChatHandler_SetUserInfoFieldsHandler_Call{Call: _e.mock.On("SetUserInfoFieldsHandler", ctx, sess, snacPayloadIn)}
+}
+
+func (_c *MockOServiceChatHandler_SetUserInfoFieldsHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields)) *MockOServiceChatHandler_SetUserInfoFieldsHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields))
+	})
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_SetUserInfoFieldsHandler_Call) Return(_a0 XMessage, _a1 error) *MockOServiceChatHandler_SetUserInfoFieldsHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_SetUserInfoFieldsHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)) *MockOServiceChatHandler_SetUserInfoFieldsHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// UserInfoQueryHandler provides a mock function with given fields: ctx, sess
+func (_m *MockOServiceChatHandler) UserInfoQueryHandler(ctx context.Context, sess *Session) XMessage {
+	ret := _m.Called(ctx, sess)
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func(context.Context, *Session) XMessage); ok {
+		r0 = rf(ctx, sess)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockOServiceChatHandler_UserInfoQueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UserInfoQueryHandler'
+type MockOServiceChatHandler_UserInfoQueryHandler_Call struct {
+	*mock.Call
+}
+
+// UserInfoQueryHandler is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *Session
+func (_e *MockOServiceChatHandler_Expecter) UserInfoQueryHandler(ctx interface{}, sess interface{}) *MockOServiceChatHandler_UserInfoQueryHandler_Call {
+	return &MockOServiceChatHandler_UserInfoQueryHandler_Call{Call: _e.mock.On("UserInfoQueryHandler", ctx, sess)}
+}
+
+func (_c *MockOServiceChatHandler_UserInfoQueryHandler_Call) Run(run func(ctx context.Context, sess *Session)) *MockOServiceChatHandler_UserInfoQueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*Session))
+	})
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_UserInfoQueryHandler_Call) Return(_a0 XMessage) *MockOServiceChatHandler_UserInfoQueryHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_UserInfoQueryHandler_Call) RunAndReturn(run func(context.Context, *Session) XMessage) *MockOServiceChatHandler_UserInfoQueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// WriteOServiceHostOnline provides a mock function with given fields: w, sequence
+func (_m *MockOServiceChatHandler) WriteOServiceHostOnline(w io.Writer, sequence *uint32) error {
+	ret := _m.Called(w, sequence)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(io.Writer, *uint32) error); ok {
+		r0 = rf(w, sequence)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockOServiceChatHandler_WriteOServiceHostOnline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteOServiceHostOnline'
+type MockOServiceChatHandler_WriteOServiceHostOnline_Call struct {
+	*mock.Call
+}
+
+// WriteOServiceHostOnline is a helper method to define mock.On call
+//   - w io.Writer
+//   - sequence *uint32
+func (_e *MockOServiceChatHandler_Expecter) WriteOServiceHostOnline(w interface{}, sequence interface{}) *MockOServiceChatHandler_WriteOServiceHostOnline_Call {
+	return &MockOServiceChatHandler_WriteOServiceHostOnline_Call{Call: _e.mock.On("WriteOServiceHostOnline", w, sequence)}
+}
+
+func (_c *MockOServiceChatHandler_WriteOServiceHostOnline_Call) Run(run func(w io.Writer, sequence *uint32)) *MockOServiceChatHandler_WriteOServiceHostOnline_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(io.Writer), args[1].(*uint32))
+	})
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_WriteOServiceHostOnline_Call) Return(_a0 error) *MockOServiceChatHandler_WriteOServiceHostOnline_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceChatHandler_WriteOServiceHostOnline_Call) RunAndReturn(run func(io.Writer, *uint32) error) *MockOServiceChatHandler_WriteOServiceHostOnline_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockOServiceChatHandler creates a new instance of MockOServiceChatHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewMockOServiceChatHandler(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockOServiceChatHandler {
+	mock := &MockOServiceChatHandler{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 25 - 177
server/oservice_mock.go

@@ -4,11 +4,9 @@ package server
 
 import (
 	context "context"
-	io "io"
-
-	mock "github.com/stretchr/testify/mock"
 
 	oscar "github.com/mkaminski/goaim/oscar"
+	mock "github.com/stretchr/testify/mock"
 )
 
 // MockOServiceHandler is an autogenerated mock type for the OServiceHandler type
@@ -24,53 +22,6 @@ func (_m *MockOServiceHandler) EXPECT() *MockOServiceHandler_Expecter {
 	return &MockOServiceHandler_Expecter{mock: &_m.Mock}
 }
 
-// ClientOnlineHandler provides a mock function with given fields: ctx, snacPayloadIn, sess, sm, fm, room
-func (_m *MockOServiceHandler) ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session, sm SessionManager, fm FeedbagManager, room ChatRoom) error {
-	ret := _m.Called(ctx, snacPayloadIn, sess, sm, fm, room)
-
-	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, oscar.SNAC_0x01_0x02_OServiceClientOnline, *Session, SessionManager, FeedbagManager, ChatRoom) error); ok {
-		r0 = rf(ctx, snacPayloadIn, sess, sm, fm, room)
-	} else {
-		r0 = ret.Error(0)
-	}
-
-	return r0
-}
-
-// MockOServiceHandler_ClientOnlineHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClientOnlineHandler'
-type MockOServiceHandler_ClientOnlineHandler_Call struct {
-	*mock.Call
-}
-
-// ClientOnlineHandler is a helper method to define mock.On call
-//   - ctx context.Context
-//   - snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline
-//   - sess *Session
-//   - sm SessionManager
-//   - fm FeedbagManager
-//   - room ChatRoom
-func (_e *MockOServiceHandler_Expecter) ClientOnlineHandler(ctx interface{}, snacPayloadIn interface{}, sess interface{}, sm interface{}, fm interface{}, room interface{}) *MockOServiceHandler_ClientOnlineHandler_Call {
-	return &MockOServiceHandler_ClientOnlineHandler_Call{Call: _e.mock.On("ClientOnlineHandler", ctx, snacPayloadIn, sess, sm, fm, room)}
-}
-
-func (_c *MockOServiceHandler_ClientOnlineHandler_Call) Run(run func(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *Session, sm SessionManager, fm FeedbagManager, room ChatRoom)) *MockOServiceHandler_ClientOnlineHandler_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(oscar.SNAC_0x01_0x02_OServiceClientOnline), args[2].(*Session), args[3].(SessionManager), args[4].(FeedbagManager), args[5].(ChatRoom))
-	})
-	return _c
-}
-
-func (_c *MockOServiceHandler_ClientOnlineHandler_Call) Return(_a0 error) *MockOServiceHandler_ClientOnlineHandler_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *MockOServiceHandler_ClientOnlineHandler_Call) RunAndReturn(run func(context.Context, oscar.SNAC_0x01_0x02_OServiceClientOnline, *Session, SessionManager, FeedbagManager, ChatRoom) error) *MockOServiceHandler_ClientOnlineHandler_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // ClientVersionsHandler provides a mock function with given fields: ctx, snacPayloadIn
 func (_m *MockOServiceHandler) ClientVersionsHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage {
 	ret := _m.Called(ctx, snacPayloadIn)
@@ -114,13 +65,13 @@ func (_c *MockOServiceHandler_ClientVersionsHandler_Call) RunAndReturn(run func(
 	return _c
 }
 
-// IdleNotificationHandler provides a mock function with given fields: ctx, sess, sm, fm, snacPayloadIn
-func (_m *MockOServiceHandler) IdleNotificationHandler(ctx context.Context, sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
-	ret := _m.Called(ctx, sess, sm, fm, snacPayloadIn)
+// IdleNotificationHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockOServiceHandler) IdleNotificationHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x11_OServiceIdleNotification) error); ok {
-		r0 = rf(ctx, sess, sm, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x11_OServiceIdleNotification) error); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -136,16 +87,14 @@ type MockOServiceHandler_IdleNotificationHandler_Call struct {
 // IdleNotificationHandler is a helper method to define mock.On call
 //   - ctx context.Context
 //   - sess *Session
-//   - sm SessionManager
-//   - fm *FeedbagStore
 //   - snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification
-func (_e *MockOServiceHandler_Expecter) IdleNotificationHandler(ctx interface{}, sess interface{}, sm interface{}, fm interface{}, snacPayloadIn interface{}) *MockOServiceHandler_IdleNotificationHandler_Call {
-	return &MockOServiceHandler_IdleNotificationHandler_Call{Call: _e.mock.On("IdleNotificationHandler", ctx, sess, sm, fm, snacPayloadIn)}
+func (_e *MockOServiceHandler_Expecter) IdleNotificationHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockOServiceHandler_IdleNotificationHandler_Call {
+	return &MockOServiceHandler_IdleNotificationHandler_Call{Call: _e.mock.On("IdleNotificationHandler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockOServiceHandler_IdleNotificationHandler_Call) Run(run func(ctx context.Context, sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification)) *MockOServiceHandler_IdleNotificationHandler_Call {
+func (_c *MockOServiceHandler_IdleNotificationHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification)) *MockOServiceHandler_IdleNotificationHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*Session), args[2].(SessionManager), args[3].(*FeedbagStore), args[4].(oscar.SNAC_0x01_0x11_OServiceIdleNotification))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x01_0x11_OServiceIdleNotification))
 	})
 	return _c
 }
@@ -155,7 +104,7 @@ func (_c *MockOServiceHandler_IdleNotificationHandler_Call) Return(_a0 error) *M
 	return _c
 }
 
-func (_c *MockOServiceHandler_IdleNotificationHandler_Call) RunAndReturn(run func(context.Context, *Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x11_OServiceIdleNotification) error) *MockOServiceHandler_IdleNotificationHandler_Call {
+func (_c *MockOServiceHandler_IdleNotificationHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x01_0x11_OServiceIdleNotification) error) *MockOServiceHandler_IdleNotificationHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }
@@ -236,79 +185,23 @@ func (_c *MockOServiceHandler_RateParamsSubAddHandler_Call) RunAndReturn(run fun
 	return _c
 }
 
-// ServiceRequestHandler provides a mock function with given fields: ctx, cfg, cr, sess, snacPayloadIn
-func (_m *MockOServiceHandler) ServiceRequestHandler(ctx context.Context, cfg Config, cr *ChatRegistry, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error) {
-	ret := _m.Called(ctx, cfg, cr, sess, snacPayloadIn)
-
-	var r0 XMessage
-	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, Config, *ChatRegistry, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)); ok {
-		return rf(ctx, cfg, cr, sess, snacPayloadIn)
-	}
-	if rf, ok := ret.Get(0).(func(context.Context, Config, *ChatRegistry, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) XMessage); ok {
-		r0 = rf(ctx, cfg, cr, sess, snacPayloadIn)
-	} else {
-		r0 = ret.Get(0).(XMessage)
-	}
-
-	if rf, ok := ret.Get(1).(func(context.Context, Config, *ChatRegistry, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) error); ok {
-		r1 = rf(ctx, cfg, cr, sess, snacPayloadIn)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
-// MockOServiceHandler_ServiceRequestHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServiceRequestHandler'
-type MockOServiceHandler_ServiceRequestHandler_Call struct {
-	*mock.Call
-}
-
-// ServiceRequestHandler is a helper method to define mock.On call
-//   - ctx context.Context
-//   - cfg Config
-//   - cr *ChatRegistry
-//   - sess *Session
-//   - snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest
-func (_e *MockOServiceHandler_Expecter) ServiceRequestHandler(ctx interface{}, cfg interface{}, cr interface{}, sess interface{}, snacPayloadIn interface{}) *MockOServiceHandler_ServiceRequestHandler_Call {
-	return &MockOServiceHandler_ServiceRequestHandler_Call{Call: _e.mock.On("ServiceRequestHandler", ctx, cfg, cr, sess, snacPayloadIn)}
-}
-
-func (_c *MockOServiceHandler_ServiceRequestHandler_Call) Run(run func(ctx context.Context, cfg Config, cr *ChatRegistry, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest)) *MockOServiceHandler_ServiceRequestHandler_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(Config), args[2].(*ChatRegistry), args[3].(*Session), args[4].(oscar.SNAC_0x01_0x04_OServiceServiceRequest))
-	})
-	return _c
-}
-
-func (_c *MockOServiceHandler_ServiceRequestHandler_Call) Return(_a0 XMessage, _a1 error) *MockOServiceHandler_ServiceRequestHandler_Call {
-	_c.Call.Return(_a0, _a1)
-	return _c
-}
-
-func (_c *MockOServiceHandler_ServiceRequestHandler_Call) RunAndReturn(run func(context.Context, Config, *ChatRegistry, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)) *MockOServiceHandler_ServiceRequestHandler_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
-// SetUserInfoFieldsHandler provides a mock function with given fields: ctx, sess, sm, fm, snacPayloadIn
-func (_m *MockOServiceHandler) SetUserInfoFieldsHandler(ctx context.Context, sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error) {
-	ret := _m.Called(ctx, sess, sm, fm, snacPayloadIn)
+// SetUserInfoFieldsHandler provides a mock function with given fields: ctx, sess, snacPayloadIn
+func (_m *MockOServiceHandler) SetUserInfoFieldsHandler(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error) {
+	ret := _m.Called(ctx, sess, snacPayloadIn)
 
 	var r0 XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)); ok {
-		return rf(ctx, sess, sm, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)); ok {
+		return rf(ctx, sess, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) XMessage); ok {
-		r0 = rf(ctx, sess, sm, fm, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) XMessage); ok {
+		r0 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r0 = ret.Get(0).(XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) error); ok {
-		r1 = rf(ctx, sess, sm, fm, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) error); ok {
+		r1 = rf(ctx, sess, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -324,16 +217,14 @@ type MockOServiceHandler_SetUserInfoFieldsHandler_Call struct {
 // SetUserInfoFieldsHandler is a helper method to define mock.On call
 //   - ctx context.Context
 //   - sess *Session
-//   - sm SessionManager
-//   - fm *FeedbagStore
 //   - snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields
-func (_e *MockOServiceHandler_Expecter) SetUserInfoFieldsHandler(ctx interface{}, sess interface{}, sm interface{}, fm interface{}, snacPayloadIn interface{}) *MockOServiceHandler_SetUserInfoFieldsHandler_Call {
-	return &MockOServiceHandler_SetUserInfoFieldsHandler_Call{Call: _e.mock.On("SetUserInfoFieldsHandler", ctx, sess, sm, fm, snacPayloadIn)}
+func (_e *MockOServiceHandler_Expecter) SetUserInfoFieldsHandler(ctx interface{}, sess interface{}, snacPayloadIn interface{}) *MockOServiceHandler_SetUserInfoFieldsHandler_Call {
+	return &MockOServiceHandler_SetUserInfoFieldsHandler_Call{Call: _e.mock.On("SetUserInfoFieldsHandler", ctx, sess, snacPayloadIn)}
 }
 
-func (_c *MockOServiceHandler_SetUserInfoFieldsHandler_Call) Run(run func(ctx context.Context, sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields)) *MockOServiceHandler_SetUserInfoFieldsHandler_Call {
+func (_c *MockOServiceHandler_SetUserInfoFieldsHandler_Call) Run(run func(ctx context.Context, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields)) *MockOServiceHandler_SetUserInfoFieldsHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*Session), args[2].(SessionManager), args[3].(*FeedbagStore), args[4].(oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields))
+		run(args[0].(context.Context), args[1].(*Session), args[2].(oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields))
 	})
 	return _c
 }
@@ -343,7 +234,7 @@ func (_c *MockOServiceHandler_SetUserInfoFieldsHandler_Call) Return(_a0 XMessage
 	return _c
 }
 
-func (_c *MockOServiceHandler_SetUserInfoFieldsHandler_Call) RunAndReturn(run func(context.Context, *Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)) *MockOServiceHandler_SetUserInfoFieldsHandler_Call {
+func (_c *MockOServiceHandler_SetUserInfoFieldsHandler_Call) RunAndReturn(run func(context.Context, *Session, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)) *MockOServiceHandler_SetUserInfoFieldsHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }
@@ -391,49 +282,6 @@ func (_c *MockOServiceHandler_UserInfoQueryHandler_Call) RunAndReturn(run func(c
 	return _c
 }
 
-// WriteOServiceHostOnline provides a mock function with given fields: w, sequence
-func (_m *MockOServiceHandler) WriteOServiceHostOnline(w io.Writer, sequence *uint32) error {
-	ret := _m.Called(w, sequence)
-
-	var r0 error
-	if rf, ok := ret.Get(0).(func(io.Writer, *uint32) error); ok {
-		r0 = rf(w, sequence)
-	} else {
-		r0 = ret.Error(0)
-	}
-
-	return r0
-}
-
-// MockOServiceHandler_WriteOServiceHostOnline_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteOServiceHostOnline'
-type MockOServiceHandler_WriteOServiceHostOnline_Call struct {
-	*mock.Call
-}
-
-// WriteOServiceHostOnline is a helper method to define mock.On call
-//   - w io.Writer
-//   - sequence *uint32
-func (_e *MockOServiceHandler_Expecter) WriteOServiceHostOnline(w interface{}, sequence interface{}) *MockOServiceHandler_WriteOServiceHostOnline_Call {
-	return &MockOServiceHandler_WriteOServiceHostOnline_Call{Call: _e.mock.On("WriteOServiceHostOnline", w, sequence)}
-}
-
-func (_c *MockOServiceHandler_WriteOServiceHostOnline_Call) Run(run func(w io.Writer, sequence *uint32)) *MockOServiceHandler_WriteOServiceHostOnline_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(io.Writer), args[1].(*uint32))
-	})
-	return _c
-}
-
-func (_c *MockOServiceHandler_WriteOServiceHostOnline_Call) Return(_a0 error) *MockOServiceHandler_WriteOServiceHostOnline_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *MockOServiceHandler_WriteOServiceHostOnline_Call) RunAndReturn(run func(io.Writer, *uint32) error) *MockOServiceHandler_WriteOServiceHostOnline_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // NewMockOServiceHandler creates a new instance of MockOServiceHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
 // The first argument is typically a *testing.T value.
 func NewMockOServiceHandler(t interface {

+ 321 - 15
server/oservice_test.go

@@ -135,8 +135,15 @@ func TestReceiveAndSendServiceRequest(t *testing.T) {
 			//
 			// send input SNAC
 			//
-			svc := OServiceService{}
-			outputSNAC, err := svc.ServiceRequestHandler(nil, tc.cfg, cr, tc.userSession, tc.inputSNAC)
+			svc := OServiceServiceForBOS{
+				OServiceService: OServiceService{
+					cfg: tc.cfg,
+					sm:  sm,
+				},
+				cr: cr,
+			}
+
+			outputSNAC, err := svc.ServiceRequestHandler(nil, tc.userSession, tc.inputSNAC)
 			assert.ErrorIs(t, err, tc.expectErr)
 			if tc.expectErr != nil {
 				return
@@ -149,7 +156,7 @@ func TestReceiveAndSendServiceRequest(t *testing.T) {
 	}
 }
 
-func TestOServiceRouter_RouteOService(t *testing.T) {
+func TestOServiceRouter_RouteOService_ForBOS(t *testing.T) {
 	cases := []struct {
 		// name is the unit test name
 		name string
@@ -358,10 +365,6 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			svc := NewMockOServiceHandler(t)
-			svc.EXPECT().
-				ServiceRequestHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
-				Return(tc.output, tc.handlerErr).
-				Maybe()
 			svc.EXPECT().
 				RateParamsQueryHandler(mock.Anything).
 				Return(tc.output).
@@ -375,26 +378,329 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 				Return(tc.output).
 				Maybe()
 			svc.EXPECT().
-				SetUserInfoFieldsHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				SetUserInfoFieldsHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
-				ClientOnlineHandler(mock.Anything, tc.input.snacOut, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+				RateParamsSubAddHandler(mock.Anything, tc.input.snacOut).
+				Maybe()
+			svc.EXPECT().
+				IdleNotificationHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.handlerErr).
 				Maybe()
+
+			svcBOS := NewMockOServiceBOSHandler(t)
+			svcBOS.EXPECT().
+				ServiceRequestHandler(mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+			svcBOS.EXPECT().
+				ClientOnlineHandler(mock.Anything, tc.input.snacOut, mock.Anything).
+				Return(tc.handlerErr).
+				Maybe()
+
+			router := OServiceBOSRouter{
+				OServiceRouter: OServiceRouter{
+					OServiceHandler: svc,
+					RouteLogger: RouteLogger{
+						Logger: NewLogger(Config{}),
+					},
+				},
+				OServiceBOSHandler: svcBOS,
+			}
+
+			bufIn := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.input.snacOut, bufIn))
+
+			bufOut := &bytes.Buffer{}
+			seq := uint32(1)
+
+			err := router.RouteOService(nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
+			}
+
+			if tc.output == (XMessage{}) {
+				// make sure no response was sent
+				assert.Empty(t, bufOut.Bytes())
+				return
+			}
+
+			// verify the FLAP frame
+			flap := oscar.FlapFrame{}
+			assert.NoError(t, oscar.Unmarshal(&flap, bufOut))
+
+			// make sure the sequence number was incremented
+			assert.Equal(t, uint32(2), seq)
+
+			flapBuf, err := flap.SNACBuffer(bufOut)
+			assert.NoError(t, err)
+
+			// verify the SNAC frame
+			snacFrame := oscar.SnacFrame{}
+			assert.NoError(t, oscar.Unmarshal(&snacFrame, flapBuf))
+			assert.Equal(t, tc.output.snacFrame, snacFrame)
+
+			// verify the SNAC message
+			snacBuf := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.output.snacOut, snacBuf))
+			assert.Equal(t, snacBuf.Bytes(), flapBuf.Bytes())
+		})
+	}
+}
+
+func TestOServiceRouter_RouteOService_ForChat(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// input is the request payload
+		input XMessage
+		// output is the response payload
+		output XMessage
+		// handlerErr is the mocked handler error response
+		handlerErr error
+		// expectErr is the expected error returned by the router
+		expectErr error
+	}{
+		{
+			name: "receive OServiceClientOnline, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceClientOnline,
+				},
+				snacOut: oscar.SNAC_0x01_0x02_OServiceClientOnline{
+					GroupVersions: []struct {
+						FoodGroup   uint16
+						Version     uint16
+						ToolID      uint16
+						ToolVersion uint16
+					}{
+						{
+							FoodGroup: 10,
+						},
+					},
+				},
+			},
+			output: XMessage{},
+		},
+		{
+			name: "receive OServiceServiceRequest, return OServiceServiceResponse",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceServiceRequest,
+				},
+				snacOut: oscar.SNAC_0x01_0x04_OServiceServiceRequest{
+					FoodGroup: 10,
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceServiceResponse,
+				},
+				snacOut: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							oscar.NewTLV(0x01, uint16(1000)),
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "receive OServiceRateParamsQuery, return OServiceRateParamsReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceRateParamsQuery,
+				},
+				snacOut: struct{}{},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceRateParamsReply,
+				},
+				snacOut: oscar.SNAC_0x01_0x07_OServiceRateParamsReply{
+					RateGroups: []struct {
+						ID    uint16
+						Pairs []struct {
+							FoodGroup uint16
+							SubGroup  uint16
+						} `count_prefix:"uint16"`
+					}{
+						{
+							ID: 1,
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "receive OServiceRateParamsSubAdd, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceRateParamsSubAdd,
+				},
+				snacOut: oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							oscar.NewTLV(0x01, []byte{1, 2, 3, 4}),
+						},
+					},
+				},
+			},
+			output: XMessage{},
+		},
+		{
+			name: "receive OServiceUserInfoQuery, return OServiceUserInfoUpdate",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceUserInfoQuery,
+				},
+				snacOut: struct{}{},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceUserInfoUpdate,
+				},
+				snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
+					TLVUserInfo: oscar.TLVUserInfo{
+						ScreenName: "screen-name",
+					},
+				},
+			},
+		},
+		{
+			name: "receive OServiceIdleNotification, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceIdleNotification,
+				},
+				snacOut: oscar.SNAC_0x01_0x11_OServiceIdleNotification{
+					IdleTime: 10,
+				},
+			},
+			output: XMessage{},
+		},
+		{
+			name: "receive OServiceClientVersions, return OServiceHostVersions",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceClientVersions,
+				},
+				snacOut: oscar.SNAC_0x01_0x17_OServiceClientVersions{
+					Versions: []uint16{
+						10,
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceHostVersions,
+				},
+				snacOut: oscar.SNAC_0x01_0x18_OServiceHostVersions{
+					Versions: []uint16{
+						10,
+					},
+				},
+			},
+		},
+		{
+			name: "receive OServiceSetUserInfoFields, return OServiceUserInfoUpdate",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceSetUserInfoFields,
+				},
+				snacOut: oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							oscar.NewTLV(0x01, []byte{1, 2, 3, 4}),
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServiceUserInfoUpdate,
+				},
+				snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
+					TLVUserInfo: oscar.TLVUserInfo{
+						ScreenName: "screen-name",
+					},
+				},
+			},
+		},
+		{
+			name: "receive OServicePauseReq, expect ErrUnsupportedSubGroup",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.OSERVICE,
+					SubGroup:  oscar.OServicePauseReq,
+				},
+				snacOut: struct{}{}, // empty SNAC
+			},
+			output:    XMessage{}, // empty SNAC
+			expectErr: ErrUnsupportedSubGroup,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			svc := NewMockOServiceHandler(t)
+			svc.EXPECT().
+				RateParamsQueryHandler(mock.Anything).
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				UserInfoQueryHandler(mock.Anything, mock.Anything).
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				ClientVersionsHandler(mock.Anything, tc.input.snacOut).
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				SetUserInfoFieldsHandler(mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
 			svc.EXPECT().
 				RateParamsSubAddHandler(mock.Anything, tc.input.snacOut).
 				Maybe()
 			svc.EXPECT().
-				IdleNotificationHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				IdleNotificationHandler(mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.handlerErr).
 				Maybe()
 
-			router := OServiceRouter{
-				OServiceHandler: svc,
-				RouteLogger: RouteLogger{
-					Logger: NewLogger(Config{}),
+			svcBOS := NewMockOServiceChatHandler(t)
+			svcBOS.EXPECT().
+				ServiceRequestHandler(mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+			svcBOS.EXPECT().
+				ClientOnlineHandler(mock.Anything, tc.input.snacOut, mock.Anything, mock.Anything).
+				Return(tc.handlerErr).
+				Maybe()
+
+			router := OServiceChatRouter{
+				OServiceRouter: OServiceRouter{
+					OServiceHandler: svc,
+					RouteLogger: RouteLogger{
+						Logger: NewLogger(Config{}),
+					},
 				},
+				OServiceChatHandler: svcBOS,
 			}
 
 			bufIn := &bytes.Buffer{}
@@ -403,7 +709,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 			bufOut := &bytes.Buffer{}
 			seq := uint32(1)
 
-			err := router.RouteOService(nil, Config{}, nil, nil, nil, nil, ChatRoom{}, tc.input.snacFrame, bufIn, bufOut, &seq)
+			err := router.RouteOService(nil, nil, ChatRoom{}, tc.input.snacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
 			if tc.expectErr != nil {
 				return

+ 75 - 26
server/router.go

@@ -3,59 +3,63 @@ package server
 import (
 	"bytes"
 	"context"
+	"errors"
+	"fmt"
 	"github.com/mkaminski/goaim/oscar"
 	"io"
 	"log/slog"
+	"net"
 )
 
-func NewRouter(logger *slog.Logger) Router {
-	return Router{
-		AlertRouter:    NewAlertRouter(logger),
-		BuddyRouter:    NewBuddyRouter(logger),
-		ChatNavRouter:  NewChatNavRouter(logger),
-		ChatRouter:     NewChatRouter(logger),
-		FeedbagRouter:  NewFeedbagRouter(logger),
-		ICBMRouter:     NewICBMRouter(logger),
-		LocateRouter:   NewLocateRouter(logger),
-		OServiceRouter: NewOServiceRouter(logger),
+func NewBOSServiceRouter(logger *slog.Logger, cfg Config, fm FeedbagManager, sm SessionManager, cr *ChatRegistry, pm ProfileManager) BOSServiceRouter {
+	return BOSServiceRouter{
+		AlertRouter:       NewAlertRouter(logger),
+		BuddyRouter:       NewBuddyRouter(logger),
+		ChatNavRouter:     NewChatNavRouter(logger, cr),
+		FeedbagRouter:     NewFeedbagRouter(logger, sm, fm),
+		ICBMRouter:        NewICBMRouter(logger, sm, fm),
+		LocateRouter:      NewLocateRouter(logger, sm, fm, pm),
+		OServiceBOSRouter: NewOServiceRouterForBOS(logger, cfg, fm, sm, cr),
+		sm:                sm,
+		fm:                fm,
 	}
 }
 
-func NewRouterForChat(logger *slog.Logger) Router {
-	r := NewRouter(logger)
-	r.OServiceRouter = NewOServiceRouterForChat(logger)
-	return r
+func NewChatServiceRouter(logger *slog.Logger, cfg Config, fm FeedbagManager, sm SessionManager) ChatServiceRouter {
+	return ChatServiceRouter{
+		OServiceChatRouter: NewOServiceRouterForChat(logger, cfg, fm, sm),
+		ChatRouter:         NewChatRouter(logger),
+	}
 }
 
-type Router struct {
+type BOSServiceRouter struct {
 	AlertRouter
 	BuddyRouter
 	ChatNavRouter
-	ChatRouter
 	FeedbagRouter
 	ICBMRouter
 	LocateRouter
-	OServiceRouter
+	OServiceBOSRouter
+	sm SessionManager
+	fm FeedbagManager
 }
 
-func (rt *Router) routeIncomingRequests(ctx context.Context, cfg Config, sm SessionManager, sess *Session, fm *FeedbagStore, cr *ChatRegistry, rw io.ReadWriter, sequence *uint32, snac oscar.SnacFrame, buf io.Reader, room ChatRoom) error {
+func (rt *BOSServiceRouter) Route(ctx context.Context, sess *Session, w io.Writer, sequence *uint32, snac oscar.SnacFrame, buf io.Reader) error {
 	switch snac.FoodGroup {
 	case oscar.OSERVICE:
-		return rt.RouteOService(ctx, cfg, cr, sm, fm, sess, room, snac, buf, rw, sequence)
+		return rt.RouteOService(ctx, sess, snac, buf, w, sequence)
 	case oscar.LOCATE:
-		return rt.RouteLocate(ctx, sess, sm, fm, snac, buf, rw, sequence)
+		return rt.RouteLocate(ctx, sess, snac, buf, w, sequence)
 	case oscar.BUDDY:
-		return rt.RouteBuddy(ctx, snac, buf, rw, sequence)
+		return rt.RouteBuddy(ctx, snac, buf, w, sequence)
 	case oscar.ICBM:
-		return rt.RouteICBM(ctx, sm, fm, sess, snac, buf, rw, sequence)
+		return rt.RouteICBM(ctx, sess, snac, buf, w, sequence)
 	case oscar.CHAT_NAV:
-		return rt.RouteChatNav(ctx, sess, cr, snac, buf, rw, sequence)
+		return rt.RouteChatNav(ctx, sess, snac, buf, w, sequence)
 	case oscar.FEEDBAG:
-		return rt.RouteFeedbag(ctx, sm, sess, fm, snac, buf, rw, sequence)
+		return rt.RouteFeedbag(ctx, sess, snac, buf, w, sequence)
 	case oscar.BUCP:
 		return routeBUCP(ctx)
-	case oscar.CHAT:
-		return rt.RouteChat(ctx, sess, sm, snac, buf, rw, sequence)
 	case oscar.ALERT:
 		return rt.RouteAlert(ctx, snac)
 	default:
@@ -63,6 +67,35 @@ func (rt *Router) routeIncomingRequests(ctx context.Context, cfg Config, sm Sess
 	}
 }
 
+func (rt *BOSServiceRouter) Signout(ctx context.Context, logger *slog.Logger, sess *Session) {
+	if err := BroadcastDeparture(ctx, sess, rt.sm, rt.fm); err != nil {
+		logger.ErrorContext(ctx, "error notifying departure", "err", err.Error())
+	}
+	rt.sm.Remove(sess)
+}
+
+func (rt *BOSServiceRouter) VerifyLogin(conn net.Conn) (*Session, uint32, error) {
+	seq := uint32(100)
+
+	flap, err := SendAndReceiveSignonFrame(conn, &seq)
+	if err != nil {
+		return nil, 0, err
+	}
+
+	var ok bool
+	ID, ok := flap.GetSlice(oscar.OServiceTLVTagsLoginCookie)
+	if !ok {
+		return nil, 0, errors.New("unable to get session ID from payload")
+	}
+
+	sess, ok := rt.sm.Retrieve(string(ID))
+	if !ok {
+		return nil, 0, fmt.Errorf("unable to find session by ID %s", ID)
+	}
+
+	return sess, seq, nil
+}
+
 func writeOutSNAC(originsnac oscar.SnacFrame, snacFrame oscar.SnacFrame, snacOut any, sequence *uint32, w io.Writer) error {
 	if originsnac.RequestID != 0 {
 		snacFrame.RequestID = originsnac.RequestID
@@ -110,3 +143,19 @@ func sendInvalidSNACErr(snac oscar.SnacFrame, w io.Writer, sequence *uint32) err
 	}
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
+
+type ChatServiceRouter struct {
+	ChatRouter
+	OServiceChatRouter
+}
+
+func (rt *ChatServiceRouter) Route(ctx context.Context, sess *Session, w io.Writer, sequence *uint32, snac oscar.SnacFrame, buf io.Reader, room ChatRoom) error {
+	switch snac.FoodGroup {
+	case oscar.OSERVICE:
+		return rt.RouteOService(ctx, sess, room, snac, buf, w, sequence)
+	case oscar.CHAT:
+		return rt.RouteChat(ctx, sess, room, snac, buf, w, sequence)
+	default:
+		return ErrUnsupportedFoodGroup
+	}
+}