events.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package handlers
  2. import (
  3. "context"
  4. "encoding/xml"
  5. "fmt"
  6. "log/slog"
  7. "net/http"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/mk6i/open-oscar-server/server/webapi/types"
  12. "github.com/mk6i/open-oscar-server/state"
  13. )
  14. // EventsHandler handles Web AIM API event fetching endpoints.
  15. type EventsHandler struct {
  16. SessionManager *state.WebAPISessionManager
  17. Logger *slog.Logger
  18. }
  19. // FetchEventsResponse represents the response for fetchEvents endpoint.
  20. type FetchEventsResponse struct {
  21. Response struct {
  22. StatusCode int `json:"statusCode"`
  23. StatusText string `json:"statusText"`
  24. Data FetchEventsData `json:"data"`
  25. } `json:"response"`
  26. }
  27. // FetchEventsData contains the events and metadata.
  28. type FetchEventsData struct {
  29. Events []types.Event `json:"events"`
  30. LastSeqNum uint64 `json:"lastSeqNum"`
  31. TimeToNextFetch int `json:"timeToNextFetch"`
  32. FetchBaseURL string `json:"fetchBaseURL"`
  33. }
  34. // FetchEventsXMLResponse represents the XML response for fetchEvents endpoint.
  35. type FetchEventsXMLResponse struct {
  36. XMLName xml.Name `xml:"response"`
  37. StatusCode int `xml:"statusCode"`
  38. StatusText string `xml:"statusText"`
  39. Data struct {
  40. Events []types.Event `xml:"events>event"`
  41. LastSeqNum uint64 `xml:"lastSeqNum"`
  42. TimeToNextFetch int `xml:"timeToNextFetch"`
  43. FetchBaseURL string `xml:"fetchBaseURL"`
  44. } `xml:"data"`
  45. }
  46. // FetchEvents handles GET /aim/fetchEvents requests with long-polling support.
  47. func (h *EventsHandler) FetchEvents(w http.ResponseWriter, r *http.Request) {
  48. ctx := r.Context()
  49. // Get session ID from parameters
  50. aimsid := r.URL.Query().Get("aimsid")
  51. if aimsid == "" {
  52. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  53. return
  54. }
  55. // Get session
  56. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  57. if err != nil {
  58. switch err {
  59. case state.ErrNoWebAPISession:
  60. h.sendError(w, http.StatusNotFound, "session not found")
  61. case state.ErrWebAPISessionExpired:
  62. h.sendError(w, http.StatusGone, "session expired")
  63. default:
  64. h.sendError(w, http.StatusInternalServerError, "internal server error")
  65. }
  66. return
  67. }
  68. // Touch the session to update last accessed time
  69. _ = h.SessionManager.TouchSession(r.Context(), aimsid)
  70. // Get sequence number parameter
  71. var lastSeqNum uint64
  72. if seqStr := r.URL.Query().Get("seqNum"); seqStr != "" {
  73. if val, err := strconv.ParseUint(seqStr, 10, 64); err == nil {
  74. lastSeqNum = val
  75. }
  76. }
  77. // Get timeout parameter (in seconds, convert to milliseconds)
  78. timeout := time.Duration(session.FetchTimeout) * time.Millisecond
  79. if timeoutStr := r.URL.Query().Get("timeout"); timeoutStr != "" {
  80. if val, err := strconv.Atoi(timeoutStr); err == nil && val > 0 {
  81. timeout = time.Duration(val) * time.Second
  82. }
  83. }
  84. // Limit maximum timeout to 60 seconds
  85. if timeout > 60*time.Second {
  86. timeout = 60 * time.Second
  87. }
  88. // Create a context with timeout for the fetch operation
  89. fetchCtx, cancel := context.WithTimeout(ctx, timeout)
  90. defer cancel()
  91. // Fetch events from the queue (will block until events available or timeout)
  92. events, err := session.EventQueue.Fetch(fetchCtx, lastSeqNum, timeout)
  93. if err != nil {
  94. if err == context.DeadlineExceeded {
  95. // Timeout is normal - return empty events array
  96. events = []types.Event{}
  97. } else {
  98. h.Logger.ErrorContext(ctx, "failed to fetch events", "err", err.Error())
  99. h.sendError(w, http.StatusInternalServerError, "failed to fetch events")
  100. return
  101. }
  102. }
  103. // Determine the last sequence number
  104. newLastSeqNum := lastSeqNum
  105. if len(events) > 0 {
  106. newLastSeqNum = events[len(events)-1].SeqNum
  107. }
  108. // Prepare response
  109. resp := FetchEventsResponse{}
  110. resp.Response.StatusCode = 200
  111. resp.Response.StatusText = "OK"
  112. resp.Response.Data.Events = events
  113. resp.Response.Data.LastSeqNum = newLastSeqNum
  114. resp.Response.Data.TimeToNextFetch = session.TimeToNextFetch
  115. // Include fetchBaseURL with updated sequence number for next request
  116. resp.Response.Data.FetchBaseURL = fmt.Sprintf("http://%s/aim/fetchEvents?aimsid=%s&seqNum=%d",
  117. r.Host, aimsid, newLastSeqNum)
  118. // Check response format
  119. format := strings.ToLower(r.URL.Query().Get("f"))
  120. switch format {
  121. case "xml":
  122. // Send XML response
  123. xmlResp := FetchEventsXMLResponse{}
  124. xmlResp.StatusCode = 200
  125. xmlResp.StatusText = "OK"
  126. xmlResp.Data.Events = events
  127. xmlResp.Data.LastSeqNum = newLastSeqNum
  128. xmlResp.Data.TimeToNextFetch = session.TimeToNextFetch
  129. xmlResp.Data.FetchBaseURL = fmt.Sprintf("http://%s/aim/fetchEvents?aimsid=%s&seqNum=%d",
  130. r.Host, aimsid, newLastSeqNum)
  131. w.Header().Set("Content-Type", "text/xml")
  132. _, _ = fmt.Fprint(w, `<?xml version="1.0" encoding="UTF-8"?>`)
  133. if err := xml.NewEncoder(w).Encode(xmlResp); err != nil {
  134. h.Logger.Error("failed to encode XML response", "error", err)
  135. }
  136. case "amf", "amf3":
  137. // For AMF3, build the response with fields in the correct order
  138. // The working implementation has: response { data {...}, statusCode, statusText, statusDetailCode }
  139. // Convert events to ensure timestamps are float64 for AMF3
  140. convertedEvents := ConvertEventsForAMF3(events)
  141. amfResp := map[string]interface{}{
  142. "response": map[string]interface{}{
  143. // Data comes FIRST (Gromit processes this large object)
  144. "data": map[string]interface{}{
  145. "events": convertedEvents,
  146. "lastSeqNum": newLastSeqNum,
  147. "timeToNextFetch": session.TimeToNextFetch,
  148. "fetchBaseURL": fmt.Sprintf("http://%s/aim/fetchEvents?aimsid=%s&seqNum=%d",
  149. r.Host, aimsid, newLastSeqNum),
  150. },
  151. // Status fields come AFTER data
  152. "statusCode": 200,
  153. "statusText": "OK",
  154. "statusDetailCode": 0,
  155. },
  156. }
  157. // Use SendResponse which will detect AMF format and encode properly
  158. SendResponse(w, r, amfResp, h.Logger)
  159. default:
  160. // Send JSON/JSONP response with standard structure
  161. SendResponse(w, r, resp, h.Logger)
  162. }
  163. if len(events) > 0 {
  164. h.Logger.DebugContext(ctx, "events fetched",
  165. "aimsid", aimsid,
  166. "count", len(events),
  167. "last_seq", newLastSeqNum,
  168. )
  169. }
  170. }
  171. // sendError is a convenience method that wraps the common SendError function.
  172. func (h *EventsHandler) sendError(w http.ResponseWriter, statusCode int, message string) {
  173. SendError(w, statusCode, message)
  174. }