session_manager.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package state
  2. import (
  3. "context"
  4. "log/slog"
  5. "sync"
  6. "github.com/mk6i/retro-aim-server/wire"
  7. )
  8. // InMemorySessionManager handles the lifecycle of a user session and provides
  9. // synchronized message relay between sessions in the session pool. An
  10. // InMemorySessionManager is safe for concurrent use by multiple goroutines.
  11. type InMemorySessionManager struct {
  12. store map[IdentScreenName]*Session
  13. mapMutex sync.RWMutex
  14. logger *slog.Logger
  15. }
  16. // NewInMemorySessionManager creates a new instance of InMemorySessionManager.
  17. func NewInMemorySessionManager(logger *slog.Logger) *InMemorySessionManager {
  18. return &InMemorySessionManager{
  19. logger: logger,
  20. store: make(map[IdentScreenName]*Session),
  21. }
  22. }
  23. // RelayToAll relays a message to all sessions in the session pool.
  24. func (s *InMemorySessionManager) RelayToAll(ctx context.Context, msg wire.SNACMessage) {
  25. s.mapMutex.RLock()
  26. defer s.mapMutex.RUnlock()
  27. for _, sess := range s.store {
  28. s.maybeRelayMessage(ctx, msg, sess)
  29. }
  30. }
  31. // RelayToScreenName relays a message to a session with a matching screen name.
  32. func (s *InMemorySessionManager) RelayToScreenName(ctx context.Context, screenName IdentScreenName, msg wire.SNACMessage) {
  33. sess := s.RetrieveByScreenName(screenName)
  34. if sess == nil {
  35. s.logger.WarnContext(ctx, "can't send notification because user is not online", "recipient", screenName, "message", msg)
  36. return
  37. }
  38. s.maybeRelayMessage(ctx, msg, sess)
  39. }
  40. // RelayToScreenNames relays a message to sessions with matching screenNames.
  41. func (s *InMemorySessionManager) RelayToScreenNames(ctx context.Context, screenNames []IdentScreenName, msg wire.SNACMessage) {
  42. for _, sess := range s.retrieveByScreenNames(screenNames) {
  43. s.maybeRelayMessage(ctx, msg, sess)
  44. }
  45. }
  46. func (s *InMemorySessionManager) maybeRelayMessage(ctx context.Context, msg wire.SNACMessage, sess *Session) {
  47. switch sess.RelayMessage(msg) {
  48. case SessSendClosed:
  49. s.logger.WarnContext(ctx, "can't send notification because the user's session is closed", "recipient", sess.IdentScreenName(), "message", msg)
  50. case SessQueueFull:
  51. s.logger.WarnContext(ctx, "can't send notification because queue is full", "recipient", sess.IdentScreenName(), "message", msg)
  52. sess.Close()
  53. }
  54. }
  55. // AddSession adds a new session to the pool. It replaces an existing session
  56. // with a matching screen name, ensuring that each screen name is unique in the
  57. // pool. This method does not return a nil session value. It's possible to add
  58. // a non-existent user to the session. Callers should ensure that the account
  59. // represented by displayScreenName is valid.
  60. func (s *InMemorySessionManager) AddSession(displayScreenName DisplayScreenName) *Session {
  61. s.mapMutex.Lock()
  62. defer s.mapMutex.Unlock()
  63. identScreenName := NewIdentScreenName(string(displayScreenName))
  64. // Only allow one session at a time per screen name. A session may already
  65. // exist because:
  66. // 1) the user is signing on using an already logged-on screen name.
  67. // 2) the session might be orphaned due to an undetected client
  68. // disconnection.
  69. for _, sess := range s.store {
  70. if identScreenName == sess.IdentScreenName() {
  71. sess.Close()
  72. delete(s.store, identScreenName)
  73. break
  74. }
  75. }
  76. sess := NewSession()
  77. sess.SetIdentScreenName(identScreenName)
  78. sess.SetDisplayScreenName(displayScreenName)
  79. s.store[identScreenName] = sess
  80. return sess
  81. }
  82. // RemoveSession takes a session out of the session pool.
  83. func (s *InMemorySessionManager) RemoveSession(sess *Session) {
  84. s.mapMutex.Lock()
  85. defer s.mapMutex.Unlock()
  86. delete(s.store, sess.IdentScreenName())
  87. }
  88. // RetrieveSession finds a session with a matching sessionID. Returns nil if
  89. // session is not found.
  90. func (s *InMemorySessionManager) RetrieveSession(screenName IdentScreenName) *Session {
  91. s.mapMutex.RLock()
  92. defer s.mapMutex.RUnlock()
  93. return s.store[screenName]
  94. }
  95. // RetrieveByScreenName find a session with a matching screen name. Returns nil
  96. // if session is not found.
  97. func (s *InMemorySessionManager) RetrieveByScreenName(screenName IdentScreenName) *Session {
  98. s.mapMutex.RLock()
  99. defer s.mapMutex.RUnlock()
  100. for _, sess := range s.store {
  101. if screenName == sess.IdentScreenName() {
  102. return sess
  103. }
  104. }
  105. return nil
  106. }
  107. func (s *InMemorySessionManager) retrieveByScreenNames(screenNames []IdentScreenName) []*Session {
  108. s.mapMutex.RLock()
  109. defer s.mapMutex.RUnlock()
  110. var ret []*Session
  111. for _, sn := range screenNames {
  112. for _, sess := range s.store {
  113. if sn == sess.IdentScreenName() {
  114. ret = append(ret, sess)
  115. }
  116. }
  117. }
  118. return ret
  119. }
  120. // Empty returns true if the session pool contains 0 sessions.
  121. func (s *InMemorySessionManager) Empty() bool {
  122. s.mapMutex.RLock()
  123. defer s.mapMutex.RUnlock()
  124. return len(s.store) == 0
  125. }
  126. // AllSessions returns all sessions in the session pool.
  127. func (s *InMemorySessionManager) AllSessions() []*Session {
  128. s.mapMutex.RLock()
  129. defer s.mapMutex.RUnlock()
  130. var sessions []*Session
  131. for _, sess := range s.store {
  132. sessions = append(sessions, sess)
  133. }
  134. return sessions
  135. }
  136. // NewInMemoryChatSessionManager creates a new instance of
  137. // InMemoryChatSessionManager.
  138. func NewInMemoryChatSessionManager(logger *slog.Logger) *InMemoryChatSessionManager {
  139. return &InMemoryChatSessionManager{
  140. store: make(map[string]*InMemorySessionManager),
  141. logger: logger,
  142. }
  143. }
  144. // InMemoryChatSessionManager manages chat sessions for multiple chat rooms
  145. // stored in memory. It provides thread-safe operations to add, remove, and
  146. // manipulate sessions as well as relay messages to participants.
  147. type InMemoryChatSessionManager struct {
  148. logger *slog.Logger
  149. mapMutex sync.RWMutex
  150. store map[string]*InMemorySessionManager
  151. }
  152. // AddSession adds a user to a chat room.
  153. func (s *InMemoryChatSessionManager) AddSession(chatCookie string, screenName DisplayScreenName) *Session {
  154. s.mapMutex.Lock()
  155. defer s.mapMutex.Unlock()
  156. if _, ok := s.store[chatCookie]; !ok {
  157. s.store[chatCookie] = NewInMemorySessionManager(s.logger)
  158. }
  159. sessionManager := s.store[chatCookie]
  160. sess := sessionManager.AddSession(screenName)
  161. sess.SetChatRoomCookie(chatCookie)
  162. return sess
  163. }
  164. // RemoveSession removes a user session from a chat room.
  165. func (s *InMemoryChatSessionManager) RemoveSession(sess *Session) {
  166. s.mapMutex.Lock()
  167. defer s.mapMutex.Unlock()
  168. sessionManager, ok := s.store[sess.ChatRoomCookie()]
  169. if !ok {
  170. panic("attempting to remove a session after its room has been deleted")
  171. }
  172. sessionManager.RemoveSession(sess)
  173. if sessionManager.Empty() {
  174. delete(s.store, sess.ChatRoomCookie())
  175. }
  176. }
  177. // AllSessions returns all chat room participants. Returns
  178. // ErrChatRoomNotFound if the room does not exist.
  179. func (s *InMemoryChatSessionManager) AllSessions(cookie string) []*Session {
  180. s.mapMutex.RLock()
  181. defer s.mapMutex.RUnlock()
  182. sessionManager, ok := s.store[cookie]
  183. if !ok {
  184. s.logger.Debug("trying to get sessions for non-existent room", "cookie", cookie)
  185. return nil
  186. }
  187. return sessionManager.AllSessions()
  188. }
  189. // RelayToAllExcept sends a message to all chat room participants except for
  190. // the participant with a particular screen name. Returns ErrChatRoomNotFound
  191. // if the room does not exist for cookie.
  192. func (s *InMemoryChatSessionManager) RelayToAllExcept(ctx context.Context, cookie string, except IdentScreenName, msg wire.SNACMessage) {
  193. s.mapMutex.RLock()
  194. defer s.mapMutex.RUnlock()
  195. sessionManager, ok := s.store[cookie]
  196. if !ok {
  197. s.logger.Error("trying to relay message to all for non-existent room", "cookie", cookie)
  198. return
  199. }
  200. for _, sess := range sessionManager.AllSessions() {
  201. if sess.IdentScreenName() == except {
  202. continue
  203. }
  204. sessionManager.maybeRelayMessage(ctx, msg, sess)
  205. }
  206. }
  207. // RelayToScreenName sends a message to a chat room user. Returns
  208. // ErrChatRoomNotFound if the room does not exist for cookie.
  209. func (s *InMemoryChatSessionManager) RelayToScreenName(ctx context.Context, cookie string, recipient IdentScreenName, msg wire.SNACMessage) {
  210. s.mapMutex.RLock()
  211. defer s.mapMutex.RUnlock()
  212. sessionManager, ok := s.store[cookie]
  213. if !ok {
  214. s.logger.Error("trying to relay message to screen name for non-existent room", "cookie", cookie)
  215. return
  216. }
  217. sessionManager.RelayToScreenName(ctx, recipient, msg)
  218. }