events.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. package handlers
  2. import (
  3. "context"
  4. "fmt"
  5. "log/slog"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/mk6i/open-oscar-server/server/webapi/types"
  11. "github.com/mk6i/open-oscar-server/state"
  12. )
  13. // EventsHandler handles Web AIM API event fetching endpoints.
  14. type EventsHandler struct {
  15. SessionManager *state.WebAPISessionManager
  16. Logger *slog.Logger
  17. }
  18. // FetchEventsData contains the events and metadata.
  19. type FetchEventsData struct {
  20. Events []types.Event `json:"events" xml:"events>event"`
  21. LastSeqNum uint64 `json:"lastSeqNum" xml:"lastSeqNum"`
  22. TimeToNextFetch int `json:"timeToNextFetch" xml:"timeToNextFetch"`
  23. FetchBaseURL string `json:"fetchBaseURL" xml:"fetchBaseURL"`
  24. }
  25. // FetchEvents handles GET /aim/fetchEvents requests with long-polling support.
  26. func (h *EventsHandler) FetchEvents(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  27. ctx := r.Context()
  28. aimsid := session.AimSID
  29. // Get sequence number parameter
  30. var lastSeqNum uint64
  31. if seqStr := r.URL.Query().Get("seqNum"); seqStr != "" {
  32. if val, err := strconv.ParseUint(seqStr, 10, 64); err == nil {
  33. lastSeqNum = val
  34. }
  35. }
  36. // Timeout is in milliseconds (per Web API spec and client behavior).
  37. timeout := time.Duration(session.FetchTimeout) * time.Millisecond
  38. if timeoutStr := r.URL.Query().Get("timeout"); timeoutStr != "" {
  39. if val, err := strconv.Atoi(timeoutStr); err == nil && val > 0 {
  40. timeout = time.Duration(val) * time.Millisecond
  41. }
  42. }
  43. // Limit maximum timeout to 60 seconds
  44. if timeout > 60*time.Second {
  45. timeout = 60 * time.Second
  46. }
  47. // Create a context with timeout for the fetch operation
  48. fetchCtx, cancel := context.WithTimeout(ctx, timeout)
  49. defer cancel()
  50. // Fetch events from the queue (will block until events available or timeout)
  51. events, err := session.EventQueue.Fetch(fetchCtx, lastSeqNum, timeout)
  52. if err != nil {
  53. if err == context.DeadlineExceeded {
  54. // Timeout is normal - return empty events array
  55. events = []types.Event{}
  56. } else {
  57. h.Logger.ErrorContext(ctx, "failed to fetch events", "err", err.Error())
  58. h.sendError(w, r, http.StatusInternalServerError, "failed to fetch events")
  59. return
  60. }
  61. }
  62. // Determine the last sequence number
  63. newLastSeqNum := lastSeqNum
  64. if len(events) > 0 {
  65. newLastSeqNum = events[len(events)-1].SeqNum
  66. }
  67. // Prepare response
  68. resp := BaseResponse{}
  69. resp.Response.StatusCode = 200
  70. resp.Response.StatusText = "OK"
  71. resp.Response.Data = &FetchEventsData{
  72. Events: events,
  73. LastSeqNum: newLastSeqNum,
  74. TimeToNextFetch: session.TimeToNextFetch,
  75. // Include fetchBaseURL with updated sequence number for next request
  76. FetchBaseURL: fmt.Sprintf("%s/aim/fetchEvents?aimsid=%s&seqNum=%d",
  77. baseURLFromRequest(r), aimsid, newLastSeqNum),
  78. }
  79. // AMF3 clients (e.g. Gromit) take the events reshaped: timestamps as floats
  80. // and the source/dest user objects flattened. That is a payload difference,
  81. // not just an encoding one, so it stays here rather than in the encoder.
  82. format := strings.ToLower(r.URL.Query().Get("f"))
  83. if format == "amf" || format == "amf3" {
  84. amfResp := map[string]interface{}{
  85. "response": map[string]interface{}{
  86. "data": map[string]interface{}{
  87. "events": ConvertEventsForAMF3(events),
  88. "lastSeqNum": newLastSeqNum,
  89. "timeToNextFetch": session.TimeToNextFetch,
  90. "fetchBaseURL": fmt.Sprintf("%s/aim/fetchEvents?aimsid=%s&seqNum=%d",
  91. baseURLFromRequest(r), aimsid, newLastSeqNum),
  92. },
  93. "statusCode": 200,
  94. "statusText": "OK",
  95. "statusDetailCode": 0,
  96. },
  97. }
  98. SendResponse(w, r, amfResp, h.Logger)
  99. } else {
  100. // Send response in requested format (JSON, JSONP, or XML)
  101. SendResponse(w, r, resp, h.Logger)
  102. }
  103. if len(events) > 0 {
  104. h.Logger.DebugContext(ctx, "events fetched",
  105. "aimsid", aimsid,
  106. "count", len(events),
  107. "last_seq", newLastSeqNum,
  108. )
  109. }
  110. }
  111. // sendError is a convenience method that wraps the common SendError function.
  112. func (h *EventsHandler) sendError(w http.ResponseWriter, r *http.Request, statusCode int, message string) {
  113. SendError(w, r, statusCode, message)
  114. }