Skip to content

Commit

Permalink
impl public chat exchange and persistent rooms
Browse files Browse the repository at this point in the history
This commit adds several improvements to chat functionality.

- Users can join public chat rooms on exchange 5 created by the server
  opator.
- Users can join existing chat rooms without invitations by opening
  a hyperlink. Example: aim:gochat?roomname=testroom&exchange=4
- Chat rooms are persistent across server restarts. AIM clients can
  automatically rejoin rooms after client disconnect/reconnect.
- Server operators can manage chat rooms via the management API.
  • Loading branch information
mk6i committed Jun 27, 2024
1 parent 2201e12 commit ce63818
Show file tree
Hide file tree
Showing 40 changed files with 2,373 additions and 1,099 deletions.
16 changes: 14 additions & 2 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ packages:
SessionRetriever:
config:
filename: "mock_session_retriever_test.go"
ChatRoomRetriever:
config:
filename: "mock_chat_room_retriever_test.go"
ChatRoomCreator:
config:
filename: "mock_chat_room_creator_test.go"
ChatSessionRetriever:
config:
filename: "mock_chat_session_retriever_test.go"
github.com/mk6i/retro-aim-server/server/oscar/handler:
interfaces:
ResponseWriter:
Expand Down Expand Up @@ -68,6 +77,9 @@ packages:
ProfileManager:
config:
filename: "mock_profile_manager_test.go"
ChatSessionRegistry:
config:
filename: "mock_chat_session_registry_test.go"
SessionManager:
config:
filename: "mock_session_manager_test.go"
Expand All @@ -77,9 +89,9 @@ packages:
ChatMessageRelayer:
config:
filename: "mock_chat_message_relayer_test.go"
ChatRegistry:
ChatRoomRegistry:
config:
filename: "mock_chat_registry_test.go"
filename: "mock_chat_room_registry_test.go"
BARTManager:
config:
filename: "mock_bart_manager_test.go"
Expand Down
94 changes: 93 additions & 1 deletion api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,96 @@ paths:
'400':
description: Bad request. Invalid input data.
'404':
description: User not found.
description: User not found.

/chat/room/public:
get:
summary: List all public chat rooms
description: Retrieve a list of all public chat rooms in exchange 5.
responses:
'200':
description: Successful response containing a list of chat rooms.
content:
application/json:
schema:
type: array
items:
type: object
properties:
name:
type: string
description: Name of the chat room.
create_time:
type: string
format: date-time
description: The timestamp when the chat room was created.
participants:
type: array
description: List of participants in the chat room.
items:
type: object
properties:
id:
type: string
description: Unique identifier of the participant.
screen_name:
type: string
description: Screen name of the participant.

post:
summary: Create a new public chat room
description: Create a new public chat room in exchange 5.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: Name of the chat room.
responses:
'201':
description: Chat room created successfully.
'400':
description: Bad request. Invalid input data.
'409':
description: Chat room already exists.

/chat/room/private:
get:
summary: List all private chat rooms
description: Retrieve a list of all private chat rooms in exchange 4.
responses:
'200':
description: Successful response containing a list of chat rooms.
content:
application/json:
schema:
type: array
items:
type: object
properties:
name:
type: string
description: Name of the chat room.
create_time:
type: string
format: date-time
description: The timestamp when the chat room was created.
creator_id:
type: string
description: The chat room creator user ID.
participants:
type: array
description: List of participants in the chat room.
items:
type: object
properties:
id:
type: string
description: Unique identifier of the participant.
screen_name:
type: string
description: Screen name of the participant.
28 changes: 13 additions & 15 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,28 +40,27 @@ func main() {

logger := middleware.NewLogger(cfg)
sessionManager := state.NewInMemorySessionManager(logger)
chatRegistry := state.NewChatRegistry()
chatSessionManager := state.NewInMemoryChatSessionManager(logger)
adjListBuddyListStore := state.NewAdjListBuddyListStore()

wg := sync.WaitGroup{}
wg.Add(7)

go func() {
http.StartManagementAPI(cfg, feedbagStore, sessionManager, logger)
http.StartManagementAPI(cfg, feedbagStore, sessionManager, feedbagStore, feedbagStore, chatSessionManager, logger)
wg.Done()
}()
go func(logger *slog.Logger) {
logger = logger.With("svc", "BOS")
authService := foodgroup.NewAuthService(cfg, sessionManager, feedbagStore, chatRegistry, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore)
authService := foodgroup.NewAuthService(cfg, sessionManager, chatSessionManager, feedbagStore, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore, chatSessionManager)
bartService := foodgroup.NewBARTService(logger, feedbagStore, sessionManager, feedbagStore, adjListBuddyListStore)
buddyService := foodgroup.NewBuddyService(sessionManager, feedbagStore, adjListBuddyListStore)
newChatSessMgr := func() foodgroup.SessionManager { return state.NewInMemorySessionManager(logger) }
chatNavService := foodgroup.NewChatNavService(logger, chatRegistry, state.NewChatRoom, newChatSessMgr)
chatNavService := foodgroup.NewChatNavService(logger, feedbagStore, state.NewChatRoom)
feedbagService := foodgroup.NewFeedbagService(logger, sessionManager, feedbagStore, feedbagStore, adjListBuddyListStore)
foodgroupService := foodgroup.NewPermitDenyService()
icbmService := foodgroup.NewICBMService(sessionManager, feedbagStore, adjListBuddyListStore)
locateService := foodgroup.NewLocateService(sessionManager, feedbagStore, feedbagStore, adjListBuddyListStore)
oServiceService := foodgroup.NewOServiceServiceForBOS(cfg, sessionManager, adjListBuddyListStore, logger, cookieBaker, chatRegistry, feedbagStore)
oServiceService := foodgroup.NewOServiceServiceForBOS(cfg, sessionManager, adjListBuddyListStore, logger, cookieBaker, feedbagStore, feedbagStore)

oscar.BOSServer{
AuthService: authService,
Expand All @@ -86,9 +85,9 @@ func main() {
go func(logger *slog.Logger) {
logger = logger.With("svc", "CHAT")
sessionManager := state.NewInMemorySessionManager(logger)
authService := foodgroup.NewAuthService(cfg, sessionManager, feedbagStore, chatRegistry, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore)
chatService := foodgroup.NewChatService(chatRegistry)
oServiceService := foodgroup.NewOServiceServiceForChat(cfg, logger, chatRegistry, sessionManager, adjListBuddyListStore, feedbagStore)
authService := foodgroup.NewAuthService(cfg, sessionManager, chatSessionManager, feedbagStore, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore, chatSessionManager)
chatService := foodgroup.NewChatService(chatSessionManager)
oServiceService := foodgroup.NewOServiceServiceForChat(cfg, logger, sessionManager, adjListBuddyListStore, feedbagStore, feedbagStore, chatSessionManager)

oscar.ChatServer{
AuthService: authService,
Expand All @@ -105,9 +104,8 @@ func main() {
go func(logger *slog.Logger) {
logger = logger.With("svc", "CHAT_NAV")
sessionManager := state.NewInMemorySessionManager(logger)
authService := foodgroup.NewAuthService(cfg, sessionManager, feedbagStore, chatRegistry, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore)
newChatSessMgr := func() foodgroup.SessionManager { return state.NewInMemorySessionManager(logger) }
chatNavService := foodgroup.NewChatNavService(logger, chatRegistry, state.NewChatRoom, newChatSessMgr)
authService := foodgroup.NewAuthService(cfg, sessionManager, chatSessionManager, feedbagStore, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore, chatSessionManager)
chatNavService := foodgroup.NewChatNavService(logger, feedbagStore, state.NewChatRoom)
oServiceService := foodgroup.NewOServiceServiceForChatNav(cfg, logger, sessionManager, adjListBuddyListStore, feedbagStore)

oscar.BOSServer{
Expand All @@ -126,7 +124,7 @@ func main() {
go func(logger *slog.Logger) {
logger = logger.With("svc", "ALERT")
sessionManager := state.NewInMemorySessionManager(logger)
authService := foodgroup.NewAuthService(cfg, sessionManager, feedbagStore, chatRegistry, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore)
authService := foodgroup.NewAuthService(cfg, sessionManager, chatSessionManager, feedbagStore, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore, chatSessionManager)
oServiceService := foodgroup.NewOServiceServiceForAlert(cfg, logger, sessionManager, adjListBuddyListStore, feedbagStore)

oscar.BOSServer{
Expand All @@ -146,7 +144,7 @@ func main() {
logger = logger.With("svc", "BART")
sessionManager := state.NewInMemorySessionManager(logger)
bartService := foodgroup.NewBARTService(logger, feedbagStore, sessionManager, feedbagStore, adjListBuddyListStore)
authService := foodgroup.NewAuthService(cfg, sessionManager, feedbagStore, chatRegistry, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore)
authService := foodgroup.NewAuthService(cfg, sessionManager, chatSessionManager, feedbagStore, adjListBuddyListStore, cookieBaker, sessionManager, feedbagStore, chatSessionManager)
oServiceService := foodgroup.NewOServiceServiceForBART(cfg, logger, sessionManager, adjListBuddyListStore, feedbagStore)

oscar.BOSServer{
Expand All @@ -164,7 +162,7 @@ func main() {
}(logger)
go func(logger *slog.Logger) {
logger = logger.With("svc", "AUTH")
authHandler := foodgroup.NewAuthService(cfg, sessionManager, feedbagStore, chatRegistry, adjListBuddyListStore, cookieBaker, nil, nil)
authHandler := foodgroup.NewAuthService(cfg, sessionManager, chatSessionManager, feedbagStore, adjListBuddyListStore, cookieBaker, nil, nil, chatSessionManager)

oscar.AuthServer{
AuthService: authHandler,
Expand Down
52 changes: 19 additions & 33 deletions foodgroup/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,23 @@ import (
func NewAuthService(
cfg config.Config,
sessionManager SessionManager,
chatSessionRegistry ChatSessionRegistry,
userManager UserManager,
chatRegistry ChatRegistry,
legacyBuddyListManager LegacyBuddyListManager,
cookieBaker CookieBaker,
messageRelayer MessageRelayer,
feedbagManager FeedbagManager,
chatMessageRelayer ChatMessageRelayer,
) *AuthService {
return &AuthService{
buddyUpdateBroadcaster: NewBuddyService(messageRelayer, feedbagManager, legacyBuddyListManager),
chatRegistry: chatRegistry,
chatSessionRegistry: chatSessionRegistry,
config: cfg,
cookieBaker: cookieBaker,
legacyBuddyListManager: legacyBuddyListManager,
sessionManager: sessionManager,
userManager: userManager,
chatMessageRelayer: chatMessageRelayer,
}
}

Expand All @@ -41,15 +43,23 @@ func NewAuthService(
// modes.
type AuthService struct {
buddyUpdateBroadcaster buddyBroadcaster
chatRegistry ChatRegistry
chatMessageRelayer ChatMessageRelayer
chatSessionRegistry ChatSessionRegistry
config config.Config
cookieBaker CookieBaker
legacyBuddyListManager LegacyBuddyListManager
sessionManager SessionManager
userManager UserManager
chatRoomManager ChatRoomRegistry
}

// RegisterChatSession creates and returns a chat room session.
// RegisterChatSession adds a user to a chat room. The authCookie param is an
// opaque token returned by {{OServiceService.ServiceRequest}} that identifies
// the user and chat room. It returns the session object registered in the
// ChatSessionRegistry.
// This method does not verify that the user and chat room exist because it
// implicitly trusts the contents of the token signed by
// {{OServiceService.ServiceRequest}}.
func (s AuthService) RegisterChatSession(authCookie []byte) (*state.Session, error) {
token, err := s.cookieBaker.Crack(authCookie)
if err != nil {
Expand All @@ -59,23 +69,7 @@ func (s AuthService) RegisterChatSession(authCookie []byte) (*state.Session, err
if err := wire.Unmarshal(&c, bytes.NewBuffer(token)); err != nil {
return nil, err
}

room, chatSessMgr, err := s.chatRegistry.Retrieve(c.ChatCookie)
if err != nil {
return nil, err
}
u, err := s.userManager.User(state.NewIdentScreenName(c.ScreenName))
if err != nil {
return nil, fmt.Errorf("failed to retrieve user: %w", err)
}
if u == nil {
return nil, fmt.Errorf("user not found")
}

chatSess := chatSessMgr.(SessionManager).AddSession(u.DisplayScreenName)
chatSess.SetChatRoomCookie(room.Cookie)

return chatSess, nil
return s.chatSessionRegistry.AddSession(c.ChatCookie, c.ScreenName), nil
}

// RegisterBOSSession creates and returns a user's session.
Expand Down Expand Up @@ -108,18 +102,10 @@ func (s AuthService) Signout(ctx context.Context, sess *state.Session) error {
}

// SignoutChat removes user from chat room and notifies remaining participants
// of their departure. If user is the last to leave, the chat room is deleted.
func (s AuthService) SignoutChat(ctx context.Context, sess *state.Session) error {
chatRoom, chatSessMgr, err := s.chatRegistry.Retrieve(sess.ChatRoomCookie())
if err != nil {
return err
}
alertUserLeft(ctx, sess, chatSessMgr.(ChatMessageRelayer))
chatSessMgr.(SessionManager).RemoveSession(sess)
if chatSessMgr.(SessionManager).Empty() {
s.chatRegistry.Remove(chatRoom.Cookie)
}
return nil
// of their departure.
func (s AuthService) SignoutChat(ctx context.Context, sess *state.Session) {
alertUserLeft(ctx, sess, s.chatMessageRelayer)
s.chatSessionRegistry.RemoveSession(sess)
}

// BUCPChallenge processes a BUCP authentication challenge request. It
Expand Down
Loading

0 comments on commit ce63818

Please sign in to comment.