chat.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. package handlers
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "log/slog"
  6. "net/http"
  7. "strings"
  8. "github.com/mk6i/open-oscar-server/state"
  9. )
  10. // ChatHandler handles Web API chat endpoints
  11. type ChatHandler struct {
  12. SessionManager *state.WebAPISessionManager
  13. ChatManager *state.WebAPIChatManager
  14. Logger *slog.Logger
  15. }
  16. // CreateAndJoinChat creates (if needed) and joins a chat room
  17. // GET /chat/createAndJoinChat
  18. func (h *ChatHandler) CreateAndJoinChat(w http.ResponseWriter, r *http.Request) {
  19. // Extract parameters
  20. aimsid := r.URL.Query().Get("aimsid")
  21. roomID := r.URL.Query().Get("roomId")
  22. roomName := r.URL.Query().Get("roomName")
  23. // Validate session
  24. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  25. if err != nil {
  26. h.Logger.Error("invalid session", "aimsid", aimsid, "error", err)
  27. SendError(w, http.StatusUnauthorized, "Authentication Required")
  28. return
  29. }
  30. // Validate parameters - exactly one of roomId or roomName must be provided
  31. if (roomID == "" && roomName == "") || (roomID != "" && roomName != "") {
  32. SendError(w, http.StatusBadRequest, "Exactly one of roomId or roomName must be provided")
  33. return
  34. }
  35. // Create or join the chat room
  36. chatSession, room, err := h.ChatManager.CreateAndJoinChat(r.Context(), aimsid, roomID, roomName, string(session.ScreenName))
  37. if err != nil {
  38. h.Logger.Error("failed to create/join chat", "error", err, "aimsid", aimsid)
  39. // Determine appropriate error code
  40. statusCode := http.StatusInternalServerError
  41. message := "Internal Server Error"
  42. if strings.Contains(err.Error(), "maximum capacity") {
  43. statusCode = http.StatusServiceUnavailable
  44. message = "Room is at maximum capacity"
  45. } else if strings.Contains(err.Error(), "must be provided") {
  46. statusCode = http.StatusBadRequest
  47. message = err.Error()
  48. }
  49. SendError(w, statusCode, message)
  50. return
  51. }
  52. // Build response
  53. roomData := map[string]interface{}{
  54. "roomName": room.RoomName,
  55. "roomId": room.RoomID,
  56. "instanceId": room.InstanceID,
  57. "description": room.Description,
  58. "roomType": string(room.RoomType),
  59. }
  60. // Add category ID if present
  61. if room.CategoryID != "" {
  62. roomData["categoryId"] = room.CategoryID
  63. }
  64. response := BaseResponse{
  65. Response: ResponseBody{
  66. StatusCode: 200,
  67. StatusText: "OK",
  68. Data: map[string]interface{}{
  69. "chatsid": chatSession.ChatSID,
  70. "room": roomData,
  71. },
  72. },
  73. }
  74. // Send response
  75. SendResponse(w, r, response, h.Logger)
  76. h.Logger.Info("user joined chat room",
  77. "screenName", session.ScreenName,
  78. "roomName", room.RoomName,
  79. "roomID", room.RoomID,
  80. "chatsid", chatSession.ChatSID)
  81. }
  82. // SendMessage sends a message to a chat room
  83. // GET /chat/sendMessage
  84. func (h *ChatHandler) SendMessage(w http.ResponseWriter, r *http.Request) {
  85. // Extract parameters
  86. aimsid := r.URL.Query().Get("aimsid")
  87. chatsid := r.URL.Query().Get("chatsid")
  88. message := r.URL.Query().Get("message")
  89. whisperTarget := r.URL.Query().Get("whisperTarget")
  90. // Validate session
  91. _, err := h.SessionManager.GetSession(r.Context(), aimsid)
  92. if err != nil {
  93. h.Logger.Error("invalid session", "aimsid", aimsid, "error", err)
  94. SendError(w, http.StatusUnauthorized, "Authentication Required")
  95. return
  96. }
  97. // Validate required parameters
  98. if chatsid == "" {
  99. SendError(w, http.StatusBadRequest, "chatsid is required")
  100. return
  101. }
  102. if message == "" {
  103. SendError(w, http.StatusBadRequest, "message is required")
  104. return
  105. }
  106. // Send the message
  107. err = h.ChatManager.SendMessage(r.Context(), chatsid, message, whisperTarget)
  108. if err != nil {
  109. h.Logger.Error("failed to send message", "error", err, "chatsid", chatsid)
  110. // Determine appropriate error code
  111. statusCode := http.StatusInternalServerError
  112. message := "Internal Server Error"
  113. if strings.Contains(err.Error(), "invalid chat session") || strings.Contains(err.Error(), "user has left") {
  114. statusCode = http.StatusNotFound
  115. message = "Chat session not found"
  116. }
  117. SendError(w, statusCode, message)
  118. return
  119. }
  120. // Build response
  121. response := BaseResponse{
  122. Response: ResponseBody{
  123. StatusCode: 200,
  124. StatusText: "OK",
  125. Data: map[string]interface{}{},
  126. },
  127. }
  128. // Send response
  129. SendResponse(w, r, response, h.Logger)
  130. logMsg := "message sent to chat room"
  131. if whisperTarget != "" {
  132. logMsg = "whisper sent in chat room"
  133. }
  134. h.Logger.Debug(logMsg, "chatsid", chatsid, "whisperTarget", whisperTarget)
  135. }
  136. // SetTyping sets typing status for a chat room
  137. // GET /chat/setTyping
  138. func (h *ChatHandler) SetTyping(w http.ResponseWriter, r *http.Request) {
  139. // Extract parameters
  140. aimsid := r.URL.Query().Get("aimsid")
  141. chatsid := r.URL.Query().Get("chatsid")
  142. typingStatus := r.URL.Query().Get("typingStatus")
  143. // Validate session
  144. _, err := h.SessionManager.GetSession(r.Context(), aimsid)
  145. if err != nil {
  146. h.Logger.Error("invalid session", "aimsid", aimsid, "error", err)
  147. SendError(w, http.StatusUnauthorized, "Authentication Required")
  148. return
  149. }
  150. // Validate required parameters
  151. if chatsid == "" {
  152. SendError(w, http.StatusBadRequest, "chatsid is required")
  153. return
  154. }
  155. if typingStatus == "" {
  156. SendError(w, http.StatusBadRequest, "typingStatus is required")
  157. return
  158. }
  159. // Validate typing status value
  160. validStatuses := map[string]bool{
  161. "none": true,
  162. "typing": true,
  163. "typed": true,
  164. }
  165. if !validStatuses[typingStatus] {
  166. SendError(w, http.StatusBadRequest, "Invalid typingStatus value")
  167. return
  168. }
  169. // Set typing status
  170. err = h.ChatManager.SetTyping(r.Context(), chatsid, typingStatus)
  171. if err != nil {
  172. h.Logger.Error("failed to set typing status", "error", err, "chatsid", chatsid)
  173. // Determine appropriate error code
  174. statusCode := http.StatusInternalServerError
  175. errMessage := "Internal Server Error"
  176. if strings.Contains(err.Error(), "invalid chat session") || strings.Contains(err.Error(), "user has left") {
  177. statusCode = http.StatusNotFound
  178. errMessage = "Chat session not found"
  179. }
  180. SendError(w, statusCode, errMessage)
  181. return
  182. }
  183. // Build response
  184. response := BaseResponse{
  185. Response: ResponseBody{
  186. StatusCode: 200,
  187. StatusText: "OK",
  188. Data: map[string]interface{}{},
  189. },
  190. }
  191. // Send response
  192. SendResponse(w, r, response, h.Logger)
  193. h.Logger.Debug("typing status updated", "chatsid", chatsid, "status", typingStatus)
  194. }
  195. // LeaveChat leaves the current chat room
  196. // GET /chat/leaveChat
  197. func (h *ChatHandler) LeaveChat(w http.ResponseWriter, r *http.Request) {
  198. // Extract parameters
  199. aimsid := r.URL.Query().Get("aimsid")
  200. chatsid := r.URL.Query().Get("chatsid")
  201. // Validate session
  202. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  203. if err != nil {
  204. h.Logger.Error("invalid session", "aimsid", aimsid, "error", err)
  205. SendError(w, http.StatusUnauthorized, "Authentication Required")
  206. return
  207. }
  208. // Validate required parameters
  209. if chatsid == "" {
  210. SendError(w, http.StatusBadRequest, "chatsid is required")
  211. return
  212. }
  213. // Leave the chat room
  214. err = h.ChatManager.LeaveChat(r.Context(), chatsid)
  215. if err != nil {
  216. h.Logger.Error("failed to leave chat", "error", err, "chatsid", chatsid)
  217. // Determine appropriate error code
  218. statusCode := http.StatusInternalServerError
  219. message := "Internal Server Error"
  220. if strings.Contains(err.Error(), "invalid chat session") {
  221. statusCode = http.StatusNotFound
  222. message = "Chat session not found"
  223. }
  224. SendError(w, statusCode, message)
  225. return
  226. }
  227. // Build response
  228. response := BaseResponse{
  229. Response: ResponseBody{
  230. StatusCode: 200,
  231. StatusText: "OK",
  232. Data: map[string]interface{}{},
  233. },
  234. }
  235. // Send response
  236. SendResponse(w, r, response, h.Logger)
  237. h.Logger.Info("user left chat room",
  238. "screenName", session.ScreenName,
  239. "chatsid", chatsid)
  240. }
  241. // Helper to validate and convert typed JSON data for chat events
  242. func validateChatEventData(data json.RawMessage, eventType string) (interface{}, error) {
  243. switch eventType {
  244. case "message":
  245. var msgData state.ChatMessageEventData
  246. if err := json.Unmarshal(data, &msgData); err != nil {
  247. return nil, err
  248. }
  249. return msgData, nil
  250. case "userEntered", "userLeft":
  251. var userData state.ChatUserEventData
  252. if err := json.Unmarshal(data, &userData); err != nil {
  253. return nil, err
  254. }
  255. return userData, nil
  256. case "typing":
  257. var typingData state.ChatTypingEventData
  258. if err := json.Unmarshal(data, &typingData); err != nil {
  259. return nil, err
  260. }
  261. return typingData, nil
  262. case "userInRoom":
  263. var participantData state.ChatParticipantList
  264. if err := json.Unmarshal(data, &participantData); err != nil {
  265. return nil, err
  266. }
  267. return participantData, nil
  268. case "closed":
  269. // No additional data for closed event
  270. return nil, nil
  271. default:
  272. return nil, errors.New("unknown chat event type")
  273. }
  274. }