Mike 2 лет назад
Родитель
Сommit
234e78800b

+ 2 - 1
.mockery.yaml

@@ -11,4 +11,5 @@ packages:
       ProfileManager:
       ICBMHandler:
       LocateHandler:
-      OServiceHandler:
+      OServiceHandler:
+      FeedbagHandler:

+ 23 - 10
cmd/main.go

@@ -125,15 +125,28 @@ func handleBOSConnection(cfg server.Config, sm *server.InMemorySessionManager, f
 		conn.Close()
 	}()
 
-	onClientReady := func(sess *server.Session, sm server.SessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
+	onClientReady := func(sess *server.Session, sm server.SessionManager) ([]server.XMessage, error) {
 		if err := server.NotifyArrival(sess, sm, fm); err != nil {
-			return err
+			return []server.XMessage{}, err
 		}
-		return server.GetAllOnlineBuddies(w, sess, sm, fm, sequence)
+		buddies, err := fm.Buddies(sess.ScreenName)
+		if err != nil {
+			return []server.XMessage{}, err
+		}
+		for _, buddy := range buddies {
+			err := server.NotifyBuddyArrived(buddy, sess.ScreenName, sm)
+			switch {
+			case errors.Is(err, server.ErrSessNotFound):
+				continue
+			case err != nil:
+				return []server.XMessage{}, err
+			}
+		}
+		return []server.XMessage{}, nil
 	}
 
 	foodGroups := []uint16{0x0001, 0x0002, 0x0003, 0x0004, 0x0009, 0x0013, 0x000D}
-	if err := server.ReadBos(cfg, onClientReady, sess, seq, sm, fm, cr, conn, foodGroups); err != nil && err != io.EOF {
+	if err := server.ReadBos(cfg, onClientReady, sess, seq, sm, fm, cr, conn, foodGroups); err != nil {
 		switch {
 		case errors.Is(io.EOF, err):
 			fallthrough
@@ -175,15 +188,15 @@ func handleChatConnection(cfg server.Config, fm *server.FeedbagStore, cr *server
 
 	foodGroups := []uint16{0x0001, 0x0002, 0x0003, 0x0004, 0x0009, 0x0013, 0x000D, 0x000E}
 
-	onClientReady := func(sess *server.Session, sm server.SessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
-		if err := server.SendChatRoomInfoUpdate(room, w, sequence); err != nil {
-			return err
-		}
+	onClientReady := func(sess *server.Session, sm server.SessionManager) ([]server.XMessage, error) {
 		server.AlertUserJoined(sess, sm)
-		return server.SetOnlineChatUsers(sm, w, sequence)
+		return []server.XMessage{
+			server.SendChatRoomInfoUpdateTmp(room),
+			server.SetOnlineChatUsersTmp(sm),
+		}, nil
 	}
 
-	if err := server.ReadBos(cfg, onClientReady, chatSess, seq, room.SessionManager, fm, cr, conn, foodGroups); err != nil && err != io.EOF {
+	if err := server.ReadBos(cfg, onClientReady, chatSess, seq, room.SessionManager, fm, cr, conn, foodGroups); err != nil {
 		if err != io.EOF {
 			fmt.Printf("user disconnected with error: %s\n", err.Error())
 		} else {

+ 1 - 1
oscar/frames.go

@@ -17,7 +17,7 @@ type FlapFrame struct {
 	PayloadLength uint16
 }
 
-func (f FlapFrame) SNACBuffer(r io.Reader) (io.Reader, error) {
+func (f FlapFrame) SNACBuffer(r io.Reader) (*bytes.Buffer, error) {
 	b := make([]byte, f.PayloadLength)
 	if _, err := r.Read(b); err != nil {
 		return nil, err

+ 140 - 0
oscar/snacs.go

@@ -415,6 +415,142 @@ type SNAC_0x0E_0x06_ChatChannelMsgToClient struct {
 // 0x13: Feedbag
 //
 
+const (
+	FeedbagClassIdBuddy            uint16 = 0x0000
+	FeedbagClassIdGroup            uint16 = 0x0001
+	FeedbagClassIDPermit           uint16 = 0x0002
+	FeedbagClassIDDeny             uint16 = 0x0003
+	FeedbagClassIdPdinfo           uint16 = 0x0004
+	FeedbagClassIdBuddyPrefs       uint16 = 0x0005
+	FeedbagClassIdNonbuddy         uint16 = 0x0006
+	FeedbagClassIdTpaProvider      uint16 = 0x0007
+	FeedbagClassIdTpaSubscription  uint16 = 0x0008
+	FeedbagClassIdClientPrefs      uint16 = 0x0009
+	FeedbagClassIdStock            uint16 = 0x000A
+	FeedbagClassIdWeather          uint16 = 0x000B
+	FeedbagClassIdWatchList        uint16 = 0x000D
+	FeedbagClassIdIgnoreList       uint16 = 0x000E
+	FeedbagClassIdDateTime         uint16 = 0x000F
+	FeedbagClassIdExternalUser     uint16 = 0x0010
+	FeedbagClassIdRootCreator      uint16 = 0x0011
+	FeedbagClassIdFish             uint16 = 0x0012
+	FeedbagClassIdImportTimestamp  uint16 = 0x0013
+	FeedbagClassIdBart             uint16 = 0x0014
+	FeedbagClassIdRbOrder          uint16 = 0x0015
+	FeedbagClassIdPersonality      uint16 = 0x0016
+	FeedbagClassIdAlProf           uint16 = 0x0017
+	FeedbagClassIdAlInfo           uint16 = 0x0018
+	FeedbagClassIdInteraction      uint16 = 0x0019
+	FeedbagClassIdVanityInfo       uint16 = 0x001D
+	FeedbagClassIdFavoriteLocation uint16 = 0x001E
+	FeedbagClassIdBartPdinfo       uint16 = 0x001F
+	FeedbagClassIdCustomEmoticons  uint16 = 0x0024
+	FeedbagClassIdMaxPredefined    uint16 = 0x0024
+	FeedbagClassIdXIcqStatusNote   uint16 = 0x015C
+	FeedbagClassIdMin              uint16 = 0x0400
+
+	FeedbagAttributesShared                  uint16 = 0x0064
+	FeedbagAttributesInvited                 uint16 = 0x0065
+	FeedbagAttributesPending                 uint16 = 0x0066
+	FeedbagAttributesTimeT                   uint16 = 0x0067
+	FeedbagAttributesDenied                  uint16 = 0x0068
+	FeedbagAttributesSwimIndex               uint16 = 0x0069
+	FeedbagAttributesRecentBuddy             uint16 = 0x006A
+	FeedbagAttributesAutoBot                 uint16 = 0x006B
+	FeedbagAttributesInteraction             uint16 = 0x006D
+	FeedbagAttributesMegaBot                 uint16 = 0x006F
+	FeedbagAttributesOrder                   uint16 = 0x00C8
+	FeedbagAttributesBuddyPrefs              uint16 = 0x00C9
+	FeedbagAttributesPdMode                  uint16 = 0x00CA
+	FeedbagAttributesPdMask                  uint16 = 0x00CB
+	FeedbagAttributesPdFlags                 uint16 = 0x00CC
+	FeedbagAttributesClientPrefs             uint16 = 0x00CD
+	FeedbagAttributesLanguage                uint16 = 0x00CE
+	FeedbagAttributesFishUri                 uint16 = 0x00CF
+	FeedbagAttributesWirelessPdMode          uint16 = 0x00D0
+	FeedbagAttributesWirelessIgnoreMode      uint16 = 0x00D1
+	FeedbagAttributesFishPdMode              uint16 = 0x00D2
+	FeedbagAttributesFishIgnoreMode          uint16 = 0x00D3
+	FeedbagAttributesCreateTime              uint16 = 0x00D4
+	FeedbagAttributesBartInfo                uint16 = 0x00D5
+	FeedbagAttributesBuddyPrefsValid         uint16 = 0x00D6
+	FeedbagAttributesBuddyPrefs2             uint16 = 0x00D7
+	FeedbagAttributesBuddyPrefs2Valid        uint16 = 0x00D8
+	FeedbagAttributesBartList                uint16 = 0x00D9
+	FeedbagAttributesArriveSound             uint16 = 0x012C
+	FeedbagAttributesLeaveSound              uint16 = 0x012D
+	FeedbagAttributesImage                   uint16 = 0x012E
+	FeedbagAttributesColorBg                 uint16 = 0x012F
+	FeedbagAttributesColorFg                 uint16 = 0x0130
+	FeedbagAttributesAlias                   uint16 = 0x0131
+	FeedbagAttributesPassword                uint16 = 0x0132
+	FeedbagAttributesDisabled                uint16 = 0x0133
+	FeedbagAttributesCollapsed               uint16 = 0x0134
+	FeedbagAttributesUrl                     uint16 = 0x0135
+	FeedbagAttributesActiveList              uint16 = 0x0136
+	FeedbagAttributesEmailAddr               uint16 = 0x0137
+	FeedbagAttributesPhoneNumber             uint16 = 0x0138
+	FeedbagAttributesCellPhoneNumber         uint16 = 0x0139
+	FeedbagAttributesSmsPhoneNumber          uint16 = 0x013A
+	FeedbagAttributesWireless                uint16 = 0x013B
+	FeedbagAttributesNote                    uint16 = 0x013C
+	FeedbagAttributesAlertPrefs              uint16 = 0x013D
+	FeedbagAttributesBudalertSound           uint16 = 0x013E
+	FeedbagAttributesStockalertValue         uint16 = 0x013F
+	FeedbagAttributesTpalertEditUrl          uint16 = 0x0140
+	FeedbagAttributesTpalertDeleteUrl        uint16 = 0x0141
+	FeedbagAttributesTpprovMorealertsUrl     uint16 = 0x0142
+	FeedbagAttributesFish                    uint16 = 0x0143
+	FeedbagAttributesXunconfirmedxLastAccess uint16 = 0x0145
+	FeedbagAttributesImSent                  uint16 = 0x0150
+	FeedbagAttributesOnlineTime              uint16 = 0x0151
+	FeedbagAttributesAwayMsg                 uint16 = 0x0152
+	FeedbagAttributesImReceived              uint16 = 0x0153
+	FeedbagAttributesBuddyfeedView           uint16 = 0x0154
+	FeedbagAttributesWorkPhoneNumber         uint16 = 0x0158
+	FeedbagAttributesOtherPhoneNumber        uint16 = 0x0159
+	FeedbagAttributesWebPdMode               uint16 = 0x015F
+	FeedbagAttributesFirstCreationTimeXc     uint16 = 0x0167
+	FeedbagAttributesPdModeXc                uint16 = 0x016E
+
+	FeedbagErr                      uint16 = 0x0001
+	FeedbagRightsQuery              uint16 = 0x0002
+	FeedbagRightsReply              uint16 = 0x0003
+	FeedbagQuery                    uint16 = 0x0004
+	FeedbagQueryIfModified          uint16 = 0x0005
+	FeedbagReply                    uint16 = 0x0006
+	FeedbagUse                      uint16 = 0x0007
+	FeedbagInsertItem               uint16 = 0x0008
+	FeedbagUpdateItem               uint16 = 0x0009
+	FeedbagDeleteItem               uint16 = 0x000A
+	FeedbagInsertClass              uint16 = 0x000B
+	FeedbagUpdateClass              uint16 = 0x000C
+	FeedbagDeleteClass              uint16 = 0x000D
+	FeedbagStatus                   uint16 = 0x000E
+	FeedbagReplyNotModified         uint16 = 0x000F
+	FeedbagDeleteUser               uint16 = 0x0010
+	FeedbagStartCluster             uint16 = 0x0011
+	FeedbagEndCluster               uint16 = 0x0012
+	FeedbagAuthorizeBuddy           uint16 = 0x0013
+	FeedbagPreAuthorizeBuddy        uint16 = 0x0014
+	FeedbagPreAuthorizedBuddy       uint16 = 0x0015
+	FeedbagRemoveMe                 uint16 = 0x0016
+	FeedbagRemoveMe2                uint16 = 0x0017
+	FeedbagRequestAuthorizeToHost   uint16 = 0x0018
+	FeedbagRequestAuthorizeToClient uint16 = 0x0019
+	FeedbagRespondAuthorizeToHost   uint16 = 0x001A
+	FeedbagRespondAuthorizeToClient uint16 = 0x001B
+	FeedbagBuddyAdded               uint16 = 0x001C
+	FeedbagRequestAuthorizeToBadog  uint16 = 0x001D
+	FeedbagRespondAuthorizeToBadog  uint16 = 0x001E
+	FeedbagBuddyAddedToBadog        uint16 = 0x001F
+	FeedbagTestSnac                 uint16 = 0x0021
+	FeedbagForwardMsg               uint16 = 0x0022
+	FeedbagIsAuthRequiredQuery      uint16 = 0x0023
+	FeedbagIsAuthRequiredReply      uint16 = 0x0024
+	FeedbagRecentBuddyUpdate        uint16 = 0x0025
+)
+
 type SNAC_0x13_0x02_FeedbagRightsQuery struct {
 	TLVRestBlock
 }
@@ -450,6 +586,10 @@ type SNAC_0x13_0x0E_FeedbagStatus struct {
 	Results []uint16
 }
 
+type SNAC_0x13_0x11_FeedbagStartCluster struct {
+	TLVRestBlock
+}
+
 //
 // 0x17: BUCP
 //

+ 381 - 0
server/MockFeedbagHandler.go

@@ -0,0 +1,381 @@
+// Code generated by mockery v2.35.2. DO NOT EDIT.
+
+package server
+
+import (
+	oscar "github.com/mkaminski/goaim/oscar"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// MockFeedbagHandler is an autogenerated mock type for the FeedbagHandler type
+type MockFeedbagHandler struct {
+	mock.Mock
+}
+
+type MockFeedbagHandler_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockFeedbagHandler) EXPECT() *MockFeedbagHandler_Expecter {
+	return &MockFeedbagHandler_Expecter{mock: &_m.Mock}
+}
+
+// DeleteItemHandler provides a mock function with given fields: sm, sess, fm, snacPayloadIn
+func (_m *MockFeedbagHandler) DeleteItemHandler(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error) {
+	ret := _m.Called(sm, sess, fm, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error)); ok {
+		return rf(sm, sess, fm, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) XMessage); ok {
+		r0 = rf(sm, sess, fm, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) error); ok {
+		r1 = rf(sm, sess, fm, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockFeedbagHandler_DeleteItemHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteItemHandler'
+type MockFeedbagHandler_DeleteItemHandler_Call struct {
+	*mock.Call
+}
+
+// DeleteItemHandler is a helper method to define mock.On call
+//   - sm SessionManager
+//   - sess *Session
+//   - fm FeedbagManager
+//   - snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem
+func (_e *MockFeedbagHandler_Expecter) DeleteItemHandler(sm interface{}, sess interface{}, fm interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_DeleteItemHandler_Call {
+	return &MockFeedbagHandler_DeleteItemHandler_Call{Call: _e.mock.On("DeleteItemHandler", sm, sess, fm, snacPayloadIn)}
+}
+
+func (_c *MockFeedbagHandler_DeleteItemHandler_Call) Run(run func(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem)) *MockFeedbagHandler_DeleteItemHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(SessionManager), args[1].(*Session), args[2].(FeedbagManager), args[3].(oscar.SNAC_0x13_0x0A_FeedbagDeleteItem))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagHandler_DeleteItemHandler_Call) Return(_a0 XMessage, _a1 error) *MockFeedbagHandler_DeleteItemHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFeedbagHandler_DeleteItemHandler_Call) RunAndReturn(run func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error)) *MockFeedbagHandler_DeleteItemHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// InsertItemHandler provides a mock function with given fields: sm, sess, fm, snacPayloadIn
+func (_m *MockFeedbagHandler) InsertItemHandler(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error) {
+	ret := _m.Called(sm, sess, fm, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error)); ok {
+		return rf(sm, sess, fm, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x08_FeedbagInsertItem) XMessage); ok {
+		r0 = rf(sm, sess, fm, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x08_FeedbagInsertItem) error); ok {
+		r1 = rf(sm, sess, fm, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockFeedbagHandler_InsertItemHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertItemHandler'
+type MockFeedbagHandler_InsertItemHandler_Call struct {
+	*mock.Call
+}
+
+// InsertItemHandler is a helper method to define mock.On call
+//   - sm SessionManager
+//   - sess *Session
+//   - fm FeedbagManager
+//   - snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem
+func (_e *MockFeedbagHandler_Expecter) InsertItemHandler(sm interface{}, sess interface{}, fm interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_InsertItemHandler_Call {
+	return &MockFeedbagHandler_InsertItemHandler_Call{Call: _e.mock.On("InsertItemHandler", sm, sess, fm, snacPayloadIn)}
+}
+
+func (_c *MockFeedbagHandler_InsertItemHandler_Call) Run(run func(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem)) *MockFeedbagHandler_InsertItemHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(SessionManager), args[1].(*Session), args[2].(FeedbagManager), args[3].(oscar.SNAC_0x13_0x08_FeedbagInsertItem))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagHandler_InsertItemHandler_Call) Return(_a0 XMessage, _a1 error) *MockFeedbagHandler_InsertItemHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFeedbagHandler_InsertItemHandler_Call) RunAndReturn(run func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error)) *MockFeedbagHandler_InsertItemHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// QueryHandler provides a mock function with given fields: sess, fm
+func (_m *MockFeedbagHandler) QueryHandler(sess *Session, fm FeedbagManager) (XMessage, error) {
+	ret := _m.Called(sess, fm)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(*Session, FeedbagManager) (XMessage, error)); ok {
+		return rf(sess, fm)
+	}
+	if rf, ok := ret.Get(0).(func(*Session, FeedbagManager) XMessage); ok {
+		r0 = rf(sess, fm)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(*Session, FeedbagManager) error); ok {
+		r1 = rf(sess, fm)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockFeedbagHandler_QueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryHandler'
+type MockFeedbagHandler_QueryHandler_Call struct {
+	*mock.Call
+}
+
+// QueryHandler is a helper method to define mock.On call
+//   - sess *Session
+//   - fm FeedbagManager
+func (_e *MockFeedbagHandler_Expecter) QueryHandler(sess interface{}, fm interface{}) *MockFeedbagHandler_QueryHandler_Call {
+	return &MockFeedbagHandler_QueryHandler_Call{Call: _e.mock.On("QueryHandler", sess, fm)}
+}
+
+func (_c *MockFeedbagHandler_QueryHandler_Call) Run(run func(sess *Session, fm FeedbagManager)) *MockFeedbagHandler_QueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session), args[1].(FeedbagManager))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagHandler_QueryHandler_Call) Return(_a0 XMessage, _a1 error) *MockFeedbagHandler_QueryHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFeedbagHandler_QueryHandler_Call) RunAndReturn(run func(*Session, FeedbagManager) (XMessage, error)) *MockFeedbagHandler_QueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// QueryIfModifiedHandler provides a mock function with given fields: sess, fm, snacPayloadIn
+func (_m *MockFeedbagHandler) QueryIfModifiedHandler(sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error) {
+	ret := _m.Called(sess, fm, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(*Session, FeedbagManager, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error)); ok {
+		return rf(sess, fm, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(*Session, FeedbagManager, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) XMessage); ok {
+		r0 = rf(sess, fm, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(*Session, FeedbagManager, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) error); ok {
+		r1 = rf(sess, fm, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockFeedbagHandler_QueryIfModifiedHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryIfModifiedHandler'
+type MockFeedbagHandler_QueryIfModifiedHandler_Call struct {
+	*mock.Call
+}
+
+// QueryIfModifiedHandler is a helper method to define mock.On call
+//   - sess *Session
+//   - fm FeedbagManager
+//   - snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified
+func (_e *MockFeedbagHandler_Expecter) QueryIfModifiedHandler(sess interface{}, fm interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_QueryIfModifiedHandler_Call {
+	return &MockFeedbagHandler_QueryIfModifiedHandler_Call{Call: _e.mock.On("QueryIfModifiedHandler", sess, fm, snacPayloadIn)}
+}
+
+func (_c *MockFeedbagHandler_QueryIfModifiedHandler_Call) Run(run func(sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified)) *MockFeedbagHandler_QueryIfModifiedHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session), args[1].(FeedbagManager), args[2].(oscar.SNAC_0x13_0x05_FeedbagQueryIfModified))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagHandler_QueryIfModifiedHandler_Call) Return(_a0 XMessage, _a1 error) *MockFeedbagHandler_QueryIfModifiedHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFeedbagHandler_QueryIfModifiedHandler_Call) RunAndReturn(run func(*Session, FeedbagManager, oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error)) *MockFeedbagHandler_QueryIfModifiedHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RightsQueryHandler provides a mock function with given fields:
+func (_m *MockFeedbagHandler) RightsQueryHandler() XMessage {
+	ret := _m.Called()
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func() XMessage); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockFeedbagHandler_RightsQueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RightsQueryHandler'
+type MockFeedbagHandler_RightsQueryHandler_Call struct {
+	*mock.Call
+}
+
+// RightsQueryHandler is a helper method to define mock.On call
+func (_e *MockFeedbagHandler_Expecter) RightsQueryHandler() *MockFeedbagHandler_RightsQueryHandler_Call {
+	return &MockFeedbagHandler_RightsQueryHandler_Call{Call: _e.mock.On("RightsQueryHandler")}
+}
+
+func (_c *MockFeedbagHandler_RightsQueryHandler_Call) Run(run func()) *MockFeedbagHandler_RightsQueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockFeedbagHandler_RightsQueryHandler_Call) Return(_a0 XMessage) *MockFeedbagHandler_RightsQueryHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFeedbagHandler_RightsQueryHandler_Call) RunAndReturn(run func() XMessage) *MockFeedbagHandler_RightsQueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// StartClusterHandler provides a mock function with given fields: _a0
+func (_m *MockFeedbagHandler) StartClusterHandler(_a0 oscar.SNAC_0x13_0x11_FeedbagStartCluster) {
+	_m.Called(_a0)
+}
+
+// MockFeedbagHandler_StartClusterHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartClusterHandler'
+type MockFeedbagHandler_StartClusterHandler_Call struct {
+	*mock.Call
+}
+
+// StartClusterHandler is a helper method to define mock.On call
+//   - _a0 oscar.SNAC_0x13_0x11_FeedbagStartCluster
+func (_e *MockFeedbagHandler_Expecter) StartClusterHandler(_a0 interface{}) *MockFeedbagHandler_StartClusterHandler_Call {
+	return &MockFeedbagHandler_StartClusterHandler_Call{Call: _e.mock.On("StartClusterHandler", _a0)}
+}
+
+func (_c *MockFeedbagHandler_StartClusterHandler_Call) Run(run func(_a0 oscar.SNAC_0x13_0x11_FeedbagStartCluster)) *MockFeedbagHandler_StartClusterHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(oscar.SNAC_0x13_0x11_FeedbagStartCluster))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagHandler_StartClusterHandler_Call) Return() *MockFeedbagHandler_StartClusterHandler_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockFeedbagHandler_StartClusterHandler_Call) RunAndReturn(run func(oscar.SNAC_0x13_0x11_FeedbagStartCluster)) *MockFeedbagHandler_StartClusterHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// UpdateItemHandler provides a mock function with given fields: sm, sess, fm, snacPayloadIn
+func (_m *MockFeedbagHandler) UpdateItemHandler(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error) {
+	ret := _m.Called(sm, sess, fm, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error)); ok {
+		return rf(sm, sess, fm, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) XMessage); ok {
+		r0 = rf(sm, sess, fm, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) error); ok {
+		r1 = rf(sm, sess, fm, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockFeedbagHandler_UpdateItemHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateItemHandler'
+type MockFeedbagHandler_UpdateItemHandler_Call struct {
+	*mock.Call
+}
+
+// UpdateItemHandler is a helper method to define mock.On call
+//   - sm SessionManager
+//   - sess *Session
+//   - fm FeedbagManager
+//   - snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem
+func (_e *MockFeedbagHandler_Expecter) UpdateItemHandler(sm interface{}, sess interface{}, fm interface{}, snacPayloadIn interface{}) *MockFeedbagHandler_UpdateItemHandler_Call {
+	return &MockFeedbagHandler_UpdateItemHandler_Call{Call: _e.mock.On("UpdateItemHandler", sm, sess, fm, snacPayloadIn)}
+}
+
+func (_c *MockFeedbagHandler_UpdateItemHandler_Call) Run(run func(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem)) *MockFeedbagHandler_UpdateItemHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(SessionManager), args[1].(*Session), args[2].(FeedbagManager), args[3].(oscar.SNAC_0x13_0x09_FeedbagUpdateItem))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagHandler_UpdateItemHandler_Call) Return(_a0 XMessage, _a1 error) *MockFeedbagHandler_UpdateItemHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFeedbagHandler_UpdateItemHandler_Call) RunAndReturn(run func(SessionManager, *Session, FeedbagManager, oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error)) *MockFeedbagHandler_UpdateItemHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockFeedbagHandler creates a new instance of MockFeedbagHandler. 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 NewMockFeedbagHandler(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockFeedbagHandler {
+	mock := &MockFeedbagHandler{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 27 - 20
server/MockOServiceHandler.go

@@ -3,8 +3,6 @@
 package server
 
 import (
-	io "io"
-
 	oscar "github.com/mkaminski/goaim/oscar"
 	mock "github.com/stretchr/testify/mock"
 )
@@ -22,18 +20,30 @@ func (_m *MockOServiceHandler) EXPECT() *MockOServiceHandler_Expecter {
 	return &MockOServiceHandler_Expecter{mock: &_m.Mock}
 }
 
-// ClientOnlineHandler provides a mock function with given fields: snacPayloadIn, onReadyCB, sess, sm, r, w, sequence
-func (_m *MockOServiceHandler) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
-	ret := _m.Called(snacPayloadIn, onReadyCB, sess, sm, r, w, sequence)
+// ClientOnlineHandler provides a mock function with given fields: snacPayloadIn, onReadyCB, sess, sm
+func (_m *MockOServiceHandler) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager) ([]XMessage, error) {
+	ret := _m.Called(snacPayloadIn, onReadyCB, sess, sm)
 
-	var r0 error
-	if rf, ok := ret.Get(0).(func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager, io.Reader, io.Writer, *uint32) error); ok {
-		r0 = rf(snacPayloadIn, onReadyCB, sess, sm, r, w, sequence)
+	var r0 []XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager) ([]XMessage, error)); ok {
+		return rf(snacPayloadIn, onReadyCB, sess, sm)
+	}
+	if rf, ok := ret.Get(0).(func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager) []XMessage); ok {
+		r0 = rf(snacPayloadIn, onReadyCB, sess, sm)
 	} else {
-		r0 = ret.Error(0)
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]XMessage)
+		}
 	}
 
-	return r0
+	if rf, ok := ret.Get(1).(func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager) error); ok {
+		r1 = rf(snacPayloadIn, onReadyCB, sess, sm)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
 }
 
 // MockOServiceHandler_ClientOnlineHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClientOnlineHandler'
@@ -46,26 +56,23 @@ type MockOServiceHandler_ClientOnlineHandler_Call struct {
 //   - onReadyCB OnReadyCB
 //   - sess *Session
 //   - sm SessionManager
-//   - r io.Reader
-//   - w io.Writer
-//   - sequence *uint32
-func (_e *MockOServiceHandler_Expecter) ClientOnlineHandler(snacPayloadIn interface{}, onReadyCB interface{}, sess interface{}, sm interface{}, r interface{}, w interface{}, sequence interface{}) *MockOServiceHandler_ClientOnlineHandler_Call {
-	return &MockOServiceHandler_ClientOnlineHandler_Call{Call: _e.mock.On("ClientOnlineHandler", snacPayloadIn, onReadyCB, sess, sm, r, w, sequence)}
+func (_e *MockOServiceHandler_Expecter) ClientOnlineHandler(snacPayloadIn interface{}, onReadyCB interface{}, sess interface{}, sm interface{}) *MockOServiceHandler_ClientOnlineHandler_Call {
+	return &MockOServiceHandler_ClientOnlineHandler_Call{Call: _e.mock.On("ClientOnlineHandler", snacPayloadIn, onReadyCB, sess, sm)}
 }
 
-func (_c *MockOServiceHandler_ClientOnlineHandler_Call) Run(run func(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager, r io.Reader, w io.Writer, sequence *uint32)) *MockOServiceHandler_ClientOnlineHandler_Call {
+func (_c *MockOServiceHandler_ClientOnlineHandler_Call) Run(run func(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager)) *MockOServiceHandler_ClientOnlineHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(oscar.SNAC_0x01_0x02_OServiceClientOnline), args[1].(OnReadyCB), args[2].(*Session), args[3].(SessionManager), args[4].(io.Reader), args[5].(io.Writer), args[6].(*uint32))
+		run(args[0].(oscar.SNAC_0x01_0x02_OServiceClientOnline), args[1].(OnReadyCB), args[2].(*Session), args[3].(SessionManager))
 	})
 	return _c
 }
 
-func (_c *MockOServiceHandler_ClientOnlineHandler_Call) Return(_a0 error) *MockOServiceHandler_ClientOnlineHandler_Call {
-	_c.Call.Return(_a0)
+func (_c *MockOServiceHandler_ClientOnlineHandler_Call) Return(_a0 []XMessage, _a1 error) *MockOServiceHandler_ClientOnlineHandler_Call {
+	_c.Call.Return(_a0, _a1)
 	return _c
 }
 
-func (_c *MockOServiceHandler_ClientOnlineHandler_Call) RunAndReturn(run func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager, io.Reader, io.Writer, *uint32) error) *MockOServiceHandler_ClientOnlineHandler_Call {
+func (_c *MockOServiceHandler_ClientOnlineHandler_Call) RunAndReturn(run func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager) ([]XMessage, error)) *MockOServiceHandler_ClientOnlineHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 40 - 0
server/chat.go

@@ -124,6 +124,28 @@ func SetOnlineChatUsers(sm SessionManager, w io.Writer, sequence *uint32) error
 	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
+func SetOnlineChatUsersTmp(sm SessionManager) XMessage {
+	snacPayloadOut := oscar.SNAC_0x0E_0x03_ChatUsersJoined{}
+	sessions := sm.Participants()
+
+	for _, uSess := range sessions {
+		snacPayloadOut.Users = append(snacPayloadOut.Users, oscar.TLVUserInfo{
+			ScreenName:   uSess.ScreenName,
+			WarningLevel: uSess.GetWarning(),
+			TLVBlock: oscar.TLVBlock{
+				TLVList: uSess.GetUserInfo(),
+			},
+		})
+	}
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: CHAT,
+			SubGroup:  ChatUsersJoined,
+		},
+		snacOut: snacPayloadOut,
+	}
+}
+
 func AlertUserJoined(sess *Session, sm SessionManager) {
 	sm.BroadcastExcept(sess, XMessage{
 		snacFrame: oscar.SnacFrame{
@@ -180,3 +202,21 @@ func SendChatRoomInfoUpdate(room ChatRoom, w io.Writer, sequence *uint32) error
 	}
 	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 }
+
+func SendChatRoomInfoUpdateTmp(room ChatRoom) XMessage {
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: CHAT,
+			SubGroup:  ChatRoomInfoUpdate,
+		},
+		snacOut: oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+			Exchange:       4,
+			Cookie:         room.Cookie,
+			InstanceNumber: 100,
+			DetailLevel:    2,
+			TLVBlock: oscar.TLVBlock{
+				TLVList: room.TLVList(),
+			},
+		},
+	}
+}

+ 319 - 409
server/feedbag.go

@@ -2,269 +2,191 @@ package server
 
 import (
 	"errors"
-	"fmt"
-	"github.com/mkaminski/goaim/oscar"
 	"io"
 	"time"
-)
 
-const (
-	FeedbagErr                      uint16 = 0x0001
-	FeedbagRightsQuery                     = 0x0002
-	FeedbagQuery                           = 0x0004
-	FeedbagQueryIfModified                 = 0x0005
-	FeedbagReply                           = 0x0006
-	FeedbagUse                             = 0x0007
-	FeedbagInsertItem                      = 0x0008
-	FeedbagUpdateItem                      = 0x0009
-	FeedbagDeleteItem                      = 0x000A
-	FeedbagInsertClass                     = 0x000B
-	FeedbagUpdateClass                     = 0x000C
-	FeedbagDeleteClass                     = 0x000D
-	FeedbagStatus                          = 0x000E
-	FeedbagReplyNotModified                = 0x000F
-	FeedbagDeleteUser                      = 0x0010
-	FeedbagStartCluster                    = 0x0011
-	FeedbagEndCluster                      = 0x0012
-	FeedbagAuthorizeBuddy                  = 0x0013
-	FeedbagPreAuthorizeBuddy               = 0x0014
-	FeedbagPreAuthorizedBuddy              = 0x0015
-	FeedbagRemoveMe                        = 0x0016
-	FeedbagRemoveMe2                       = 0x0017
-	FeedbagRequestAuthorizeToHost          = 0x0018
-	FeedbagRequestAuthorizeToClient        = 0x0019
-	FeedbagRespondAuthorizeToHost          = 0x001A
-	FeedbagRespondAuthorizeToClient        = 0x001B
-	FeedbagBuddyAdded                      = 0x001C
-	FeedbagRequestAuthorizeToBadog         = 0x001D
-	FeedbagRespondAuthorizeToBadog         = 0x001E
-	FeedbagBuddyAddedToBadog               = 0x001F
-	FeedbagTestSnac                        = 0x0021
-	FeedbagForwardMsg                      = 0x0022
-	FeedbagIsAuthRequiredQuery             = 0x0023
-	FeedbagIsAuthRequiredReply             = 0x0024
-	FeedbagRecentBuddyUpdate               = 0x0025
+	"github.com/mkaminski/goaim/oscar"
 )
 
-const (
-	FeedbagAttributesShared                  uint16 = 0x0064
-	FeedbagAttributesInvited                        = 0x0065
-	FeedbagAttributesPending                        = 0x0066
-	FeedbagAttributesTimeT                          = 0x0067
-	FeedbagAttributesDenied                         = 0x0068
-	FeedbagAttributesSwimIndex                      = 0x0069
-	FeedbagAttributesRecentBuddy                    = 0x006A
-	FeedbagAttributesAutoBot                        = 0x006B
-	FeedbagAttributesInteraction                    = 0x006D
-	FeedbagAttributesMegaBot                        = 0x006F
-	FeedbagAttributesOrder                          = 0x00C8
-	FeedbagAttributesBuddyPrefs                     = 0x00C9
-	FeedbagAttributesPdMode                         = 0x00CA
-	FeedbagAttributesPdMask                         = 0x00CB
-	FeedbagAttributesPdFlags                        = 0x00CC
-	FeedbagAttributesClientPrefs                    = 0x00CD
-	FeedbagAttributesLanguage                       = 0x00CE
-	FeedbagAttributesFishUri                        = 0x00CF
-	FeedbagAttributesWirelessPdMode                 = 0x00D0
-	FeedbagAttributesWirelessIgnoreMode             = 0x00D1
-	FeedbagAttributesFishPdMode                     = 0x00D2
-	FeedbagAttributesFishIgnoreMode                 = 0x00D3
-	FeedbagAttributesCreateTime                     = 0x00D4
-	FeedbagAttributesBartInfo                       = 0x00D5
-	FeedbagAttributesBuddyPrefsValid                = 0x00D6
-	FeedbagAttributesBuddyPrefs2                    = 0x00D7
-	FeedbagAttributesBuddyPrefs2Valid               = 0x00D8
-	FeedbagAttributesBartList                       = 0x00D9
-	FeedbagAttributesArriveSound                    = 0x012C
-	FeedbagAttributesLeaveSound                     = 0x012D
-	FeedbagAttributesImage                          = 0x012E
-	FeedbagAttributesColorBg                        = 0x012F
-	FeedbagAttributesColorFg                        = 0x0130
-	FeedbagAttributesAlias                          = 0x0131
-	FeedbagAttributesPassword                       = 0x0132
-	FeedbagAttributesDisabled                       = 0x0133
-	FeedbagAttributesCollapsed                      = 0x0134
-	FeedbagAttributesUrl                            = 0x0135
-	FeedbagAttributesActiveList                     = 0x0136
-	FeedbagAttributesEmailAddr                      = 0x0137
-	FeedbagAttributesPhoneNumber                    = 0x0138
-	FeedbagAttributesCellPhoneNumber                = 0x0139
-	FeedbagAttributesSmsPhoneNumber                 = 0x013A
-	FeedbagAttributesWireless                       = 0x013B
-	FeedbagAttributesNote                           = 0x013C
-	FeedbagAttributesAlertPrefs                     = 0x013D
-	FeedbagAttributesBudalertSound                  = 0x013E
-	FeedbagAttributesStockalertValue                = 0x013F
-	FeedbagAttributesTpalertEditUrl                 = 0x0140
-	FeedbagAttributesTpalertDeleteUrl               = 0x0141
-	FeedbagAttributesTpprovMorealertsUrl            = 0x0142
-	FeedbagAttributesFish                           = 0x0143
-	FeedbagAttributesXunconfirmedxLastAccess        = 0x0145
-	FeedbagAttributesImSent                         = 0x0150
-	FeedbagAttributesOnlineTime                     = 0x0151
-	FeedbagAttributesAwayMsg                        = 0x0152
-	FeedbagAttributesImReceived                     = 0x0153
-	FeedbagAttributesBuddyfeedView                  = 0x0154
-	FeedbagAttributesWorkPhoneNumber                = 0x0158
-	FeedbagAttributesOtherPhoneNumber               = 0x0159
-	FeedbagAttributesWebPdMode                      = 0x015F
-	FeedbagAttributesFirstCreationTimeXc            = 0x0167
-	FeedbagAttributesPdModeXc                       = 0x016E
-)
-const (
-	FeedbagClassIdBuddy            uint16 = 0x0000
-	FeedbagClassIdGroup                   = 0x0001
-	FeedbagClassIdPermit                  = 0x0002
-	FeedbagClassIdDeny                    = 0x0003
-	FeedbagClassIdPdinfo                  = 0x0004
-	FeedbagClassIdBuddyPrefs              = 0x0005
-	FeedbagClassIdNonbuddy                = 0x0006
-	FeedbagClassIdTpaProvider             = 0x0007
-	FeedbagClassIdTpaSubscription         = 0x0008
-	FeedbagClassIdClientPrefs             = 0x0009
-	FeedbagClassIdStock                   = 0x000A
-	FeedbagClassIdWeather                 = 0x000B
-	FeedbagClassIdWatchList               = 0x000D
-	FeedbagClassIdIgnoreList              = 0x000E
-	FeedbagClassIdDateTime                = 0x000F
-	FeedbagClassIdExternalUser            = 0x0010
-	FeedbagClassIdRootCreator             = 0x0011
-	FeedbagClassIdFish                    = 0x0012
-	FeedbagClassIdImportTimestamp         = 0x0013
-	FeedbagClassIdBart                    = 0x0014
-	FeedbagClassIdRbOrder                 = 0x0015
-	FeedbagClassIdPersonality             = 0x0016
-	FeedbagClassIdAlProf                  = 0x0017
-	FeedbagClassIdAlInfo                  = 0x0018
-	FeedbagClassIdInteraction             = 0x0019
-	FeedbagClassIdVanityInfo              = 0x001D
-	FeedbagClassIdFavoriteLocation        = 0x001E
-	FeedbagClassIdBartPdinfo              = 0x001F
-	FeedbagClassIdCustomEmoticons         = 0x0024
-	FeedbagClassIdMaxPredefined           = 0x0024
-	FeedbagClassIdXIcqStatusNote          = 0x015C
-	FeedbagClassIdMin                     = 0x0400
-)
+type FeedbagHandler interface {
+	DeleteItemHandler(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error)
+	InsertItemHandler(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x08_FeedbagInsertItem) (XMessage, error)
+	QueryHandler(sess *Session, fm FeedbagManager) (XMessage, error)
+	QueryIfModifiedHandler(sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error)
+	RightsQueryHandler() XMessage
+	StartClusterHandler(oscar.SNAC_0x13_0x11_FeedbagStartCluster)
+	UpdateItemHandler(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error)
+}
 
-func routeFeedbag(sm SessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	switch snac.SubGroup {
-	case FeedbagRightsQuery:
-		return SendAndReceiveFeedbagRightsQuery(snac, r, w, sequence)
-	case FeedbagQuery:
-		return ReceiveAndSendFeedbagQuery(sess, fm, snac, w, sequence)
-	case FeedbagQueryIfModified:
-		return ReceiveAndSendFeedbagQueryIfModified(sess, fm, snac, r, w, sequence)
-	case FeedbagUse:
-		return ReceiveUse(snac, r, w, sequence)
-	case FeedbagInsertItem:
-		return ReceiveInsertItem(sm, sess, fm, snac, r, w, sequence)
-	case FeedbagUpdateItem:
-		return ReceiveUpdateItem(sm, sess, fm, snac, r, w, sequence)
-	case FeedbagDeleteItem:
-		return ReceiveDeleteItem(sm, sess, fm, snac, r, w, sequence)
-	case FeedbagStartCluster:
-		return ReceiveFeedbagStartCluster(snac, r, w, sequence)
-	case FeedbagEndCluster:
-		return ReceiveFeedbagEndCluster(snac, r, w, sequence)
-	default:
-		return ErrUnsupportedSubGroup
+func NewFeedbagRouter() FeedbagRouter {
+	return FeedbagRouter{
+		FeedbagHandler: FeedbagService{},
 	}
 }
 
-func SendAndReceiveFeedbagRightsQuery(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("sendAndReceiveFeedbagRightsQuery read SNAC frame: %+v\n", snac)
+type FeedbagRouter struct {
+	FeedbagHandler
+}
 
-	snacPayloadIn := oscar.SNAC_0x13_0x02_FeedbagRightsQuery{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
+func (rt FeedbagRouter) RouteFeedbag(sm SessionManager, sess *Session, fm FeedbagManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	switch snac.SubGroup {
+	case oscar.FeedbagRightsQuery:
+		inSNAC := oscar.SNAC_0x13_0x02_FeedbagRightsQuery{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC := rt.RightsQueryHandler()
+		return writeOutSNAC(snac, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.FeedbagQuery:
+		inSNAC, err := rt.QueryHandler(sess, fm)
+		if err != nil {
+			return err
+		}
+		return writeOutSNAC(snac, inSNAC.snacFrame, inSNAC.snacOut, sequence, w)
+	case oscar.FeedbagQueryIfModified:
+		inSNAC := oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.QueryIfModifiedHandler(sess, fm, inSNAC)
+		if err != nil {
+			return err
+		}
+		return writeOutSNAC(snac, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.FeedbagUse:
+		return nil
+	case oscar.FeedbagInsertItem:
+		inSNAC := oscar.SNAC_0x13_0x08_FeedbagInsertItem{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.InsertItemHandler(sm, sess, fm, inSNAC)
+		if err != nil {
+			return err
+		}
+		return writeOutSNAC(snac, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.FeedbagUpdateItem:
+		inSNAC := oscar.SNAC_0x13_0x09_FeedbagUpdateItem{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.UpdateItemHandler(sm, sess, fm, inSNAC)
+		if err != nil {
+			return err
+		}
+		return writeOutSNAC(snac, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.FeedbagDeleteItem:
+		inSNAC := oscar.SNAC_0x13_0x0A_FeedbagDeleteItem{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.DeleteItemHandler(sm, sess, fm, inSNAC)
+		if err != nil {
+			return err
+		}
+		return writeOutSNAC(snac, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.FeedbagStartCluster:
+		inSNAC := oscar.SNAC_0x13_0x11_FeedbagStartCluster{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		rt.StartClusterHandler(inSNAC)
+		return nil
+	case oscar.FeedbagEndCluster:
+		return nil
+	default:
+		return ErrUnsupportedSubGroup
 	}
+}
 
-	fmt.Printf("sendAndReceiveFeedbagRightsQuery read SNAC payload: %+v\n", snacPayloadIn)
+type FeedbagService struct {
+}
 
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: 0x13,
-		SubGroup:  0x03,
-	}
-	snacPayloadOut := oscar.SNAC_0x13_0x03_FeedbagRightsReply{
-		TLVRestBlock: oscar.TLVRestBlock{
-			TLVList: oscar.TLVList{
-				{
-					TType: 0x03,
-					Val:   uint16(200),
-				},
-				{
-					TType: 0x04,
-					Val: []uint16{
-						0x3D,
-						0x3D,
-						0x64,
-						0x64,
-						0x01,
-						0x01,
-						0x32,
-						0x00,
-						0x00,
-						0x03,
-						0x00,
-						0x00,
-						0x00,
-						0x80,
-						0xFF,
-						0x14,
-						0xC8,
-						0x01,
-						0x00,
-						0x01,
-						0x00,
+func (s FeedbagService) RightsQueryHandler() XMessage {
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: FEEDBAG,
+			SubGroup:  oscar.FeedbagRightsReply,
+		},
+		snacOut: oscar.SNAC_0x13_0x03_FeedbagRightsReply{
+			TLVRestBlock: oscar.TLVRestBlock{
+				TLVList: oscar.TLVList{
+					{
+						TType: 0x03,
+						Val:   uint16(200),
+					},
+					{
+						TType: 0x04,
+						Val: []uint16{
+							0x3D,
+							0x3D,
+							0x64,
+							0x64,
+							0x01,
+							0x01,
+							0x32,
+							0x00,
+							0x00,
+							0x03,
+							0x00,
+							0x00,
+							0x00,
+							0x80,
+							0xFF,
+							0x14,
+							0xC8,
+							0x01,
+							0x00,
+							0x01,
+							0x00,
+						},
+					},
+					{
+						TType: 0x05,
+						Val:   uint16(200),
+					},
+					{
+						TType: 0x06,
+						Val:   uint16(200),
+					},
+					{
+						TType: 0x07,
+						Val:   uint16(200),
+					},
+					{
+						TType: 0x08,
+						Val:   uint16(200),
+					},
+					{
+						TType: 0x09,
+						Val:   uint16(200),
+					},
+					{
+						TType: 0x0A,
+						Val:   uint16(200),
+					},
+					{
+						TType: 0x0C,
+						Val:   uint16(200),
+					},
+					{
+						TType: 0x0D,
+						Val:   uint16(200),
+					},
+					{
+						TType: 0x0E,
+						Val:   uint16(100),
 					},
-				},
-				{
-					TType: 0x05,
-					Val:   uint16(200),
-				},
-				{
-					TType: 0x06,
-					Val:   uint16(200),
-				},
-				{
-					TType: 0x07,
-					Val:   uint16(200),
-				},
-				{
-					TType: 0x08,
-					Val:   uint16(200),
-				},
-				{
-					TType: 0x09,
-					Val:   uint16(200),
-				},
-				{
-					TType: 0x0A,
-					Val:   uint16(200),
-				},
-				{
-					TType: 0x0C,
-					Val:   uint16(200),
-				},
-				{
-					TType: 0x0D,
-					Val:   uint16(200),
-				},
-				{
-					TType: 0x0E,
-					Val:   uint16(100),
 				},
 			},
 		},
 	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func ReceiveAndSendFeedbagQuery(sess *Session, fm FeedbagManager, snac oscar.SnacFrame, w io.Writer, sequence *uint32) error {
+func (s FeedbagService) QueryHandler(sess *Session, fm FeedbagManager) (XMessage, error) {
 	fb, err := fm.Retrieve(sess.ScreenName)
 	if err != nil {
-		return err
+		return XMessage{}, err
 	}
 
 	lm := time.UnixMilli(0)
@@ -272,32 +194,27 @@ func ReceiveAndSendFeedbagQuery(sess *Session, fm FeedbagManager, snac oscar.Sna
 	if len(fb) > 0 {
 		lm, err = fm.LastModified(sess.ScreenName)
 		if err != nil {
-			return err
+			return XMessage{}, err
 		}
 	}
 
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: FEEDBAG,
-		SubGroup:  FeedbagReply,
-	}
-	snacPayloadOut := oscar.SNAC_0x13_0x06_FeedbagReply{
-		Version:    0,
-		Items:      fb,
-		LastUpdate: uint32(lm.Unix()),
-	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: FEEDBAG,
+			SubGroup:  oscar.FeedbagReply,
+		},
+		snacOut: oscar.SNAC_0x13_0x06_FeedbagReply{
+			Version:    0,
+			Items:      fb,
+			LastUpdate: uint32(lm.Unix()),
+		},
+	}, nil
 }
 
-func ReceiveAndSendFeedbagQueryIfModified(sess *Session, fm FeedbagManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	snacPayloadIn := oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s FeedbagService) QueryIfModifiedHandler(sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x05_FeedbagQueryIfModified) (XMessage, error) {
 	fb, err := fm.Retrieve(sess.ScreenName)
 	if err != nil {
-		return err
+		return XMessage{}, err
 	}
 
 	lm := time.UnixMilli(0)
@@ -305,210 +222,203 @@ func ReceiveAndSendFeedbagQueryIfModified(sess *Session, fm FeedbagManager, snac
 	if len(fb) > 0 {
 		lm, err = fm.LastModified(sess.ScreenName)
 		if err != nil {
-			return err
+			return XMessage{}, err
 		}
 		if lm.Before(time.Unix(int64(snacPayloadIn.LastUpdate), 0)) {
-			snacFrameOut := oscar.SnacFrame{
-				FoodGroup: FEEDBAG,
-				SubGroup:  FeedbagReplyNotModified,
-			}
-			snacPayloadOut := oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
-				LastUpdate: uint32(lm.Unix()),
-				Count:      uint8(len(fb)),
-			}
-			return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+			return XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagReplyNotModified,
+				},
+				snacOut: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
+					LastUpdate: uint32(lm.Unix()),
+					Count:      uint8(len(fb)),
+				},
+			}, nil
 		}
 	}
 
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: FEEDBAG,
-		SubGroup:  FeedbagReply,
-	}
-	snacPayloadOut := oscar.SNAC_0x13_0x06_FeedbagReply{
-		Version:    0,
-		Items:      fb,
-		LastUpdate: uint32(lm.Unix()),
-	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: FEEDBAG,
+			SubGroup:  oscar.FeedbagReply,
+		},
+		snacOut: oscar.SNAC_0x13_0x06_FeedbagReply{
+			Version:    0,
+			Items:      fb,
+			LastUpdate: uint32(lm.Unix()),
+		},
+	}, nil
 }
 
-func ReceiveInsertItem(sm SessionManager, sess *Session, fm FeedbagManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	snacPayloadIn := oscar.SNAC_0x13_0x08_FeedbagInsertItem{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s FeedbagService) InsertItemHandler(sm SessionManager, sess *Session, fm FeedbagManager, 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.
 		if item.ClassID == 3 && item.Name == sess.ScreenName {
-			snacFrameOut := oscar.SnacFrame{
-				FoodGroup: FEEDBAG,
-				SubGroup:  FeedbagErr,
-			}
-			snacPayloadOut := oscar.SnacError{
-				Code: ErrorCodeNotSupportedByHost,
-			}
-			return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+			return XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagErr,
+				},
+				snacOut: oscar.SnacError{
+					Code: ErrorCodeNotSupportedByHost,
+				},
+			}, nil
 		}
 	}
 
 	if err := fm.Upsert(sess.ScreenName, snacPayloadIn.Items); err != nil {
-		return err
-	}
-
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: FEEDBAG,
-		SubGroup:  FeedbagStatus,
-	}
-	snacPayloadOut := oscar.SNAC_0x13_0x0E_FeedbagStatus{}
-
-	for range snacPayloadIn.Items {
-		snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000)
-	}
-
-	if err := writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w); err != nil {
-		return err
+		return XMessage{}, nil
 	}
 
 	for _, item := range snacPayloadIn.Items {
 		switch item.ClassID {
-		case 2:
-			// notify that added buddy is online
-			if err := NotifyBuddyOnline(w, item.Name, sm, sequence); err != nil {
-				return err
+		case oscar.FeedbagClassIdBuddy, oscar.FeedbagClassIDPermit: // add new buddy
+			err := NotifyBuddyArrived(item.Name, sess.ScreenName, sm)
+			switch {
+			case errors.Is(err, ErrSessNotFound):
+				continue
+			case err != nil:
+				return XMessage{}, err
 			}
-		case 3:
-			// DENY, block buddy
-			if err := blockBuddy(sm, sess, item.Name, sequence, w); err != nil {
-				return err
+		case oscar.FeedbagClassIDDeny: // block buddy
+			// notify this user that buddy is offline
+			err := NotifyBuddyDeparted(item.Name, sess.ScreenName, sm)
+			switch {
+			case errors.Is(err, ErrSessNotFound):
+				continue
+			case err != nil:
+				return XMessage{}, err
+			}
+			// notify former buddy that this user is offline
+			if err := NotifyBuddyDeparted(sess.ScreenName, item.Name, sm); err != nil {
+				return XMessage{}, err
 			}
 		}
 	}
-	return nil
-}
 
-func blockBuddy(sm SessionManager, sess *Session, screenName string, sequence *uint32, w io.Writer) error {
-	buddySess, err := sm.RetrieveByScreenName(screenName)
-	if err != nil {
-		if errors.Is(err, errSessNotFound) {
-			// former buddy is offline
-			return nil
-		}
-		return err
+	snacPayloadOut := oscar.SNAC_0x13_0x0E_FeedbagStatus{}
+	for range snacPayloadIn.Items {
+		snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000)
 	}
 
-	// tell the blocked buddy you've signed off
-	sm.SendToScreenName(screenName, XMessage{
+	return XMessage{
 		snacFrame: oscar.SnacFrame{
-			FoodGroup: BUDDY,
-			SubGroup:  BuddyDeparted,
-		},
-		snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
-			TLVUserInfo: oscar.TLVUserInfo{
-				ScreenName:   sess.ScreenName,
-				WarningLevel: sess.GetWarning(),
-			},
+			FoodGroup: FEEDBAG,
+			SubGroup:  oscar.FeedbagStatus,
 		},
-	})
-
-	// tell yourself blocked buddy has signed off
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: BUDDY,
-		SubGroup:  BuddyDeparted,
-	}
-	snacPayloadOut := oscar.SNAC_0x03_0x0A_BuddyArrived{
-		TLVUserInfo: oscar.TLVUserInfo{
-			ScreenName:   buddySess.ScreenName,
-			WarningLevel: buddySess.GetWarning(),
-		},
-	}
-
-	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
+		snacOut: snacPayloadOut,
+	}, nil
 }
 
-func ReceiveUpdateItem(sm SessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("ReceiveUpdateItem read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x13_0x09_FeedbagUpdateItem{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s FeedbagService) UpdateItemHandler(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x09_FeedbagUpdateItem) (XMessage, error) {
 	if err := fm.Upsert(sess.ScreenName, snacPayloadIn.Items); err != nil {
-		return err
+		return XMessage{}, nil
 	}
 
-	snacPayloadOut := oscar.SNAC_0x13_0x0E_FeedbagStatus{}
-
 	for _, item := range snacPayloadIn.Items {
-		snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000) // success by default
-		fmt.Printf("ReceiveUpdateItem read SNAC feedbag item: %+v\n", item)
-	}
-
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: FEEDBAG,
-		SubGroup:  FeedbagStatus,
+		switch item.ClassID {
+		case oscar.FeedbagClassIdBuddy, oscar.FeedbagClassIDPermit:
+			err := NotifyBuddyArrived(item.Name, sess.ScreenName, sm)
+			switch {
+			case errors.Is(err, ErrSessNotFound):
+				continue
+			case err != nil:
+				return XMessage{}, err
+			}
+		}
 	}
 
-	if err := writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w); err != nil {
-		return err
+	snacPayloadOut := oscar.SNAC_0x13_0x0E_FeedbagStatus{}
+	for range snacPayloadIn.Items {
+		snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000)
 	}
 
-	return GetAllOnlineBuddies(w, sess, sm, fm, sequence)
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: FEEDBAG,
+			SubGroup:  oscar.FeedbagStatus,
+		},
+		snacOut: snacPayloadOut,
+	}, nil
 }
 
-func ReceiveDeleteItem(sm SessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("ReceiveUpdateItem read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x13_0x0A_FeedbagDeleteItem{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s FeedbagService) DeleteItemHandler(sm SessionManager, sess *Session, fm FeedbagManager, snacPayloadIn oscar.SNAC_0x13_0x0A_FeedbagDeleteItem) (XMessage, error) {
 	if err := fm.Delete(sess.ScreenName, snacPayloadIn.Items); err != nil {
-		return err
+		return XMessage{}, err
 	}
 
-	snacPayloadOut := oscar.SNAC_0x13_0x0E_FeedbagStatus{}
-
-	hasUnblock := false
 	for _, item := range snacPayloadIn.Items {
-		if item.ClassID == 3 {
-			hasUnblock = true
-		}
-		snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000) // success by default
-		fmt.Printf("ReceiveDeleteItem read SNAC feedbag item: %+v\n", item)
-	}
-
-	if hasUnblock {
-		// notify previously blocked users that user is back online
-		if err := NotifyArrival(sess, sm, fm); err != nil {
-			return err
+		if item.ClassID == oscar.FeedbagClassIDDeny {
+			// notify previously blocked users that user is back online
+			if err := NotifyArrival(sess, sm, fm); err != nil {
+				return XMessage{}, err
+			}
 		}
 	}
 
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: FEEDBAG,
-		SubGroup:  FeedbagStatus,
+	snacPayloadOut := oscar.SNAC_0x13_0x0E_FeedbagStatus{}
+	for range snacPayloadIn.Items {
+		snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000) // success by default
 	}
 
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: FEEDBAG,
+			SubGroup:  oscar.FeedbagStatus,
+		},
+		snacOut: snacPayloadOut,
+	}, nil
 }
 
-func ReceiveFeedbagStartCluster(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("ReceiveFeedbagStartCluster read SNAC frame: %+v\n", snac)
-	tlv := oscar.TLVRestBlock{}
-	return tlv.Read(r)
+// StartClusterHandler exists to capture the SNAC input in unit tests to verify
+// it's correctly unmarshalled.
+func (s FeedbagService) StartClusterHandler(oscar.SNAC_0x13_0x11_FeedbagStartCluster) {
 }
 
-func ReceiveFeedbagEndCluster(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("receiveFeedbagEndCluster read SNAC frame: %+v\n", snac)
+func NotifyBuddyArrived(screenNameFrom, screenNameTo string, sm SessionManager) error {
+	sess, err := sm.RetrieveByScreenName(screenNameFrom)
+	switch {
+	case errors.Is(err, ErrSessNotFound):
+		fallthrough
+	case sess.Invisible(): // don't tell user this buddy is online
+		return nil
+	case err != nil:
+		return err
+	}
+
+	sm.SendToScreenName(screenNameTo, XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: BUDDY,
+			SubGroup:  BuddyArrived,
+		},
+		snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
+			TLVUserInfo: sess.GetTLVUserInfo(),
+		},
+	})
+
 	return nil
 }
 
-func ReceiveUse(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("ReceiveUse read SNAC frame: %+v\n", snac)
+func NotifyBuddyDeparted(screenNameFrom, screenNameTo string, sm SessionManager) error {
+	sess, err := sm.RetrieveByScreenName(screenNameFrom)
+	switch {
+	case err != nil:
+		return err
+	case sess.Invisible(): // don't tell user this buddy is online
+		return nil
+	}
+
+	sm.SendToScreenName(screenNameTo, XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: BUDDY,
+			SubGroup:  BuddyDeparted,
+		},
+		snacOut: oscar.SNAC_0x03_0x0B_BuddyDeparted{
+			TLVUserInfo: sess.GetTLVUserInfo(),
+		},
+	})
+
 	return nil
 }

+ 497 - 239
server/feedbag_test.go

@@ -9,7 +9,7 @@ import (
 	"time"
 )
 
-func TestReceiveAndSendFeedbagQuery(t *testing.T) {
+func TestQueryHandler(t *testing.T) {
 	cases := []struct {
 		// name is the unit test name
 		name string
@@ -19,23 +19,24 @@ func TestReceiveAndSendFeedbagQuery(t *testing.T) {
 		feedbagItems []oscar.FeedbagItem
 		// lastModified is the time the buddy list was last changed
 		lastModified time.Time
-		// inputSNAC is the SNAC frame sent from the server to the recipient
-		// client
-		expectSNACFrame oscar.SnacFrame
-		// expectSNACBody is the SNAC payload sent from the server to the
+		// expectOutput is the SNAC payload sent from the server to the
 		// recipient client
-		expectSNACBody oscar.SNAC_0x13_0x06_FeedbagReply
+		expectOutput XMessage
 	}{
 		{
 			name:         "retrieve empty feedbag",
 			screenName:   "user_screen_name",
 			feedbagItems: []oscar.FeedbagItem{},
 			lastModified: time.UnixMilli(0),
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: FEEDBAG,
-				SubGroup:  FeedbagReply,
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagReply,
+				},
+				snacOut: oscar.SNAC_0x13_0x06_FeedbagReply{
+					Items: []oscar.FeedbagItem{},
+				},
 			},
-			expectSNACBody: oscar.SNAC_0x13_0x06_FeedbagReply{},
 		},
 		{
 			name:       "retrieve feedbag with items",
@@ -49,21 +50,23 @@ func TestReceiveAndSendFeedbagQuery(t *testing.T) {
 				},
 			},
 			lastModified: time.UnixMilli(1696472198082),
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: FEEDBAG,
-				SubGroup:  FeedbagReply,
-			},
-			expectSNACBody: oscar.SNAC_0x13_0x06_FeedbagReply{
-				Version: 0,
-				Items: []oscar.FeedbagItem{
-					{
-						Name: "buddy_1",
-					},
-					{
-						Name: "buddy_2",
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagReply,
+				},
+				snacOut: oscar.SNAC_0x13_0x06_FeedbagReply{
+					Version: 0,
+					Items: []oscar.FeedbagItem{
+						{
+							Name: "buddy_1",
+						},
+						{
+							Name: "buddy_2",
+						},
 					},
+					LastUpdate: uint32(time.UnixMilli(1696472198082).Unix()),
 				},
-				LastUpdate: uint32(time.UnixMilli(1696472198082).Unix()),
 			},
 		},
 	}
@@ -85,36 +88,18 @@ func TestReceiveAndSendFeedbagQuery(t *testing.T) {
 			//
 			// send input SNAC
 			//
-			var seq uint32
-			snac := oscar.SnacFrame{
-				FoodGroup: FEEDBAG,
-				SubGroup:  FeedbagQuery,
-			}
 			senderSession := &Session{
 				ScreenName: tc.screenName,
 			}
-			output := &bytes.Buffer{}
-			assert.NoError(t, ReceiveAndSendFeedbagQuery(senderSession, fm, snac, output, &seq))
-			//
-			// verify output
-			//
-			flap := oscar.FlapFrame{}
-			assert.NoError(t, oscar.Unmarshal(&flap, output))
-			snacFrame := oscar.SnacFrame{}
-			assert.NoError(t, oscar.Unmarshal(&snacFrame, output))
-			assert.Equal(t, tc.expectSNACFrame, snacFrame)
-			//
-			// verify output SNAC body
-			//
-			actual := oscar.SNAC_0x13_0x06_FeedbagReply{}
-			assert.NoError(t, oscar.Unmarshal(&actual, output))
-			assert.Equal(t, tc.expectSNACBody, actual)
-			assert.Equalf(t, 0, output.Len(), "the rest of the buffer is unread")
+			svc := FeedbagService{}
+			outputSNAC, err := svc.QueryHandler(senderSession, fm)
+			assert.NoError(t, err)
+			assert.Equal(t, tc.expectOutput, outputSNAC)
 		})
 	}
 }
 
-func TestReceiveAndSendFeedbagQueryIfModified(t *testing.T) {
+func TestQueryIfModifiedHandler(t *testing.T) {
 	cases := []struct {
 		// name is the unit test name
 		name string
@@ -124,28 +109,29 @@ func TestReceiveAndSendFeedbagQueryIfModified(t *testing.T) {
 		feedbagItems []oscar.FeedbagItem
 		// lastModified is the time the buddy list was last changed
 		lastModified time.Time
-		// inputSNAC is the SNAC frame sent from the server to the recipient
-		// client
-		expectSNACFrame oscar.SnacFrame
 		// inputSNAC is the SNAC sent by the sender client
 		inputSNAC oscar.SNAC_0x13_0x05_FeedbagQueryIfModified
-		// expectSNACBody is the SNAC payload sent from the server to the
+		// expectOutput is the SNAC payload sent from the server to the
 		// recipient client
-		expectSNACBody any
+		expectOutput XMessage
 	}{
 		{
 			name:         "retrieve empty feedbag",
 			screenName:   "user_screen_name",
 			feedbagItems: []oscar.FeedbagItem{},
 			lastModified: time.UnixMilli(0),
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: FEEDBAG,
-				SubGroup:  FeedbagReply,
-			},
 			inputSNAC: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
 				LastUpdate: uint32(time.UnixMilli(100000).Unix()),
 			},
-			expectSNACBody: oscar.SNAC_0x13_0x06_FeedbagReply{},
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagReply,
+				},
+				snacOut: oscar.SNAC_0x13_0x06_FeedbagReply{
+					Items: []oscar.FeedbagItem{},
+				},
+			},
 		},
 		{
 			name:       "retrieve feedbag with items",
@@ -159,24 +145,26 @@ func TestReceiveAndSendFeedbagQueryIfModified(t *testing.T) {
 				},
 			},
 			lastModified: time.UnixMilli(200000),
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: FEEDBAG,
-				SubGroup:  FeedbagReply,
-			},
 			inputSNAC: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
 				LastUpdate: uint32(time.UnixMilli(100000).Unix()),
 			},
-			expectSNACBody: oscar.SNAC_0x13_0x06_FeedbagReply{
-				Version: 0,
-				Items: []oscar.FeedbagItem{
-					{
-						Name: "buddy_1",
-					},
-					{
-						Name: "buddy_2",
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagReply,
+				},
+				snacOut: oscar.SNAC_0x13_0x06_FeedbagReply{
+					Version: 0,
+					Items: []oscar.FeedbagItem{
+						{
+							Name: "buddy_1",
+						},
+						{
+							Name: "buddy_2",
+						},
 					},
+					LastUpdate: uint32(time.UnixMilli(200000).Unix()),
 				},
-				LastUpdate: uint32(time.UnixMilli(200000).Unix()),
 			},
 		},
 		{
@@ -191,16 +179,18 @@ func TestReceiveAndSendFeedbagQueryIfModified(t *testing.T) {
 				},
 			},
 			lastModified: time.UnixMilli(100000),
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: FEEDBAG,
-				SubGroup:  FeedbagReplyNotModified,
-			},
 			inputSNAC: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
 				LastUpdate: uint32(time.UnixMilli(200000).Unix()),
 			},
-			expectSNACBody: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
-				LastUpdate: uint32(time.UnixMilli(100000).Unix()),
-				Count:      2,
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagReplyNotModified,
+				},
+				snacOut: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
+					LastUpdate: uint32(time.UnixMilli(100000).Unix()),
+					Count:      2,
+				},
 			},
 		},
 	}
@@ -222,47 +212,21 @@ func TestReceiveAndSendFeedbagQueryIfModified(t *testing.T) {
 			//
 			// send input SNAC
 			//
-			input := &bytes.Buffer{}
-			var seq uint32
-			assert.NoError(t, oscar.Marshal(tc.inputSNAC, input))
-			output := &bytes.Buffer{}
-			snac := oscar.SnacFrame{
-				FoodGroup: FEEDBAG,
-				SubGroup:  FeedbagQuery,
-			}
 			senderSession := &Session{
 				ScreenName: tc.screenName,
 			}
-			assert.NoError(t, ReceiveAndSendFeedbagQueryIfModified(senderSession, fm, snac, input, output, &seq))
+			svc := FeedbagService{}
+			outputSNAC, err := svc.QueryIfModifiedHandler(senderSession, fm, tc.inputSNAC)
+			assert.NoError(t, err)
 			//
 			// verify output
 			//
-			flap := oscar.FlapFrame{}
-			assert.NoError(t, oscar.Unmarshal(&flap, output))
-			snacFrame := oscar.SnacFrame{}
-			assert.NoError(t, oscar.Unmarshal(&snacFrame, output))
-			assert.Equal(t, tc.expectSNACFrame, snacFrame)
-			//
-			// verify output SNAC body
-			//
-			switch v := tc.expectSNACBody.(type) {
-			case oscar.SNAC_0x13_0x06_FeedbagReply:
-				outputSNAC := oscar.SNAC_0x13_0x06_FeedbagReply{}
-				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-				assert.Equal(t, v, outputSNAC)
-			case oscar.SNAC_0x13_0x05_FeedbagQueryIfModified:
-				outputSNAC := oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{}
-				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-				assert.Equal(t, v, outputSNAC)
-			default:
-				t.Fatalf("unexpected output SNAC type")
-			}
-			assert.Equalf(t, 0, output.Len(), "the rest of the buffer is unread")
+			assert.Equal(t, tc.expectOutput, outputSNAC)
 		})
 	}
 }
 
-func TestReceiveInsertItem(t *testing.T) {
+func TestInsertItemHandler(t *testing.T) {
 	defaultSess := &Session{}
 	cases := []struct {
 		// name is the unit test name
@@ -278,13 +242,16 @@ func TestReceiveInsertItem(t *testing.T) {
 			sess *Session
 			err  error
 		}
-		// clientResponses is messages returned to the client
-		clientResponses []XMessage
+		// clientResponse is the message returned to the client
+		clientResponse XMessage
 		// buddyMessages are events forwarded to buddy clients
-		buddyMessages map[string]XMessage
+		buddyMessages []struct {
+			user string
+			msg  XMessage
+		}
 	}{
 		{
-			name: "user adds 2 online buddies, expect OK response and 2 buddy arrived client events",
+			name: "user adds 2 online buddies, expect OK response",
 			userSession: &Session{
 				ScreenName: "user_screen_name",
 			},
@@ -304,6 +271,9 @@ func TestReceiveInsertItem(t *testing.T) {
 				sess *Session
 				err  error
 			}{
+				"user_screen_name": {
+					sess: &Session{ScreenName: "user_screen_name"},
+				},
 				"buddy_1_online": {
 					sess: &Session{ScreenName: "buddy_1_online"},
 				},
@@ -311,40 +281,49 @@ func TestReceiveInsertItem(t *testing.T) {
 					sess: &Session{ScreenName: "buddy_2_online"},
 				},
 			},
-			clientResponses: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: FEEDBAG,
-						SubGroup:  FeedbagStatus,
-					},
-					snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
-						Results: []uint16{0x0000, 0x0000},
-					},
+			clientResponse: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagStatus,
+				},
+				snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
+					Results: []uint16{0x0000, 0x0000},
 				},
+			},
+			buddyMessages: []struct {
+				user string
+				msg  XMessage
+			}{
 				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: BUDDY,
-						SubGroup:  BuddyArrived,
-					},
-					snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
-						TLVUserInfo: oscar.TLVUserInfo{
-							ScreenName: "buddy_1_online",
-							TLVBlock: oscar.TLVBlock{
-								TLVList: defaultSess.GetUserInfo(),
+					user: "user_screen_name",
+					msg: XMessage{
+						snacFrame: oscar.SnacFrame{
+							FoodGroup: BUDDY,
+							SubGroup:  BuddyArrived,
+						},
+						snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
+							TLVUserInfo: oscar.TLVUserInfo{
+								ScreenName: "buddy_1_online",
+								TLVBlock: oscar.TLVBlock{
+									TLVList: defaultSess.GetUserInfo(),
+								},
 							},
 						},
 					},
 				},
 				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: BUDDY,
-						SubGroup:  BuddyArrived,
-					},
-					snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
-						TLVUserInfo: oscar.TLVUserInfo{
-							ScreenName: "buddy_2_online",
-							TLVBlock: oscar.TLVBlock{
-								TLVList: defaultSess.GetUserInfo(),
+					user: "user_screen_name",
+					msg: XMessage{
+						snacFrame: oscar.SnacFrame{
+							FoodGroup: BUDDY,
+							SubGroup:  BuddyArrived,
+						},
+						snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
+							TLVUserInfo: oscar.TLVUserInfo{
+								ScreenName: "buddy_2_online",
+								TLVBlock: oscar.TLVBlock{
+									TLVList: defaultSess.GetUserInfo(),
+								},
 							},
 						},
 					},
@@ -369,18 +348,16 @@ func TestReceiveInsertItem(t *testing.T) {
 				err  error
 			}{
 				"buddy_offline": {
-					err: errSessNotFound,
+					err: ErrSessNotFound,
 				},
 			},
-			clientResponses: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: FEEDBAG,
-						SubGroup:  FeedbagStatus,
-					},
-					snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
-						Results: []uint16{0x0000},
-					},
+			clientResponse: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagStatus,
+				},
+				snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
+					Results: []uint16{0x0000},
 				},
 			},
 		},
@@ -408,15 +385,13 @@ func TestReceiveInsertItem(t *testing.T) {
 					},
 				},
 			},
-			clientResponses: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: FEEDBAG,
-						SubGroup:  FeedbagStatus,
-					},
-					snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
-						Results: []uint16{0x0000},
-					},
+			clientResponse: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagStatus,
+				},
+				snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
+					Results: []uint16{0x0000},
 				},
 			},
 		},
@@ -438,43 +413,59 @@ func TestReceiveInsertItem(t *testing.T) {
 				sess *Session
 				err  error
 			}{
+				"user_screen_name": {
+					sess: &Session{ScreenName: "user_screen_name"},
+				},
 				"buddy_1": {
 					sess: &Session{ScreenName: "buddy_1"},
 				},
 			},
-			clientResponses: []XMessage{
+			buddyMessages: []struct {
+				user string
+				msg  XMessage
+			}{
 				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: FEEDBAG,
-						SubGroup:  FeedbagStatus,
-					},
-					snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
-						Results: []uint16{0x0000},
+					user: "buddy_1",
+					msg: XMessage{
+						snacFrame: oscar.SnacFrame{
+							FoodGroup: BUDDY,
+							SubGroup:  BuddyDeparted,
+						},
+						snacOut: oscar.SNAC_0x03_0x0B_BuddyDeparted{
+							TLVUserInfo: oscar.TLVUserInfo{
+								ScreenName: "user_screen_name",
+								TLVBlock: oscar.TLVBlock{
+									TLVList: defaultSess.GetUserInfo(),
+								},
+							},
+						},
 					},
 				},
 				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: BUDDY,
-						SubGroup:  BuddyDeparted,
-					},
-					snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
-						TLVUserInfo: oscar.TLVUserInfo{
-							ScreenName: "buddy_1",
+					user: "user_screen_name",
+					msg: XMessage{
+						snacFrame: oscar.SnacFrame{
+							FoodGroup: BUDDY,
+							SubGroup:  BuddyDeparted,
+						},
+						snacOut: oscar.SNAC_0x03_0x0B_BuddyDeparted{
+							TLVUserInfo: oscar.TLVUserInfo{
+								ScreenName: "buddy_1",
+								TLVBlock: oscar.TLVBlock{
+									TLVList: defaultSess.GetUserInfo(),
+								},
+							},
 						},
 					},
 				},
 			},
-			buddyMessages: map[string]XMessage{
-				"buddy_1": {
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: BUDDY,
-						SubGroup:  BuddyDeparted,
-					},
-					snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
-						TLVUserInfo: oscar.TLVUserInfo{
-							ScreenName: "user_screen_name",
-						},
-					},
+			clientResponse: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagStatus,
+				},
+				snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
+					Results: []uint16{0x0000},
 				},
 			},
 		},
@@ -495,19 +486,20 @@ func TestReceiveInsertItem(t *testing.T) {
 				sess *Session
 				err  error
 			}{
+				"user_screen_name": {
+					sess: &Session{ScreenName: "user_screen_name"},
+				},
 				"buddy_1": {
-					err: errSessNotFound,
+					err: ErrSessNotFound,
 				},
 			},
-			clientResponses: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: FEEDBAG,
-						SubGroup:  FeedbagStatus,
-					},
-					snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
-						Results: []uint16{0x0000},
-					},
+			clientResponse: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagStatus,
+				},
+				snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
+					Results: []uint16{0x0000},
 				},
 			},
 		},
@@ -524,15 +516,13 @@ func TestReceiveInsertItem(t *testing.T) {
 					},
 				},
 			},
-			clientResponses: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: FEEDBAG,
-						SubGroup:  FeedbagErr,
-					},
-					snacOut: oscar.SnacError{
-						Code: ErrorCodeNotSupportedByHost,
-					},
+			clientResponse: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagErr,
+				},
+				snacOut: oscar.SnacError{
+					Code: ErrorCodeNotSupportedByHost,
 				},
 			},
 		},
@@ -559,56 +549,324 @@ func TestReceiveInsertItem(t *testing.T) {
 					Return(val.sess, val.err).
 					Maybe()
 			}
-			for _, msg := range tc.buddyMessages {
+			for _, n := range tc.buddyMessages {
 				sm.EXPECT().
-					SendToScreenName(mock.Anything, msg).
+					SendToScreenName(n.user, n.msg).
 					Maybe()
 			}
 			//
 			// send input SNAC
 			//
-			input := &bytes.Buffer{}
-			var seq uint32
-			assert.NoError(t, oscar.Marshal(tc.inputSNAC, input))
-			output := &bytes.Buffer{}
-			snac := oscar.SnacFrame{
-				FoodGroup: FEEDBAG,
-				SubGroup:  FeedbagQuery,
-			}
-
-			assert.NoError(t, ReceiveInsertItem(sm, tc.userSession, fm, snac, input, output, &seq))
+			svc := FeedbagService{}
+			output, err := svc.InsertItemHandler(sm, tc.userSession, fm, tc.inputSNAC)
+			assert.NoError(t, err)
 			//
 			// verify response
 			//
-			for _, xMsg := range tc.clientResponses {
-				flap := oscar.FlapFrame{}
-				assert.NoError(t, oscar.Unmarshal(&flap, output))
-				snacBuf, err := flap.SNACBuffer(output)
-				assert.NoError(t, err)
+			assert.Equal(t, output, tc.clientResponse)
+		})
+	}
+}
+
+func TestFeedbagRouter_RouteFeedbag(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 FeedbagRightsQuery, return FeedbagRightsReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagRightsQuery,
+				},
+				snacOut: oscar.SNAC_0x13_0x02_FeedbagRightsQuery{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagRightsReply,
+				},
+				snacOut: oscar.SNAC_0x13_0x03_FeedbagRightsReply{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "receive FeedbagQuery, return FeedbagReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagQuery,
+				},
+				snacOut: oscar.SNAC_0x13_0x02_FeedbagRightsQuery{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagReply,
+				},
+				snacOut: oscar.SNAC_0x13_0x06_FeedbagReply{
+					Version: 4,
+				},
+			},
+		},
+		{
+			name: "receive FeedbagQueryIfModified, return FeedbagRightsReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagQueryIfModified,
+				},
+				snacOut: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
+					LastUpdate: 1234,
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagReply,
+				},
+				snacOut: oscar.SNAC_0x13_0x06_FeedbagReply{
+					LastUpdate: 1234,
+				},
+			},
+		},
+		{
+			name: "receive FeedbagUse, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagUse,
+				},
+				snacOut: struct{}{},
+			},
+			output: XMessage{},
+		},
+		{
+			name: "receive FeedbagInsertItem, return BuddyArrived and FeedbagStatus",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagInsertItem,
+				},
+				snacOut: oscar.SNAC_0x13_0x08_FeedbagInsertItem{
+					Items: []oscar.FeedbagItem{
+						{
+							Name: "my-item",
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagStatus,
+				},
+				snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
+					Results: []uint16{1234},
+				},
+			},
+		},
+		{
+			name: "receive FeedbagUpdateItem, return BuddyArrived and FeedbagStatus",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagUpdateItem,
+				},
+				snacOut: oscar.SNAC_0x13_0x09_FeedbagUpdateItem{
+					Items: []oscar.FeedbagItem{
+						{
+							Name: "my-item",
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagStatus,
+				},
+				snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
+					Results: []uint16{1234},
+				},
+			},
+		},
+		{
+			name: "receive FeedbagDeleteItem, return FeedbagStatus",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagDeleteItem,
+				},
+				snacOut: oscar.SNAC_0x13_0x0A_FeedbagDeleteItem{
+					Items: []oscar.FeedbagItem{
+						{
+							Name: "my-item",
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagStatus,
+				},
+				snacOut: oscar.SNAC_0x13_0x0E_FeedbagStatus{
+					Results: []uint16{1234},
+				},
+			},
+		},
+		{
+			name: "receive FeedbagStartCluster, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagStartCluster,
+				},
+				snacOut: oscar.SNAC_0x13_0x11_FeedbagStartCluster{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+			output: XMessage{},
+		},
+		{
+			name: "receive FeedbagEndCluster, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagEndCluster,
+				},
+				snacOut: struct{}{},
+			},
+			output: XMessage{},
+		},
+		{
+			name: "receive FeedbagDeleteUser, return ErrUnsupportedSubGroup",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: FEEDBAG,
+					SubGroup:  oscar.FeedbagDeleteUser,
+				},
+				snacOut: struct{}{},
+			},
+			output:    XMessage{},
+			expectErr: ErrUnsupportedSubGroup,
+		},
+	}
 
-				snacFrame := oscar.SnacFrame{}
-				assert.NoError(t, oscar.Unmarshal(&snacFrame, snacBuf))
-				assert.Equal(t, xMsg.snacFrame, snacFrame)
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			svc := NewMockFeedbagHandler(t)
+			svc.EXPECT().
+				DeleteItemHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				QueryHandler(mock.Anything, mock.Anything).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				QueryIfModifiedHandler(mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				RightsQueryHandler().
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				InsertItemHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				UpdateItemHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				StartClusterHandler(tc.input.snacOut).
+				Maybe()
 
-				switch v := xMsg.snacOut.(type) {
-				case oscar.SNAC_0x13_0x0E_FeedbagStatus:
-					outputSNAC := oscar.SNAC_0x13_0x0E_FeedbagStatus{}
-					assert.NoError(t, oscar.Unmarshal(&outputSNAC, snacBuf))
-					assert.Equal(t, v, outputSNAC)
-				case oscar.SNAC_0x03_0x0A_BuddyArrived:
-					assert.NoError(t, v.SerializeInPlace())
-					outputSNAC := oscar.SNAC_0x03_0x0A_BuddyArrived{}
-					assert.NoError(t, oscar.Unmarshal(&outputSNAC, snacBuf))
-					assert.Equal(t, v, outputSNAC)
-				case oscar.SnacError:
-					outputSNAC := oscar.SnacError{}
-					assert.NoError(t, oscar.Unmarshal(&outputSNAC, snacBuf))
-					assert.Equal(t, v, outputSNAC)
-				default:
-					t.Fatalf("unexpected output SNAC type")
-				}
+			router := FeedbagRouter{
+				FeedbagHandler: svc,
 			}
-			assert.Equalf(t, 0, output.Len(), "the rest of the buffer is unread")
+
+			bufIn := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.input.snacOut, bufIn))
+
+			bufOut := &bytes.Buffer{}
+			seq := uint32(0)
+
+			err := router.RouteFeedbag(nil, nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
+			}
+
+			if tc.output.snacFrame == (oscar.SnacFrame{}) {
+				return
+			}
+
+			// verify the FLAP frame
+			flap := oscar.FlapFrame{}
+			assert.NoError(t, oscar.Unmarshal(&flap, bufOut))
+
+			// make sure the sequence increments
+			assert.Equal(t, seq, uint32(1))
+			assert.Equal(t, flap.Sequence, uint16(0))
+
+			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())
 		})
 	}
 }

+ 1 - 1
server/icbm.go

@@ -120,7 +120,7 @@ func (s ICBMService) ChannelMsgToHostHandler(sm SessionManager, fm FeedbagManage
 
 	recipSess, err := sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
 	switch {
-	case errors.Is(err, errSessNotFound):
+	case errors.Is(err, ErrSessNotFound):
 		return &XMessage{
 			snacFrame: oscar.SnacFrame{
 				FoodGroup: ICBM,

+ 1 - 1
server/icbm_test.go

@@ -190,7 +190,7 @@ func TestSendAndReceiveChannelMsgTohost(t *testing.T) {
 		{
 			name:             "don't transmit message from sender to recipient because recipient doesn't exist",
 			blockedState:     BlockedNo,
-			recipRetrieveErr: errSessNotFound,
+			recipRetrieveErr: ErrSessNotFound,
 			senderSession: &Session{
 				ScreenName: "sender-screen-name",
 				Warning:    10,

+ 1 - 1
server/locate.go

@@ -143,7 +143,7 @@ func (s LocateService) UserInfoQuery2Handler(sess *Session, sm SessionManager, f
 
 	buddySess, err := sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
 	switch {
-	case errors.Is(err, errSessNotFound):
+	case errors.Is(err, ErrSessNotFound):
 		return XMessage{
 			snacFrame: oscar.SnacFrame{
 				FoodGroup: LOCATE,

+ 1 - 1
server/locate_test.go

@@ -258,7 +258,7 @@ func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
 				err  error
 			}{
 				"non_existent_requested_user": {
-					err: errSessNotFound,
+					err: ErrSessNotFound,
 				},
 			},
 			userSession: &Session{

+ 14 - 49
server/oservice.go

@@ -10,7 +10,7 @@ import (
 )
 
 type OServiceHandler interface {
-	ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager, r io.Reader, w io.Writer, sequence *uint32) error
+	ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager) ([]XMessage, error)
 	ClientVersionsHandler(snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage
 	IdleNotificationHandler(sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error
 	RateParamsQueryHandler() XMessage
@@ -37,7 +37,16 @@ func (rt OServiceRouter) RouteOService(cfg Config, ready OnReadyCB, cr *ChatRegi
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		return rt.ClientOnlineHandler(inSNAC, ready, sess, sm, r, w, sequence)
+		batch, err := rt.ClientOnlineHandler(inSNAC, ready, sess, sm)
+		for _, msg := range batch {
+			switch {
+			case err != nil:
+				return err
+			case batch != nil:
+				return writeOutSNAC(SNACFrame, msg.snacFrame, msg.snacOut, sequence, w)
+			}
+		}
+		return nil
 	case oscar.OServiceServiceRequest:
 		inSNAC := oscar.SNAC_0x01_0x04_OServiceServiceRequest{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -198,57 +207,13 @@ func (s OServiceService) UserInfoQueryHandler(sess *Session) XMessage {
 	}
 }
 
-type OnReadyCB func(sess *Session, sm SessionManager, r io.Reader, w io.Writer, sequence *uint32) error
+type OnReadyCB func(sess *Session, sm SessionManager) ([]XMessage, error)
 
-func (s OServiceService) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
+func (s OServiceService) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager) ([]XMessage, error) {
 	for _, version := range snacPayloadIn.GroupVersions {
 		fmt.Printf("ClientOnlineHandler read SNAC client messageType: %+v\n", version)
 	}
-	return onReadyCB(sess, sm, r, w, sequence)
-}
-
-func GetAllOnlineBuddies(w io.Writer, sess *Session, sm SessionManager, fm FeedbagManager, sequence *uint32) error {
-	screenNames, err := fm.Buddies(sess.ScreenName)
-	if err != nil {
-		return err
-	}
-	for _, screenName := range screenNames {
-		if err := NotifyBuddyOnline(w, screenName, sm, sequence); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func NotifyBuddyOnline(w io.Writer, screenName string, sm SessionManager, sequence *uint32) error {
-	sess, err := sm.RetrieveByScreenName(screenName)
-	if err != nil {
-		if errors.Is(err, errSessNotFound) {
-			// buddy isn't online
-			return nil
-		}
-		return err
-	}
-	if sess.Invisible() {
-		// don't tell user this buddy is online
-		return nil
-	}
-
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: BUDDY,
-		SubGroup:  BuddyArrived,
-	}
-	snacPayloadOut := oscar.SNAC_0x03_0x0A_BuddyArrived{
-		TLVUserInfo: oscar.TLVUserInfo{
-			ScreenName:   screenName,
-			WarningLevel: sess.GetWarning(),
-			TLVBlock: oscar.TLVBlock{
-				TLVList: sess.GetUserInfo(),
-			},
-		},
-	}
-
-	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
+	return onReadyCB(sess, sm)
 }
 
 func (s OServiceService) SetUserInfoFieldsHandler(sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error) {

+ 118 - 94
server/oservice_test.go

@@ -178,7 +178,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 		// input is the request payload
 		input XMessage
 		// output is the response payload
-		output XMessage
+		output []XMessage
 		// handlerErr is the mocked handler error response
 		handlerErr error
 		// expectErr is the expected error returned by the router
@@ -204,7 +204,19 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					},
 				},
 			},
-			output: XMessage{},
+			output: []XMessage{
+				{
+					snacFrame: oscar.SnacFrame{
+						FoodGroup: BUDDY,
+						SubGroup:  BuddyArrived,
+					},
+					snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
+						TLVUserInfo: oscar.TLVUserInfo{
+							ScreenName: "my-screen-name-1",
+						},
+					},
+				},
+			},
 		},
 		{
 			name: "receive OServiceServiceRequest, return OServiceServiceResponse",
@@ -217,17 +229,19 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					FoodGroup: 10,
 				},
 			},
-			output: XMessage{
-				snacFrame: oscar.SnacFrame{
-					FoodGroup: OSERVICE,
-					SubGroup:  oscar.OServiceServiceResponse,
-				},
-				snacOut: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
-					TLVRestBlock: oscar.TLVRestBlock{
-						TLVList: oscar.TLVList{
-							{
-								TType: 0x01,
-								Val:   uint16(1000),
+			output: []XMessage{
+				{
+					snacFrame: oscar.SnacFrame{
+						FoodGroup: OSERVICE,
+						SubGroup:  oscar.OServiceServiceResponse,
+					},
+					snacOut: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
+						TLVRestBlock: oscar.TLVRestBlock{
+							TLVList: oscar.TLVList{
+								{
+									TType: 0x01,
+									Val:   uint16(1000),
+								},
 							},
 						},
 					},
@@ -243,21 +257,23 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 				},
 				snacOut: struct{}{},
 			},
-			output: XMessage{
-				snacFrame: oscar.SnacFrame{
-					FoodGroup: 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,
+			output: []XMessage{
+				{
+					snacFrame: oscar.SnacFrame{
+						FoodGroup: 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,
+							},
 						},
 					},
 				},
@@ -281,7 +297,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					},
 				},
 			},
-			output: XMessage{},
+			output: []XMessage{},
 		},
 		{
 			name: "receive OServiceUserInfoQuery, return OServiceUserInfoUpdate",
@@ -292,14 +308,16 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 				},
 				snacOut: struct{}{},
 			},
-			output: XMessage{
-				snacFrame: oscar.SnacFrame{
-					FoodGroup: OSERVICE,
-					SubGroup:  oscar.OServiceUserInfoUpdate,
-				},
-				snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
-					TLVUserInfo: oscar.TLVUserInfo{
-						ScreenName: "screen-name",
+			output: []XMessage{
+				{
+					snacFrame: oscar.SnacFrame{
+						FoodGroup: OSERVICE,
+						SubGroup:  oscar.OServiceUserInfoUpdate,
+					},
+					snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
+						TLVUserInfo: oscar.TLVUserInfo{
+							ScreenName: "screen-name",
+						},
 					},
 				},
 			},
@@ -315,7 +333,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					IdleTime: 10,
 				},
 			},
-			output: XMessage{},
+			output: []XMessage{},
 		},
 		{
 			name: "receive OServiceClientVersions, return OServiceHostVersions",
@@ -330,14 +348,16 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					},
 				},
 			},
-			output: XMessage{
-				snacFrame: oscar.SnacFrame{
-					FoodGroup: OSERVICE,
-					SubGroup:  oscar.OServiceHostVersions,
-				},
-				snacOut: oscar.SNAC_0x01_0x18_OServiceHostVersions{
-					Versions: []uint16{
-						10,
+			output: []XMessage{
+				{
+					snacFrame: oscar.SnacFrame{
+						FoodGroup: OSERVICE,
+						SubGroup:  oscar.OServiceHostVersions,
+					},
+					snacOut: oscar.SNAC_0x01_0x18_OServiceHostVersions{
+						Versions: []uint16{
+							10,
+						},
 					},
 				},
 			},
@@ -360,14 +380,16 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					},
 				},
 			},
-			output: XMessage{
-				snacFrame: oscar.SnacFrame{
-					FoodGroup: OSERVICE,
-					SubGroup:  oscar.OServiceUserInfoUpdate,
-				},
-				snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
-					TLVUserInfo: oscar.TLVUserInfo{
-						ScreenName: "screen-name",
+			output: []XMessage{
+				{
+					snacFrame: oscar.SnacFrame{
+						FoodGroup: OSERVICE,
+						SubGroup:  oscar.OServiceUserInfoUpdate,
+					},
+					snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
+						TLVUserInfo: oscar.TLVUserInfo{
+							ScreenName: "screen-name",
+						},
 					},
 				},
 			},
@@ -381,7 +403,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 				},
 				snacOut: struct{}{}, // empty SNAC
 			},
-			output:    XMessage{}, // empty SNAC
+			output:    []XMessage{}, // empty SNAC
 			expectErr: ErrUnsupportedSubGroup,
 		},
 	}
@@ -389,37 +411,39 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			svc := NewMockOServiceHandler(t)
+			if len(tc.output) > 0 {
+				svc.EXPECT().
+					ServiceRequestHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+					Return(tc.output[0], tc.handlerErr).
+					Maybe()
+				svc.EXPECT().
+					RateParamsQueryHandler().
+					Return(tc.output[0]).
+					Maybe()
+				svc.EXPECT().
+					UserInfoQueryHandler(mock.Anything).
+					Return(tc.output[0]).
+					Maybe()
+				svc.EXPECT().
+					ClientVersionsHandler(tc.input.snacOut).
+					Return(tc.output[0]).
+					Maybe()
+				svc.EXPECT().
+					SetUserInfoFieldsHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+					Return(tc.output[0], tc.handlerErr).
+					Maybe()
+			}
 			svc.EXPECT().
-				ClientOnlineHandler(tc.input.snacOut, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
-				Return(tc.handlerErr).
-				Maybe()
-			svc.EXPECT().
-				ServiceRequestHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				ClientOnlineHandler(tc.input.snacOut, mock.Anything, mock.Anything, mock.Anything).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
-			svc.EXPECT().
-				RateParamsQueryHandler().
-				Return(tc.output).
-				Maybe()
 			svc.EXPECT().
 				RateParamsSubAddHandler(tc.input.snacOut).
 				Maybe()
-			svc.EXPECT().
-				UserInfoQueryHandler(mock.Anything).
-				Return(tc.output).
-				Maybe()
 			svc.EXPECT().
 				IdleNotificationHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.handlerErr).
 				Maybe()
-			svc.EXPECT().
-				ClientVersionsHandler(tc.input.snacOut).
-				Return(tc.output).
-				Maybe()
-			svc.EXPECT().
-				SetUserInfoFieldsHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
-				Return(tc.output, tc.handlerErr).
-				Maybe()
 
 			router := OServiceRouter{
 				OServiceHandler: svc,
@@ -429,7 +453,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 			assert.NoError(t, oscar.Marshal(tc.input.snacOut, bufIn))
 
 			bufOut := &bytes.Buffer{}
-			seq := uint32(1)
+			seq := uint32(0)
 
 			err := router.RouteOService(Config{}, nil, nil, nil, nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
@@ -437,27 +461,27 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 				return
 			}
 
-			if tc.output.snacFrame == (oscar.SnacFrame{}) {
-				return // handler doesn't return response
-			}
+			for i, msg := range tc.output {
+				// 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)
+				// make sure the sequence increments
+				assert.Equal(t, flap.Sequence, uint16(i))
 
-			// verify the FLAP frame
-			flap := oscar.FlapFrame{}
-			assert.NoError(t, oscar.Unmarshal(&flap, bufOut))
-			assert.Equal(t, uint16(1), flap.Sequence)
+				flapBuf, err := flap.SNACBuffer(bufOut)
+				assert.NoError(t, err)
 
-			// verify the SNAC frame
-			snacFrame := oscar.SnacFrame{}
-			assert.NoError(t, oscar.Unmarshal(&snacFrame, bufOut))
-			assert.Equal(t, tc.output.snacFrame, snacFrame)
+				// verify the SNAC frame
+				snacFrame := oscar.SnacFrame{}
+				assert.NoError(t, oscar.Unmarshal(&snacFrame, flapBuf))
+				assert.Equal(t, msg.snacFrame, snacFrame)
 
-			// verify the SNAC message
-			snacBuf := &bytes.Buffer{}
-			assert.NoError(t, oscar.Marshal(tc.output.snacOut, snacBuf))
-			assert.Equal(t, snacBuf.Bytes(), bufOut.Bytes())
+				// verify the SNAC message
+				snacBuf := &bytes.Buffer{}
+				assert.NoError(t, oscar.Marshal(msg.snacOut, snacBuf))
+				assert.Equal(t, snacBuf.Bytes(), flapBuf.Bytes())
+			}
 		})
 	}
 }

+ 3 - 1
server/protocol.go

@@ -317,6 +317,7 @@ func NewRouter() Router {
 		ICBMRouter:     NewICBMRouter(),
 		LocateRouter:   NewLocateRouter(),
 		OServiceRouter: NewOServiceRouter(),
+		FeedbagRouter:  NewFeedbagRouter(),
 	}
 }
 
@@ -324,6 +325,7 @@ type Router struct {
 	ICBMRouter
 	LocateRouter
 	OServiceRouter
+	FeedbagRouter
 }
 
 func (rt *Router) routeIncomingRequests(cfg Config, ready OnReadyCB, sm SessionManager, sess *Session, fm *FeedbagStore, cr *ChatRegistry, rw io.ReadWriter, sequence *uint32, snac oscar.SnacFrame, buf io.Reader) error {
@@ -341,7 +343,7 @@ func (rt *Router) routeIncomingRequests(cfg Config, ready OnReadyCB, sm SessionM
 	case CHAT_NAV:
 		return routeChatNav(sess, cr, snac, buf, rw, sequence)
 	case FEEDBAG:
-		return routeFeedbag(sm, sess, fm, snac, buf, rw, sequence)
+		return rt.RouteFeedbag(sm, sess, fm, snac, buf, rw, sequence)
 	case BUCP:
 		return routeBUCP()
 	case CHAT:

+ 2 - 2
server/session.go

@@ -9,7 +9,7 @@ import (
 )
 
 var (
-	errSessNotFound = errors.New("session was not found")
+	ErrSessNotFound = errors.New("session was not found")
 	ErrSignedOff    = errors.New("user signed off")
 )
 
@@ -264,7 +264,7 @@ func (s *InMemorySessionManager) RetrieveByScreenName(screenName string) (*Sessi
 			return sess, nil
 		}
 	}
-	return nil, fmt.Errorf("%w: %s", errSessNotFound, screenName)
+	return nil, fmt.Errorf("%w: %s", ErrSessNotFound, screenName)
 }
 
 func (s *InMemorySessionManager) retrieveByScreenNames(screenNames []string) []*Session {