webapi_chat.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  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. 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)
  300. // Broadcast the reset
  301. m.broadcastChatEvent(session.RoomID, ChatEventData{
  302. ChatSID: chatsid,
  303. EventType: ChatEventTyping,
  304. EventData: ChatTypingEventData{
  305. ScreenName: session.ScreenName,
  306. TypingStatus: "none",
  307. },
  308. })
  309. delete(m.typingTimers, timerKey)
  310. })
  311. m.typingTimers[timerKey] = timer
  312. }
  313. // Broadcast typing event
  314. m.broadcastChatEvent(session.RoomID, ChatEventData{
  315. ChatSID: chatsid,
  316. EventType: ChatEventTyping,
  317. EventData: ChatTypingEventData{
  318. ScreenName: session.ScreenName,
  319. TypingStatus: typingStatus,
  320. },
  321. })
  322. return nil
  323. }
  324. // LeaveChat removes a user from a chat room
  325. func (m *WebAPIChatManager) LeaveChat(ctx context.Context, chatsid string) error {
  326. m.mu.Lock()
  327. defer m.mu.Unlock()
  328. // Get session
  329. session, err := m.getSessionByChatSID(ctx, chatsid)
  330. if err != nil {
  331. return fmt.Errorf("invalid chat session: %w", err)
  332. }
  333. // Mark session as left
  334. now := time.Now().Unix()
  335. _, err = m.store.db.ExecContext(ctx, `
  336. UPDATE web_chat_sessions
  337. SET left_at = ?
  338. WHERE chat_sid = ?`,
  339. now, chatsid)
  340. if err != nil {
  341. return fmt.Errorf("failed to update session: %w", err)
  342. }
  343. // Remove from participants
  344. _, err = m.store.db.ExecContext(ctx, `
  345. DELETE FROM web_chat_participants
  346. WHERE room_id = ? AND screen_name = ?`,
  347. session.RoomID, session.ScreenName)
  348. if err != nil {
  349. return fmt.Errorf("failed to remove participant: %w", err)
  350. }
  351. // Cancel any typing timer
  352. timerKey := fmt.Sprintf("%s:%s", session.RoomID, session.ScreenName)
  353. if timer, exists := m.typingTimers[timerKey]; exists {
  354. timer.Stop()
  355. delete(m.typingTimers, timerKey)
  356. }
  357. // Broadcast user left event
  358. // Note: Broadcasting doesn't need context as it's fire-and-forget
  359. m.broadcastChatEvent(session.RoomID, ChatEventData{
  360. ChatSID: chatsid,
  361. EventType: ChatEventUserLeft,
  362. EventData: ChatUserEventData{
  363. ScreenName: session.ScreenName,
  364. Timestamp: now,
  365. },
  366. })
  367. // Check if room should be closed (no participants left)
  368. count, _ := m.getParticipantCount(ctx, session.RoomID)
  369. if count == 0 {
  370. m.closeRoom(ctx, session.RoomID)
  371. }
  372. return nil
  373. }
  374. // Helper methods
  375. func (m *WebAPIChatManager) getRoomByID(ctx context.Context, roomID string) (*WebAPIChatRoom, error) {
  376. var room WebAPIChatRoom
  377. err := m.store.db.QueryRowContext(ctx, `
  378. SELECT room_id, room_name, description, room_type, category_id,
  379. creator_screen_name, created_at, closed_at, max_participants
  380. FROM web_chat_rooms
  381. WHERE room_id = ? AND closed_at IS NULL`,
  382. roomID).Scan(
  383. &room.RoomID, &room.RoomName, &room.Description, &room.RoomType,
  384. &room.CategoryID, &room.CreatorScreenName, &room.CreatedAt,
  385. &room.ClosedAt, &room.MaxParticipants)
  386. if err != nil {
  387. return nil, err
  388. }
  389. room.InstanceID = m.generateInstanceID()
  390. return &room, nil
  391. }
  392. func (m *WebAPIChatManager) getRoomByName(ctx context.Context, roomName string) (*WebAPIChatRoom, error) {
  393. var room WebAPIChatRoom
  394. err := m.store.db.QueryRowContext(ctx, `
  395. SELECT room_id, room_name, description, room_type, category_id,
  396. creator_screen_name, created_at, closed_at, max_participants
  397. FROM web_chat_rooms
  398. WHERE room_name = ? AND closed_at IS NULL`,
  399. roomName).Scan(
  400. &room.RoomID, &room.RoomName, &room.Description, &room.RoomType,
  401. &room.CategoryID, &room.CreatorScreenName, &room.CreatedAt,
  402. &room.ClosedAt, &room.MaxParticipants)
  403. if err != nil {
  404. return nil, err
  405. }
  406. room.InstanceID = m.generateInstanceID()
  407. return &room, nil
  408. }
  409. func (m *WebAPIChatManager) createRoom(ctx context.Context, roomName, creatorScreenName string) (*WebAPIChatRoom, error) {
  410. room := &WebAPIChatRoom{
  411. RoomID: m.generateRoomID(),
  412. RoomName: roomName,
  413. Description: fmt.Sprintf("Chat room created by %s", creatorScreenName),
  414. RoomType: ChatRoomTypeUserCreated,
  415. CreatorScreenName: creatorScreenName,
  416. CreatedAt: time.Now().Unix(),
  417. MaxParticipants: 100,
  418. InstanceID: m.generateInstanceID(),
  419. }
  420. _, err := m.store.db.ExecContext(ctx, `
  421. INSERT INTO web_chat_rooms (room_id, room_name, description, room_type,
  422. category_id, creator_screen_name, created_at, max_participants)
  423. VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
  424. room.RoomID, room.RoomName, room.Description, room.RoomType,
  425. room.CategoryID, room.CreatorScreenName, room.CreatedAt, room.MaxParticipants)
  426. if err != nil {
  427. return nil, err
  428. }
  429. // Cache the room
  430. m.activeRooms[room.RoomID] = room
  431. return room, nil
  432. }
  433. func (m *WebAPIChatManager) getSessionByChatSID(ctx context.Context, chatsid string) (*ChatSession, error) {
  434. var session ChatSession
  435. err := m.store.db.QueryRowContext(ctx, `
  436. SELECT chat_sid, aimsid, room_id, screen_name, instance_id, joined_at, left_at
  437. FROM web_chat_sessions
  438. WHERE chat_sid = ?`,
  439. chatsid).Scan(
  440. &session.ChatSID, &session.AIMSid, &session.RoomID,
  441. &session.ScreenName, &session.InstanceID, &session.JoinedAt, &session.LeftAt)
  442. if err != nil {
  443. return nil, err
  444. }
  445. return &session, nil
  446. }
  447. func (m *WebAPIChatManager) getUserSessionInRoom(ctx context.Context, aimsid, roomID string) (*ChatSession, error) {
  448. var session ChatSession
  449. err := m.store.db.QueryRowContext(ctx, `
  450. SELECT chat_sid, aimsid, room_id, screen_name, instance_id, joined_at, left_at
  451. FROM web_chat_sessions
  452. WHERE aimsid = ? AND room_id = ? AND left_at IS NULL`,
  453. aimsid, roomID).Scan(
  454. &session.ChatSID, &session.AIMSid, &session.RoomID,
  455. &session.ScreenName, &session.InstanceID, &session.JoinedAt, &session.LeftAt)
  456. if err != nil {
  457. return nil, err
  458. }
  459. return &session, nil
  460. }
  461. func (m *WebAPIChatManager) getUserSessionInRoomByScreenName(ctx context.Context, roomID, screenName string) (*ChatSession, error) {
  462. var session ChatSession
  463. err := m.store.db.QueryRowContext(ctx, `
  464. SELECT chat_sid, aimsid, room_id, screen_name, instance_id, joined_at, left_at
  465. FROM web_chat_sessions
  466. WHERE room_id = ? AND screen_name = ? AND left_at IS NULL`,
  467. roomID, screenName).Scan(
  468. &session.ChatSID, &session.AIMSid, &session.RoomID,
  469. &session.ScreenName, &session.InstanceID, &session.JoinedAt, &session.LeftAt)
  470. if err != nil {
  471. return nil, err
  472. }
  473. return &session, nil
  474. }
  475. func (m *WebAPIChatManager) getParticipantCount(ctx context.Context, roomID string) (int, error) {
  476. var count int
  477. err := m.store.db.QueryRowContext(ctx, `
  478. SELECT COUNT(*) FROM web_chat_participants WHERE room_id = ?`,
  479. roomID).Scan(&count)
  480. return count, err
  481. }
  482. func (m *WebAPIChatManager) getParticipants(ctx context.Context, roomID string) ([]string, error) {
  483. rows, err := m.store.db.QueryContext(ctx, `
  484. SELECT screen_name FROM web_chat_participants WHERE room_id = ?`,
  485. roomID)
  486. if err != nil {
  487. return nil, err
  488. }
  489. defer rows.Close()
  490. var participants []string
  491. for rows.Next() {
  492. var screenName string
  493. if err := rows.Scan(&screenName); err != nil {
  494. continue
  495. }
  496. participants = append(participants, screenName)
  497. }
  498. return participants, nil
  499. }
  500. func (m *WebAPIChatManager) closeRoom(ctx context.Context, roomID string) {
  501. now := time.Now().Unix()
  502. m.store.db.ExecContext(ctx, `
  503. UPDATE web_chat_rooms SET closed_at = ? WHERE room_id = ?`,
  504. now, roomID)
  505. // Remove from cache
  506. delete(m.activeRooms, roomID)
  507. // Broadcast room closed event
  508. // Note: Broadcasting doesn't need context as it's fire-and-forget
  509. m.broadcastChatEvent(roomID, ChatEventData{
  510. EventType: ChatEventClosed,
  511. })
  512. }
  513. func (m *WebAPIChatManager) broadcastChatEvent(roomID string, event ChatEventData) {
  514. // Get all active sessions in the room
  515. rows, err := m.store.db.Query(`
  516. SELECT aimsid, chat_sid FROM web_chat_sessions
  517. WHERE room_id = ? AND left_at IS NULL`,
  518. roomID)
  519. if err != nil {
  520. m.logger.Error("failed to get sessions for broadcast", "error", err, "roomID", roomID)
  521. return
  522. }
  523. defer rows.Close()
  524. for rows.Next() {
  525. var aimsid, chatsid string
  526. if err := rows.Scan(&aimsid, &chatsid); err != nil {
  527. continue
  528. }
  529. // Update event with the recipient's chat session ID if not set
  530. if event.ChatSID == "" {
  531. event.ChatSID = chatsid
  532. }
  533. m.sendChatEventToUser(aimsid, event)
  534. }
  535. }
  536. func (m *WebAPIChatManager) sendChatEventToUser(aimsid string, event ChatEventData) {
  537. // Get the user's Web API session
  538. // Using background context for async event sending
  539. session, err := m.sessions.GetSession(context.Background(), aimsid)
  540. if err != nil {
  541. m.logger.Error("failed to get session for chat event", "error", err, "aimsid", aimsid)
  542. return
  543. }
  544. // Queue the chat event
  545. session.EventQueue.Push("chat", event)
  546. }
  547. func (m *WebAPIChatManager) generateRoomID() string {
  548. b := make([]byte, 16)
  549. rand.Read(b)
  550. return hex.EncodeToString(b)
  551. }
  552. func (m *WebAPIChatManager) generateChatSID() string {
  553. b := make([]byte, 16)
  554. rand.Read(b)
  555. return hex.EncodeToString(b)
  556. }
  557. func (m *WebAPIChatManager) generateInstanceID() int {
  558. // In production, this might be based on server instance or other factors
  559. // For now, use a simple random number
  560. return int(time.Now().Unix() % 1000000)
  561. }
  562. // GetRecentMessages returns recent messages from a chat room (for history)
  563. func (m *WebAPIChatManager) GetRecentMessages(ctx context.Context, roomID string, limit int) ([]*ChatMessage, error) {
  564. rows, err := m.store.db.QueryContext(ctx, `
  565. SELECT id, room_id, screen_name, message, whisper_target, timestamp
  566. FROM web_chat_messages
  567. WHERE room_id = ?
  568. ORDER BY timestamp DESC
  569. LIMIT ?`,
  570. roomID, limit)
  571. if err != nil {
  572. return nil, err
  573. }
  574. defer rows.Close()
  575. var messages []*ChatMessage
  576. for rows.Next() {
  577. var msg ChatMessage
  578. err := rows.Scan(&msg.ID, &msg.RoomID, &msg.ScreenName,
  579. &msg.Message, &msg.WhisperTarget, &msg.Timestamp)
  580. if err != nil {
  581. continue
  582. }
  583. messages = append(messages, &msg)
  584. }
  585. // Reverse to get chronological order
  586. for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 {
  587. messages[i], messages[j] = messages[j], messages[i]
  588. }
  589. return messages, nil
  590. }
  591. // CleanupInactiveSessions removes sessions that have been inactive for too long
  592. func (m *WebAPIChatManager) CleanupInactiveSessions(ctx context.Context) {
  593. m.mu.Lock()
  594. defer m.mu.Unlock()
  595. // Mark sessions as left if they've been inactive for more than 30 minutes
  596. cutoff := time.Now().Add(-30 * time.Minute).Unix()
  597. rows, err := m.store.db.QueryContext(ctx, `
  598. SELECT chat_sid, room_id, screen_name
  599. FROM web_chat_sessions
  600. WHERE left_at IS NULL AND joined_at < ?`,
  601. cutoff)
  602. if err != nil {
  603. m.logger.Error("failed to get inactive sessions", "error", err)
  604. return
  605. }
  606. defer rows.Close()
  607. for rows.Next() {
  608. var chatsid, roomID, screenName string
  609. if err := rows.Scan(&chatsid, &roomID, &screenName); err != nil {
  610. continue
  611. }
  612. // Mark as left
  613. now := time.Now().Unix()
  614. m.store.db.ExecContext(ctx, `UPDATE web_chat_sessions SET left_at = ? WHERE chat_sid = ?`, now, chatsid)
  615. m.store.db.ExecContext(ctx, `DELETE FROM web_chat_participants WHERE room_id = ? AND screen_name = ?`,
  616. roomID, screenName)
  617. // Broadcast user left
  618. // Note: Broadcasting doesn't need context as it's fire-and-forget
  619. m.broadcastChatEvent(roomID, ChatEventData{
  620. ChatSID: chatsid,
  621. EventType: ChatEventUserLeft,
  622. EventData: ChatUserEventData{
  623. ScreenName: screenName,
  624. Timestamp: now,
  625. },
  626. })
  627. }
  628. }