events.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  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, session *state.WebAPISession) {
  48. ctx := r.Context()
  49. aimsid := session.AimSID
  50. // Get sequence number parameter
  51. var lastSeqNum uint64
  52. if seqStr := r.URL.Query().Get("seqNum"); seqStr != "" {
  53. if val, err := strconv.ParseUint(seqStr, 10, 64); err == nil {
  54. lastSeqNum = val
  55. }
  56. }
  57. // Timeout is in milliseconds (per Web API spec and client behavior).
  58. timeout := time.Duration(session.FetchTimeout) * time.Millisecond
  59. if timeoutStr := r.URL.Query().Get("timeout"); timeoutStr != "" {
  60. if val, err := strconv.Atoi(timeoutStr); err == nil && val > 0 {
  61. timeout = time.Duration(val) * time.Millisecond
  62. }
  63. }
  64. // Limit maximum timeout to 60 seconds
  65. if timeout > 60*time.Second {
  66. timeout = 60 * time.Second
  67. }
  68. // Create a context with timeout for the fetch operation
  69. fetchCtx, cancel := context.WithTimeout(ctx, timeout)
  70. defer cancel()
  71. // Fetch events from the queue (will block until events available or timeout)
  72. events, err := session.EventQueue.Fetch(fetchCtx, lastSeqNum, timeout)
  73. if err != nil {
  74. if err == context.DeadlineExceeded {
  75. // Timeout is normal - return empty events array
  76. events = []types.Event{}
  77. } else {
  78. h.Logger.ErrorContext(ctx, "failed to fetch events", "err", err.Error())
  79. h.sendError(w, http.StatusInternalServerError, "failed to fetch events")
  80. return
  81. }
  82. }
  83. // Determine the last sequence number
  84. newLastSeqNum := lastSeqNum
  85. if len(events) > 0 {
  86. newLastSeqNum = events[len(events)-1].SeqNum
  87. }
  88. // Prepare response
  89. resp := FetchEventsResponse{}
  90. resp.Response.StatusCode = 200
  91. resp.Response.StatusText = "OK"
  92. resp.Response.Data.Events = events
  93. resp.Response.Data.LastSeqNum = newLastSeqNum
  94. resp.Response.Data.TimeToNextFetch = session.TimeToNextFetch
  95. // Include fetchBaseURL with updated sequence number for next request
  96. resp.Response.Data.FetchBaseURL = fmt.Sprintf("http://%s/aim/fetchEvents?aimsid=%s&seqNum=%d",
  97. r.Host, aimsid, newLastSeqNum)
  98. // Check response format
  99. format := strings.ToLower(r.URL.Query().Get("f"))
  100. switch format {
  101. case "xml":
  102. // Send XML response
  103. xmlResp := FetchEventsXMLResponse{}
  104. xmlResp.StatusCode = 200
  105. xmlResp.StatusText = "OK"
  106. xmlResp.Data.Events = events
  107. xmlResp.Data.LastSeqNum = newLastSeqNum
  108. xmlResp.Data.TimeToNextFetch = session.TimeToNextFetch
  109. xmlResp.Data.FetchBaseURL = fmt.Sprintf("http://%s/aim/fetchEvents?aimsid=%s&seqNum=%d",
  110. r.Host, aimsid, newLastSeqNum)
  111. w.Header().Set("Content-Type", "text/xml")
  112. _, _ = fmt.Fprint(w, `<?xml version="1.0" encoding="UTF-8"?>`)
  113. if err := xml.NewEncoder(w).Encode(xmlResp); err != nil {
  114. h.Logger.Error("failed to encode XML response", "error", err)
  115. }
  116. case "amf", "amf3":
  117. // For AMF3, build the response with fields in the correct order
  118. // The working implementation has: response { data {...}, statusCode, statusText, statusDetailCode }
  119. // Convert events to ensure timestamps are float64 for AMF3
  120. convertedEvents := ConvertEventsForAMF3(events)
  121. amfResp := map[string]interface{}{
  122. "response": map[string]interface{}{
  123. // Data comes FIRST (Gromit processes this large object)
  124. "data": map[string]interface{}{
  125. "events": convertedEvents,
  126. "lastSeqNum": newLastSeqNum,
  127. "timeToNextFetch": session.TimeToNextFetch,
  128. "fetchBaseURL": fmt.Sprintf("http://%s/aim/fetchEvents?aimsid=%s&seqNum=%d",
  129. r.Host, aimsid, newLastSeqNum),
  130. },
  131. // Status fields come AFTER data
  132. "statusCode": 200,
  133. "statusText": "OK",
  134. "statusDetailCode": 0,
  135. },
  136. }
  137. // Use SendResponse which will detect AMF format and encode properly
  138. SendResponse(w, r, amfResp, h.Logger)
  139. default:
  140. // Send JSON/JSONP response with standard structure
  141. SendResponse(w, r, resp, h.Logger)
  142. }
  143. if len(events) > 0 {
  144. h.Logger.DebugContext(ctx, "events fetched",
  145. "aimsid", aimsid,
  146. "count", len(events),
  147. "last_seq", newLastSeqNum,
  148. )
  149. }
  150. }
  151. // sendError is a convenience method that wraps the common SendError function.
  152. func (h *EventsHandler) sendError(w http.ResponseWriter, statusCode int, message string) {
  153. SendError(w, statusCode, message)
  154. }