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

issue #48 - fix chat in pidgin client

This commit implements the following changes that makes Pidgin chat
work:

- chat detail level value must be "2"
- set TLV 0xDA in chat room info
- change the chat cookie to this format: %s-%s-%s, where the third %s is
  the chat room name.

All chat room business logic is now consolidated in the state package.

Co-authored-by: Josh Knight <josh@noobbox.com>
Mike 2 лет назад
Родитель
Сommit
3a3bc85d6f

+ 2 - 2
cmd/server/main.go

@@ -55,7 +55,7 @@ func main() {
 		authService := foodgroup.NewAuthService(cfg, sessionManager, chatSessionManager, feedbagStore, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore, chatSessionManager, feedbagStore)
 		bartService := foodgroup.NewBARTService(logger, feedbagStore, sessionManager, feedbagStore, adjListBuddyListStore)
 		buddyService := foodgroup.NewBuddyService(sessionManager, feedbagStore, adjListBuddyListStore)
-		chatNavService := foodgroup.NewChatNavService(logger, feedbagStore, state.NewChatRoom)
+		chatNavService := foodgroup.NewChatNavService(logger, feedbagStore)
 		feedbagService := foodgroup.NewFeedbagService(logger, sessionManager, feedbagStore, feedbagStore, adjListBuddyListStore)
 		foodgroupService := foodgroup.NewPermitDenyService()
 		icbmService := foodgroup.NewICBMService(sessionManager, feedbagStore, adjListBuddyListStore)
@@ -105,7 +105,7 @@ func main() {
 		logger = logger.With("svc", "CHAT_NAV")
 		sessionManager := state.NewInMemorySessionManager(logger)
 		authService := foodgroup.NewAuthService(cfg, sessionManager, chatSessionManager, feedbagStore, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore, chatSessionManager, feedbagStore)
-		chatNavService := foodgroup.NewChatNavService(logger, feedbagStore, state.NewChatRoom)
+		chatNavService := foodgroup.NewChatNavService(logger, feedbagStore)
 		oServiceService := foodgroup.NewOServiceServiceForChatNav(cfg, logger, sessionManager, adjListBuddyListStore, feedbagStore)
 
 		oscar.BOSServer{

+ 1 - 9
foodgroup/auth_test.go

@@ -1056,8 +1056,6 @@ func TestAuthService_SignoutChat(t *testing.T) {
 		name string
 		// userSession is the session of the user signing out
 		userSession *state.Session
-		// chatRoom is the chat room user is exiting
-		chatRoom state.ChatRoom
 		// mockParams is the list of params sent to mocks that satisfy this
 		// method's dependencies
 		mockParams mockParams
@@ -1065,9 +1063,6 @@ func TestAuthService_SignoutChat(t *testing.T) {
 		{
 			name:        "user signs out of chat room, room is empty after user leaves",
 			userSession: newTestSession("the-screen-name", sessOptCannedSignonTime, sessOptChatRoomCookie("the-chat-cookie")),
-			chatRoom: state.ChatRoom{
-				Cookie: "the-chat-cookie",
-			},
 			mockParams: mockParams{
 				chatMessageRelayerParams: chatMessageRelayerParams{
 					chatRelayToAllExceptParams: chatRelayToAllExceptParams{
@@ -1104,9 +1099,6 @@ func TestAuthService_SignoutChat(t *testing.T) {
 		{
 			name:        "user signs out of chat room, room is not empty after user leaves",
 			userSession: newTestSession("the-screen-name", sessOptCannedSignonTime, sessOptChatRoomCookie("the-chat-cookie")),
-			chatRoom: state.ChatRoom{
-				Cookie: "the-chat-cookie",
-			},
 			mockParams: mockParams{
 				chatMessageRelayerParams: chatMessageRelayerParams{
 					chatRelayToAllExceptParams: chatRelayToAllExceptParams{
@@ -1146,7 +1138,7 @@ func TestAuthService_SignoutChat(t *testing.T) {
 			chatMessageRelayer := newMockChatMessageRelayer(t)
 			for _, params := range tt.mockParams.chatRelayToAllExceptParams {
 				chatMessageRelayer.EXPECT().
-					RelayToAllExcept(nil, tt.chatRoom.Cookie, params.screenName, params.message)
+					RelayToAllExcept(nil, tt.userSession.ChatRoomCookie(), params.screenName, params.message)
 			}
 			sessionManager := newMockChatSessionRegistry(t)
 			for _, params := range tt.mockParams.removeSessionParams {

+ 4 - 4
foodgroup/chat.go

@@ -121,10 +121,10 @@ func sendChatRoomInfoUpdate(ctx context.Context, sess *state.Session, chatMessag
 			SubGroup:  wire.ChatRoomInfoUpdate,
 		},
 		Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-			Exchange:       room.Exchange,
-			Cookie:         room.Cookie,
-			InstanceNumber: room.InstanceNumber,
-			DetailLevel:    room.DetailLevel,
+			Exchange:       room.Exchange(),
+			Cookie:         room.Cookie(),
+			InstanceNumber: room.InstanceNumber(),
+			DetailLevel:    room.DetailLevel(),
 			TLVBlock: wire.TLVBlock{
 				TLVList: room.TLVList(),
 			},

+ 12 - 19
foodgroup/chat_nav.go

@@ -32,11 +32,10 @@ var (
 )
 
 // NewChatNavService creates a new instance of NewChatNavService.
-func NewChatNavService(logger *slog.Logger, chatRoomManager ChatRoomRegistry, fnNewChatRoom func() state.ChatRoom) *ChatNavService {
+func NewChatNavService(logger *slog.Logger, chatRoomManager ChatRoomRegistry) *ChatNavService {
 	return &ChatNavService{
 		logger:          logger,
 		chatRoomManager: chatRoomManager,
-		fnNewChatRoom:   fnNewChatRoom,
 	}
 }
 
@@ -45,7 +44,6 @@ func NewChatNavService(logger *slog.Logger, chatRoomManager ChatRoomRegistry, fn
 type ChatNavService struct {
 	logger          *slog.Logger
 	chatRoomManager ChatRoomRegistry
-	fnNewChatRoom   func() state.ChatRoom
 }
 
 // RequestChatRights returns SNAC wire.ChatNavNavInfo, which contains chat
@@ -102,14 +100,9 @@ func (s ChatNavService) CreateRoom(_ context.Context, sess *state.Session, inFra
 			return sendChatNavErrorSNAC(inFrame, wire.ErrorCodeNoMatch)
 		}
 
-		room = s.fnNewChatRoom()
-		room.Creator = sess.IdentScreenName()
-		room.DetailLevel = inBody.DetailLevel
-		room.Exchange = inBody.Exchange
-		room.InstanceNumber = inBody.InstanceNumber
-		room.Name = name
+		room = state.NewChatRoom(name, sess.IdentScreenName(), inBody.Exchange)
 
-		if err := s.chatRoomManager.CreateChatRoom(room); err != nil {
+		if err := s.chatRoomManager.CreateChatRoom(&room); err != nil {
 			return wire.SNACMessage{}, fmt.Errorf("%w: %w", errChatNavRoomCreateFailed, err)
 		}
 		break
@@ -126,10 +119,10 @@ func (s ChatNavService) CreateRoom(_ context.Context, sess *state.Session, inFra
 			TLVRestBlock: wire.TLVRestBlock{
 				TLVList: wire.TLVList{
 					wire.NewTLV(wire.ChatNavTLVRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-						Exchange:       room.Exchange,
-						Cookie:         room.Cookie,
-						InstanceNumber: room.InstanceNumber,
-						DetailLevel:    room.DetailLevel,
+						Cookie:         room.Cookie(),
+						Exchange:       room.Exchange(),
+						DetailLevel:    room.DetailLevel(),
+						InstanceNumber: room.InstanceNumber(),
 						TLVBlock: wire.TLVBlock{
 							TLVList: room.TLVList(),
 						},
@@ -153,7 +146,7 @@ func (s ChatNavService) RequestRoomInfo(_ context.Context, inFrame wire.SNACFram
 		return wire.SNACMessage{}, fmt.Errorf("%w: %w", state.ErrChatRoomNotFound, err)
 	}
 
-	if room.Exchange != inBody.Exchange {
+	if room.Exchange() != inBody.Exchange {
 		return wire.SNACMessage{}, errChatNavMismatchedExchange
 	}
 
@@ -167,10 +160,10 @@ func (s ChatNavService) RequestRoomInfo(_ context.Context, inFrame wire.SNACFram
 			TLVRestBlock: wire.TLVRestBlock{
 				TLVList: wire.TLVList{
 					wire.NewTLV(wire.ChatNavTLVRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-						Exchange:       room.Exchange,
-						Cookie:         room.Cookie,
-						InstanceNumber: room.InstanceNumber,
-						DetailLevel:    room.DetailLevel,
+						Cookie:         room.Cookie(),
+						Exchange:       room.Exchange(),
+						DetailLevel:    room.DetailLevel(),
+						InstanceNumber: room.InstanceNumber(),
 						TLVBlock: wire.TLVBlock{
 							TLVList: room.TLVList(),
 						},

+ 97 - 132
foodgroup/chat_nav_test.go

@@ -5,7 +5,6 @@ import (
 	"errors"
 	"log/slog"
 	"testing"
-	"time"
 
 	"github.com/mk6i/retro-aim-server/state"
 	"github.com/mk6i/retro-aim-server/wire"
@@ -14,28 +13,12 @@ import (
 )
 
 func TestChatNavService_CreateRoom(t *testing.T) {
-	basicChatRoom := state.ChatRoom{
-		Cookie:         "dummy-cookie",
-		CreateTime:     time.UnixMilli(0),
-		Creator:        state.NewIdentScreenName("the-screen-name"),
-		DetailLevel:    3,
-		Exchange:       4,
-		InstanceNumber: 2,
-		Name:           "the-chat-room-name",
-	}
-	publicChatRoom := state.ChatRoom{
-		Cookie:         "dummy-cookie",
-		CreateTime:     time.UnixMilli(0),
-		Creator:        state.NewIdentScreenName("the-screen-name"),
-		DetailLevel:    3,
-		Exchange:       5,
-		InstanceNumber: 2,
-		Name:           "the-public-chat-room-name",
-	}
+	basicChatRoom := state.NewChatRoom("the-chat-room-name", state.NewIdentScreenName("the-screen-name"), state.PrivateExchange)
+	publicChatRoom := state.NewChatRoom("the-public-chat-room-name", state.NewIdentScreenName("the-screen-name"), state.PublicExchange)
 
 	tests := []struct {
 		name          string
-		chatRoom      state.ChatRoom
+		chatRoom      *state.ChatRoom
 		sess          *state.Session
 		inputSNAC     wire.SNACMessage
 		want          wire.SNACMessage
@@ -45,20 +28,20 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 	}{
 		{
 			name:     "create private room that already exists",
-			chatRoom: basicChatRoom,
+			chatRoom: &basicChatRoom,
 			sess:     newTestSession("the-screen-name"),
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
 				},
 				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-					Exchange:       basicChatRoom.Exchange,
+					Exchange:       basicChatRoom.Exchange(),
 					Cookie:         "create", // actual canned value sent by AIM client
-					InstanceNumber: basicChatRoom.InstanceNumber,
-					DetailLevel:    basicChatRoom.DetailLevel,
+					InstanceNumber: basicChatRoom.InstanceNumber(),
+					DetailLevel:    basicChatRoom.DetailLevel(),
 					TLVBlock: wire.TLVBlock{
 						TLVList: wire.TLVList{
-							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name),
+							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name()),
 						},
 					},
 				},
@@ -75,10 +58,10 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 							wire.NewTLV(
 								wire.ChatNavRequestRoomInfo,
 								wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-									Exchange:       basicChatRoom.Exchange,
-									Cookie:         basicChatRoom.Cookie,
-									InstanceNumber: basicChatRoom.InstanceNumber,
-									DetailLevel:    basicChatRoom.DetailLevel,
+									Exchange:       basicChatRoom.Exchange(),
+									Cookie:         basicChatRoom.Cookie(),
+									InstanceNumber: basicChatRoom.InstanceNumber(),
+									DetailLevel:    basicChatRoom.DetailLevel(),
 									TLVBlock: wire.TLVBlock{
 										TLVList: basicChatRoom.TLVList(),
 									},
@@ -92,8 +75,8 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByNameParams: chatRoomByNameParams{
 						{
-							exchange: basicChatRoom.Exchange,
-							name:     basicChatRoom.Name,
+							exchange: basicChatRoom.Exchange(),
+							name:     basicChatRoom.Name(),
 							room:     basicChatRoom,
 						},
 					},
@@ -102,20 +85,20 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 		},
 		{
 			name:     "create private room that doesn't already exist",
-			chatRoom: basicChatRoom,
+			chatRoom: &basicChatRoom,
 			sess:     newTestSession("the-screen-name"),
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
 				},
 				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-					Exchange:       basicChatRoom.Exchange,
+					Exchange:       basicChatRoom.Exchange(),
 					Cookie:         "create", // actual canned value sent by AIM client
-					InstanceNumber: basicChatRoom.InstanceNumber,
-					DetailLevel:    basicChatRoom.DetailLevel,
+					InstanceNumber: basicChatRoom.InstanceNumber(),
+					DetailLevel:    basicChatRoom.DetailLevel(),
 					TLVBlock: wire.TLVBlock{
 						TLVList: wire.TLVList{
-							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name),
+							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name()),
 						},
 					},
 				},
@@ -132,10 +115,10 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 							wire.NewTLV(
 								wire.ChatNavRequestRoomInfo,
 								wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-									Exchange:       basicChatRoom.Exchange,
-									Cookie:         basicChatRoom.Cookie,
-									InstanceNumber: basicChatRoom.InstanceNumber,
-									DetailLevel:    basicChatRoom.DetailLevel,
+									Exchange:       basicChatRoom.Exchange(),
+									Cookie:         basicChatRoom.Cookie(),
+									InstanceNumber: basicChatRoom.InstanceNumber(),
+									DetailLevel:    basicChatRoom.DetailLevel(),
 									TLVBlock: wire.TLVBlock{
 										TLVList: basicChatRoom.TLVList(),
 									},
@@ -149,14 +132,14 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByNameParams: chatRoomByNameParams{
 						{
-							exchange: basicChatRoom.Exchange,
-							name:     basicChatRoom.Name,
+							exchange: basicChatRoom.Exchange(),
+							name:     basicChatRoom.Name(),
 							err:      state.ErrChatRoomNotFound,
 						},
 					},
 					createChatRoomParams: createChatRoomParams{
 						{
-							room: basicChatRoom,
+							room: &basicChatRoom,
 						},
 					},
 				},
@@ -167,20 +150,20 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 		},
 		{
 			name:     "create public room that already exists",
-			chatRoom: publicChatRoom,
+			chatRoom: &publicChatRoom,
 			sess:     newTestSession("the-screen-name"),
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
 				},
 				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-					Exchange:       publicChatRoom.Exchange,
+					Exchange:       publicChatRoom.Exchange(),
 					Cookie:         "create", // actual canned value sent by AIM client
-					InstanceNumber: publicChatRoom.InstanceNumber,
-					DetailLevel:    publicChatRoom.DetailLevel,
+					InstanceNumber: publicChatRoom.InstanceNumber(),
+					DetailLevel:    publicChatRoom.DetailLevel(),
 					TLVBlock: wire.TLVBlock{
 						TLVList: wire.TLVList{
-							wire.NewTLV(wire.ChatRoomTLVRoomName, publicChatRoom.Name),
+							wire.NewTLV(wire.ChatRoomTLVRoomName, publicChatRoom.Name()),
 						},
 					},
 				},
@@ -197,10 +180,10 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 							wire.NewTLV(
 								wire.ChatNavRequestRoomInfo,
 								wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-									Exchange:       publicChatRoom.Exchange,
-									Cookie:         publicChatRoom.Cookie,
-									InstanceNumber: publicChatRoom.InstanceNumber,
-									DetailLevel:    publicChatRoom.DetailLevel,
+									Exchange:       publicChatRoom.Exchange(),
+									Cookie:         publicChatRoom.Cookie(),
+									InstanceNumber: publicChatRoom.InstanceNumber(),
+									DetailLevel:    publicChatRoom.DetailLevel(),
 									TLVBlock: wire.TLVBlock{
 										TLVList: publicChatRoom.TLVList(),
 									},
@@ -214,8 +197,8 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByNameParams: chatRoomByNameParams{
 						{
-							exchange: publicChatRoom.Exchange,
-							name:     publicChatRoom.Name,
+							exchange: publicChatRoom.Exchange(),
+							name:     publicChatRoom.Name(),
 							room:     publicChatRoom,
 						},
 					},
@@ -224,20 +207,20 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 		},
 		{
 			name:     "create public room that does not exist",
-			chatRoom: publicChatRoom,
+			chatRoom: &publicChatRoom,
 			sess:     newTestSession("the-screen-name"),
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
 				},
 				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-					Exchange:       publicChatRoom.Exchange,
+					Exchange:       publicChatRoom.Exchange(),
 					Cookie:         "create", // actual canned value sent by AIM client
-					InstanceNumber: publicChatRoom.InstanceNumber,
-					DetailLevel:    publicChatRoom.DetailLevel,
+					InstanceNumber: publicChatRoom.InstanceNumber(),
+					DetailLevel:    publicChatRoom.DetailLevel(),
 					TLVBlock: wire.TLVBlock{
 						TLVList: wire.TLVList{
-							wire.NewTLV(wire.ChatRoomTLVRoomName, publicChatRoom.Name),
+							wire.NewTLV(wire.ChatRoomTLVRoomName, publicChatRoom.Name()),
 						},
 					},
 				},
@@ -256,8 +239,8 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByNameParams: chatRoomByNameParams{
 						{
-							exchange: publicChatRoom.Exchange,
-							name:     publicChatRoom.Name,
+							exchange: publicChatRoom.Exchange(),
+							name:     publicChatRoom.Name(),
 							err:      state.ErrChatRoomNotFound,
 						},
 					},
@@ -269,7 +252,7 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 		},
 		{
 			name:     "create room invalid exchange number",
-			chatRoom: basicChatRoom,
+			chatRoom: &basicChatRoom,
 			sess:     newTestSession("the-screen-name"),
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
@@ -278,11 +261,11 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
 					Exchange:       1337,
 					Cookie:         "create", // actual canned value sent by AIM client
-					InstanceNumber: basicChatRoom.InstanceNumber,
-					DetailLevel:    basicChatRoom.DetailLevel,
+					InstanceNumber: basicChatRoom.InstanceNumber(),
+					DetailLevel:    basicChatRoom.DetailLevel(),
 					TLVBlock: wire.TLVBlock{
 						TLVList: wire.TLVList{
-							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name),
+							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name()),
 						},
 					},
 				},
@@ -300,17 +283,17 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 		},
 		{
 			name:     "incoming create room missing name tlv",
-			chatRoom: basicChatRoom,
+			chatRoom: &basicChatRoom,
 			sess:     newTestSession("the-screen-name"),
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
 				},
 				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-					Exchange:       basicChatRoom.Exchange,
+					Exchange:       basicChatRoom.Exchange(),
 					Cookie:         "create", // actual canned value sent by AIM client
-					InstanceNumber: basicChatRoom.InstanceNumber,
-					DetailLevel:    basicChatRoom.DetailLevel,
+					InstanceNumber: basicChatRoom.InstanceNumber(),
+					DetailLevel:    basicChatRoom.DetailLevel(),
 					TLVBlock: wire.TLVBlock{
 						TLVList: wire.TLVList{}, // intentionally empty for test
 					},
@@ -321,20 +304,20 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 		},
 		{
 			name:     "create private room failed",
-			chatRoom: basicChatRoom,
+			chatRoom: &basicChatRoom,
 			sess:     newTestSession("the-screen-name"),
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
 				},
 				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-					Exchange:       basicChatRoom.Exchange,
+					Exchange:       basicChatRoom.Exchange(),
 					Cookie:         "create", // actual canned value sent by AIM client
-					InstanceNumber: basicChatRoom.InstanceNumber,
-					DetailLevel:    basicChatRoom.DetailLevel,
+					InstanceNumber: basicChatRoom.InstanceNumber(),
+					DetailLevel:    basicChatRoom.DetailLevel(),
 					TLVBlock: wire.TLVBlock{
 						TLVList: wire.TLVList{
-							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name),
+							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name()),
 						},
 					},
 				},
@@ -345,14 +328,14 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByNameParams: chatRoomByNameParams{
 						{
-							exchange: basicChatRoom.Exchange,
-							name:     basicChatRoom.Name,
+							exchange: basicChatRoom.Exchange(),
+							name:     basicChatRoom.Name(),
 							err:      state.ErrChatRoomNotFound,
 						},
 					},
 					createChatRoomParams: createChatRoomParams{
 						{
-							room: basicChatRoom,
+							room: &basicChatRoom,
 							err:  errors.New("fake database error"),
 						},
 					},
@@ -364,20 +347,20 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 		},
 		{
 			name:     "create private room failed unknown error",
-			chatRoom: basicChatRoom,
+			chatRoom: &basicChatRoom,
 			sess:     newTestSession("the-screen-name"),
 			inputSNAC: wire.SNACMessage{
 				Frame: wire.SNACFrame{
 					RequestID: 1234,
 				},
 				Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-					Exchange:       basicChatRoom.Exchange,
+					Exchange:       basicChatRoom.Exchange(),
 					Cookie:         "create", // actual canned value sent by AIM client
-					InstanceNumber: basicChatRoom.InstanceNumber,
-					DetailLevel:    basicChatRoom.DetailLevel,
+					InstanceNumber: basicChatRoom.InstanceNumber(),
+					DetailLevel:    basicChatRoom.DetailLevel(),
 					TLVBlock: wire.TLVBlock{
 						TLVList: wire.TLVList{
-							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name),
+							wire.NewTLV(wire.ChatRoomTLVRoomName, basicChatRoom.Name()),
 						},
 					},
 				},
@@ -388,8 +371,8 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByNameParams: chatRoomByNameParams{
 						{
-							exchange: basicChatRoom.Exchange,
-							name:     basicChatRoom.Name,
+							exchange: basicChatRoom.Exchange(),
+							name:     basicChatRoom.Name(),
 							err:      errors.New("fake database error"),
 						},
 					},
@@ -415,7 +398,7 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 					Return(params.err)
 			}
 
-			svc := NewChatNavService(slog.Default(), chatRoomRegistry, tt.fnNewChatRoom)
+			svc := NewChatNavService(slog.Default(), chatRoomRegistry)
 			outputSNAC, err := svc.CreateRoom(context.Background(), tt.sess, tt.inputSNAC.Frame, tt.inputSNAC.Body.(wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate))
 			assert.ErrorIs(t, err, tt.wantErr)
 			assert.Equal(t, tt.want, outputSNAC)
@@ -424,6 +407,9 @@ func TestChatNavService_CreateRoom(t *testing.T) {
 }
 
 func TestChatNavService_RequestRoomInfo(t *testing.T) {
+	privateChatRoom := state.NewChatRoom("the-chat-room", state.NewIdentScreenName("the-user"), state.PrivateExchange)
+	publicChatRoom := state.NewChatRoom("the-chat-room", state.NewIdentScreenName("the-user"), state.PublicExchange)
+
 	tests := []struct {
 		name       string
 		inputSNAC  wire.SNACMessage
@@ -439,7 +425,7 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 				},
 				Body: wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
 					Exchange: state.PrivateExchange,
-					Cookie:   "the-chat-cookie",
+					Cookie:   privateChatRoom.Cookie(),
 				},
 			},
 			want: wire.SNACMessage{
@@ -452,12 +438,12 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 					TLVRestBlock: wire.TLVRestBlock{
 						TLVList: wire.TLVList{
 							wire.NewTLV(0x04, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-								Cookie:         "the-chat-cookie",
-								DetailLevel:    2,
-								Exchange:       state.PrivateExchange,
-								InstanceNumber: 8,
+								Cookie:         privateChatRoom.Cookie(),
+								DetailLevel:    privateChatRoom.DetailLevel(),
+								Exchange:       privateChatRoom.Exchange(),
+								InstanceNumber: privateChatRoom.InstanceNumber(),
 								TLVBlock: wire.TLVBlock{
-									TLVList: state.ChatRoom{Cookie: "the-chat-cookie"}.TLVList(),
+									TLVList: privateChatRoom.TLVList(),
 								},
 							}),
 						},
@@ -468,13 +454,8 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByCookieParams: chatRoomByCookieParams{
 						{
-							cookie: "the-chat-cookie",
-							room: state.ChatRoom{
-								Cookie:         "the-chat-cookie",
-								DetailLevel:    2,
-								Exchange:       state.PrivateExchange,
-								InstanceNumber: 8,
-							},
+							cookie: privateChatRoom.Cookie(),
+							room:   privateChatRoom,
 						},
 					},
 				},
@@ -487,8 +468,8 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 					RequestID: 1234,
 				},
 				Body: wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
-					Exchange: state.PublicExchange,
-					Cookie:   "the-chat-cookie",
+					Exchange: publicChatRoom.Exchange(),
+					Cookie:   publicChatRoom.Cookie(),
 				},
 			},
 			want: wire.SNACMessage{
@@ -501,12 +482,12 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 					TLVRestBlock: wire.TLVRestBlock{
 						TLVList: wire.TLVList{
 							wire.NewTLV(0x04, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-								Cookie:         "the-chat-cookie",
-								DetailLevel:    2,
-								Exchange:       state.PublicExchange,
-								InstanceNumber: 8,
+								Cookie:         publicChatRoom.Cookie(),
+								DetailLevel:    publicChatRoom.DetailLevel(),
+								Exchange:       publicChatRoom.Exchange(),
+								InstanceNumber: publicChatRoom.InstanceNumber(),
 								TLVBlock: wire.TLVBlock{
-									TLVList: state.ChatRoom{Cookie: "the-chat-cookie"}.TLVList(),
+									TLVList: publicChatRoom.TLVList(),
 								},
 							}),
 						},
@@ -517,13 +498,8 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByCookieParams: chatRoomByCookieParams{
 						{
-							cookie: "the-chat-cookie",
-							room: state.ChatRoom{
-								Cookie:         "the-chat-cookie",
-								DetailLevel:    2,
-								Exchange:       state.PublicExchange,
-								InstanceNumber: 8,
-							},
+							cookie: publicChatRoom.Cookie(),
+							room:   publicChatRoom,
 						},
 					},
 				},
@@ -559,7 +535,7 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 				},
 				Body: wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
 					Exchange: state.PrivateExchange,
-					Cookie:   "the-chat-cookie",
+					Cookie:   privateChatRoom.Cookie(),
 				},
 			},
 			want:    wire.SNACMessage{},
@@ -568,13 +544,8 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByCookieParams: chatRoomByCookieParams{
 						{
-							cookie: "the-chat-cookie",
-							room: state.ChatRoom{
-								Cookie:         "the-chat-cookie",
-								DetailLevel:    2,
-								Exchange:       state.PublicExchange,
-								InstanceNumber: 8,
-							},
+							cookie: privateChatRoom.Cookie(),
+							room:   publicChatRoom,
 						},
 					},
 				},
@@ -588,7 +559,7 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 				},
 				Body: wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
 					Exchange: state.PrivateExchange,
-					Cookie:   "the-chat-cookie",
+					Cookie:   privateChatRoom.Cookie(),
 				},
 			},
 			want:    wire.SNACMessage{},
@@ -597,14 +568,8 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByCookieParams: chatRoomByCookieParams{
 						{
-							cookie: "the-chat-cookie",
-							room: state.ChatRoom{
-								Cookie:         "the-chat-cookie",
-								DetailLevel:    2,
-								Exchange:       state.PrivateExchange,
-								InstanceNumber: 8,
-							},
-							err: state.ErrChatRoomNotFound,
+							cookie: privateChatRoom.Cookie(),
+							err:    state.ErrChatRoomNotFound,
 						},
 					},
 				},
@@ -620,7 +585,7 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 					Return(params.room, params.err)
 			}
 
-			svc := NewChatNavService(slog.Default(), chatRoomRegistry, nil)
+			svc := NewChatNavService(slog.Default(), chatRoomRegistry)
 			got, err := svc.RequestRoomInfo(nil, tt.inputSNAC.Frame,
 				tt.inputSNAC.Body.(wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo))
 			assert.ErrorIs(t, err, tt.wantErr)
@@ -633,7 +598,7 @@ func TestChatNavService_RequestRoomInfo(t *testing.T) {
 }
 
 func TestChatNavService_RequestChatRights(t *testing.T) {
-	svc := NewChatNavService(nil, nil, nil)
+	svc := NewChatNavService(nil, nil)
 
 	have := svc.RequestChatRights(nil, wire.SNACFrame{RequestID: 1234})
 
@@ -802,7 +767,7 @@ func TestChatNavService_ExchangeInfo(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			svc := NewChatNavService(slog.Default(), nil, nil)
+			svc := NewChatNavService(slog.Default(), nil)
 			outputSNAC, err := svc.ExchangeInfo(context.Background(), tt.inputSNAC.Frame,
 				tt.inputSNAC.Body.(wire.SNAC_0x0D_0x03_ChatNavRequestExchangeInfo))
 			assert.ErrorIs(t, err, tt.wantErr)

+ 6 - 6
foodgroup/mock_chat_room_registry_test.go

@@ -134,7 +134,7 @@ func (_c *mockChatRoomRegistry_ChatRoomByName_Call) RunAndReturn(run func(uint16
 }
 
 // CreateChatRoom provides a mock function with given fields: chatRoom
-func (_m *mockChatRoomRegistry) CreateChatRoom(chatRoom state.ChatRoom) error {
+func (_m *mockChatRoomRegistry) CreateChatRoom(chatRoom *state.ChatRoom) error {
 	ret := _m.Called(chatRoom)
 
 	if len(ret) == 0 {
@@ -142,7 +142,7 @@ func (_m *mockChatRoomRegistry) CreateChatRoom(chatRoom state.ChatRoom) error {
 	}
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(state.ChatRoom) error); ok {
+	if rf, ok := ret.Get(0).(func(*state.ChatRoom) error); ok {
 		r0 = rf(chatRoom)
 	} else {
 		r0 = ret.Error(0)
@@ -157,14 +157,14 @@ type mockChatRoomRegistry_CreateChatRoom_Call struct {
 }
 
 // CreateChatRoom is a helper method to define mock.On call
-//   - chatRoom state.ChatRoom
+//   - chatRoom *state.ChatRoom
 func (_e *mockChatRoomRegistry_Expecter) CreateChatRoom(chatRoom interface{}) *mockChatRoomRegistry_CreateChatRoom_Call {
 	return &mockChatRoomRegistry_CreateChatRoom_Call{Call: _e.mock.On("CreateChatRoom", chatRoom)}
 }
 
-func (_c *mockChatRoomRegistry_CreateChatRoom_Call) Run(run func(chatRoom state.ChatRoom)) *mockChatRoomRegistry_CreateChatRoom_Call {
+func (_c *mockChatRoomRegistry_CreateChatRoom_Call) Run(run func(chatRoom *state.ChatRoom)) *mockChatRoomRegistry_CreateChatRoom_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(state.ChatRoom))
+		run(args[0].(*state.ChatRoom))
 	})
 	return _c
 }
@@ -174,7 +174,7 @@ func (_c *mockChatRoomRegistry_CreateChatRoom_Call) Return(_a0 error) *mockChatR
 	return _c
 }
 
-func (_c *mockChatRoomRegistry_CreateChatRoom_Call) RunAndReturn(run func(state.ChatRoom) error) *mockChatRoomRegistry_CreateChatRoom_Call {
+func (_c *mockChatRoomRegistry_CreateChatRoom_Call) RunAndReturn(run func(*state.ChatRoom) error) *mockChatRoomRegistry_CreateChatRoom_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 1 - 1
foodgroup/oservice.go

@@ -670,7 +670,7 @@ func (s OServiceServiceForBOS) ServiceRequest(ctx context.Context, sess *state.S
 		}
 
 		loginCookie := chatLoginCookie{
-			ChatCookie: room.Cookie,
+			ChatCookie: room.Cookie(),
 			ScreenName: sess.DisplayScreenName(),
 		}
 		buf := &bytes.Buffer{}

+ 34 - 49
foodgroup/oservice_test.go

@@ -17,6 +17,8 @@ import (
 )
 
 func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
+	chatRoom := state.NewChatRoom("the-chat-room", state.NewIdentScreenName(""), state.PrivateExchange)
+
 	cases := []struct {
 		// name is the unit test name
 		name string
@@ -200,9 +202,9 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 					TLVRestBlock: wire.TLVRestBlock{
 						TLVList: wire.TLVList{
 							wire.NewTLV(0x01, wire.SNAC_0x01_0x04_TLVRoomInfo{
-								Exchange:       8,
-								Cookie:         "the-chat-cookie",
-								InstanceNumber: 16,
+								Exchange:       chatRoom.Exchange(),
+								Cookie:         chatRoom.Cookie(),
+								InstanceNumber: chatRoom.InstanceNumber(),
 							}),
 						},
 					},
@@ -226,32 +228,27 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 					},
 				},
 			},
-			mockParams: mockParams{
-				chatRoomRegistryParams: chatRoomRegistryParams{
-					chatRoomByCookieParams: chatRoomByCookieParams{
-						{
-							cookie: "the-chat-cookie",
-							room: state.ChatRoom{
-								CreateTime:     time.UnixMilli(0),
-								DetailLevel:    4,
-								Exchange:       8,
-								Cookie:         "the-chat-cookie",
-								InstanceNumber: 16,
-								Name:           "my new chat",
+			mockParams: func() mockParams {
+				return mockParams{
+					chatRoomRegistryParams: chatRoomRegistryParams{
+						chatRoomByCookieParams: chatRoomByCookieParams{
+							{
+								cookie: chatRoom.Cookie(),
+								room:   chatRoom,
 							},
 						},
 					},
-				},
-				cookieIssuerParams: cookieIssuerParams{
-					{
-						data: []byte{
-							0x0F, 't', 'h', 'e', '-', 'c', 'h', 'a', 't', '-', 'c', 'o', 'o', 'k', 'i', 'e',
-							0x10, 'u', 's', 'e', 'r', '_', 's', 'c', 'r', 'e', 'e', 'n', '_', 'n', 'a', 'm', 'e',
+					cookieIssuerParams: cookieIssuerParams{
+						{
+							data: []byte{
+								0x11, '4', '-', '0', '-', 't', 'h', 'e', '-', 'c', 'h', 'a', 't', '-', 'r', 'o', 'o', 'm',
+								0x10, 'u', 's', 'e', 'r', '_', 's', 'c', 'r', 'e', 'e', 'n', '_', 'n', 'a', 'm', 'e',
+							},
+							cookie: []byte("the-auth-cookie"),
 						},
-						cookie: []byte("the-auth-cookie"),
 					},
-				},
-			},
+				}
+			}(),
 		},
 		{
 			name: "request info for connecting to non-existent chat room, return ErrChatRoomNotFound",
@@ -1702,15 +1699,9 @@ func TestOServiceServiceForBOS_ClientOnline(t *testing.T) {
 }
 
 func TestOServiceServiceForChat_ClientOnline(t *testing.T) {
-	chatter1 := newTestSession("chatter-1", sessOptChatRoomCookie("the-cookie"))
-	chatter2 := newTestSession("chatter-2", sessOptChatRoomCookie("the-cookie"))
-	chatRoom := state.ChatRoom{
-		Cookie:         "the-cookie",
-		DetailLevel:    1,
-		Exchange:       2,
-		InstanceNumber: 3,
-		Name:           "the-chat-room",
-	}
+	chatRoom := state.NewChatRoom("the-chat-room", state.NewIdentScreenName("creator"), state.PrivateExchange)
+	chatter1 := newTestSession("chatter-1", sessOptChatRoomCookie(chatRoom.Cookie()))
+	chatter2 := newTestSession("chatter-2", sessOptChatRoomCookie(chatRoom.Cookie()))
 
 	tests := []struct {
 		// name is the name of the test
@@ -1734,7 +1725,7 @@ func TestOServiceServiceForChat_ClientOnline(t *testing.T) {
 					chatRelayToAllExceptParams: chatRelayToAllExceptParams{
 						{
 							screenName: state.NewIdentScreenName("chatter-1"),
-							cookie:     "the-cookie",
+							cookie:     chatRoom.Cookie(),
 							message: wire.SNACMessage{
 								Frame: wire.SNACFrame{
 									FoodGroup: wire.Chat,
@@ -1750,7 +1741,7 @@ func TestOServiceServiceForChat_ClientOnline(t *testing.T) {
 					},
 					chatAllSessionsParams: chatAllSessionsParams{
 						{
-							cookie: "the-cookie",
+							cookie: chatRoom.Cookie(),
 							sessions: []*state.Session{
 								chatter1,
 								chatter2,
@@ -1759,7 +1750,7 @@ func TestOServiceServiceForChat_ClientOnline(t *testing.T) {
 					},
 					chatRelayToScreenNameParams: chatRelayToScreenNameParams{
 						{
-							cookie:     "the-cookie",
+							cookie:     chatRoom.Cookie(),
 							screenName: chatter1.IdentScreenName(),
 							message: wire.SNACMessage{
 								Frame: wire.SNACFrame{
@@ -1767,10 +1758,10 @@ func TestOServiceServiceForChat_ClientOnline(t *testing.T) {
 									SubGroup:  wire.ChatRoomInfoUpdate,
 								},
 								Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-									Exchange:       chatRoom.Exchange,
-									Cookie:         chatRoom.Cookie,
-									InstanceNumber: chatRoom.InstanceNumber,
-									DetailLevel:    chatRoom.DetailLevel,
+									Exchange:       chatRoom.Exchange(),
+									Cookie:         chatRoom.Cookie(),
+									InstanceNumber: chatRoom.InstanceNumber(),
+									DetailLevel:    chatRoom.DetailLevel(),
 									TLVBlock: wire.TLVBlock{
 										TLVList: chatRoom.TLVList(),
 									},
@@ -1778,7 +1769,7 @@ func TestOServiceServiceForChat_ClientOnline(t *testing.T) {
 							},
 						},
 						{
-							cookie:     "the-cookie",
+							cookie:     chatRoom.Cookie(),
 							screenName: chatter1.IdentScreenName(),
 							message: wire.SNACMessage{
 								Frame: wire.SNACFrame{
@@ -1798,14 +1789,8 @@ func TestOServiceServiceForChat_ClientOnline(t *testing.T) {
 				chatRoomRegistryParams: chatRoomRegistryParams{
 					chatRoomByCookieParams: chatRoomByCookieParams{
 						{
-							cookie: "the-cookie",
-							room: state.ChatRoom{
-								Cookie:         "the-cookie",
-								DetailLevel:    1,
-								Exchange:       2,
-								InstanceNumber: 3,
-								Name:           "the-chat-room",
-							},
+							cookie: chatRoom.Cookie(),
+							room:   chatRoom,
 						},
 					},
 				},

+ 1 - 1
foodgroup/test_helpers.go

@@ -433,7 +433,7 @@ type chatRoomByNameParams []struct {
 // createChatRoomParams is the list of parameters passed at the mock
 // ChatRoomRegistry.CreateChatRoom call site
 type createChatRoomParams []struct {
-	room state.ChatRoom
+	room *state.ChatRoom
 	err  error
 }
 

+ 1 - 1
foodgroup/types.go

@@ -143,7 +143,7 @@ type ChatRoomRegistry interface {
 	ChatRoomByName(exchange uint16, name string) (state.ChatRoom, error)
 
 	// CreateChatRoom creates a new chat room.
-	CreateChatRoom(chatRoom state.ChatRoom) error
+	CreateChatRoom(chatRoom *state.ChatRoom) error
 }
 
 type BARTManager interface {

+ 24 - 26
server/http/mgmt_api.go

@@ -44,7 +44,7 @@ func StartManagementAPI(
 		sessionHandler(w, r, sessionRetriever)
 	})
 	mux.HandleFunc("/chat/room/public", func(w http.ResponseWriter, r *http.Request) {
-		publicChatHandler(w, r, chatRoomRetriever, chatRoomCreator, chatSessionRetriever, state.NewChatRoom, logger)
+		publicChatHandler(w, r, chatRoomRetriever, chatRoomCreator, chatSessionRetriever, logger)
 	})
 	mux.HandleFunc("/chat/room/private", func(w http.ResponseWriter, r *http.Request) {
 		privateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
@@ -93,7 +93,7 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManag
 	}
 
 	w.WriteHeader(http.StatusNoContent)
-	fmt.Fprintln(w, "User account successfully deleted.")
+	_, _ = fmt.Fprintln(w, "User account successfully deleted.")
 }
 
 func userPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
@@ -225,7 +225,7 @@ func postUserHandler(w http.ResponseWriter, r *http.Request, userManager UserMan
 	}
 
 	w.WriteHeader(http.StatusCreated)
-	fmt.Fprintln(w, "User account created successfully.")
+	_, _ = fmt.Fprintln(w, "User account created successfully.")
 }
 
 func userFromBody(r *http.Request) (userWithPassword, error) {
@@ -244,28 +244,28 @@ func loginHandler(w http.ResponseWriter, r *http.Request, userManager UserManage
 		// No authentication header found
 		w.WriteHeader(http.StatusUnauthorized)
 		w.Header().Set("WWW-Authenticate", `Basic realm="User Login"`)
-		w.Write([]byte("401 Unauthorized\n"))
+		_, _ = w.Write([]byte("401 Unauthorized\n"))
 		return
 	}
 
 	auth := strings.SplitN(authHeader, " ", 2)
 	if len(auth) != 2 || auth[0] != "Basic" {
 		w.WriteHeader(http.StatusUnauthorized)
-		w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
+		_, _ = w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
 		return
 	}
 
 	payload, err := base64.StdEncoding.DecodeString(auth[1])
 	if err != nil {
 		w.WriteHeader(http.StatusUnauthorized)
-		w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
+		_, _ = w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
 		return
 	}
 
 	pair := strings.SplitN(string(payload), ":", 2)
 	if len(pair) != 2 {
 		w.WriteHeader(http.StatusUnauthorized)
-		w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
+		_, _ = w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
 		return
 	}
 
@@ -274,27 +274,27 @@ func loginHandler(w http.ResponseWriter, r *http.Request, userManager UserManage
 	user, err := userManager.User(username)
 	if err != nil {
 		w.WriteHeader(http.StatusInternalServerError)
-		w.Write([]byte("500 InternalServerError\n"))
+		_, _ = w.Write([]byte("500 InternalServerError\n"))
 		logger.Error("error getting user", "err", err.Error())
 		return
 	}
 	if user == nil || !user.ValidateHash(wire.StrongMD5PasswordHash(password, user.AuthKey)) {
 		w.WriteHeader(http.StatusUnauthorized)
-		w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
+		_, _ = w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
 		return
 	}
 
 	// Successfully authenticated
 	w.WriteHeader(http.StatusOK)
-	w.Write([]byte("200 OK: Successfully Authenticated\n"))
+	_, _ = w.Write([]byte("200 OK: Successfully Authenticated\n"))
 }
 
-func publicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatRoomCreator ChatRoomCreator, chatSessionRetriever ChatSessionRetriever, newChatRoom func() state.ChatRoom, logger *slog.Logger) {
+func publicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatRoomCreator ChatRoomCreator, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
 	switch r.Method {
 	case http.MethodGet:
 		getPublicChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
 	case http.MethodPost:
-		postPublicChatHandler(w, r, chatRoomCreator, newChatRoom, logger)
+		postPublicChatHandler(w, r, chatRoomCreator, logger)
 	default:
 		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
 	}
@@ -321,10 +321,10 @@ func getPublicChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetrie
 
 	out := make([]chatRoom, len(rooms))
 	for i, room := range rooms {
-		sessions := chatSessionRetriever.AllSessions(room.Cookie)
+		sessions := chatSessionRetriever.AllSessions(room.Cookie())
 		cr := chatRoom{
-			CreateTime:   room.CreateTime,
-			Name:         room.Name,
+			CreateTime:   room.CreateTime(),
+			Name:         room.Name(),
 			Participants: make([]userHandle, len(sessions)),
 			URL:          room.URL().String(),
 		}
@@ -345,7 +345,7 @@ func getPublicChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetrie
 }
 
 // postPublicChatHandler handles the POST /chat/room/public endpoint.
-func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreator ChatRoomCreator, newChatRoom func() state.ChatRoom, logger *slog.Logger) {
+func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreator ChatRoomCreator, logger *slog.Logger) {
 	input := chatRoomCreate{}
 	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
 		http.Error(w, "invalid input", http.StatusBadRequest)
@@ -358,11 +358,9 @@ func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreat
 		return
 	}
 
-	cr := newChatRoom()
-	cr.Name = input.Name
-	cr.Exchange = state.PublicExchange
+	cr := state.NewChatRoom(input.Name, state.NewIdentScreenName("system"), state.PublicExchange)
 
-	err := chatRoomCreator.CreateChatRoom(cr)
+	err := chatRoomCreator.CreateChatRoom(&cr)
 	switch {
 	case errors.Is(err, state.ErrDupChatRoom):
 		http.Error(w, "Chat room already exists.", http.StatusConflict)
@@ -374,7 +372,7 @@ func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreat
 	}
 
 	w.WriteHeader(http.StatusCreated)
-	fmt.Fprintln(w, "Chat room created successfully.")
+	_, _ = fmt.Fprintln(w, "Chat room created successfully.")
 }
 
 // getPrivateChatHandler handles the GET /chat/room/private endpoint.
@@ -389,11 +387,11 @@ func getPrivateChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetri
 
 	out := make([]chatRoom, len(rooms))
 	for i, room := range rooms {
-		sessions := chatSessionRetriever.AllSessions(room.Cookie)
+		sessions := chatSessionRetriever.AllSessions(room.Cookie())
 		cr := chatRoom{
-			CreateTime:   room.CreateTime,
-			CreatorID:    room.Creator.String(),
-			Name:         room.Name,
+			CreateTime:   room.CreateTime(),
+			CreatorID:    room.Creator().String(),
+			Name:         room.Name(),
 			Participants: make([]userHandle, len(sessions)),
 			URL:          room.URL().String(),
 		}
@@ -457,5 +455,5 @@ func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, messageRe
 	messageRelayer.RelayToScreenName(context.Background(), state.NewIdentScreenName(input.To), msg)
 
 	w.WriteHeader(http.StatusOK)
-	fmt.Fprintln(w, "Message sent successfully.")
+	_, _ = fmt.Fprintln(w, "Message sent successfully.")
 }

+ 22 - 47
server/http/mgmt_api_test.go

@@ -7,7 +7,6 @@ import (
 	"net/http/httptest"
 	"strings"
 	"testing"
-	"time"
 
 	"github.com/google/uuid"
 	"github.com/stretchr/testify/assert"
@@ -442,6 +441,9 @@ func TestPublicChatHandler_GET(t *testing.T) {
 		result []*state.Session
 	}
 
+	chatRoom1 := state.NewChatRoom("chat-room-1-name", state.NewIdentScreenName("chat-room-1-creator"), state.PublicExchange)
+	chatRoom2 := state.NewChatRoom("chat-room-2-name", state.NewIdentScreenName("chat-room-1-creator"), state.PublicExchange)
+
 	tt := []struct {
 		name               string
 		allChatRoomsParams allChatRoomsParams
@@ -455,37 +457,27 @@ func TestPublicChatHandler_GET(t *testing.T) {
 			allChatRoomsParams: allChatRoomsParams{
 				exchange: state.PublicExchange,
 				result: []state.ChatRoom{
-					{
-						Cookie:     "chat-room-1-cookie",
-						Creator:    state.NewIdentScreenName("chat-room-1-creator"),
-						Name:       "chat-room-1-name",
-						CreateTime: time.Date(2024, 06, 01, 1, 2, 3, 4, time.UTC),
-					},
-					{
-						Cookie:     "chat-room-2-cookie",
-						Creator:    state.NewIdentScreenName("chat-room-2-creator"),
-						Name:       "chat-room-2-name",
-						CreateTime: time.Date(2022, 01, 04, 6, 8, 1, 2, time.UTC),
-					},
+					chatRoom1,
+					chatRoom2,
 				},
 			},
 			allSessionsParams: []allSessionsParams{
 				{
-					cookie: "chat-room-1-cookie",
+					cookie: chatRoom1.Cookie(),
 					result: []*state.Session{
 						fnNewSess("userA"),
 						fnNewSess("userB"),
 					},
 				},
 				{
-					cookie: "chat-room-2-cookie",
+					cookie: chatRoom2.Cookie(),
 					result: []*state.Session{
 						fnNewSess("userC"),
 						fnNewSess("userD"),
 					},
 				},
 			},
-			want:       `[{"name":"chat-room-1-name","create_time":"2024-06-01T01:02:03.000000004Z","url":"aim:gochat?exchange=0\u0026roomname=chat-room-1-name","participants":[{"id":"usera","screen_name":"userA"},{"id":"userb","screen_name":"userB"}]},{"name":"chat-room-2-name","create_time":"2022-01-04T06:08:01.000000002Z","url":"aim:gochat?exchange=0\u0026roomname=chat-room-2-name","participants":[{"id":"userc","screen_name":"userC"},{"id":"userd","screen_name":"userD"}]}]`,
+			want:       `[{"name":"chat-room-1-name","create_time":"0001-01-01T00:00:00Z","url":"aim:gochat?exchange=5\u0026roomname=chat-room-1-name","participants":[{"id":"usera","screen_name":"userA"},{"id":"userb","screen_name":"userB"}]},{"name":"chat-room-2-name","create_time":"0001-01-01T00:00:00Z","url":"aim:gochat?exchange=5\u0026roomname=chat-room-2-name","participants":[{"id":"userc","screen_name":"userC"},{"id":"userd","screen_name":"userD"}]}]`,
 			statusCode: http.StatusOK,
 		},
 		{
@@ -493,21 +485,16 @@ func TestPublicChatHandler_GET(t *testing.T) {
 			allChatRoomsParams: allChatRoomsParams{
 				exchange: state.PublicExchange,
 				result: []state.ChatRoom{
-					{
-						Cookie:     "chat-room-1-cookie",
-						Creator:    state.NewIdentScreenName("chat-room-1-creator"),
-						Name:       "chat-room-1-name",
-						CreateTime: time.Date(2024, 06, 01, 1, 2, 3, 4, time.UTC),
-					},
+					chatRoom1,
 				},
 			},
 			allSessionsParams: []allSessionsParams{
 				{
-					cookie: "chat-room-1-cookie",
+					cookie: chatRoom1.Cookie(),
 					result: []*state.Session{},
 				},
 			},
-			want:       `[{"name":"chat-room-1-name","create_time":"2024-06-01T01:02:03.000000004Z","url":"aim:gochat?exchange=0\u0026roomname=chat-room-1-name","participants":[]}]`,
+			want:       `[{"name":"chat-room-1-name","create_time":"0001-01-01T00:00:00Z","url":"aim:gochat?exchange=5\u0026roomname=chat-room-1-name","participants":[]}]`,
 			statusCode: http.StatusOK,
 		},
 		{
@@ -569,6 +556,9 @@ func TestPrivateChatHandler_GET(t *testing.T) {
 		result []*state.Session
 	}
 
+	chatRoom1 := state.NewChatRoom("chat-room-1-name", state.NewIdentScreenName("chat-room-1-creator"), state.PrivateExchange)
+	chatRoom2 := state.NewChatRoom("chat-room-2-name", state.NewIdentScreenName("chat-room-2-creator"), state.PrivateExchange)
+
 	tt := []struct {
 		name               string
 		allChatRoomsParams allChatRoomsParams
@@ -582,37 +572,27 @@ func TestPrivateChatHandler_GET(t *testing.T) {
 			allChatRoomsParams: allChatRoomsParams{
 				exchange: state.PrivateExchange,
 				result: []state.ChatRoom{
-					{
-						Cookie:     "chat-room-1-cookie",
-						Creator:    state.NewIdentScreenName("chat-room-1-creator"),
-						Name:       "chat-room-1-name",
-						CreateTime: time.Date(2024, 06, 01, 1, 2, 3, 4, time.UTC),
-					},
-					{
-						Cookie:     "chat-room-2-cookie",
-						Creator:    state.NewIdentScreenName("chat-room-2-creator"),
-						Name:       "chat-room-2-name",
-						CreateTime: time.Date(2022, 01, 04, 6, 8, 1, 2, time.UTC),
-					},
+					chatRoom1,
+					chatRoom2,
 				},
 			},
 			allSessionsParams: []allSessionsParams{
 				{
-					cookie: "chat-room-1-cookie",
+					cookie: chatRoom1.Cookie(),
 					result: []*state.Session{
 						fnNewSess("userA"),
 						fnNewSess("userB"),
 					},
 				},
 				{
-					cookie: "chat-room-2-cookie",
+					cookie: chatRoom2.Cookie(),
 					result: []*state.Session{
 						fnNewSess("userC"),
 						fnNewSess("userD"),
 					},
 				},
 			},
-			want:       `[{"name":"chat-room-1-name","create_time":"2024-06-01T01:02:03.000000004Z","creator_id":"chat-room-1-creator","url":"aim:gochat?exchange=0\u0026roomname=chat-room-1-name","participants":[{"id":"usera","screen_name":"userA"},{"id":"userb","screen_name":"userB"}]},{"name":"chat-room-2-name","create_time":"2022-01-04T06:08:01.000000002Z","creator_id":"chat-room-2-creator","url":"aim:gochat?exchange=0\u0026roomname=chat-room-2-name","participants":[{"id":"userc","screen_name":"userC"},{"id":"userd","screen_name":"userD"}]}]`,
+			want:       `[{"name":"chat-room-1-name","create_time":"0001-01-01T00:00:00Z","creator_id":"chat-room-1-creator","url":"aim:gochat?exchange=4\u0026roomname=chat-room-1-name","participants":[{"id":"usera","screen_name":"userA"},{"id":"userb","screen_name":"userB"}]},{"name":"chat-room-2-name","create_time":"0001-01-01T00:00:00Z","creator_id":"chat-room-2-creator","url":"aim:gochat?exchange=4\u0026roomname=chat-room-2-name","participants":[{"id":"userc","screen_name":"userC"},{"id":"userd","screen_name":"userD"}]}]`,
 			statusCode: http.StatusOK,
 		},
 		{
@@ -620,21 +600,16 @@ func TestPrivateChatHandler_GET(t *testing.T) {
 			allChatRoomsParams: allChatRoomsParams{
 				exchange: state.PrivateExchange,
 				result: []state.ChatRoom{
-					{
-						Cookie:     "chat-room-1-cookie",
-						Creator:    state.NewIdentScreenName("chat-room-1-creator"),
-						Name:       "chat-room-1-name",
-						CreateTime: time.Date(2024, 06, 01, 1, 2, 3, 4, time.UTC),
-					},
+					chatRoom1,
 				},
 			},
 			allSessionsParams: []allSessionsParams{
 				{
-					cookie: "chat-room-1-cookie",
+					cookie: chatRoom1.Cookie(),
 					result: []*state.Session{},
 				},
 			},
-			want:       `[{"name":"chat-room-1-name","create_time":"2024-06-01T01:02:03.000000004Z","creator_id":"chat-room-1-creator","url":"aim:gochat?exchange=0\u0026roomname=chat-room-1-name","participants":[]}]`,
+			want:       `[{"name":"chat-room-1-name","create_time":"0001-01-01T00:00:00Z","creator_id":"chat-room-1-creator","url":"aim:gochat?exchange=4\u0026roomname=chat-room-1-name","participants":[]}]`,
 			statusCode: http.StatusOK,
 		},
 		{

+ 6 - 6
server/http/mock_chat_room_creator_test.go

@@ -21,7 +21,7 @@ func (_m *mockChatRoomCreator) EXPECT() *mockChatRoomCreator_Expecter {
 }
 
 // CreateChatRoom provides a mock function with given fields: chatRoom
-func (_m *mockChatRoomCreator) CreateChatRoom(chatRoom state.ChatRoom) error {
+func (_m *mockChatRoomCreator) CreateChatRoom(chatRoom *state.ChatRoom) error {
 	ret := _m.Called(chatRoom)
 
 	if len(ret) == 0 {
@@ -29,7 +29,7 @@ func (_m *mockChatRoomCreator) CreateChatRoom(chatRoom state.ChatRoom) error {
 	}
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(state.ChatRoom) error); ok {
+	if rf, ok := ret.Get(0).(func(*state.ChatRoom) error); ok {
 		r0 = rf(chatRoom)
 	} else {
 		r0 = ret.Error(0)
@@ -44,14 +44,14 @@ type mockChatRoomCreator_CreateChatRoom_Call struct {
 }
 
 // CreateChatRoom is a helper method to define mock.On call
-//   - chatRoom state.ChatRoom
+//   - chatRoom *state.ChatRoom
 func (_e *mockChatRoomCreator_Expecter) CreateChatRoom(chatRoom interface{}) *mockChatRoomCreator_CreateChatRoom_Call {
 	return &mockChatRoomCreator_CreateChatRoom_Call{Call: _e.mock.On("CreateChatRoom", chatRoom)}
 }
 
-func (_c *mockChatRoomCreator_CreateChatRoom_Call) Run(run func(chatRoom state.ChatRoom)) *mockChatRoomCreator_CreateChatRoom_Call {
+func (_c *mockChatRoomCreator_CreateChatRoom_Call) Run(run func(chatRoom *state.ChatRoom)) *mockChatRoomCreator_CreateChatRoom_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(state.ChatRoom))
+		run(args[0].(*state.ChatRoom))
 	})
 	return _c
 }
@@ -61,7 +61,7 @@ func (_c *mockChatRoomCreator_CreateChatRoom_Call) Return(_a0 error) *mockChatRo
 	return _c
 }
 
-func (_c *mockChatRoomCreator_CreateChatRoom_Call) RunAndReturn(run func(state.ChatRoom) error) *mockChatRoomCreator_CreateChatRoom_Call {
+func (_c *mockChatRoomCreator_CreateChatRoom_Call) RunAndReturn(run func(*state.ChatRoom) error) *mockChatRoomCreator_CreateChatRoom_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 1 - 1
server/http/types.go

@@ -13,7 +13,7 @@ type ChatRoomRetriever interface {
 }
 
 type ChatRoomCreator interface {
-	CreateChatRoom(chatRoom state.ChatRoom) error
+	CreateChatRoom(chatRoom *state.ChatRoom) error
 }
 
 type ChatSessionRetriever interface {

+ 61 - 30
state/chat.go

@@ -7,8 +7,6 @@ import (
 	"time"
 
 	"github.com/mk6i/retro-aim-server/wire"
-
-	"github.com/google/uuid"
 )
 
 const (
@@ -26,29 +24,69 @@ var (
 	ErrDupChatRoom      = errors.New("chat room already exists")
 )
 
-// ChatRoom is the representation of a chat room's metadata.
+// NewChatRoom creates a new ChatRoom instance.
+func NewChatRoom(name string, creator IdentScreenName, exchange uint16) ChatRoom {
+	return ChatRoom{
+		name:     name,
+		creator:  creator,
+		exchange: exchange,
+	}
+}
+
+// ChatRoom represents of a chat room.
 type ChatRoom struct {
-	// Cookie is the unique chat room identifier.
-	Cookie string
-	// CreateTime indicates when the chat room was created.
-	CreateTime time.Time
-	// Creator is the screen name of the user who created the chat room.
-	Creator IdentScreenName
-	// DetailLevel is the detail level of the chat room.  Unclear what this value means.
-	DetailLevel uint8
-	// Exchange indicates which exchange the chatroom belongs to. Typically, a canned value.
-	Exchange uint16
-	// InstanceNumber indicates which instance chatroom exists in. Typically, a canned value.
-	InstanceNumber uint16
-	// Name is the name of the chat room.
-	Name string
+	createTime time.Time
+	creator    IdentScreenName
+	exchange   uint16
+	name       string
+}
+
+// Creator returns the screen name of the user who created the chat room.
+func (c ChatRoom) Creator() IdentScreenName {
+	return c.creator
+}
+
+// Exchange returns which exchange the chat room belongs to.
+func (c ChatRoom) Exchange() uint16 {
+	return c.exchange
+}
+
+// Name returns the chat room name.
+func (c ChatRoom) Name() string {
+	return c.name
+}
+
+// InstanceNumber returns which instance chatroom exists in. Overflow chat
+// rooms do not exist yet, so all chats happen in the same instance.
+func (c ChatRoom) InstanceNumber() uint16 {
+	return 0
+}
+
+// CreateTime returns when the chat room was inserted in the database.
+func (c ChatRoom) CreateTime() time.Time {
+	return c.createTime
+}
+
+// DetailLevel returns the detail level of the chat room (whatever that means).
+func (c ChatRoom) DetailLevel() uint8 {
+	return 0x02 // Pidgin 2.13.0 expects value 0x02
+}
+
+// Cookie returns the chat room unique identifier.
+func (c ChatRoom) Cookie() string {
+	// According to Pidgin, the chat cookie is a 3-part identifier. The third
+	// segment is the chat name, which is shown explicitly in the Pidgin code.
+	// We can assume that the first two parts were the exchange and instance
+	// number. As of now, Pidgin is the only client that cares about the cookie
+	// format, and it only cares about the chat name segment.
+	return fmt.Sprintf("%d-%d-%s", c.exchange, c.InstanceNumber(), c.name)
 }
 
 // URL creates a URL that can be used to join a chat room.
 func (c ChatRoom) URL() *url.URL {
 	v := url.Values{}
-	v.Set("roomname", c.Name)
-	v.Set("exchange", fmt.Sprintf("%d", c.Exchange))
+	v.Set("roomname", c.name)
+	v.Set("exchange", fmt.Sprintf("%d", c.exchange))
 
 	return &url.URL{
 		Scheme: "aim",
@@ -67,7 +105,7 @@ func (c ChatRoom) TLVList() []wire.TLV {
 		// - 8 Occupant Peek Allowed
 		// It's unclear what effect they actually have.
 		wire.NewTLV(wire.ChatRoomTLVFlags, uint16(15)),
-		wire.NewTLV(wire.ChatRoomTLVCreateTime, uint32(c.CreateTime.Unix())),
+		wire.NewTLV(wire.ChatRoomTLVCreateTime, uint32(c.createTime.Unix())),
 		wire.NewTLV(wire.ChatRoomTLVMaxMsgLen, uint16(1024)),
 		wire.NewTLV(wire.ChatRoomTLVMaxOccupancy, uint16(100)),
 		// From protocols/oscar/family_chatnav.c in lib purple, these are the
@@ -77,15 +115,8 @@ func (c ChatRoom) TLVList() []wire.TLV {
 		// - 2  exchange creation allowed
 		// It's unclear what effect they actually have.
 		wire.NewTLV(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
-		wire.NewTLV(wire.ChatRoomTLVFullyQualifiedName, c.Name),
-		wire.NewTLV(wire.ChatRoomTLVRoomName, c.Name),
-	}
-}
-
-// NewChatRoom creates new state.ChatRoom objects
-func NewChatRoom() ChatRoom {
-	return ChatRoom{
-		Cookie:     uuid.New().String(),
-		CreateTime: time.Now().UTC(),
+		wire.NewTLV(wire.ChatRoomTLVFullyQualifiedName, c.name),
+		wire.NewTLV(wire.ChatRoomTLVRoomName, c.name),
+		wire.NewTLV(wire.ChatRoomTLVMaxMsgVisLen, uint16(1024)),
 	}
 }

+ 5 - 5
state/chat_test.go

@@ -9,18 +9,18 @@ import (
 )
 
 func TestChatRoom_TLVList(t *testing.T) {
-	room := NewChatRoom()
-	room.Name = "chat-room-name"
+	room := NewChatRoom("chat-room-name", NewIdentScreenName(""), PublicExchange)
 
 	have := room.TLVList()
 	want := []wire.TLV{
 		wire.NewTLV(wire.ChatRoomTLVFlags, uint16(15)),
-		wire.NewTLV(wire.ChatRoomTLVCreateTime, uint32(room.CreateTime.Unix())),
+		wire.NewTLV(wire.ChatRoomTLVCreateTime, uint32(room.createTime.Unix())),
 		wire.NewTLV(wire.ChatRoomTLVMaxMsgLen, uint16(1024)),
 		wire.NewTLV(wire.ChatRoomTLVMaxOccupancy, uint16(100)),
 		wire.NewTLV(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
-		wire.NewTLV(wire.ChatRoomTLVFullyQualifiedName, room.Name),
-		wire.NewTLV(wire.ChatRoomTLVRoomName, room.Name),
+		wire.NewTLV(wire.ChatRoomTLVFullyQualifiedName, room.name),
+		wire.NewTLV(wire.ChatRoomTLVRoomName, room.name),
+		wire.NewTLV(wire.ChatRoomTLVMaxMsgVisLen, uint16(1024)),
 	}
 
 	assert.Equal(t, want, have)

+ 1 - 0
state/migrations/0006_chat_cookie.down.sql

@@ -0,0 +1 @@
+-- nothing to do here

+ 2 - 0
state/migrations/0006_chat_cookie.up.sql

@@ -0,0 +1,2 @@
+UPDATE chatRoom
+SET cookie = exchange || '-' || '0' || '-' || name;

+ 23 - 24
state/user_store.go

@@ -479,9 +479,7 @@ func (f SQLiteUserStore) BARTRetrieve(hash []byte) ([]byte, error) {
 // ChatRoomByCookie looks up a chat room by cookie. Returns
 // ErrChatRoomNotFound if the room does not exist for cookie.
 func (f SQLiteUserStore) ChatRoomByCookie(cookie string) (ChatRoom, error) {
-	chatRoom := ChatRoom{
-		Cookie: cookie,
-	}
+	chatRoom := ChatRoom{}
 
 	q := `
 		SELECT exchange, name, created, creator
@@ -490,15 +488,15 @@ func (f SQLiteUserStore) ChatRoomByCookie(cookie string) (ChatRoom, error) {
 	`
 	var creator string
 	err := f.db.QueryRow(q, cookie).Scan(
-		&chatRoom.Exchange,
-		&chatRoom.Name,
-		&chatRoom.CreateTime,
+		&chatRoom.exchange,
+		&chatRoom.name,
+		&chatRoom.createTime,
 		&creator,
 	)
 	if errors.Is(err, sql.ErrNoRows) {
 		err = ErrChatRoomNotFound
 	}
-	chatRoom.Creator = NewIdentScreenName(creator)
+	chatRoom.creator = NewIdentScreenName(creator)
 
 	return chatRoom, err
 }
@@ -507,42 +505,43 @@ func (f SQLiteUserStore) ChatRoomByCookie(cookie string) (ChatRoom, error) {
 // ErrChatRoomNotFound if the room does not exist for exchange and name.
 func (f SQLiteUserStore) ChatRoomByName(exchange uint16, name string) (ChatRoom, error) {
 	chatRoom := ChatRoom{
-		Exchange: exchange,
-		Name:     name,
+		exchange: exchange,
+		name:     name,
 	}
 
 	q := `
-		SELECT cookie, created, creator
+		SELECT created, creator
 		FROM chatRoom
 		WHERE exchange = ? AND name = ?
 	`
 	var creator string
 	err := f.db.QueryRow(q, exchange, name).Scan(
-		&chatRoom.Cookie,
-		&chatRoom.CreateTime,
+		&chatRoom.createTime,
 		&creator,
 	)
 	if errors.Is(err, sql.ErrNoRows) {
 		err = ErrChatRoomNotFound
 	}
-	chatRoom.Creator = NewIdentScreenName(creator)
+	chatRoom.creator = NewIdentScreenName(creator)
 
 	return chatRoom, err
 }
 
-// CreateChatRoom creates a new chat room.
-func (f SQLiteUserStore) CreateChatRoom(chatRoom ChatRoom) error {
+// CreateChatRoom creates a new chat room. It sets createTime on chatRoom to
+// the current timestamp.
+func (f SQLiteUserStore) CreateChatRoom(chatRoom *ChatRoom) error {
+	chatRoom.createTime = time.Now().UTC()
 	q := `
 		INSERT INTO chatRoom (cookie, exchange, name, created, creator)
 		VALUES (?, ?, ?, ?, ?)
 	`
 	_, err := f.db.Exec(
 		q,
-		chatRoom.Cookie,
-		chatRoom.Exchange,
-		chatRoom.Name,
-		chatRoom.CreateTime,
-		chatRoom.Creator.String(),
+		chatRoom.Cookie(),
+		chatRoom.Exchange(),
+		chatRoom.Name(),
+		chatRoom.createTime,
+		chatRoom.Creator().String(),
 	)
 
 	if err != nil {
@@ -558,7 +557,7 @@ func (f SQLiteUserStore) CreateChatRoom(chatRoom ChatRoom) error {
 
 func (f SQLiteUserStore) AllChatRooms(exchange uint16) ([]ChatRoom, error) {
 	q := `
-		SELECT cookie, created, creator, name
+		SELECT created, creator, name
 		FROM chatRoom
 		WHERE exchange = ?
 		ORDER BY created ASC
@@ -572,13 +571,13 @@ func (f SQLiteUserStore) AllChatRooms(exchange uint16) ([]ChatRoom, error) {
 	var users []ChatRoom
 	for rows.Next() {
 		cr := ChatRoom{
-			Exchange: exchange,
+			exchange: exchange,
 		}
 		var creator string
-		if err := rows.Scan(&cr.Cookie, &cr.CreateTime, &creator, &cr.Name); err != nil {
+		if err := rows.Scan(&cr.createTime, &creator, &cr.name); err != nil {
 			return nil, err
 		}
-		cr.Creator = NewIdentScreenName(creator)
+		cr.creator = NewIdentScreenName(creator)
 		users = append(users, cr)
 	}
 

+ 19 - 71
state/user_store_test.go

@@ -691,15 +691,12 @@ func TestSQLiteUserStore_ChatRoomByCookie_RoomFound(t *testing.T) {
 	userStore, err := NewSQLiteUserStore(testFile)
 	assert.NoError(t, err)
 
-	chatRoom := NewChatRoom()
-	chatRoom.Exchange = 4
-	chatRoom.Name = "my new chat room!"
-	chatRoom.Creator = NewIdentScreenName("the-screen-name")
+	chatRoom := NewChatRoom("my new chat room!", NewIdentScreenName("the-screen-name"), PrivateExchange)
 
-	err = userStore.CreateChatRoom(chatRoom)
+	err = userStore.CreateChatRoom(&chatRoom)
 	assert.NoError(t, err)
 
-	gotRoom, err := userStore.ChatRoomByCookie(chatRoom.Cookie)
+	gotRoom, err := userStore.ChatRoomByCookie(chatRoom.Cookie())
 	assert.NoError(t, err)
 	assert.Equal(t, chatRoom, gotRoom)
 }
@@ -724,15 +721,12 @@ func TestSQLiteUserStore_ChatRoomByName_RoomFound(t *testing.T) {
 	userStore, err := NewSQLiteUserStore(testFile)
 	assert.NoError(t, err)
 
-	chatRoom := NewChatRoom()
-	chatRoom.Exchange = 4
-	chatRoom.Name = "my new chat room!"
-	chatRoom.Creator = NewIdentScreenName("the-screen-name")
+	chatRoom := NewChatRoom("my new chat room!", NewIdentScreenName("the-screen-name"), PrivateExchange)
 
-	err = userStore.CreateChatRoom(chatRoom)
+	err = userStore.CreateChatRoom(&chatRoom)
 	assert.NoError(t, err)
 
-	gotRoom, err := userStore.ChatRoomByName(chatRoom.Exchange, chatRoom.Name)
+	gotRoom, err := userStore.ChatRoomByName(chatRoom.Exchange(), chatRoom.Name())
 	assert.NoError(t, err)
 	assert.Equal(t, chatRoom, gotRoom)
 }
@@ -758,26 +752,16 @@ func TestSQLiteUserStore_AllChatRooms(t *testing.T) {
 	assert.NoError(t, err)
 
 	chatRooms := []ChatRoom{
-		{
-			Cookie:   "chat-room-1",
-			Exchange: 4,
-			Name:     "chat room 1",
-		},
-		{
-			Cookie:   "chat-room-2",
-			Exchange: 4,
-			Name:     "chat room 2",
-		},
-		{
-			Cookie:   "chat-room-3",
-			Exchange: 5,
-			Name:     "chat room 3",
-		},
+		NewChatRoom("chat room 1", NewIdentScreenName("creator"), PrivateExchange),
+		NewChatRoom("chat room 2", NewIdentScreenName("creator"), PrivateExchange),
+		NewChatRoom("chat room 3", NewIdentScreenName("creator"), PublicExchange),
 	}
 
-	for _, room := range chatRooms {
-		err = userStore.CreateChatRoom(room)
+	for i := range chatRooms {
+		tBefore := (&chatRooms[i]).CreateTime()
+		err = userStore.CreateChatRoom(&chatRooms[i])
 		assert.NoError(t, err)
+		assert.True(t, chatRooms[i].CreateTime().After(tBefore))
 	}
 
 	// public exchange
@@ -802,46 +786,10 @@ func TestSQLiteUserStore_CreateChatRoom_ErrChatRoomExists(t *testing.T) {
 		wantErr      error
 	}{
 		{
-			name: "create two rooms with same exchange/name, different cookie",
-			firstInsert: ChatRoom{
-				Cookie:   "chat-room-1",
-				Exchange: 4,
-				Name:     "chat room 1",
-			},
-			secondInsert: ChatRoom{
-				Cookie:   "chat-room-1-new",
-				Exchange: 4,
-				Name:     "chat room 1",
-			},
-			wantErr: ErrDupChatRoom,
-		},
-		{
-			name: "create two rooms with different exchange/name, same cookie",
-			firstInsert: ChatRoom{
-				Cookie:   "chat-room",
-				Exchange: 5,
-				Name:     "chat room 1",
-			},
-			secondInsert: ChatRoom{
-				Cookie:   "chat-room",
-				Exchange: 4,
-				Name:     "chat room 2",
-			},
-			wantErr: ErrDupChatRoom,
-		},
-		{
-			name: "create two rooms with different cookie/exchange, same name",
-			firstInsert: ChatRoom{
-				Cookie:   "chat-room-1",
-				Exchange: 5,
-				Name:     "chat room",
-			},
-			secondInsert: ChatRoom{
-				Cookie:   "chat-room-2",
-				Exchange: 4,
-				Name:     "chat room",
-			},
-			wantErr: nil,
+			name:         "create two rooms with different cookie/exchange, same name",
+			firstInsert:  NewChatRoom("chat room", NewIdentScreenName("creator"), PublicExchange),
+			secondInsert: NewChatRoom("chat room", NewIdentScreenName("creator"), PrivateExchange),
+			wantErr:      nil,
 		},
 	}
 
@@ -854,10 +802,10 @@ func TestSQLiteUserStore_CreateChatRoom_ErrChatRoomExists(t *testing.T) {
 			userStore, err := NewSQLiteUserStore(testFile)
 			assert.NoError(t, err)
 
-			err = userStore.CreateChatRoom(tc.firstInsert)
+			err = userStore.CreateChatRoom(&tc.firstInsert)
 			assert.NoError(t, err)
 
-			err = userStore.CreateChatRoom(tc.secondInsert)
+			err = userStore.CreateChatRoom(&tc.secondInsert)
 			assert.ErrorIs(t, err, tc.wantErr)
 		})
 	}

+ 1 - 0
wire/snacs.go

@@ -890,6 +890,7 @@ const (
 	ChatRoomTLVLang1              uint16 = 0xD7
 	ChatRoomTLVCharSet2           uint16 = 0xD8
 	ChatRoomTLVLang2              uint16 = 0xD9
+	ChatRoomTLVMaxMsgVisLen       uint16 = 0xDA
 )
 
 type SNAC_0x0E_0x02_ChatRoomInfoUpdate struct {