Pārlūkot izejas kodu

make chat work for AIM 2.x-3.5

- implement SNAC(0x0D,0x03) RequestExchangeInfo
- rearrange TLV ordering in SNAC(0x0E,0x05) ChatChannelMsgToHost so
  that screen names appear correctly in chat window.
Mike 2 gadi atpakaļ
vecāks
revīzija
e78cfc3ecc

+ 1 - 1
README.md

@@ -19,7 +19,7 @@ The following features are supported:
 - [x] Away Messages
 - [x] Buddy Icons (v4.x, v5.x)
 - [x] Buddy List
-- [x] Chat Rooms (v4.x, v5.x)
+- [x] Chat Rooms
 - [x] Instant Messaging
 - [x] User Profiles
 - [x] Blocking (v3.5+)

+ 14 - 3
foodgroup/chat.go

@@ -2,6 +2,7 @@ package foodgroup
 
 import (
 	"context"
+	"errors"
 
 	"github.com/mk6i/retro-aim-server/state"
 	"github.com/mk6i/retro-aim-server/wire"
@@ -29,20 +30,30 @@ func (s ChatService) ChannelMsgToHost(ctx context.Context, sess *state.Session,
 		FoodGroup: wire.Chat,
 		SubGroup:  wire.ChatChannelMsgToClient,
 	}
+
+	msg, hasMessage := inBody.Slice(wire.ChatTLVMessageInformation)
+	if !hasMessage {
+		return nil, errors.New("SNAC(0x0E,0x05) does not contain a message TLV")
+	}
+
 	bodyOut := wire.SNAC_0x0E_0x06_ChatChannelMsgToClient{
 		Cookie:  inBody.Cookie,
 		Channel: inBody.Channel,
 		TLVRestBlock: wire.TLVRestBlock{
-			TLVList: inBody.TLVList,
+			TLVList: wire.TLVList{
+				// The order of these TLVs matters for AIM 2.x. if out of
+				// order, screen names do not appear with each chat message.
+				wire.NewTLV(wire.ChatTLVSenderInformation, sess.TLVUserInfo()),
+				wire.NewTLV(wire.ChatTLVPublicWhisperFlag, []byte{}),
+				wire.NewTLV(wire.ChatTLVMessageInformation, msg),
+			},
 		},
 	}
-	bodyOut.Append(wire.NewTLV(wire.ChatTLVSenderInformation, sess.TLVUserInfo()))
 
 	_, chatSessMgr, err := s.chatRegistry.Retrieve(sess.ChatRoomCookie())
 	if err != nil {
 		return nil, err
 	}
-
 	// send message to all the participants except sender
 	chatSessMgr.(ChatMessageRelayer).RelayToAllExcept(ctx, sess, wire.SNACMessage{
 		Frame: frameOut,

+ 37 - 14
foodgroup/chat_nav.go

@@ -10,6 +10,21 @@ import (
 	"github.com/mk6i/retro-aim-server/wire"
 )
 
+var defaultExchangeCfg = wire.TLVBlock{
+	TLVList: wire.TLVList{
+		wire.NewTLV(wire.ChatRoomTLVMaxConcurrentRooms, uint8(10)),
+		wire.NewTLV(wire.ChatRoomTLVClassPerms, uint16(0x0010)),
+		wire.NewTLV(wire.ChatRoomTLVMaxNameLen, uint16(100)),
+		wire.NewTLV(wire.ChatRoomTLVFlags, uint16(15)),
+		wire.NewTLV(wire.ChatRoomTLVRoomName, "default exchange"),
+		wire.NewTLV(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
+		wire.NewTLV(wire.ChatRoomTLVCharSet1, "us-ascii"),
+		wire.NewTLV(wire.ChatRoomTLVLang1, "en"),
+		wire.NewTLV(wire.ChatRoomTLVCharSet2, "us-ascii"),
+		wire.NewTLV(wire.ChatRoomTLVLang2, "en"),
+	},
+}
+
 // NewChatNavService creates a new instance of NewChatNavService.
 func NewChatNavService(logger *slog.Logger, chatRegistry *state.ChatRegistry, newChatRoom func() state.ChatRoom, newChatSessMgr func() SessionManager) *ChatNavService {
 	return &ChatNavService{
@@ -44,20 +59,7 @@ func (s ChatNavService) RequestChatRights(_ context.Context, inFrame wire.SNACFr
 					wire.NewTLV(wire.ChatNavTLVMaxConcurrentRooms, uint8(10)),
 					wire.NewTLV(wire.ChatNavTLVExchangeInfo, wire.SNAC_0x0D_0x09_TLVExchangeInfo{
 						Identifier: 4,
-						TLVBlock: wire.TLVBlock{
-							TLVList: wire.TLVList{
-								wire.NewTLV(wire.ChatRoomTLVMaxConcurrentRooms, uint8(10)),
-								wire.NewTLV(wire.ChatRoomTLVClassPerms, uint16(0x0010)),
-								wire.NewTLV(wire.ChatRoomTLVMaxNameLen, uint16(100)),
-								wire.NewTLV(wire.ChatRoomTLVFlags, uint16(15)),
-								wire.NewTLV(wire.ChatRoomTLVRoomName, "default exchange"),
-								wire.NewTLV(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
-								wire.NewTLV(wire.ChatRoomTLVCharSet1, "us-ascii"),
-								wire.NewTLV(wire.ChatRoomTLVLang1, "en"),
-								wire.NewTLV(wire.ChatRoomTLVCharSet2, "us-ascii"),
-								wire.NewTLV(wire.ChatRoomTLVLang2, "en"),
-							},
-						},
+						TLVBlock:   defaultExchangeCfg,
 					}),
 				},
 			},
@@ -143,3 +145,24 @@ func (s ChatNavService) RequestRoomInfo(_ context.Context, inFrame wire.SNACFram
 		},
 	}, nil
 }
+
+func (s ChatNavService) ExchangeInfo(_ context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo) wire.SNACMessage {
+	return wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.ChatNav,
+			SubGroup:  wire.ChatNavNavInfo,
+			RequestID: inFrame.RequestID,
+		},
+		Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
+			TLVRestBlock: wire.TLVRestBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLV(wire.ChatNavTLVMaxConcurrentRooms, uint8(10)),
+					wire.NewTLV(wire.ChatNavTLVExchangeInfo, wire.SNAC_0x0D_0x09_TLVExchangeInfo{
+						Identifier: inBody.Exchange,
+						TLVBlock:   defaultExchangeCfg,
+					}),
+				},
+			},
+		},
+	}
+}

+ 44 - 0
foodgroup/chat_nav_test.go

@@ -198,3 +198,47 @@ func TestChatNavService_RequestChatRights(t *testing.T) {
 
 	assert.Equal(t, want, have)
 }
+
+func TestChatNavService_ExchangeInfo(t *testing.T) {
+	svc := NewChatNavService(nil, nil, nil, nil)
+
+	frame := wire.SNACFrame{RequestID: 1234}
+	snac := wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo{
+		Exchange: 4,
+	}
+	have := svc.ExchangeInfo(nil, frame, snac)
+
+	want := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.ChatNav,
+			SubGroup:  wire.ChatNavNavInfo,
+			RequestID: frame.RequestID,
+		},
+		Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
+			TLVRestBlock: wire.TLVRestBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLV(wire.ChatNavTLVMaxConcurrentRooms, uint8(10)),
+					wire.NewTLV(wire.ChatNavTLVExchangeInfo, wire.SNAC_0x0D_0x09_TLVExchangeInfo{
+						Identifier: snac.Exchange,
+						TLVBlock: wire.TLVBlock{
+							TLVList: wire.TLVList{
+								wire.NewTLV(wire.ChatRoomTLVMaxConcurrentRooms, uint8(10)),
+								wire.NewTLV(wire.ChatRoomTLVClassPerms, uint16(0x0010)),
+								wire.NewTLV(wire.ChatRoomTLVMaxNameLen, uint16(100)),
+								wire.NewTLV(wire.ChatRoomTLVFlags, uint16(15)),
+								wire.NewTLV(wire.ChatRoomTLVRoomName, "default exchange"),
+								wire.NewTLV(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
+								wire.NewTLV(wire.ChatRoomTLVCharSet1, "us-ascii"),
+								wire.NewTLV(wire.ChatRoomTLVLang1, "en"),
+								wire.NewTLV(wire.ChatRoomTLVCharSet2, "us-ascii"),
+								wire.NewTLV(wire.ChatRoomTLVLang2, "en"),
+							},
+						},
+					}),
+				},
+			},
+		},
+	}
+
+	assert.Equal(t, want, have)
+}

+ 19 - 5
foodgroup/chat_test.go

@@ -49,6 +49,10 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 								Tag:   wire.ChatTLVEnableReflectionFlag,
 								Value: []byte{},
 							},
+							{
+								Tag:   wire.ChatTLVMessageInformation,
+								Value: []byte{},
+							},
 						},
 					},
 				},
@@ -70,10 +74,10 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 					Channel: 14,
 					TLVRestBlock: wire.TLVRestBlock{
 						TLVList: wire.TLVList{
-							wire.NewTLV(wire.ChatTLVPublicWhisperFlag, []byte{}),
-							wire.NewTLV(wire.ChatTLVEnableReflectionFlag, []byte{}),
 							wire.NewTLV(wire.ChatTLVSenderInformation,
 								newTestSession("user_sending_chat_msg", sessOptCannedSignonTime).TLVUserInfo()),
+							wire.NewTLV(wire.ChatTLVPublicWhisperFlag, []byte{}),
+							wire.NewTLV(wire.ChatTLVMessageInformation, []byte{}),
 						},
 					},
 				},
@@ -89,9 +93,10 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 					Channel: 14,
 					TLVRestBlock: wire.TLVRestBlock{
 						TLVList: wire.TLVList{
+							wire.NewTLV(wire.ChatTLVSenderInformation,
+								newTestSession("user_sending_chat_msg", sessOptCannedSignonTime).TLVUserInfo()),
 							wire.NewTLV(wire.ChatTLVPublicWhisperFlag, []byte{}),
-							wire.NewTLV(wire.ChatTLVEnableReflectionFlag, []byte{}),
-							wire.NewTLV(wire.ChatTLVSenderInformation, newTestSession("user_sending_chat_msg", sessOptCannedSignonTime).TLVUserInfo()),
+							wire.NewTLV(wire.ChatTLVMessageInformation, []byte{}),
 						},
 					},
 				},
@@ -114,6 +119,10 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 								Tag:   wire.ChatTLVPublicWhisperFlag,
 								Value: []byte{},
 							},
+							{
+								Tag:   wire.ChatTLVMessageInformation,
+								Value: []byte{},
+							},
 						},
 					},
 				},
@@ -135,9 +144,10 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 					Channel: 14,
 					TLVRestBlock: wire.TLVRestBlock{
 						TLVList: wire.TLVList{
-							wire.NewTLV(wire.ChatTLVPublicWhisperFlag, []byte{}),
 							wire.NewTLV(wire.ChatTLVSenderInformation,
 								newTestSession("user_sending_chat_msg", sessOptCannedSignonTime).TLVUserInfo()),
+							wire.NewTLV(wire.ChatTLVPublicWhisperFlag, []byte{}),
+							wire.NewTLV(wire.ChatTLVMessageInformation, []byte{}),
 						},
 					},
 				},
@@ -160,6 +170,10 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 								Tag:   wire.ChatTLVPublicWhisperFlag,
 								Value: []byte{},
 							},
+							{
+								Tag:   wire.ChatTLVMessageInformation,
+								Value: []byte{},
+							},
 						},
 					},
 				},

+ 11 - 0
server/oscar/handler/chat_nav.go

@@ -14,6 +14,7 @@ import (
 
 type ChatNavService interface {
 	CreateRoom(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (wire.SNACMessage, error)
+	ExchangeInfo(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo) wire.SNACMessage
 	RequestChatRights(ctx context.Context, inFrame wire.SNACFrame) wire.SNACMessage
 	RequestRoomInfo(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (wire.SNACMessage, error)
 }
@@ -38,6 +39,16 @@ func (rt ChatNavHandler) RequestChatRights(ctx context.Context, _ *state.Session
 	return rw.SendSNAC(outSNAC.Frame, outSNAC.Body)
 }
 
+func (rt ChatNavHandler) RequestExchangeInfo(ctx context.Context, _ *state.Session, inFrame wire.SNACFrame, r io.Reader, rw oscar.ResponseWriter) error {
+	inBody := wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo{}
+	if err := wire.Unmarshal(&inBody, r); err != nil {
+		return err
+	}
+	outSNAC := rt.ChatNavService.ExchangeInfo(ctx, inFrame, inBody)
+	rt.LogRequestAndResponse(ctx, inFrame, inBody, outSNAC.Frame, outSNAC.Body)
+	return rw.SendSNAC(outSNAC.Frame, outSNAC.Body)
+}
+
 func (rt ChatNavHandler) RequestRoomInfo(ctx context.Context, _ *state.Session, inFrame wire.SNACFrame, r io.Reader, rw oscar.ResponseWriter) error {
 	inBody := wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{}
 	if err := wire.Unmarshal(&inBody, r); err != nil {

+ 36 - 0
server/oscar/handler/chat_nav_test.go

@@ -152,3 +152,39 @@ func TestChatNavHandler_RequestRoomInfo(t *testing.T) {
 
 	assert.NoError(t, h.RequestRoomInfo(nil, nil, input.Frame, buf, ss))
 }
+
+func TestChatNavHandler_RequestExchangeInfo(t *testing.T) {
+	input := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.ChatNav,
+			SubGroup:  wire.ChatNavRequestExchangeInfo,
+		},
+		Body: wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo{
+			Exchange: 4,
+		},
+	}
+	output := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.ChatNav,
+			SubGroup:  wire.ChatNavNavInfo,
+		},
+		Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{},
+	}
+
+	svc := newMockChatNavService(t)
+	svc.EXPECT().
+		ExchangeInfo(mock.Anything, input.Frame, input.Body).
+		Return(output)
+
+	h := NewChatNavHandler(svc, slog.Default())
+
+	ss := newMockResponseWriter(t)
+	ss.EXPECT().
+		SendSNAC(output.Frame, output.Body).
+		Return(nil)
+
+	buf := &bytes.Buffer{}
+	assert.NoError(t, wire.Marshal(input.Body, buf))
+
+	assert.NoError(t, h.RequestExchangeInfo(nil, nil, input.Frame, buf, ss))
+}

+ 48 - 0
server/oscar/handler/mock_chat_nav_test.go

@@ -83,6 +83,54 @@ func (_c *mockChatNavService_CreateRoom_Call) RunAndReturn(run func(context.Cont
 	return _c
 }
 
+// ExchangeInfo provides a mock function with given fields: ctx, inFrame, inBody
+func (_m *mockChatNavService) ExchangeInfo(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo) wire.SNACMessage {
+	ret := _m.Called(ctx, inFrame, inBody)
+
+	if len(ret) == 0 {
+		panic("no return value specified for ExchangeInfo")
+	}
+
+	var r0 wire.SNACMessage
+	if rf, ok := ret.Get(0).(func(context.Context, wire.SNACFrame, wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo) wire.SNACMessage); ok {
+		r0 = rf(ctx, inFrame, inBody)
+	} else {
+		r0 = ret.Get(0).(wire.SNACMessage)
+	}
+
+	return r0
+}
+
+// mockChatNavService_ExchangeInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExchangeInfo'
+type mockChatNavService_ExchangeInfo_Call struct {
+	*mock.Call
+}
+
+// ExchangeInfo is a helper method to define mock.On call
+//   - ctx context.Context
+//   - inFrame wire.SNACFrame
+//   - inBody wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo
+func (_e *mockChatNavService_Expecter) ExchangeInfo(ctx interface{}, inFrame interface{}, inBody interface{}) *mockChatNavService_ExchangeInfo_Call {
+	return &mockChatNavService_ExchangeInfo_Call{Call: _e.mock.On("ExchangeInfo", ctx, inFrame, inBody)}
+}
+
+func (_c *mockChatNavService_ExchangeInfo_Call) Run(run func(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo)) *mockChatNavService_ExchangeInfo_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(wire.SNACFrame), args[2].(wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo))
+	})
+	return _c
+}
+
+func (_c *mockChatNavService_ExchangeInfo_Call) Return(_a0 wire.SNACMessage) *mockChatNavService_ExchangeInfo_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *mockChatNavService_ExchangeInfo_Call) RunAndReturn(run func(context.Context, wire.SNACFrame, wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo) wire.SNACMessage) *mockChatNavService_ExchangeInfo_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // RequestChatRights provides a mock function with given fields: ctx, inFrame
 func (_m *mockChatNavService) RequestChatRights(ctx context.Context, inFrame wire.SNACFrame) wire.SNACMessage {
 	ret := _m.Called(ctx, inFrame)

+ 2 - 0
server/oscar/handler/routes.go

@@ -42,6 +42,7 @@ func NewBOSRouter(h Handlers) oscar.Router {
 
 	router.Register(wire.ChatNav, wire.ChatNavCreateRoom, h.ChatNavHandler.CreateRoom)
 	router.Register(wire.ChatNav, wire.ChatNavRequestChatRights, h.ChatNavHandler.RequestChatRights)
+	router.Register(wire.ChatNav, wire.ChatNavRequestExchangeInfo, h.ChatNavHandler.RequestExchangeInfo)
 	router.Register(wire.ChatNav, wire.ChatNavRequestRoomInfo, h.ChatNavHandler.RequestRoomInfo)
 
 	router.Register(wire.Feedbag, wire.FeedbagDeleteItem, h.FeedbagHandler.DeleteItem)
@@ -110,6 +111,7 @@ func NewChatNavRouter(h Handlers) oscar.Router {
 
 	router.Register(wire.ChatNav, wire.ChatNavCreateRoom, h.ChatNavHandler.CreateRoom)
 	router.Register(wire.ChatNav, wire.ChatNavRequestChatRights, h.ChatNavHandler.RequestChatRights)
+	router.Register(wire.ChatNav, wire.ChatNavRequestExchangeInfo, h.ChatNavHandler.RequestExchangeInfo)
 	router.Register(wire.ChatNav, wire.ChatNavRequestRoomInfo, h.ChatNavHandler.RequestRoomInfo)
 
 	router.Register(wire.OService, wire.OServiceClientOnline, h.OServiceChatNavHandler.ClientOnline)

+ 5 - 0
wire/snacs.go

@@ -689,6 +689,10 @@ const (
 	ChatNavTLVRoomInfo           uint16 = 0x0004
 )
 
+type SNAC_0x0D_0x03_ChatNavRequestExchangeInfo struct {
+	Exchange uint16
+}
+
 type SNAC_0x0D_0x04_ChatNavRequestRoomInfo struct {
 	Exchange       uint16
 	Cookie         string `len_prefix:"uint8"`
@@ -755,6 +759,7 @@ const (
 
 	ChatTLVPublicWhisperFlag    uint16 = 0x01
 	ChatTLVSenderInformation    uint16 = 0x03
+	ChatTLVMessageInformation   uint16 = 0x05
 	ChatTLVEnableReflectionFlag uint16 = 0x06
 
 	// referenced from protocols/oscar/family_chatnav.c in lib purple