Mike 2 лет назад
Родитель
Сommit
dff8044fb5
12 измененных файлов с 349 добавлено и 136 удалено
  1. 6 5
      .mockery.yaml
  2. 17 0
      oscar/snacs.go
  3. 73 0
      server/MockBuddyHandler.go
  4. 101 56
      server/buddy.go
  5. 123 0
      server/buddy_test.go
  6. 6 54
      server/feedbag.go
  7. 4 4
      server/feedbag_test.go
  8. 1 1
      server/icbm.go
  9. 2 2
      server/icbm_test.go
  10. 1 1
      server/locate.go
  11. 5 5
      server/oservice.go
  12. 10 8
      server/protocol.go

+ 6 - 5
.mockery.yaml

@@ -6,12 +6,13 @@ dir: ./server
 packages:
   github.com/mkaminski/goaim/server:
     interfaces:
+      BuddyHandler:
+      ChatHandler:
+      ChatNavHandler:
+      FeedbagHandler:
       FeedbagManager:
-      SessionManager:
-      ProfileManager:
       ICBMHandler:
       LocateHandler:
       OServiceHandler:
-      FeedbagHandler:
-      ChatNavHandler:
-      ChatHandler:
+      ProfileManager:
+      SessionManager:

+ 17 - 0
oscar/snacs.go

@@ -258,6 +258,23 @@ func (s SNAC_0x02_0x15_LocateUserInfoQuery2) RequestAwayMessage() bool {
 // 0x03: Buddy
 //
 
+const (
+	BuddyErr                 uint16 = 0x0001
+	BuddyRightsQuery         uint16 = 0x0002
+	BuddyRightsReply         uint16 = 0x0003
+	BuddyAddBuddies          uint16 = 0x0004
+	BuddyDelBuddies          uint16 = 0x0005
+	BuddyWatcherListQuery    uint16 = 0x0006
+	BuddyWatcherListResponse uint16 = 0x0007
+	BuddyWatcherSubRequest   uint16 = 0x0008
+	BuddyWatcherNotification uint16 = 0x0009
+	BuddyRejectNotification  uint16 = 0x000A
+	BuddyArrived             uint16 = 0x000B
+	BuddyDeparted            uint16 = 0x000C
+	BuddyAddTempBuddies      uint16 = 0x000F
+	BuddyDelTempBuddies      uint16 = 0x0010
+)
+
 type SNAC_0x03_0x02_BuddyRightsQuery struct {
 	TLVRestBlock
 }

+ 73 - 0
server/MockBuddyHandler.go

@@ -0,0 +1,73 @@
+// Code generated by mockery v2.35.2. DO NOT EDIT.
+
+package server
+
+import mock "github.com/stretchr/testify/mock"
+
+// MockBuddyHandler is an autogenerated mock type for the BuddyHandler type
+type MockBuddyHandler struct {
+	mock.Mock
+}
+
+type MockBuddyHandler_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockBuddyHandler) EXPECT() *MockBuddyHandler_Expecter {
+	return &MockBuddyHandler_Expecter{mock: &_m.Mock}
+}
+
+// RightsQueryHandler provides a mock function with given fields:
+func (_m *MockBuddyHandler) 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
+}
+
+// MockBuddyHandler_RightsQueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RightsQueryHandler'
+type MockBuddyHandler_RightsQueryHandler_Call struct {
+	*mock.Call
+}
+
+// RightsQueryHandler is a helper method to define mock.On call
+func (_e *MockBuddyHandler_Expecter) RightsQueryHandler() *MockBuddyHandler_RightsQueryHandler_Call {
+	return &MockBuddyHandler_RightsQueryHandler_Call{Call: _e.mock.On("RightsQueryHandler")}
+}
+
+func (_c *MockBuddyHandler_RightsQueryHandler_Call) Run(run func()) *MockBuddyHandler_RightsQueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockBuddyHandler_RightsQueryHandler_Call) Return(_a0 XMessage) *MockBuddyHandler_RightsQueryHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockBuddyHandler_RightsQueryHandler_Call) RunAndReturn(run func() XMessage) *MockBuddyHandler_RightsQueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockBuddyHandler creates a new instance of MockBuddyHandler. 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 NewMockBuddyHandler(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockBuddyHandler {
+	mock := &MockBuddyHandler{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 101 - 56
server/buddy.go

@@ -1,77 +1,74 @@
 package server
 
 import (
-	"fmt"
-	"github.com/mkaminski/goaim/oscar"
 	"io"
-)
 
-const (
-	BuddyErr                 uint16 = 0x0001
-	BuddyRightsQuery                = 0x0002
-	BuddyAddBuddies                 = 0x0004
-	BuddyDelBuddies                 = 0x0005
-	BuddyWatcherListQuery           = 0x0006
-	BuddyWatcherSubRequest          = 0x0008
-	BuddyWatcherNotification        = 0x0009
-	BuddyRejectNotification         = 0x000A
-	BuddyArrived                    = 0x000B
-	BuddyDeparted                   = 0x000C
-	BuddyAddTempBuddies             = 0x000F
-	BuddyDelTempBuddies             = 0x0010
+	"github.com/mkaminski/goaim/oscar"
 )
 
-func routeBuddy(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+type BuddyHandler interface {
+	RightsQueryHandler() XMessage
+}
 
-	switch snac.SubGroup {
-	case BuddyRightsQuery:
-		return SendAndReceiveBuddyRights(snac, r, w, sequence)
-	default:
-		return ErrUnsupportedSubGroup
+func NewBuddyRouter() BuddyRouter {
+	return BuddyRouter{
+		BuddyHandler: BuddyService{},
 	}
 }
 
-func SendAndReceiveBuddyRights(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("sendAndReceiveBuddyRights read SNAC frame: %+v\n", snac)
+type BuddyRouter struct {
+	BuddyHandler
+}
 
-	snacPayloadIn := oscar.SNAC_0x03_0x02_BuddyRightsQuery{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
+func (rt *BuddyRouter) RouteBuddy(SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	switch SNACFrame.SubGroup {
+	case oscar.BuddyRightsQuery:
+		inSNAC := oscar.SNAC_0x03_0x02_BuddyRightsQuery{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC := rt.RightsQueryHandler()
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	default:
+		return ErrUnsupportedSubGroup
 	}
+}
 
-	fmt.Printf("sendAndReceiveBuddyRights read SNAC payload: %+v\n", snacPayloadIn)
+type BuddyService struct {
+}
 
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: 0x03,
-		SubGroup:  0x03,
-	}
-	snacPayloadOut := oscar.SNAC_0x03_0x03_BuddyRightsReply{
-		TLVRestBlock: oscar.TLVRestBlock{
-			TLVList: oscar.TLVList{
-				{
-					TType: 0x01,
-					Val:   uint16(100),
-				},
-				{
-					TType: 0x02,
-					Val:   uint16(100),
-				},
-				{
-					TType: 0x03,
-					Val:   uint16(100),
-				},
-				{
-					TType: 0x04,
-					Val:   uint16(100),
+func (s BuddyService) RightsQueryHandler() XMessage {
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: oscar.BUDDY,
+			SubGroup:  oscar.BuddyRightsReply,
+		},
+		snacOut: oscar.SNAC_0x03_0x03_BuddyRightsReply{
+			TLVRestBlock: oscar.TLVRestBlock{
+				TLVList: oscar.TLVList{
+					{
+						TType: 0x01,
+						Val:   uint16(100),
+					},
+					{
+						TType: 0x02,
+						Val:   uint16(100),
+					},
+					{
+						TType: 0x03,
+						Val:   uint16(100),
+					},
+					{
+						TType: 0x04,
+						Val:   uint16(100),
+					},
 				},
 			},
 		},
 	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func NotifyArrival(sess *Session, sm SessionManager, fm FeedbagManager) error {
+func BroadcastArrival(sess *Session, sm SessionManager, fm FeedbagManager) error {
 	screenNames, err := fm.InterestedUsers(sess.ScreenName)
 	if err != nil {
 		return err
@@ -80,7 +77,7 @@ func NotifyArrival(sess *Session, sm SessionManager, fm FeedbagManager) error {
 	sm.BroadcastToScreenNames(screenNames, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.BUDDY,
-			SubGroup:  BuddyArrived,
+			SubGroup:  oscar.BuddyArrived,
 		},
 		snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
 			TLVUserInfo: oscar.TLVUserInfo{
@@ -96,7 +93,7 @@ func NotifyArrival(sess *Session, sm SessionManager, fm FeedbagManager) error {
 	return nil
 }
 
-func NotifyDeparture(sess *Session, sm SessionManager, fm *FeedbagStore) error {
+func BroadcastDeparture(sess *Session, sm SessionManager, fm *FeedbagStore) error {
 	screenNames, err := fm.InterestedUsers(sess.ScreenName)
 	if err != nil {
 		return err
@@ -105,10 +102,58 @@ func NotifyDeparture(sess *Session, sm SessionManager, fm *FeedbagStore) error {
 	sm.BroadcastToScreenNames(screenNames, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.BUDDY,
-			SubGroup:  BuddyDeparted,
+			SubGroup:  oscar.BuddyDeparted,
+		},
+		snacOut: oscar.SNAC_0x03_0x0B_BuddyDeparted{
+			TLVUserInfo: oscar.TLVUserInfo{
+				ScreenName:   sess.ScreenName,
+				WarningLevel: sess.GetWarning(),
+			},
+		},
+	})
+
+	return nil
+}
+
+func UnicastArrival(srcScreenName, destScreenName string, sm SessionManager) error {
+	sess, err := sm.RetrieveByScreenName(srcScreenName)
+	switch {
+	case err != nil:
+		return err
+	case sess.Invisible(): // don't tell user this buddy is online
+		return nil
+	}
+	sm.SendToScreenName(destScreenName, XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: oscar.BUDDY,
+			SubGroup:  oscar.BuddyArrived,
+		},
+		snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
+			TLVUserInfo: sess.GetTLVUserInfo(),
+		},
+	})
+
+	return nil
+}
+
+func UnicastDeparture(srcScreenName, destScreenName string, sm SessionManager) error {
+	sess, err := sm.RetrieveByScreenName(srcScreenName)
+	switch {
+	case err != nil:
+		return err
+	case sess.Invisible(): // don't tell user this buddy is online
+		return nil
+	}
+
+	sm.SendToScreenName(destScreenName, XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: oscar.BUDDY,
+			SubGroup:  oscar.BuddyDeparted,
 		},
 		snacOut: oscar.SNAC_0x03_0x0B_BuddyDeparted{
 			TLVUserInfo: oscar.TLVUserInfo{
+				// don't include the TLV block, otherwise the AIM client fails
+				// to process the block event
 				ScreenName:   sess.ScreenName,
 				WarningLevel: sess.GetWarning(),
 			},

+ 123 - 0
server/buddy_test.go

@@ -0,0 +1,123 @@
+package server
+
+import (
+	"bytes"
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestBuddyRouter_RouteBuddy(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 BuddyRightsQuery, return BuddyRightsReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.BUDDY,
+					SubGroup:  oscar.BuddyRightsQuery,
+				},
+				snacOut: oscar.SNAC_0x03_0x02_BuddyRightsQuery{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.BUDDY,
+					SubGroup:  oscar.BuddyRightsReply,
+				},
+				snacOut: oscar.SNAC_0x03_0x03_BuddyRightsReply{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "receive ErrorCodeReplyTooBig, expect ErrUnsupportedSubGroup",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: oscar.BUDDY,
+					SubGroup:  ErrorCodeReplyTooBig,
+				},
+				snacOut: struct{}{}, // empty SNAC
+			},
+			output:    XMessage{},
+			expectErr: ErrUnsupportedSubGroup,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			svc := NewMockBuddyHandler(t)
+			svc.EXPECT().
+				RightsQueryHandler().
+				Return(tc.output).
+				Maybe()
+
+			router := BuddyRouter{
+				BuddyHandler: svc,
+			}
+
+			bufIn := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.input.snacOut, bufIn))
+
+			bufOut := &bytes.Buffer{}
+			seq := uint32(1)
+
+			err := router.RouteBuddy(tc.input.snacFrame, bufIn, bufOut, &seq)
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
+			}
+
+			if tc.output == (XMessage{}) {
+				// make sure no response was sent
+				assert.Empty(t, bufOut.Bytes())
+				return
+			}
+
+			// verify the FLAP frame
+			flap := oscar.FlapFrame{}
+			assert.NoError(t, oscar.Unmarshal(&flap, bufOut))
+
+			// make sure the sequence number was incremented
+			assert.Equal(t, uint32(2), seq)
+
+			flapBuf, err := flap.SNACBuffer(bufOut)
+			assert.NoError(t, err)
+
+			// verify the SNAC frame
+			snacFrame := oscar.SnacFrame{}
+			assert.NoError(t, oscar.Unmarshal(&snacFrame, flapBuf))
+			assert.Equal(t, tc.output.snacFrame, snacFrame)
+
+			// verify the SNAC message
+			snacBuf := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.output.snacOut, snacBuf))
+			assert.Equal(t, snacBuf.Bytes(), flapBuf.Bytes())
+		})
+	}
+}

+ 6 - 54
server/feedbag.go

@@ -275,7 +275,7 @@ func (s FeedbagService) InsertItemHandler(sm SessionManager, sess *Session, fm F
 	for _, item := range snacPayloadIn.Items {
 		switch item.ClassID {
 		case oscar.FeedbagClassIdBuddy, oscar.FeedbagClassIDPermit: // add new buddy
-			err := NotifyBuddyArrived(item.Name, sess.ScreenName, sm)
+			err := UnicastArrival(item.Name, sess.ScreenName, sm)
 			switch {
 			case errors.Is(err, ErrSessNotFound):
 				continue
@@ -284,7 +284,7 @@ func (s FeedbagService) InsertItemHandler(sm SessionManager, sess *Session, fm F
 			}
 		case oscar.FeedbagClassIDDeny: // block buddy
 			// notify this user that buddy is offline
-			err := NotifyBuddyDeparted(item.Name, sess.ScreenName, sm)
+			err := UnicastDeparture(item.Name, sess.ScreenName, sm)
 			switch {
 			case errors.Is(err, ErrSessNotFound):
 				continue
@@ -292,7 +292,7 @@ func (s FeedbagService) InsertItemHandler(sm SessionManager, sess *Session, fm F
 				return XMessage{}, err
 			}
 			// notify former buddy that this user is offline
-			if err := NotifyBuddyDeparted(sess.ScreenName, item.Name, sm); err != nil {
+			if err := UnicastDeparture(sess.ScreenName, item.Name, sm); err != nil {
 				return XMessage{}, err
 			}
 		}
@@ -320,7 +320,7 @@ func (s FeedbagService) UpdateItemHandler(sm SessionManager, sess *Session, fm F
 	for _, item := range snacPayloadIn.Items {
 		switch item.ClassID {
 		case oscar.FeedbagClassIdBuddy, oscar.FeedbagClassIDPermit:
-			err := NotifyBuddyArrived(item.Name, sess.ScreenName, sm)
+			err := UnicastArrival(item.Name, sess.ScreenName, sm)
 			switch {
 			case errors.Is(err, ErrSessNotFound):
 				continue
@@ -351,14 +351,14 @@ func (s FeedbagService) DeleteItemHandler(sm SessionManager, sess *Session, fm F
 
 	for _, item := range snacPayloadIn.Items {
 		if item.ClassID == oscar.FeedbagClassIDDeny {
-			err := NotifyBuddyArrived(item.Name, sess.ScreenName, sm)
+			err := UnicastArrival(item.Name, sess.ScreenName, sm)
 			switch {
 			case errors.Is(err, ErrSessNotFound):
 				continue
 			case err != nil:
 				return XMessage{}, err
 			}
-			err = NotifyBuddyArrived(sess.ScreenName, item.Name, sm)
+			err = UnicastArrival(sess.ScreenName, item.Name, sm)
 			switch {
 			case errors.Is(err, ErrSessNotFound):
 				continue
@@ -386,51 +386,3 @@ func (s FeedbagService) DeleteItemHandler(sm SessionManager, sess *Session, fm F
 // it's correctly unmarshalled.
 func (s FeedbagService) StartClusterHandler(oscar.SNAC_0x13_0x11_FeedbagStartCluster) {
 }
-
-func NotifyBuddyArrived(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: oscar.BUDDY,
-			SubGroup:  BuddyArrived,
-		},
-		snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
-			TLVUserInfo: sess.GetTLVUserInfo(),
-		},
-	})
-
-	return nil
-}
-
-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: oscar.BUDDY,
-			SubGroup:  BuddyDeparted,
-		},
-		snacOut: oscar.SNAC_0x03_0x0B_BuddyDeparted{
-			TLVUserInfo: oscar.TLVUserInfo{
-				// don't include the TLV block, otherwise the AIM client fails
-				// to process the block event
-				ScreenName:   sess.ScreenName,
-				WarningLevel: sess.GetWarning(),
-			},
-		},
-	})
-
-	return nil
-}

+ 4 - 4
server/feedbag_test.go

@@ -299,7 +299,7 @@ func TestInsertItemHandler(t *testing.T) {
 					msg: XMessage{
 						snacFrame: oscar.SnacFrame{
 							FoodGroup: oscar.BUDDY,
-							SubGroup:  BuddyArrived,
+							SubGroup:  oscar.BuddyArrived,
 						},
 						snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
 							TLVUserInfo: oscar.TLVUserInfo{
@@ -316,7 +316,7 @@ func TestInsertItemHandler(t *testing.T) {
 					msg: XMessage{
 						snacFrame: oscar.SnacFrame{
 							FoodGroup: oscar.BUDDY,
-							SubGroup:  BuddyArrived,
+							SubGroup:  oscar.BuddyArrived,
 						},
 						snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
 							TLVUserInfo: oscar.TLVUserInfo{
@@ -429,7 +429,7 @@ func TestInsertItemHandler(t *testing.T) {
 					msg: XMessage{
 						snacFrame: oscar.SnacFrame{
 							FoodGroup: oscar.BUDDY,
-							SubGroup:  BuddyDeparted,
+							SubGroup:  oscar.BuddyDeparted,
 						},
 						snacOut: oscar.SNAC_0x03_0x0B_BuddyDeparted{
 							TLVUserInfo: oscar.TLVUserInfo{
@@ -444,7 +444,7 @@ func TestInsertItemHandler(t *testing.T) {
 					msg: XMessage{
 						snacFrame: oscar.SnacFrame{
 							FoodGroup: oscar.BUDDY,
-							SubGroup:  BuddyDeparted,
+							SubGroup:  oscar.BuddyDeparted,
 						},
 						snacOut: oscar.SNAC_0x03_0x0B_BuddyDeparted{
 							TLVUserInfo: oscar.TLVUserInfo{

+ 1 - 1
server/icbm.go

@@ -267,7 +267,7 @@ func (s ICBMService) EvilRequestHandler(sm SessionManager, fm FeedbagManager, se
 		snacOut: notif,
 	})
 
-	if err := NotifyArrival(recipSess, sm, fm); err != nil {
+	if err := BroadcastArrival(recipSess, sm, fm); err != nil {
 		return XMessage{}, nil
 	}
 

+ 2 - 2
server/icbm_test.go

@@ -365,7 +365,7 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 			broadcastMessage: XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: oscar.BUDDY,
-					SubGroup:  BuddyArrived,
+					SubGroup:  oscar.BuddyArrived,
 				},
 				snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
 					TLVUserInfo: oscar.TLVUserInfo{
@@ -413,7 +413,7 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 			broadcastMessage: XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: oscar.BUDDY,
-					SubGroup:  BuddyArrived,
+					SubGroup:  oscar.BuddyArrived,
 				},
 				snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
 					TLVUserInfo: oscar.TLVUserInfo{

+ 1 - 1
server/locate.go

@@ -117,7 +117,7 @@ func (s LocateService) SetInfoHandler(sess *Session, sm SessionManager, fm Feedb
 	// broadcast away message change to buddies
 	if awayMsg, hasAwayMsg := snacPayloadIn.GetString(oscar.LocateTLVTagsInfoUnavailableData); hasAwayMsg {
 		sess.SetAwayMessage(awayMsg)
-		if err := NotifyArrival(sess, sm, fm); err != nil {
+		if err := BroadcastArrival(sess, sm, fm); err != nil {
 			return err
 		}
 	}

+ 5 - 5
server/oservice.go

@@ -203,7 +203,7 @@ func (s OServiceService) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_
 	for _, version := range snacPayloadIn.GroupVersions {
 		fmt.Printf("hahaha ClientOnlineHandler read SNAC client messageType: %+v\n", version)
 	}
-	if err := NotifyArrival(sess, sm, fm); err != nil {
+	if err := BroadcastArrival(sess, sm, fm); err != nil {
 		return err
 	}
 	buddies, err := fm.Buddies(sess.ScreenName)
@@ -211,7 +211,7 @@ func (s OServiceService) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_
 		return err
 	}
 	for _, buddy := range buddies {
-		err := NotifyBuddyArrived(buddy, sess.ScreenName, sm)
+		err := UnicastArrival(buddy, sess.ScreenName, sm)
 		switch {
 		case errors.Is(err, ErrSessNotFound):
 			continue
@@ -227,12 +227,12 @@ func (s OServiceService) SetUserInfoFieldsHandler(sess *Session, sm SessionManag
 		switch status {
 		case 0x000:
 			sess.SetInvisible(false)
-			if err := NotifyArrival(sess, sm, fm); err != nil {
+			if err := BroadcastArrival(sess, sm, fm); err != nil {
 				return XMessage{}, err
 			}
 		case 0x100:
 			sess.SetInvisible(true)
-			if err := NotifyDeparture(sess, sm, fm); err != nil {
+			if err := BroadcastDeparture(sess, sm, fm); err != nil {
 				return XMessage{}, err
 			}
 		default:
@@ -256,7 +256,7 @@ func (s OServiceService) IdleNotificationHandler(sess *Session, sm SessionManage
 	} else {
 		sess.SetIdle(time.Duration(snacPayloadIn.IdleTime) * time.Second)
 	}
-	return NotifyArrival(sess, sm, fm)
+	return BroadcastArrival(sess, sm, fm)
 }
 
 func (s OServiceService) ServiceRequestHandler(cfg Config, cr *ChatRegistry, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error) {

+ 10 - 8
server/protocol.go

@@ -239,7 +239,7 @@ func readIncomingRequests(rw io.Reader, msgCh chan IncomingMessage, errCh chan e
 }
 
 func Signout(sess *Session, sm SessionManager, fm *FeedbagStore) {
-	if err := NotifyDeparture(sess, sm, fm); err != nil {
+	if err := BroadcastDeparture(sess, sm, fm); err != nil {
 		fmt.Printf("error notifying departure: %s", err.Error())
 	}
 	sm.Remove(sess)
@@ -285,12 +285,13 @@ func ReadBos(cfg Config, sess *Session, seq uint32, sm SessionManager, fm *Feedb
 
 func NewRouter() Router {
 	return Router{
+		BuddyRouter:    NewBuddyRouter(),
+		ChatNavRouter:  NewChatNavRouter(),
+		ChatRouter:     NewChatRouter(),
+		FeedbagRouter:  NewFeedbagRouter(),
 		ICBMRouter:     NewICBMRouter(),
 		LocateRouter:   NewLocateRouter(),
 		OServiceRouter: NewOServiceRouter(),
-		FeedbagRouter:  NewFeedbagRouter(),
-		ChatNavRouter:  NewChatNavRouter(),
-		ChatRouter:     NewChatRouter(),
 	}
 }
 
@@ -301,12 +302,13 @@ func NewRouterForChat() Router {
 }
 
 type Router struct {
+	BuddyRouter
+	ChatNavRouter
+	ChatRouter
+	FeedbagRouter
 	ICBMRouter
 	LocateRouter
 	OServiceRouter
-	FeedbagRouter
-	ChatNavRouter
-	ChatRouter
 }
 
 func (rt *Router) routeIncomingRequests(cfg Config, sm SessionManager, sess *Session, fm *FeedbagStore, cr *ChatRegistry, rw io.ReadWriter, sequence *uint32, snac oscar.SnacFrame, buf io.Reader, room ChatRoom) error {
@@ -316,7 +318,7 @@ func (rt *Router) routeIncomingRequests(cfg Config, sm SessionManager, sess *Ses
 	case oscar.LOCATE:
 		return rt.RouteLocate(sess, sm, fm, snac, buf, rw, sequence)
 	case oscar.BUDDY:
-		return routeBuddy(snac, buf, rw, sequence)
+		return rt.RouteBuddy(snac, buf, rw, sequence)
 	case oscar.ICBM:
 		return rt.RouteICBM(sm, fm, sess, snac, buf, rw, sequence)
 	case oscar.CHAT_NAV: