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

toc: implement toc_chat_whisper

Mike 1 год назад
Родитель
Сommit
038b8637c2
5 измененных файлов с 264 добавлено и 12 удалено
  1. 25 12
      foodgroup/chat.go
  2. 62 0
      foodgroup/chat_test.go
  3. 50 0
      server/toc/cmd_client.go
  4. 126 0
      server/toc/cmd_client_test.go
  5. 1 0
      wire/snacs.go

+ 25 - 12
foodgroup/chat.go

@@ -52,10 +52,11 @@ type ChatService struct {
 	randRollDie        func(sides int) int
 }
 
-// ChannelMsgToHost relays wire.ChatChannelMsgToClient SNAC sent from a user
-// to the other chat room participants. It returns the same
-// wire.ChatChannelMsgToClient message back to the user if the chat reflection
-// TLV flag is set, otherwise return nil.
+// ChannelMsgToHost relays wire.ChatChannelMsgToClient to chat room
+// participants. If TLV wire.ChatTLVWhisperToUser is set, "whisper" the message
+// to just that user and omit the remaining participants. If TLV
+// wire.ChatTLVEnableReflectionFlag is set, return the message ("reflect") back
+// to the caller.
 func (s ChatService) ChannelMsgToHost(ctx context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*wire.SNACMessage, error) {
 	frameOut := wire.SNACFrame{
 		FoodGroup: wire.Chat,
@@ -71,16 +72,25 @@ func (s ChatService) ChannelMsgToHost(ctx context.Context, sess *state.Session,
 	}
 
 	var err error
-	bodyOut.TLVRestBlock, err = s.transformChatMessage(inBody, sess)
-	if err != nil {
+	if bodyOut.TLVRestBlock, err = s.transformChatMessage(inBody, sess); err != nil {
 		return nil, err
 	}
 
-	// send message to all the participants except sender
-	s.chatMessageRelayer.RelayToAllExcept(ctx, sess.ChatRoomCookie(), sess.IdentScreenName(), wire.SNACMessage{
-		Frame: frameOut,
-		Body:  bodyOut,
-	})
+	if inBody.HasTag(wire.ChatTLVWhisperToUser) && !inBody.HasTag(wire.ChatTLVPublicWhisperFlag) {
+		// forward a whisper message to just one recipient
+		r, _ := inBody.String(wire.ChatTLVWhisperToUser)
+		recip := state.NewIdentScreenName(r)
+		s.chatMessageRelayer.RelayToScreenName(ctx, sess.ChatRoomCookie(), recip, wire.SNACMessage{
+			Frame: frameOut,
+			Body:  bodyOut,
+		})
+	} else {
+		// forward  message all participants, except sender
+		s.chatMessageRelayer.RelayToAllExcept(ctx, sess.ChatRoomCookie(), sess.IdentScreenName(), wire.SNACMessage{
+			Frame: frameOut,
+			Body:  bodyOut,
+		})
+	}
 
 	var ret *wire.SNACMessage
 	if _, ackMsg := inBody.Bytes(wire.ChatTLVEnableReflectionFlag); ackMsg {
@@ -116,7 +126,10 @@ func (s ChatService) transformChatMessage(inBody wire.SNAC_0x0E_0x05_ChatChannel
 		// the order of these TLVs matters for AIM 2.x. if out of order, screen
 		// names do not appear with each chat message.
 		block.Append(wire.NewTLVBE(wire.ChatTLVSenderInformation, sess.TLVUserInfo()))
-		block.Append(wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}))
+		if inBody.HasTag(wire.ChatTLVPublicWhisperFlag) {
+			// send message to all chat room participants
+			block.Append(wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}))
+		}
 		block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfo, msg))
 		return block
 	}

+ 62 - 0
foodgroup/chat_test.go

@@ -120,6 +120,64 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "send chat whisper",
+			userSession: newTestSession("user_sending_chat_msg", sessOptCannedSignonTime,
+				sessOptChatRoomCookie("the-chat-cookie")),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
+					Cookie:  1234,
+					Channel: 14,
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLVBE(wire.ChatTLVWhisperToUser, "them"),
+							wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+								TLVList: wire.TLVList{
+									wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+										"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">Hello</FONT></BODY></HTML>"),
+								},
+							}),
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				chatMessageRelayerParams: chatMessageRelayerParams{
+					chatRelayToScreenNameParams: chatRelayToScreenNameParams{
+						{
+							screenName: state.NewIdentScreenName("them"),
+							cookie:     "the-chat-cookie",
+							message: wire.SNACMessage{
+								Frame: wire.SNACFrame{
+									FoodGroup: wire.Chat,
+									SubGroup:  wire.ChatChannelMsgToClient,
+								},
+								Body: wire.SNAC_0x0E_0x06_ChatChannelMsgToClient{
+									Cookie:  1234,
+									Channel: 14,
+									TLVRestBlock: wire.TLVRestBlock{
+										TLVList: wire.TLVList{
+											wire.NewTLVBE(wire.ChatTLVSenderInformation,
+												newTestSession("user_sending_chat_msg", sessOptCannedSignonTime).TLVUserInfo()),
+											wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+												TLVList: wire.TLVList{
+													wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+														"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">Hello</FONT></BODY></HTML>"),
+												},
+											}),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectOutput: nil,
+		},
 		{
 			name: "send die roll",
 			userSession: newTestSession("user_sending_chat_msg", sessOptCannedSignonTime,
@@ -353,6 +411,10 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 				chatMessageRelayer.EXPECT().
 					RelayToAllExcept(mock.Anything, params.cookie, params.screenName, params.message)
 			}
+			for _, params := range tc.mockParams.chatRelayToScreenNameParams {
+				chatMessageRelayer.EXPECT().
+					RelayToScreenName(mock.Anything, params.cookie, params.screenName, params.message)
+			}
 
 			svc := NewChatService(chatMessageRelayer)
 			svc.randRollDie = tc.randRollDie

+ 50 - 0
server/toc/cmd_client.go

@@ -213,6 +213,8 @@ func (s OSCARProxy) RecvClientCmd(
 		return msg, true
 	case "toc_chat_send":
 		return s.ChatSend(ctx, chatRegistry, payload), true
+	case "toc_chat_whisper":
+		return s.ChatWhisper(ctx, chatRegistry, payload), true
 	case "toc_chat_leave":
 		return s.ChatLeave(ctx, chatRegistry, payload), true
 	case "toc_set_info":
@@ -740,6 +742,54 @@ func (s OSCARProxy) ChatSend(ctx context.Context, chatRegistry *ChatRegistry, cm
 	}
 }
 
+// ChatWhisper handles the toc_chat_send TOC command.
+//
+// From the TiK documentation:
+//
+//	Send a message in a chat room using the chat room id from CHAT_JOIN.
+//	This message is directed at only one person. (Currently you DO need to add
+//	this to your UI.) Remember to quote and encode the message. Chat whispering
+//	is different from IMs since it is linked to a chat room, and should usually
+//	be displayed in the chat room UI.
+//
+// Command syntax: toc_chat_whisper <Chat Room ID> <dst_user> <Message>
+func (s OSCARProxy) ChatWhisper(ctx context.Context, chatRegistry *ChatRegistry, cmd []byte) string {
+	var chatIDStr, recip, msg string
+
+	if _, err := parseArgs(cmd, "toc_chat_whisper", &chatIDStr, &recip, &msg); err != nil {
+		return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
+	}
+
+	chatID, err := strconv.Atoi(chatIDStr)
+	if err != nil {
+		return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
+	}
+
+	me := chatRegistry.RetrieveSess(chatID)
+	if me == nil {
+		return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.RetrieveSess: session for chat ID `%d` not found", chatID))
+	}
+
+	block := wire.TLVRestBlock{}
+	block.Append(wire.NewTLVBE(wire.ChatTLVSenderInformation, me.TLVUserInfo()))
+	block.Append(wire.NewTLVBE(wire.ChatTLVWhisperToUser, recip))
+	block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+		TLVList: wire.TLVList{
+			wire.NewTLVBE(wire.ChatTLVMessageInfoText, msg),
+		},
+	}))
+
+	snac := wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
+		Channel:      wire.ICBMChannelMIME,
+		TLVRestBlock: block,
+	}
+	if _, err = s.ChatService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac); err != nil {
+		return s.runtimeErr(ctx, fmt.Errorf("ChatService.ChannelMsgToHost: %w", err))
+	}
+
+	return ""
+}
+
 // Evil handles the toc_evil TOC command.
 //
 // From the TiK documentation:

+ 126 - 0
server/toc/cmd_client_test.go

@@ -1463,6 +1463,132 @@ func TestOSCARProxy_ChatSend(t *testing.T) {
 	}
 }
 
+func TestOSCARProxy_ChatWhisper(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// me is the TOC user session
+		me *state.Session
+		// givenCmd is the TOC command
+		givenCmd []byte
+		// givenChatRegistry is the chat registry passed to the function
+		givenChatRegistry *ChatRegistry
+		// wantMsg is the expected TOC response
+		wantMsg string
+		// mockParams is the list of params sent to mocks that satisfy this
+		// method's dependencies
+		mockParams mockParams
+	}{
+		{
+			name:     "successfully send chat whisper",
+			me:       newTestSession("me"),
+			givenCmd: []byte(`toc_chat_whisper 0 them "Hello world!"`),
+			givenChatRegistry: func() *ChatRegistry {
+				reg := NewChatRegistry()
+				reg.RegisterSess(0, newTestSession("me"))
+				return reg
+			}(),
+			mockParams: mockParams{
+				chatParams: chatParams{
+					channelMsgToHostParamsChat: channelMsgToHostParamsChat{
+						{
+							sender: state.NewIdentScreenName("me"),
+							inBody: wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
+								Channel: wire.ICBMChannelMIME,
+								TLVRestBlock: wire.TLVRestBlock{
+									TLVList: wire.TLVList{
+										wire.NewTLVBE(wire.ChatTLVSenderInformation, newTestSession("me").TLVUserInfo()),
+										wire.NewTLVBE(wire.ChatTLVWhisperToUser, "them"),
+										wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+											TLVList: wire.TLVList{
+												wire.NewTLVBE(wire.ChatTLVMessageInfoText, "Hello world!"),
+											},
+										}),
+									},
+								},
+							},
+							result: nil,
+						},
+					},
+				},
+			},
+			wantMsg: "",
+		},
+		{
+			name:     "send chat whisper, receive error from chat svc",
+			me:       newTestSession("me"),
+			givenCmd: []byte(`toc_chat_whisper 0 them "Hello world!"`),
+			givenChatRegistry: func() *ChatRegistry {
+				reg := NewChatRegistry()
+				reg.RegisterSess(0, newTestSession("me"))
+				return reg
+			}(),
+			mockParams: mockParams{
+				chatParams: chatParams{
+					channelMsgToHostParamsChat: channelMsgToHostParamsChat{
+						{
+							sender: state.NewIdentScreenName("me"),
+							inBody: wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
+								Channel: wire.ICBMChannelMIME,
+								TLVRestBlock: wire.TLVRestBlock{
+									TLVList: wire.TLVList{
+										wire.NewTLVBE(wire.ChatTLVSenderInformation, newTestSession("me").TLVUserInfo()),
+										wire.NewTLVBE(wire.ChatTLVWhisperToUser, "them"),
+										wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+											TLVList: wire.TLVList{
+												wire.NewTLVBE(wire.ChatTLVMessageInfoText, "Hello world!"),
+											},
+										}),
+									},
+								},
+							},
+							err: io.EOF,
+						},
+					},
+				},
+			},
+			wantMsg: cmdInternalSvcErr,
+		},
+		{
+			name:     "chat room ID with invalid format",
+			givenCmd: []byte(`toc_chat_whisper zero them "Hello world!"`),
+			wantMsg:  cmdInternalSvcErr,
+		},
+		{
+			name:              "missing chat session",
+			givenCmd:          []byte(`toc_chat_whisper 0 them "Hello world!"`),
+			givenChatRegistry: NewChatRegistry(),
+			wantMsg:           cmdInternalSvcErr,
+		},
+		{
+			name:     "bad command",
+			givenCmd: []byte(`toc_chat_whisper`),
+			wantMsg:  cmdInternalSvcErr,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+
+			chatSvc := newMockChatService(t)
+			for _, params := range tc.mockParams.channelMsgToHostParamsChat {
+				chatSvc.EXPECT().
+					ChannelMsgToHost(ctx, matchSession(params.sender), wire.SNACFrame{}, params.inBody).
+					Return(params.result, params.err)
+			}
+
+			svc := OSCARProxy{
+				Logger:      slog.Default(),
+				ChatService: chatSvc,
+			}
+			msg := svc.ChatWhisper(ctx, tc.givenChatRegistry, tc.givenCmd)
+
+			assert.Equal(t, tc.wantMsg, msg)
+		})
+	}
+}
+
 func TestOSCARProxy_Evil(t *testing.T) {
 	cases := []struct {
 		// name is the unit test name

+ 1 - 0
wire/snacs.go

@@ -1047,6 +1047,7 @@ const (
 	ChatRoomInfoOwner      uint16 = 0x0030
 
 	ChatTLVPublicWhisperFlag    uint16 = 0x01
+	ChatTLVWhisperToUser        uint16 = 0x02
 	ChatTLVSenderInformation    uint16 = 0x03
 	ChatTLVMessageInfo          uint16 = 0x05
 	ChatTLVEnableReflectionFlag uint16 = 0x06