webapi_chat.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. package state
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "database/sql"
  6. "encoding/hex"
  7. "errors"
  8. "fmt"
  9. "log/slog"
  10. "sync"
  11. "time"
  12. )
  13. // ChatRoomType represents the type of chat room
  14. type ChatRoomType string
  15. const (
  16. ChatRoomTypeUserCreated ChatRoomType = "userCreated"
  17. )
  18. // ChatEventType represents the type of chat event
  19. type ChatEventType string
  20. const (
  21. ChatEventUserInRoom ChatEventType = "userInRoom"
  22. ChatEventUserEntered ChatEventType = "userEntered"
  23. ChatEventUserLeft ChatEventType = "userLeft"
  24. ChatEventMessage ChatEventType = "message"
  25. ChatEventTyping ChatEventType = "typing"
  26. ChatEventClosed ChatEventType = "closed"
  27. )
  28. // WebAPIChatRoom represents a chat room for Web API
  29. type WebAPIChatRoom struct {
  30. RoomID string `json:"roomId"`
  31. RoomName string `json:"roomName"`
  32. Description string `json:"description,omitempty"`
  33. RoomType ChatRoomType `json:"roomType"`
  34. CategoryID string `json:"categoryId,omitempty"`
  35. CreatorScreenName string `json:"-"` // Internal only
  36. CreatedAt int64 `json:"-"`
  37. ClosedAt *int64 `json:"-"`
  38. MaxParticipants int `json:"-"`
  39. InstanceID int `json:"instanceId"`
  40. }
  41. // ChatSession represents a user's session in a chat room
  42. type ChatSession struct {
  43. ChatSID string
  44. AIMSid string
  45. RoomID string
  46. ScreenName string
  47. InstanceID int
  48. JoinedAt int64
  49. LeftAt *int64
  50. }
  51. // ChatMessage represents a message sent in a chat room
  52. type ChatMessage struct {
  53. ID int64
  54. RoomID string
  55. ScreenName string
  56. Message string
  57. WhisperTarget string
  58. Timestamp int64
  59. }
  60. // ChatParticipant represents a participant in a chat room
  61. type ChatParticipant struct {
  62. RoomID string
  63. ScreenName string
  64. ChatSID string
  65. JoinedAt int64
  66. TypingStatus string
  67. TypingUpdatedAt *int64
  68. }
  69. // ChatEventData represents data for a chat event
  70. type ChatEventData struct {
  71. ChatSID string `json:"chatsid"`
  72. EventType ChatEventType `json:"eventType"`
  73. EventData interface{} `json:"eventData"`
  74. }
  75. // ChatMessageEventData represents chat message event data
  76. type ChatMessageEventData struct {
  77. ScreenName string `json:"screenName"`
  78. Message string `json:"message"`
  79. Timestamp int64 `json:"timestamp"`
  80. WhisperTarget string `json:"whisperTarget,omitempty"`
  81. }
  82. // ChatUserEventData represents user join/leave event data
  83. type ChatUserEventData struct {
  84. ScreenName string `json:"screenName"`
  85. Timestamp int64 `json:"timestamp"`
  86. }
  87. // ChatTypingEventData represents typing status event data
  88. type ChatTypingEventData struct {
  89. ScreenName string `json:"screenName"`
  90. TypingStatus string `json:"typingStatus"`
  91. }
  92. // ChatParticipantList represents a list of participants in the room
  93. type ChatParticipantList struct {
  94. Participants []string `json:"participants"`
  95. }
  96. // WebAPIChatManager manages Web API chat rooms
  97. type WebAPIChatManager struct {
  98. store *SQLiteUserStore
  99. logger *slog.Logger
  100. sessions *WebAPISessionManager
  101. mu sync.RWMutex
  102. // In-memory cache for active rooms
  103. activeRooms map[string]*WebAPIChatRoom
  104. // Track typing timeouts
  105. typingTimers map[string]*time.Timer
  106. }
  107. // NewWebAPIChatManager creates a new WebAPIChatManager
  108. func (s *SQLiteUserStore) NewWebAPIChatManager(logger *slog.Logger, sessions *WebAPISessionManager) *WebAPIChatManager {
  109. return &WebAPIChatManager{
  110. store: s,
  111. logger: logger,
  112. sessions: sessions,
  113. activeRooms: make(map[string]*WebAPIChatRoom),
  114. typingTimers: make(map[string]*time.Timer),
  115. }
  116. }
  117. // CreateAndJoinChat creates a new chat room or joins an existing one
  118. func (m *WebAPIChatManager) CreateAndJoinChat(ctx context.Context, aimsid, roomID, roomName, screenName string) (*ChatSession, *WebAPIChatRoom, error) {
  119. m.mu.Lock()
  120. defer m.mu.Unlock()
  121. var room *WebAPIChatRoom
  122. var err error
  123. // Determine which identifier to use
  124. if roomID != "" {
  125. room, err = m.getRoomByID(ctx, roomID)
  126. if err != nil {
  127. return nil, nil, fmt.Errorf("failed to get room by ID: %w", err)
  128. }
  129. } else if roomName != "" {
  130. room, err = m.getRoomByName(ctx, roomName)
  131. if err != nil && !errors.Is(err, sql.ErrNoRows) {
  132. return nil, nil, fmt.Errorf("failed to get room by name: %w", err)
  133. }
  134. // If room doesn't exist, create it
  135. if room == nil {
  136. room, err = m.createRoom(ctx, roomName, screenName)
  137. if err != nil {
  138. return nil, nil, fmt.Errorf("failed to create room: %w", err)
  139. }
  140. }
  141. } else {
  142. return nil, nil, errors.New("either roomId or roomName must be provided")
  143. }
  144. // Check if user is already in the room
  145. existingSession, _ := m.getUserSessionInRoom(ctx, aimsid, room.RoomID)
  146. if existingSession != nil {
  147. return existingSession, room, nil
  148. }
  149. // Check room capacity
  150. count, err := m.getParticipantCount(ctx, room.RoomID)
  151. if err != nil {
  152. return nil, nil, fmt.Errorf("failed to get participant count: %w", err)
  153. }
  154. if count >= room.MaxParticipants {
  155. return nil, nil, errors.New("room is at maximum capacity")
  156. }
  157. // Create chat session
  158. session := &ChatSession{
  159. ChatSID: m.generateChatSID(),
  160. AIMSid: aimsid,
  161. RoomID: room.RoomID,
  162. ScreenName: screenName,
  163. InstanceID: room.InstanceID,
  164. JoinedAt: time.Now().Unix(),
  165. }
  166. // Insert session into database
  167. _, err = m.store.db.ExecContext(ctx, `
  168. INSERT INTO web_chat_sessions (chat_sid, aimsid, room_id, screen_name, instance_id, joined_at)
  169. VALUES (?, ?, ?, ?, ?, ?)`,
  170. session.ChatSID, session.AIMSid, session.RoomID, session.ScreenName, session.InstanceID, session.JoinedAt)
  171. if err != nil {
  172. return nil, nil, fmt.Errorf("failed to create chat session: %w", err)
  173. }
  174. // Add participant to room
  175. _, err = m.store.db.ExecContext(ctx, `
  176. INSERT INTO web_chat_participants (room_id, screen_name, chat_sid, joined_at, typing_status)
  177. VALUES (?, ?, ?, ?, 'none')`,
  178. room.RoomID, screenName, session.ChatSID, session.JoinedAt)
  179. if err != nil {
  180. return nil, nil, fmt.Errorf("failed to add participant: %w", err)
  181. }
  182. // Broadcast user joined event
  183. // Note: Broadcasting doesn't need context as it's fire-and-forget
  184. m.broadcastChatEvent(room.RoomID, ChatEventData{
  185. ChatSID: session.ChatSID,
  186. EventType: ChatEventUserEntered,
  187. EventData: ChatUserEventData{
  188. ScreenName: screenName,
  189. Timestamp: session.JoinedAt,
  190. },
  191. })
  192. // Send current participant list to the new user
  193. participants, _ := m.getParticipants(ctx, room.RoomID)
  194. m.sendChatEventToUser(aimsid, ChatEventData{
  195. ChatSID: session.ChatSID,
  196. EventType: ChatEventUserInRoom,
  197. EventData: ChatParticipantList{
  198. Participants: participants,
  199. },
  200. })
  201. return session, room, nil
  202. }
  203. // SendMessage sends a message to a chat room
  204. func (m *WebAPIChatManager) SendMessage(ctx context.Context, chatsid, message, whisperTarget string) error {
  205. m.mu.Lock()
  206. defer m.mu.Unlock()
  207. // Get session
  208. session, err := m.getSessionByChatSID(ctx, chatsid)
  209. if err != nil {
  210. return fmt.Errorf("invalid chat session: %w", err)
  211. }
  212. // Verify user is still in room
  213. if session.LeftAt != nil {
  214. return errors.New("user has left the chat room")
  215. }
  216. // Store message in database
  217. timestamp := time.Now().Unix()
  218. _, err = m.store.db.ExecContext(ctx, `
  219. INSERT INTO web_chat_messages (room_id, screen_name, message, whisper_target, timestamp)
  220. VALUES (?, ?, ?, ?, ?)`,
  221. session.RoomID, session.ScreenName, message, whisperTarget, timestamp)
  222. if err != nil {
  223. return fmt.Errorf("failed to store message: %w", err)
  224. }
  225. // Broadcast message event
  226. eventData := ChatMessageEventData{
  227. ScreenName: session.ScreenName,
  228. Message: message,
  229. Timestamp: timestamp,
  230. WhisperTarget: whisperTarget,
  231. }
  232. if whisperTarget != "" {
  233. // For whispers, only send to sender and target
  234. m.sendChatEventToUser(session.AIMSid, ChatEventData{
  235. ChatSID: chatsid,
  236. EventType: ChatEventMessage,
  237. EventData: eventData,
  238. })
  239. // Find target's session and send to them
  240. targetSession, _ := m.getUserSessionInRoomByScreenName(ctx, session.RoomID, whisperTarget)
  241. if targetSession != nil {
  242. m.sendChatEventToUser(targetSession.AIMSid, ChatEventData{
  243. ChatSID: targetSession.ChatSID,
  244. EventType: ChatEventMessage,
  245. EventData: eventData,
  246. })
  247. }
  248. } else {
  249. // Broadcast to all participants
  250. m.broadcastChatEvent(session.RoomID, ChatEventData{
  251. ChatSID: chatsid,
  252. EventType: ChatEventMessage,
  253. EventData: eventData,
  254. })
  255. }
  256. return nil
  257. }
  258. // SetTyping sets the typing status for a user in a chat room
  259. func (m *WebAPIChatManager) SetTyping(ctx context.Context, chatsid, typingStatus string) error {
  260. m.mu.Lock()
  261. defer m.mu.Unlock()
  262. // Get session
  263. session, err := m.getSessionByChatSID(ctx, chatsid)
  264. if err != nil {
  265. return fmt.Errorf("invalid chat session: %w", err)
  266. }
  267. // Verify user is still in room
  268. if session.LeftAt != nil {
  269. return errors.New("user has left the chat room")
  270. }
  271. // Update typing status
  272. now := time.Now().Unix()
  273. _, err = m.store.db.ExecContext(ctx, `
  274. UPDATE web_chat_participants
  275. SET typing_status = ?, typing_updated_at = ?
  276. WHERE room_id = ? AND screen_name = ?`,
  277. typingStatus, now, session.RoomID, session.ScreenName)
  278. if err != nil {
  279. return fmt.Errorf("failed to update typing status: %w", err)
  280. }
  281. // Cancel existing typing timer for this user
  282. timerKey := fmt.Sprintf("%s:%s", session.RoomID, session.ScreenName)
  283. if timer, exists := m.typingTimers[timerKey]; exists {
  284. timer.Stop()
  285. delete(m.typingTimers, timerKey)
  286. }
  287. // If status is "typing" or "typed", set a timer to reset it
  288. if typingStatus == "typing" || typingStatus == "typed" {
  289. timer := time.AfterFunc(10*time.Second, func() {
  290. m.mu.Lock()
  291. defer m.mu.Unlock()
  292. // Reset typing status to none
  293. // Using background context here since this is an async timer callback
  294. // and the original context may have expired
  295. if _, err := m.store.db.ExecContext(context.Background(), `
  296. UPDATE web_chat_participants
  297. SET typing_status = 'none', typing_updated_at = ?
  298. WHERE room_id = ? AND screen_name = ?`,
  299. time.Now().Unix(), session.RoomID, session.ScreenName); err != nil {
  300. m.logger.Error("failed to reset typing status", "error", err)
  301. }
  302. // Broadcast the reset
  303. m.broadcastChatEvent(session.RoomID, ChatEventData{
  304. ChatSID: chatsid,
  305. EventType: ChatEventTyping,
  306. EventData: ChatTypingEventData{
  307. ScreenName: session.ScreenName,
  308. TypingStatus: "none",
  309. },
  310. })
  311. delete(m.typingTimers, timerKey)
  312. })
  313. m.typingTimers[timerKey] = timer
  314. }
  315. // Broadcast typing event
  316. m.broadcastChatEvent(session.RoomID, ChatEventData{
  317. ChatSID: chatsid,
  318. EventType: ChatEventTyping,
  319. EventData: ChatTypingEventData{
  320. ScreenName: session.ScreenName,
  321. TypingStatus: typingStatus,
  322. },
  323. })
  324. return nil
  325. }
  326. // LeaveChat removes a user from a chat room
  327. func (m *WebAPIChatManager) LeaveChat(ctx context.Context, chatsid string) error {
  328. m.mu.Lock()
  329. defer m.mu.Unlock()
  330. // Get session
  331. session, err := m.getSessionByChatSID(ctx, chatsid)
  332. if err != nil {
  333. return fmt.Errorf("invalid chat session: %w", err)
  334. }
  335. // Mark session as left
  336. now := time.Now().Unix()
  337. _, err = m.store.db.ExecContext(ctx, `
  338. UPDATE web_chat_sessions
  339. SET left_at = ?
  340. WHERE chat_sid = ?`,
  341. now, chatsid)
  342. if err != nil {
  343. return fmt.Errorf("failed to update session: %w", err)
  344. }
  345. // Remove from participants
  346. _, err = m.store.db.ExecContext(ctx, `
  347. DELETE FROM web_chat_participants
  348. WHERE room_id = ? AND screen_name = ?`,
  349. session.RoomID, session.ScreenName)
  350. if err != nil {
  351. return fmt.Errorf("failed to remove participant: %w", err)
  352. }
  353. // Cancel any typing timer
  354. timerKey := fmt.Sprintf("%s:%s", session.RoomID, session.ScreenName)
  355. if timer, exists := m.typingTimers[timerKey]; exists {
  356. timer.Stop()
  357. delete(m.typingTimers, timerKey)
  358. }
  359. // Broadcast user left event
  360. // Note: Broadcasting doesn't need context as it's fire-and-forget
  361. m.broadcastChatEvent(session.RoomID, ChatEventData{
  362. ChatSID: chatsid,
  363. EventType: ChatEventUserLeft,
  364. EventData: ChatUserEventData{
  365. ScreenName: session.ScreenName,
  366. Timestamp: now,
  367. },
  368. })
  369. // Check if room should be closed (no participants left)
  370. count, _ := m.getParticipantCount(ctx, session.RoomID)
  371. if count == 0 {
  372. m.closeRoom(ctx, session.RoomID)
  373. }
  374. return nil
  375. }
  376. // Helper methods
  377. func (m *WebAPIChatManager) getRoomByID(ctx context.Context, roomID string) (*WebAPIChatRoom, error) {
  378. var room WebAPIChatRoom
  379. err := m.store.db.QueryRowContext(ctx, `
  380. SELECT room_id, room_name, description, room_type, category_id,
  381. creator_screen_name, created_at, closed_at, max_participants
  382. FROM web_chat_rooms
  383. WHERE room_id = ? AND closed_at IS NULL`,
  384. roomID).Scan(
  385. &room.RoomID, &room.RoomName, &room.Description, &room.RoomType,
  386. &room.CategoryID, &room.CreatorScreenName, &room.CreatedAt,
  387. &room.ClosedAt, &room.MaxParticipants)
  388. if err != nil {
  389. return nil, err
  390. }
  391. room.InstanceID = m.generateInstanceID()
  392. return &room, nil
  393. }
  394. func (m *WebAPIChatManager) getRoomByName(ctx context.Context, roomName string) (*WebAPIChatRoom, error) {
  395. var room WebAPIChatRoom
  396. err := m.store.db.QueryRowContext(ctx, `
  397. SELECT room_id, room_name, description, room_type, category_id,
  398. creator_screen_name, created_at, closed_at, max_participants
  399. FROM web_chat_rooms
  400. WHERE room_name = ? AND closed_at IS NULL`,
  401. roomName).Scan(
  402. &room.RoomID, &room.RoomName, &room.Description, &room.RoomType,
  403. &room.CategoryID, &room.CreatorScreenName, &room.CreatedAt,
  404. &room.ClosedAt, &room.MaxParticipants)
  405. if err != nil {
  406. return nil, err
  407. }
  408. room.InstanceID = m.generateInstanceID()
  409. return &room, nil
  410. }
  411. func (m *WebAPIChatManager) createRoom(ctx context.Context, roomName, creatorScreenName string) (*WebAPIChatRoom, error) {
  412. room := &WebAPIChatRoom{
  413. RoomID: m.generateRoomID(),
  414. RoomName: roomName,
  415. Description: fmt.Sprintf("Chat room created by %s", creatorScreenName),
  416. RoomType: ChatRoomTypeUserCreated,
  417. CreatorScreenName: creatorScreenName,
  418. CreatedAt: time.Now().Unix(),
  419. MaxParticipants: 100,
  420. InstanceID: m.generateInstanceID(),
  421. }
  422. _, err := m.store.db.ExecContext(ctx, `
  423. INSERT INTO web_chat_rooms (room_id, room_name, description, room_type,
  424. category_id, creator_screen_name, created_at, max_participants)
  425. VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
  426. room.RoomID, room.RoomName, room.Description, room.RoomType,
  427. room.CategoryID, room.CreatorScreenName, room.CreatedAt, room.MaxParticipants)
  428. if err != nil {
  429. return nil, err
  430. }
  431. // Cache the room
  432. m.activeRooms[room.RoomID] = room
  433. return room, nil
  434. }
  435. func (m *WebAPIChatManager) getSessionByChatSID(ctx context.Context, chatsid string) (*ChatSession, error) {
  436. var session ChatSession
  437. err := m.store.db.QueryRowContext(ctx, `
  438. SELECT chat_sid, aimsid, room_id, screen_name, instance_id, joined_at, left_at
  439. FROM web_chat_sessions
  440. WHERE chat_sid = ?`,
  441. chatsid).Scan(
  442. &session.ChatSID, &session.AIMSid, &session.RoomID,
  443. &session.ScreenName, &session.InstanceID, &session.JoinedAt, &session.LeftAt)
  444. if err != nil {
  445. return nil, err
  446. }
  447. return &session, nil
  448. }
  449. func (m *WebAPIChatManager) getUserSessionInRoom(ctx context.Context, aimsid, roomID string) (*ChatSession, error) {
  450. var session ChatSession
  451. err := m.store.db.QueryRowContext(ctx, `
  452. SELECT chat_sid, aimsid, room_id, screen_name, instance_id, joined_at, left_at
  453. FROM web_chat_sessions
  454. WHERE aimsid = ? AND room_id = ? AND left_at IS NULL`,
  455. aimsid, roomID).Scan(
  456. &session.ChatSID, &session.AIMSid, &session.RoomID,
  457. &session.ScreenName, &session.InstanceID, &session.JoinedAt, &session.LeftAt)
  458. if err != nil {
  459. return nil, err
  460. }
  461. return &session, nil
  462. }
  463. func (m *WebAPIChatManager) getUserSessionInRoomByScreenName(ctx context.Context, roomID, screenName string) (*ChatSession, error) {
  464. var session ChatSession
  465. err := m.store.db.QueryRowContext(ctx, `
  466. SELECT chat_sid, aimsid, room_id, screen_name, instance_id, joined_at, left_at
  467. FROM web_chat_sessions
  468. WHERE room_id = ? AND screen_name = ? AND left_at IS NULL`,
  469. roomID, screenName).Scan(
  470. &session.ChatSID, &session.AIMSid, &session.RoomID,
  471. &session.ScreenName, &session.InstanceID, &session.JoinedAt, &session.LeftAt)
  472. if err != nil {
  473. return nil, err
  474. }
  475. return &session, nil
  476. }
  477. func (m *WebAPIChatManager) getParticipantCount(ctx context.Context, roomID string) (int, error) {
  478. var count int
  479. err := m.store.db.QueryRowContext(ctx, `
  480. SELECT COUNT(*) FROM web_chat_participants WHERE room_id = ?`,
  481. roomID).Scan(&count)
  482. return count, err
  483. }
  484. func (m *WebAPIChatManager) getParticipants(ctx context.Context, roomID string) ([]string, error) {
  485. rows, err := m.store.db.QueryContext(ctx, `
  486. SELECT screen_name FROM web_chat_participants WHERE room_id = ?`,
  487. roomID)
  488. if err != nil {
  489. return nil, err
  490. }
  491. defer rows.Close()
  492. var participants []string
  493. for rows.Next() {
  494. var screenName string
  495. if err := rows.Scan(&screenName); err != nil {
  496. continue
  497. }
  498. participants = append(participants, screenName)
  499. }
  500. return participants, nil
  501. }
  502. func (m *WebAPIChatManager) closeRoom(ctx context.Context, roomID string) {
  503. now := time.Now().Unix()
  504. if _, err := m.store.db.ExecContext(ctx, `
  505. UPDATE web_chat_rooms SET closed_at = ? WHERE room_id = ?`,
  506. now, roomID); err != nil {
  507. m.logger.Error("failed to close chat room", "roomID", roomID, "error", err)
  508. }
  509. // Remove from cache
  510. delete(m.activeRooms, roomID)
  511. // Broadcast room closed event
  512. // Note: Broadcasting doesn't need context as it's fire-and-forget
  513. m.broadcastChatEvent(roomID, ChatEventData{
  514. EventType: ChatEventClosed,
  515. })
  516. }
  517. func (m *WebAPIChatManager) broadcastChatEvent(roomID string, event ChatEventData) {
  518. // Get all active sessions in the room
  519. rows, err := m.store.db.Query(`
  520. SELECT aimsid, chat_sid FROM web_chat_sessions
  521. WHERE room_id = ? AND left_at IS NULL`,
  522. roomID)
  523. if err != nil {
  524. m.logger.Error("failed to get sessions for broadcast", "error", err, "roomID", roomID)
  525. return
  526. }
  527. defer rows.Close()
  528. for rows.Next() {
  529. var aimsid, chatsid string
  530. if err := rows.Scan(&aimsid, &chatsid); err != nil {
  531. continue
  532. }
  533. // Update event with the recipient's chat session ID if not set
  534. if event.ChatSID == "" {
  535. event.ChatSID = chatsid
  536. }
  537. m.sendChatEventToUser(aimsid, event)
  538. }
  539. }
  540. func (m *WebAPIChatManager) sendChatEventToUser(aimsid string, event ChatEventData) {
  541. // Get the user's Web API session
  542. // Using background context for async event sending
  543. session, err := m.sessions.GetSession(context.Background(), aimsid)
  544. if err != nil {
  545. m.logger.Error("failed to get session for chat event", "error", err, "aimsid", aimsid)
  546. return
  547. }
  548. // Queue the chat event
  549. session.EventQueue.Push("chat", event)
  550. }
  551. func (m *WebAPIChatManager) generateRoomID() string {
  552. b := make([]byte, 16)
  553. rand.Read(b)
  554. return hex.EncodeToString(b)
  555. }
  556. func (m *WebAPIChatManager) generateChatSID() string {
  557. b := make([]byte, 16)
  558. rand.Read(b)
  559. return hex.EncodeToString(b)
  560. }
  561. func (m *WebAPIChatManager) generateInstanceID() int {
  562. // In production, this might be based on server instance or other factors
  563. // For now, use a simple random number
  564. return int(time.Now().Unix() % 1000000)
  565. }
  566. // GetRecentMessages returns recent messages from a chat room (for history)
  567. func (m *WebAPIChatManager) GetRecentMessages(ctx context.Context, roomID string, limit int) ([]*ChatMessage, error) {
  568. rows, err := m.store.db.QueryContext(ctx, `
  569. SELECT id, room_id, screen_name, message, whisper_target, timestamp
  570. FROM web_chat_messages
  571. WHERE room_id = ?
  572. ORDER BY timestamp DESC
  573. LIMIT ?`,
  574. roomID, limit)
  575. if err != nil {
  576. return nil, err
  577. }
  578. defer rows.Close()
  579. var messages []*ChatMessage
  580. for rows.Next() {
  581. var msg ChatMessage
  582. err := rows.Scan(&msg.ID, &msg.RoomID, &msg.ScreenName,
  583. &msg.Message, &msg.WhisperTarget, &msg.Timestamp)
  584. if err != nil {
  585. continue
  586. }
  587. messages = append(messages, &msg)
  588. }
  589. // Reverse to get chronological order
  590. for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
  591. messages[i], messages[j] = messages[j], messages[i]
  592. }
  593. return messages, nil
  594. }
  595. // CleanupInactiveSessions removes sessions that have been inactive for too long
  596. func (m *WebAPIChatManager) CleanupInactiveSessions(ctx context.Context) {
  597. m.mu.Lock()
  598. defer m.mu.Unlock()
  599. // Mark sessions as left if they've been inactive for more than 30 minutes
  600. cutoff := time.Now().Add(-30 * time.Minute).Unix()
  601. rows, err := m.store.db.QueryContext(ctx, `
  602. SELECT chat_sid, room_id, screen_name
  603. FROM web_chat_sessions
  604. WHERE left_at IS NULL AND joined_at < ?`,
  605. cutoff)
  606. if err != nil {
  607. m.logger.Error("failed to get inactive sessions", "error", err)
  608. return
  609. }
  610. defer rows.Close()
  611. for rows.Next() {
  612. var chatsid, roomID, screenName string
  613. if err := rows.Scan(&chatsid, &roomID, &screenName); err != nil {
  614. continue
  615. }
  616. // Mark as left
  617. now := time.Now().Unix()
  618. if _, err := m.store.db.ExecContext(ctx, `UPDATE web_chat_sessions SET left_at = ? WHERE chat_sid = ?`, now, chatsid); err != nil {
  619. m.logger.Error("failed to mark inactive chat session left", "chatsid", chatsid, "error", err)
  620. continue
  621. }
  622. if _, err := m.store.db.ExecContext(ctx, `DELETE FROM web_chat_participants WHERE room_id = ? AND screen_name = ?`,
  623. roomID, screenName); err != nil {
  624. m.logger.Error("failed to remove inactive chat participant", "roomID", roomID, "screenName", screenName, "error", err)
  625. continue
  626. }
  627. // Broadcast user left
  628. // Note: Broadcasting doesn't need context as it's fire-and-forget
  629. m.broadcastChatEvent(roomID, ChatEventData{
  630. ChatSID: chatsid,
  631. EventType: ChatEventUserLeft,
  632. EventData: ChatUserEventData{
  633. ScreenName: screenName,
  634. Timestamp: now,
  635. },
  636. })
  637. }
  638. }