Skip to content

Commit

Permalink
implement chat room creation in AIM 4.x
Browse files Browse the repository at this point in the history
Chat room creation in AIM 4.x differs from AIM 5.x in that AIM 4.x
always makes a separate connection for the ChatNav food group, whereas
AIM 5.x uses the ChatNav food group provided by the BOS service.

To make chat room creation work, this commit adds a ChatNav server that
listens on a dedicated TCP socket exclusively for ChatNav food group
requests and adds logic to OService ServiceRequest that provides
service discovery info for the new service.
  • Loading branch information
mk6i committed Apr 15, 2024
1 parent b4590ad commit e3aa022
Show file tree
Hide file tree
Showing 18 changed files with 894 additions and 102 deletions.
3 changes: 3 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ packages:
OServiceChatService:
config:
filename: "mock_oservice_chat_test.go"
OServiceChatNavService:
config:
filename: "mock_oservice_chat_nav_test.go"
LocateService:
config:
filename: "mock_locate_test.go"
Expand Down
20 changes: 20 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,26 @@ func main() {
}.Start()
wg.Done()
}(logger)
go func(logger *slog.Logger) {
logger = logger.With("svc", "CHAT_NAV")
authService := foodgroup.NewAuthService(cfg, sessionManager, sessionManager, feedbagStore, feedbagStore, chatRegistry)
oServiceService := foodgroup.NewOServiceService(cfg, sessionManager, feedbagStore)
oServiceServiceForChatNav := foodgroup.NewOServiceServiceForChatNav(*oServiceService, chatRegistry)
newChatSessMgr := func() foodgroup.SessionManager { return state.NewInMemorySessionManager(logger) }
chatNavService := foodgroup.NewChatNavService(logger, chatRegistry, state.NewChatRoom, newChatSessMgr)

oscar.ChatNavServer{
AuthService: authService,
Config: cfg,
Handler: handler.NewChatNavRouter(handler.Handlers{
ChatNavHandler: handler.NewChatNavHandler(chatNavService, logger),
OServiceChatNavHandler: handler.NewOServiceHandlerForChatNav(logger, oServiceService, oServiceServiceForChatNav),
}),
Logger: logger,
OnlineNotifier: oServiceServiceForChatNav,
}.Start()
wg.Done()
}(logger)
go func(logger *slog.Logger) {
logger = logger.With("svc", "AUTH")
authHandler := foodgroup.NewAuthService(cfg, sessionManager, nil, feedbagStore, feedbagStore, chatRegistry)
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "fmt"
type Config struct {
BOSPort int `envconfig:"BOS_PORT" default:"5191" description:"The port that the BOS service binds to."`
BUCPPort int `envconfig:"BUCP_PORT" default:"5190" description:"The port that the auth service binds to."`
ChatNavPort int `envconfig:"CHAT_NAV_PORT" default:"5193" description:"The port that the chat nav service binds to."`
ChatPort int `envconfig:"CHAT_PORT" default:"5192" description:"The port that the chat service binds to."`
DBPath string `envconfig:"DB_PATH" default:"oscar.sqlite" description:"The path to the SQLite database file. The file and DB schema are auto-created if they doesn't exist."`
DisableAuth bool `envconfig:"DISABLE_AUTH" default:"true" description:"Disable password check and auto-create new users at login time. Useful for quickly creating new accounts during development without having to register new users via the management API."`
Expand Down
3 changes: 3 additions & 0 deletions config/settings.bat
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ set BOS_PORT=5191
rem The port that the auth service binds to.
set BUCP_PORT=5190

rem The port that the chat nav service binds to.
set CHAT_NAV_PORT=5193

rem The port that the chat service binds to.
set CHAT_PORT=5192

Expand Down
5 changes: 4 additions & 1 deletion config/settings.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ export BOS_PORT=5191
# The port that the auth service binds to.
export BUCP_PORT=5190

# The port that the chat nav service binds to.
export CHAT_NAV_PORT=5193

# The port that the chat service binds to.
export CHAT_PORT=5192

Expand All @@ -22,7 +25,7 @@ export FAIL_FAST=false

# Set logging granularity. Possible values: 'trace', 'debug', 'info', 'warn',
# 'error'.
export LOG_LEVEL=info
export LOG_LEVEL=debug

# The hostname that AIM clients connect to in order to reach OSCAR services
# (BOS, BUCP, chat, etc). Make sure the hostname is reachable by all clients.
Expand Down
23 changes: 12 additions & 11 deletions foodgroup/chat_nav.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,15 @@ func (s ChatNavService) RequestChatRights(_ context.Context, inFrame wire.SNACFr
Identifier: 4,
TLVBlock: wire.TLVBlock{
TLVList: wire.TLVList{
wire.NewTLV(wire.ChatNavTLVClassPerms, uint16(0x0010)),
wire.NewTLV(wire.ChatNavTLVFlags, uint16(15)),
wire.NewTLV(wire.ChatNavTLVRoomName, "default exchange"),
wire.NewTLV(wire.ChatNavTLVCreatePerms, uint8(2)),
wire.NewTLV(wire.ChatNavTLVCharSet1, "us-ascii"),
wire.NewTLV(wire.ChatNavTLVLang1, "en"),
wire.NewTLV(wire.ChatNavTLVCharSet2, "us-ascii"),
wire.NewTLV(wire.ChatNavTLVLang2, "en"),
wire.NewTLV(wire.ChatRoomTLVClassPerms, uint16(0x0010)),
wire.NewTLV(wire.ChatRoomTLVMaxNameLen, uint16(100)),
wire.NewTLV(wire.ChatRoomTLVFlags, uint16(15)),
wire.NewTLV(wire.ChatRoomTLVRoomName, "default exchange"),
wire.NewTLV(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
wire.NewTLV(wire.ChatRoomTLVCharSet1, "us-ascii"),
wire.NewTLV(wire.ChatRoomTLVLang1, "en"),
wire.NewTLV(wire.ChatRoomTLVCharSet2, "us-ascii"),
wire.NewTLV(wire.ChatRoomTLVLang2, "en"),
},
},
}),
Expand All @@ -67,7 +68,7 @@ func (s ChatNavService) RequestChatRights(_ context.Context, inFrame wire.SNACFr
// participant. It returns SNAC wire.ChatNavNavInfo, which contains metadata
// for the chat room.
func (s ChatNavService) CreateRoom(_ context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (wire.SNACMessage, error) {
name, hasName := inBody.String(wire.ChatTLVRoomName)
name, hasName := inBody.String(wire.ChatRoomTLVRoomName)
if !hasName {
return wire.SNACMessage{}, errors.New("unable to find chat name")
}
Expand Down Expand Up @@ -95,7 +96,7 @@ func (s ChatNavService) CreateRoom(_ context.Context, sess *state.Session, inFra
Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
TLVRestBlock: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLV(wire.ChatNavRequestRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
wire.NewTLV(wire.ChatNavTLVRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
Exchange: inBody.Exchange,
Cookie: room.Cookie,
InstanceNumber: inBody.InstanceNumber,
Expand Down Expand Up @@ -127,7 +128,7 @@ func (s ChatNavService) RequestRoomInfo(_ context.Context, inFrame wire.SNACFram
Body: wire.SNAC_0x0D_0x09_ChatNavNavInfo{
TLVRestBlock: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLV(wire.ChatNavRequestRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
wire.NewTLV(wire.ChatNavTLVRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
Exchange: room.Exchange,
Cookie: room.Cookie,
InstanceNumber: room.InstanceNumber,
Expand Down
19 changes: 10 additions & 9 deletions foodgroup/chat_nav_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestChatNavService_CreateRoom(t *testing.T) {
DetailLevel: 3,
TLVBlock: wire.TLVBlock{
TLVList: wire.TLVList{
wire.NewTLV(wire.ChatTLVRoomName, "the-chat-room-name"),
wire.NewTLV(wire.ChatRoomTLVRoomName, "the-chat-room-name"),
},
},
}
Expand Down Expand Up @@ -178,14 +178,15 @@ func TestChatNavService_RequestChatRights(t *testing.T) {
Identifier: 4,
TLVBlock: wire.TLVBlock{
TLVList: wire.TLVList{
wire.NewTLV(wire.ChatNavTLVClassPerms, uint16(0x0010)),
wire.NewTLV(wire.ChatNavTLVFlags, uint16(15)),
wire.NewTLV(wire.ChatNavTLVRoomName, "default exchange"),
wire.NewTLV(wire.ChatNavTLVCreatePerms, uint8(2)),
wire.NewTLV(wire.ChatNavTLVCharSet1, "us-ascii"),
wire.NewTLV(wire.ChatNavTLVLang1, "en"),
wire.NewTLV(wire.ChatNavTLVCharSet2, "us-ascii"),
wire.NewTLV(wire.ChatNavTLVLang2, "en"),
wire.NewTLV(wire.ChatRoomTLVClassPerms, uint16(0x0010)),
wire.NewTLV(wire.ChatRoomTLVMaxNameLen, uint16(100)),
wire.NewTLV(wire.ChatRoomTLVFlags, uint16(15)),
wire.NewTLV(wire.ChatRoomTLVRoomName, "default exchange"),
wire.NewTLV(wire.ChatRoomTLVNavCreatePerms, uint8(2)),
wire.NewTLV(wire.ChatRoomTLVCharSet1, "us-ascii"),
wire.NewTLV(wire.ChatRoomTLVLang1, "en"),
wire.NewTLV(wire.ChatRoomTLVCharSet2, "us-ascii"),
wire.NewTLV(wire.ChatRoomTLVLang2, "en"),
},
},
}),
Expand Down
151 changes: 106 additions & 45 deletions foodgroup/oservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,62 +345,87 @@ type chatLoginCookie struct {
SessID string `len_prefix:"uint16"`
}

// ServiceRequest configures food group settings for the current user.
// This method only provides services for the Chat food group; return
// wire.ErrUnsupportedFoodGroup for any other food group. When the chat
// food group is specified in inFrame, add user to the chat room specified by
// TLV 0x01. It returns SNAC wire.OServiceServiceResponse containing
// metadata the client needs to connect to the chat service and join the chat
// room.
// ServiceRequest handles service discovery, providing a host name and metadata
// for connecting to the food group service specified in inFrame.
// Depending on the food group specified, the method behaves as follows:
// - ChatNav: Directs the user back to the current BOS server, which provides
// ChatNav services. AIM 4.8 requests ChatNav service info even though
// HostOnline reports it as available via BOS.
// - Chat: Directs the client to the chat server along with metadata for
// connecting to the chat room specified in inFrame.
// - Other Food Groups: Returns wire.ErrUnsupportedFoodGroup.
func (s OServiceServiceForBOS) ServiceRequest(_ context.Context, sess *state.Session, inFrame wire.SNACFrame, inBody wire.SNAC_0x01_0x04_OServiceServiceRequest) (wire.SNACMessage, error) {
if inBody.FoodGroup != wire.Chat {
err := fmt.Errorf("%w. food group: %s", wire.ErrUnsupportedFoodGroup, wire.FoodGroupName(inBody.FoodGroup))
return wire.SNACMessage{}, err
}

roomMeta, ok := inBody.Slice(0x01)
if !ok {
return wire.SNACMessage{}, errors.New("missing room info")
}
switch inBody.FoodGroup {
case wire.ChatNav:
return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.OService,
SubGroup: wire.OServiceServiceResponse,
RequestID: inFrame.RequestID,
},
Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
TLVRestBlock: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLV(wire.OServiceTLVTagsReconnectHere, config.Address(s.cfg.OSCARHost, s.cfg.ChatNavPort)),
wire.NewTLV(wire.OServiceTLVTagsLoginCookie, sess.ID()),
wire.NewTLV(wire.OServiceTLVTagsGroupID, wire.ChatNav),
wire.NewTLV(wire.OServiceTLVTagsSSLCertName, ""),
wire.NewTLV(wire.OServiceTLVTagsSSLState, uint8(0x00)),
},
},
},
}, nil
case wire.Chat:
roomMeta, ok := inBody.Slice(0x01)
if !ok {
return wire.SNACMessage{}, errors.New("missing room info")
}

roomSNAC := wire.SNAC_0x01_0x04_TLVRoomInfo{}
if err := wire.Unmarshal(&roomSNAC, bytes.NewBuffer(roomMeta)); err != nil {
return wire.SNACMessage{}, err
}
roomSNAC := wire.SNAC_0x01_0x04_TLVRoomInfo{}
if err := wire.Unmarshal(&roomSNAC, bytes.NewBuffer(roomMeta)); err != nil {
return wire.SNACMessage{}, err
}

room, chatSessMgr, err := s.chatRegistry.Retrieve(roomSNAC.Cookie)
if err != nil {
return wire.SNACMessage{}, err
}
chatSess := chatSessMgr.(SessionManager).AddSession(sess.ID(), sess.ScreenName())
chatSess.SetChatRoomCookie(room.Cookie)
room, chatSessMgr, err := s.chatRegistry.Retrieve(roomSNAC.Cookie)
if err != nil {
return wire.SNACMessage{}, err
}
chatSess := chatSessMgr.(SessionManager).AddSession(sess.ID(), sess.ScreenName())
chatSess.SetChatRoomCookie(room.Cookie)

return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.OService,
SubGroup: wire.OServiceServiceResponse,
RequestID: inFrame.RequestID,
},
Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
TLVRestBlock: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLV(wire.OServiceTLVTagsReconnectHere, config.Address(s.cfg.OSCARHost, s.cfg.ChatPort)),
wire.NewTLV(wire.OServiceTLVTagsLoginCookie, chatLoginCookie{
Cookie: room.Cookie,
SessID: sess.ID(),
}),
wire.NewTLV(wire.OServiceTLVTagsGroupID, wire.Chat),
wire.NewTLV(wire.OServiceTLVTagsSSLCertName, ""),
wire.NewTLV(wire.OServiceTLVTagsSSLState, uint8(0x00)),
return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.OService,
SubGroup: wire.OServiceServiceResponse,
RequestID: inFrame.RequestID,
},
Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
TLVRestBlock: wire.TLVRestBlock{
TLVList: wire.TLVList{
wire.NewTLV(wire.OServiceTLVTagsReconnectHere, config.Address(s.cfg.OSCARHost, s.cfg.ChatPort)),
wire.NewTLV(wire.OServiceTLVTagsLoginCookie, chatLoginCookie{
Cookie: room.Cookie,
SessID: sess.ID(),
}),
wire.NewTLV(wire.OServiceTLVTagsGroupID, wire.Chat),
wire.NewTLV(wire.OServiceTLVTagsSSLCertName, ""),
wire.NewTLV(wire.OServiceTLVTagsSSLState, uint8(0x00)),
},
},
},
},
}, nil
}, nil
default:
err := fmt.Errorf("%w. food group: %s", wire.ErrUnsupportedFoodGroup, wire.FoodGroupName(inBody.FoodGroup))
return wire.SNACMessage{}, err
}
}

// HostOnline initiates the BOS protocol sequence.
// It returns SNAC wire.OServiceHostOnline containing the list food groups
// supported by the BOS service.
// ChatNav is provided by BOS in addition to the standalone ChatNav service.
// AIM 4.x always creates a secondary TCP connection for ChatNav, whereas 5.x
// can use the existing BOS connection for ChatNav services.
func (s OServiceServiceForBOS) HostOnline() wire.SNACMessage {
return wire.SNACMessage{
Frame: wire.SNACFrame{
Expand Down Expand Up @@ -502,3 +527,39 @@ func (s OServiceServiceForChat) ClientOnline(ctx context.Context, sess *state.Se
setOnlineChatUsers(ctx, sess, chatSessMgr.(ChatMessageRelayer))
return nil
}

// NewOServiceServiceForChatNav creates a new instance of OServiceServiceForChat.
func NewOServiceServiceForChatNav(oserviceService OServiceService, chatRegistry *state.ChatRegistry) *OServiceServiceForChatNav {
return &OServiceServiceForChatNav{
OServiceService: oserviceService,
chatRegistry: chatRegistry,
}
}

// OServiceServiceForChatNav provides functionality for the OService food group
// running on the Chat server.
type OServiceServiceForChatNav struct {
OServiceService
chatRegistry *state.ChatRegistry
}

// HostOnline initiates the Chat protocol sequence.
// It returns SNAC wire.OServiceHostOnline containing the list of food groups
// supported by the Chat service.
// ChatNav is provided by BOS in addition to the standalone ChatNav service.
// AIM 4.x always creates a secondary TCP connection for ChatNav, whereas 5.x
// can use the existing BOS connection for ChatNav services.
func (s OServiceServiceForChatNav) HostOnline() wire.SNACMessage {
return wire.SNACMessage{
Frame: wire.SNACFrame{
FoodGroup: wire.OService,
SubGroup: wire.OServiceHostOnline,
},
Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
FoodGroups: []uint16{
wire.ChatNav,
wire.OService,
},
},
}
}
Loading

0 comments on commit e3aa022

Please sign in to comment.