session_manager.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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.RetrieveSession(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. if sess == s.store[sess.IdentScreenName()] {
  87. delete(s.store, sess.IdentScreenName())
  88. }
  89. }
  90. // RetrieveSession finds a session with a matching sessionID. Returns nil if
  91. // session is not found.
  92. func (s *InMemorySessionManager) RetrieveSession(screenName IdentScreenName) *Session {
  93. s.mapMutex.RLock()
  94. defer s.mapMutex.RUnlock()
  95. return s.store[screenName]
  96. }
  97. func (s *InMemorySessionManager) retrieveByScreenNames(screenNames []IdentScreenName) []*Session {
  98. s.mapMutex.RLock()
  99. defer s.mapMutex.RUnlock()
  100. var ret []*Session
  101. for _, sn := range screenNames {
  102. for _, sess := range s.store {
  103. if sn == sess.IdentScreenName() {
  104. ret = append(ret, sess)
  105. }
  106. }
  107. }
  108. return ret
  109. }
  110. // Empty returns true if the session pool contains 0 sessions.
  111. func (s *InMemorySessionManager) Empty() bool {
  112. s.mapMutex.RLock()
  113. defer s.mapMutex.RUnlock()
  114. return len(s.store) == 0
  115. }
  116. // AllSessions returns all sessions in the session pool.
  117. func (s *InMemorySessionManager) AllSessions() []*Session {
  118. s.mapMutex.RLock()
  119. defer s.mapMutex.RUnlock()
  120. var sessions []*Session
  121. for _, sess := range s.store {
  122. sessions = append(sessions, sess)
  123. }
  124. return sessions
  125. }
  126. // NewInMemoryChatSessionManager creates a new instance of
  127. // InMemoryChatSessionManager.
  128. func NewInMemoryChatSessionManager(logger *slog.Logger) *InMemoryChatSessionManager {
  129. return &InMemoryChatSessionManager{
  130. store: make(map[string]*InMemorySessionManager),
  131. logger: logger,
  132. }
  133. }
  134. // InMemoryChatSessionManager manages chat sessions for multiple chat rooms
  135. // stored in memory. It provides thread-safe operations to add, remove, and
  136. // manipulate sessions as well as relay messages to participants.
  137. type InMemoryChatSessionManager struct {
  138. logger *slog.Logger
  139. mapMutex sync.RWMutex
  140. store map[string]*InMemorySessionManager
  141. }
  142. // AddSession adds a user to a chat room. If screenName already exists, the old
  143. // session is closed replaced by a new one.
  144. func (s *InMemoryChatSessionManager) AddSession(chatCookie string, screenName DisplayScreenName) *Session {
  145. s.mapMutex.Lock()
  146. defer s.mapMutex.Unlock()
  147. if _, ok := s.store[chatCookie]; !ok {
  148. s.store[chatCookie] = NewInMemorySessionManager(s.logger)
  149. }
  150. sessionManager := s.store[chatCookie]
  151. sess := sessionManager.AddSession(screenName)
  152. sess.SetChatRoomCookie(chatCookie)
  153. return sess
  154. }
  155. // RemoveSession removes a user session from a chat room. It panics if you
  156. // attempt to remove the session twice.
  157. func (s *InMemoryChatSessionManager) RemoveSession(sess *Session) {
  158. s.mapMutex.Lock()
  159. defer s.mapMutex.Unlock()
  160. sessionManager, ok := s.store[sess.ChatRoomCookie()]
  161. if !ok {
  162. panic("attempting to remove a session after its room has been deleted")
  163. }
  164. sessionManager.RemoveSession(sess)
  165. if sessionManager.Empty() {
  166. delete(s.store, sess.ChatRoomCookie())
  167. }
  168. }
  169. // AllSessions returns all chat room participants. Returns
  170. // ErrChatRoomNotFound if the room does not exist.
  171. func (s *InMemoryChatSessionManager) AllSessions(cookie string) []*Session {
  172. s.mapMutex.RLock()
  173. defer s.mapMutex.RUnlock()
  174. sessionManager, ok := s.store[cookie]
  175. if !ok {
  176. s.logger.Debug("trying to get sessions for non-existent room", "cookie", cookie)
  177. return nil
  178. }
  179. return sessionManager.AllSessions()
  180. }
  181. // RelayToAllExcept sends a message to all chat room participants except for
  182. // the participant with a particular screen name. Returns ErrChatRoomNotFound
  183. // if the room does not exist for cookie.
  184. func (s *InMemoryChatSessionManager) RelayToAllExcept(ctx context.Context, cookie string, except IdentScreenName, msg wire.SNACMessage) {
  185. s.mapMutex.RLock()
  186. defer s.mapMutex.RUnlock()
  187. sessionManager, ok := s.store[cookie]
  188. if !ok {
  189. s.logger.Error("trying to relay message to all for non-existent room", "cookie", cookie)
  190. return
  191. }
  192. for _, sess := range sessionManager.AllSessions() {
  193. if sess.IdentScreenName() == except {
  194. continue
  195. }
  196. sessionManager.maybeRelayMessage(ctx, msg, sess)
  197. }
  198. }
  199. // RelayToScreenName sends a message to a chat room user. Returns
  200. // ErrChatRoomNotFound if the room does not exist for cookie.
  201. func (s *InMemoryChatSessionManager) RelayToScreenName(ctx context.Context, cookie string, recipient IdentScreenName, msg wire.SNACMessage) {
  202. s.mapMutex.RLock()
  203. defer s.mapMutex.RUnlock()
  204. sessionManager, ok := s.store[cookie]
  205. if !ok {
  206. s.logger.Error("trying to relay message to screen name for non-existent room", "cookie", cookie)
  207. return
  208. }
  209. sessionManager.RelayToScreenName(ctx, recipient, msg)
  210. }