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

issue #23 - add roll dice chatroom support

Mike 1 год назад
Родитель
Сommit
1762cf25f4
5 измененных файлов с 366 добавлено и 35 удалено
  1. 167 17
      foodgroup/chat.go
  2. 191 17
      foodgroup/chat_test.go
  3. 1 0
      go.mod
  4. 2 0
      go.sum
  5. 5 1
      wire/snacs.go

+ 167 - 17
foodgroup/chat.go

@@ -1,18 +1,47 @@
 package foodgroup
 
 import (
+	"bytes"
 	"context"
 	"errors"
+	"fmt"
+	"io"
 	"math"
+	"math/rand/v2"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"golang.org/x/net/html"
 
 	"github.com/mk6i/retro-aim-server/state"
 	"github.com/mk6i/retro-aim-server/wire"
 )
 
+var (
+	// sessOnlineHost represents the OnlineHost user that announcements die
+	// roll results.
+	sessOnlineHost = func() *state.Session {
+		sn := state.DisplayScreenName("OnlineHost")
+		sess := state.NewSession()
+		sess.SetDisplayScreenName(sn)
+		sess.SetIdentScreenName(sn.IdentScreenName())
+		return sess
+	}()
+
+	// rollDiceRgxp matches a roll dice chat command.
+	// ex: //roll //roll-sides3 //roll-dice2 //role-sides3-dice2
+	rollDiceRgxp = regexp.MustCompile(`^//roll(?:-(dice|sides)([0-9]{1,3}))?(?:-(dice|sides)([0-9]{1,3}))?\s*$`)
+)
+
 // NewChatService creates a new instance of ChatService.
 func NewChatService(chatMessageRelayer ChatMessageRelayer) *ChatService {
 	return &ChatService{
 		chatMessageRelayer: chatMessageRelayer,
+		randRollDie: func(sides int) int {
+			// generate random number between 1 and sides
+			return rand.IntN(sides) + 1
+		},
 	}
 }
 
@@ -20,6 +49,7 @@ func NewChatService(chatMessageRelayer ChatMessageRelayer) *ChatService {
 // responsible for sending and receiving chat messages.
 type ChatService struct {
 	chatMessageRelayer ChatMessageRelayer
+	randRollDie        func(sides int) int
 }
 
 // ChannelMsgToHost relays wire.ChatChannelMsgToClient SNAC sent from a user
@@ -31,31 +61,21 @@ func (s ChatService) ChannelMsgToHost(ctx context.Context, sess *state.Session,
 		FoodGroup: wire.Chat,
 		SubGroup:  wire.ChatChannelMsgToClient,
 	}
-
-	msg, hasMessage := inBody.Bytes(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: 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.NewTLVBE(wire.ChatTLVSenderInformation, sess.TLVUserInfo()),
-				wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}),
-				wire.NewTLVBE(wire.ChatTLVMessageInformation, msg),
-			},
-		},
 	}
-
 	if bodyOut.Channel == math.MaxUint16 {
-		// Fix incorrect channel bug in macOS client v4.0.9.
+		// fix incorrect channel bug in macOS client v4.0.9.
 		bodyOut.Channel = wire.ICBMChannelMIME
 	}
 
+	var err error
+	bodyOut.TLVRestBlock, err = s.transformChatMessage(inBody, sess)
+	if 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,
@@ -75,6 +95,136 @@ func (s ChatService) ChannelMsgToHost(ctx context.Context, sess *state.Session,
 	return ret, nil
 }
 
+// transformChatMessage inspects and modifies the incoming chat message payload.
+//   - If message contains a properly formatted //roll command, return a roll
+//     die response.
+//   - Else return the unmodified incoming message.
+//
+// In the future, this function will validate the incoming message for correct form.
+func (s ChatService) transformChatMessage(inBody wire.SNAC_0x0E_0x05_ChatChannelMsgToHost, sender *state.Session) (wire.TLVRestBlock, error) {
+	messageBlob, hasMessage := inBody.Bytes(wire.ChatTLVMessageInfo)
+	if !hasMessage {
+		return wire.TLVRestBlock{}, errors.New("SNAC(0x0E,0x05) does not contain a message TLV")
+	}
+	messageText, err := textFromChatMsgBlob(messageBlob)
+	if err != nil {
+		return wire.TLVRestBlock{}, err
+	}
+
+	newMessageBlock := func(sess *state.Session, msg any) wire.TLVRestBlock {
+		block := wire.TLVRestBlock{}
+		// 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{}))
+		block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfo, msg))
+		return block
+	}
+
+	if doRoll, dice, sides := parseDiceCommand(messageText); doRoll {
+		payload := s.rollDice(sender, dice, sides)
+		// send die roll results from OnlineHost user
+		return newMessageBlock(sessOnlineHost, payload), nil
+	}
+
+	// return the incoming payload without modification
+	return newMessageBlock(sender, messageBlob), nil
+}
+
+// rollDice generates a chat response for the results of a die roll.
+func (s ChatService) rollDice(sess *state.Session, dice int, sides int) wire.TLVRestBlock {
+	sb := strings.Builder{}
+	sb.WriteString("<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">")
+	sb.WriteString(fmt.Sprintf("%s rolled %d %d-sided dice:", sess.DisplayScreenName().String(), dice, sides))
+	for i := 0; i < dice; i++ {
+		sb.WriteString(fmt.Sprintf(" %d", s.randRollDie(sides)))
+	}
+	sb.WriteString("</FONT></BODY></HTML>")
+
+	block := wire.TLVRestBlock{}
+	block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfoEncoding, "us-ascii"))
+	block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfoLang, "en"))
+	block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfoText, sb.String()))
+	return block
+}
+
+// textFromChatMsgBlob extracts plaintext message text from HTML located in
+// chat message info TLV(0x05).
+func textFromChatMsgBlob(msg []byte) ([]byte, error) {
+	block := wire.TLVRestBlock{}
+	if err := wire.UnmarshalBE(&block, bytes.NewBuffer(msg)); err != nil {
+		return nil, err
+	}
+
+	b, hasMsg := block.Bytes(wire.ChatTLVMessageInfoText)
+	if !hasMsg {
+		return nil, errors.New("SNAC(0x0E,0x05) has no chat msg text TLV")
+	}
+
+	tok := html.NewTokenizer(bytes.NewBuffer(b))
+	for {
+		switch tok.Next() {
+		case html.TextToken:
+			return tok.Text(), nil
+		case html.ErrorToken:
+			err := tok.Err()
+			if err == io.EOF {
+				err = nil
+			}
+			return nil, err
+		}
+	}
+}
+
+// parseDiceCommand gets the number of dice and sides from a die roll command.
+//
+// The roll command is activated with //roll followed by up to two arguments to
+// indicate die count and side count. By default, there are 2 dice and 6 sides.
+//
+//   - //roll               2x 6-sided dice
+//   - //roll-dice4         4x 6-sided dice
+//   - //roll-sides8        2x 8-sided dice
+//   - //roll-dice4-sides8  4x 8-sided dice
+//
+// The -dice param can not exceed 15 and -sides param cannot exceed 999.
+func parseDiceCommand(in []byte) (valid bool, dice int, sides int) {
+	matches := rollDiceRgxp.FindSubmatch(in)
+	if len(matches) == 0 {
+		return false, 0, 0
+	}
+
+	args := matches[1:]
+	if len(args[0]) > 0 && bytes.Equal(args[0], args[2]) {
+		// "sides" or "dice" appears twice
+		return false, 0, 0
+	}
+
+	dice = 2
+	sides = 6
+
+	for i := 0; i < len(args); i += 2 {
+		cmd := string(args[i])
+		val := string(args[i+1])
+
+		switch cmd {
+		case "sides":
+			var err error
+			sides, err = strconv.Atoi(val)
+			if err != nil || sides == 0 || sides > 999 {
+				return false, 0, 0
+			}
+		case "dice":
+			var err error
+			dice, err = strconv.Atoi(val)
+			if err != nil || dice == 0 || dice > 15 {
+				return false, 0, 0
+			}
+		}
+	}
+
+	return true, dice, sides
+}
+
 func setOnlineChatUsers(ctx context.Context, sess *state.Session, chatMessageRelayer ChatMessageRelayer) {
 	snacPayloadOut := wire.SNAC_0x0E_0x03_ChatUsersJoined{}
 	sessions := chatMessageRelayer.AllSessions(sess.ChatRoomCookie())

+ 191 - 17
foodgroup/chat_test.go

@@ -27,7 +27,9 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 		// room participants (except the sender)
 		expectSNACToParticipants wire.SNACMessage
 		expectOutput             *wire.SNACMessage
-		wantErr                  error
+		// randRollDie generates result of rolling a die
+		randRollDie func(sides int) int
+		wantErr     error
 	}{
 		{
 			name: "send chat room message, expect acknowledgement to sender client",
@@ -50,10 +52,12 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 								Tag:   wire.ChatTLVEnableReflectionFlag,
 								Value: []byte{},
 							},
-							{
-								Tag:   wire.ChatTLVMessageInformation,
-								Value: []byte{},
-							},
+							wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+								TLVList: wire.TLVList{
+									wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+										"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">Hello</FONT></BODY></HTML>"),
+								},
+							}),
 						},
 					},
 				},
@@ -77,7 +81,12 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 											wire.NewTLVBE(wire.ChatTLVSenderInformation,
 												newTestSession("user_sending_chat_msg", sessOptCannedSignonTime).TLVUserInfo()),
 											wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}),
-											wire.NewTLVBE(wire.ChatTLVMessageInformation, []byte{}),
+											wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+												TLVList: wire.TLVList{
+													wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+														"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">Hello</FONT></BODY></HTML>"),
+												},
+											}),
 										},
 									},
 								},
@@ -100,15 +109,19 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 							wire.NewTLVBE(wire.ChatTLVSenderInformation,
 								newTestSession("user_sending_chat_msg", sessOptCannedSignonTime).TLVUserInfo()),
 							wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}),
-							wire.NewTLVBE(wire.ChatTLVMessageInformation, []byte{}),
+							wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+								TLVList: wire.TLVList{
+									wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+										"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">Hello</FONT></BODY></HTML>"),
+								},
+							}),
 						},
 					},
 				},
 			},
 		},
 		{
-			name: "send chat room message with macOS client 4.0.9 bug containing bad channel ID, expect message to " +
-				"client on MIME channel",
+			name: "send die roll",
 			userSession: newTestSession("user_sending_chat_msg", sessOptCannedSignonTime,
 				sessOptChatRoomCookie("the-chat-cookie")),
 			inputSNAC: wire.SNACMessage{
@@ -117,7 +130,7 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 				},
 				Body: wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
 					Cookie:  1234,
-					Channel: math.MaxUint16,
+					Channel: 14,
 					TLVRestBlock: wire.TLVRestBlock{
 						TLVList: wire.TLVList{
 							{
@@ -125,9 +138,112 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 								Value: []byte{},
 							},
 							{
-								Tag:   wire.ChatTLVMessageInformation,
+								Tag:   wire.ChatTLVEnableReflectionFlag,
+								Value: []byte{},
+							},
+							wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+								TLVList: wire.TLVList{
+									wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+										"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">//roll-dice3-sides8</FONT></BODY></HTML>"),
+								},
+							}),
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				chatMessageRelayerParams: chatMessageRelayerParams{
+					chatRelayToAllExceptParams: chatRelayToAllExceptParams{
+						{
+							screenName: state.NewIdentScreenName("user_sending_chat_msg"),
+							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, sessOnlineHost.TLVUserInfo()),
+											wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}),
+											wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+												TLVList: wire.TLVList{
+													wire.NewTLVBE(wire.ChatTLVMessageInfoEncoding, "us-ascii"),
+													wire.NewTLVBE(wire.ChatTLVMessageInfoLang, "en"),
+													wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+														"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">user_sending_chat_msg rolled 3 8-sided dice: 2 4 8</FONT></BODY></HTML>"),
+												},
+											}),
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectOutput: &wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Chat,
+					SubGroup:  wire.ChatChannelMsgToClient,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0E_0x06_ChatChannelMsgToClient{
+					Cookie:  1234,
+					Channel: 14,
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLVBE(wire.ChatTLVSenderInformation, sessOnlineHost.TLVUserInfo()),
+							wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}),
+							wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+								TLVList: wire.TLVList{
+									wire.NewTLVBE(wire.ChatTLVMessageInfoEncoding, "us-ascii"),
+									wire.NewTLVBE(wire.ChatTLVMessageInfoLang, "en"),
+									wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+										"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">user_sending_chat_msg rolled 3 8-sided dice: 2 4 8</FONT></BODY></HTML>"),
+								},
+							}),
+						},
+					},
+				},
+			},
+			randRollDie: func() func(sides int) int {
+				// return multiples of 2 starting with 2
+				val := 2
+				return func(sides int) int {
+					ret := val
+					val *= 2
+					return ret
+				}
+			}(),
+		},
+		{
+			name: "send chat room message with macOS client 4.0.9 bug containing bad channel ID, expect message to " +
+				"client on MIME channel",
+			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: math.MaxUint16,
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							{
+								Tag:   wire.ChatTLVPublicWhisperFlag,
 								Value: []byte{},
 							},
+							wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+								TLVList: wire.TLVList{
+									wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+										"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">Hello</FONT></BODY></HTML>"),
+								},
+							}),
 						},
 					},
 				},
@@ -151,7 +267,12 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 											wire.NewTLVBE(wire.ChatTLVSenderInformation,
 												newTestSession("user_sending_chat_msg", sessOptCannedSignonTime).TLVUserInfo()),
 											wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}),
-											wire.NewTLVBE(wire.ChatTLVMessageInformation, []byte{}),
+											wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+												TLVList: wire.TLVList{
+													wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+														"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">Hello</FONT></BODY></HTML>"),
+												},
+											}),
 										},
 									},
 								},
@@ -179,10 +300,12 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 								Tag:   wire.ChatTLVPublicWhisperFlag,
 								Value: []byte{},
 							},
-							{
-								Tag:   wire.ChatTLVMessageInformation,
-								Value: []byte{},
-							},
+							wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+								TLVList: wire.TLVList{
+									wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+										"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">Hello</FONT></BODY></HTML>"),
+								},
+							}),
 						},
 					},
 				},
@@ -206,7 +329,12 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 											wire.NewTLVBE(wire.ChatTLVSenderInformation,
 												newTestSession("user_sending_chat_msg", sessOptCannedSignonTime).TLVUserInfo()),
 											wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}),
-											wire.NewTLVBE(wire.ChatTLVMessageInformation, []byte{}),
+											wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
+												TLVList: wire.TLVList{
+													wire.NewTLVBE(wire.ChatTLVMessageInfoText,
+														"<HTML><BODY BGCOLOR=\"#ffffff\"><FONT LANG=\"0\">Hello</FONT></BODY></HTML>"),
+												},
+											}),
 										},
 									},
 								},
@@ -227,6 +355,7 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 			}
 
 			svc := NewChatService(chatMessageRelayer)
+			svc.randRollDie = tc.randRollDie
 			outputSNAC, err := svc.ChannelMsgToHost(context.Background(), tc.userSession, tc.inputSNAC.Frame,
 				tc.inputSNAC.Body.(wire.SNAC_0x0E_0x05_ChatChannelMsgToHost))
 			assert.ErrorIs(t, err, tc.wantErr)
@@ -234,3 +363,48 @@ func TestChatService_ChannelMsgToHost(t *testing.T) {
 		})
 	}
 }
+
+func TestParseDiceCommand(t *testing.T) {
+	tests := []struct {
+		input         []byte
+		expectedValid bool
+		expectedDice  int
+		expectedSides int
+	}{
+		{[]byte("//roll-sides999-dice15"), true, 15, 999},
+		{[]byte("//roll-sides999-dice15 "), true, 15, 999},
+		{[]byte("//roll-SIDES999-DICE15"), false, 0, 0},
+		{[]byte("//roll-sides999-sides15"), false, 0, 0},
+		{[]byte("//roll-sides999-dice15 as I was saying"), false, 0, 0},
+		{[]byte("i'm gonna roll some dice //roll-sides999-dice15"), false, 0, 0},
+		{[]byte("//roll-dice15-sides999"), true, 15, 999},
+		{[]byte("//roll-dice15"), true, 15, 6},
+		{[]byte("//roll-dice0"), false, 0, 0},
+		{[]byte("//roll-sides0"), false, 0, 0},
+		{[]byte("//roll-sides999"), true, 2, 999},
+		{[]byte("//roll-dice16"), false, 0, 0},
+		{[]byte("//roll-sides1000"), false, 0, 0},
+		{[]byte("//roll-dice-5"), false, 0, 0},
+		{[]byte("//roll-sides-9"), false, 0, 0},
+		{[]byte("//roll"), true, 2, 6},
+		{[]byte("invalid input"), false, 0, 0},
+	}
+
+	for _, test := range tests {
+		t.Run(string(test.input), func(t *testing.T) {
+			valid, dice, sides := parseDiceCommand(test.input)
+
+			if valid != test.expectedValid {
+				t.Errorf("For input '%s', expected valid = %v, got %v", test.input, test.expectedValid, valid)
+			}
+
+			if dice != test.expectedDice {
+				t.Errorf("For input '%s', expected dice = %d, got %d", test.input, test.expectedDice, dice)
+			}
+
+			if sides != test.expectedSides {
+				t.Errorf("For input '%s', expected sides = %d, got %d", test.input, test.expectedSides, sides)
+			}
+		})
+	}
+}

+ 1 - 0
go.mod

@@ -26,6 +26,7 @@ require (
 	github.com/stretchr/objx v0.5.2 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
+	golang.org/x/net v0.30.0 // indirect
 	golang.org/x/sys v0.26.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	modernc.org/gc/v3 v3.0.0-20241004144649-1aea3fae8852 // indirect

+ 2 - 0
go.sum

@@ -39,6 +39,8 @@ golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBn
 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
 golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
 golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 5 - 1
wire/snacs.go

@@ -960,9 +960,13 @@ const (
 
 	ChatTLVPublicWhisperFlag    uint16 = 0x01
 	ChatTLVSenderInformation    uint16 = 0x03
-	ChatTLVMessageInformation   uint16 = 0x05
+	ChatTLVMessageInfo          uint16 = 0x05
 	ChatTLVEnableReflectionFlag uint16 = 0x06
 
+	ChatTLVMessageInfoEncoding uint16 = 0x02
+	ChatTLVMessageInfoLang     uint16 = 0x03
+	ChatTLVMessageInfoText     uint16 = 0x01
+
 	// referenced from protocols/oscar/family_chatnav.c in lib purple
 	ChatRoomTLVClassPerms         uint16 = 0x02
 	ChatRoomTLVMaxConcurrentRooms uint16 = 0x03 // required by aim 2.x-3.x