webapi_event_converter.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. package handlers
  2. import "github.com/mk6i/open-oscar-server/server/webapi/types"
  3. // ConvertEventForAMF3 converts a WebAPIEvent to a map suitable for AMF3 encoding,
  4. // ensuring all timestamps are float64 to avoid uint29 overflow issues.
  5. func ConvertEventForAMF3(event types.Event) map[string]interface{} {
  6. result := map[string]interface{}{
  7. "type": string(event.Type),
  8. "seqNum": event.SeqNum,
  9. "timestamp": float64(event.Timestamp), // Convert to float64
  10. }
  11. // Convert event data based on type
  12. switch event.Type {
  13. case types.EventTypeIM:
  14. if imEvent, ok := event.Data.(types.IMEvent); ok {
  15. // Gromit expects 'source' as a user object and 'autoresponse' (lowercase)
  16. eventData := map[string]interface{}{
  17. "source": map[string]interface{}{
  18. "aimId": imEvent.Source.AimID,
  19. "displayId": imEvent.Source.DisplayID,
  20. "userType": imEvent.Source.UserType,
  21. "state": imEvent.Source.State,
  22. },
  23. "message": imEvent.Message,
  24. "timestamp": imEvent.Timestamp, // Already float64
  25. "autoresponse": imEvent.AutoResp,
  26. }
  27. if imEvent.MsgID != "" {
  28. eventData["msgId"] = imEvent.MsgID
  29. }
  30. result["eventData"] = eventData
  31. } else if dataMap, ok := event.Data.(map[string]interface{}); ok {
  32. // Already a map, ensure timestamps are float64
  33. if ts, exists := dataMap["timestamp"]; exists {
  34. if tsInt, ok := ts.(int64); ok {
  35. dataMap["timestamp"] = float64(tsInt)
  36. }
  37. }
  38. result["eventData"] = dataMap
  39. } else {
  40. result["eventData"] = event.Data
  41. }
  42. case types.EventTypeOfflineIM:
  43. if imEvent, ok := event.Data.(types.IMEvent); ok {
  44. result["eventData"] = map[string]interface{}{
  45. "aimId": imEvent.Source.AimID,
  46. "message": imEvent.Message,
  47. "timestamp": float64(imEvent.Timestamp), // Convert to float64
  48. }
  49. } else if dataMap, ok := event.Data.(map[string]interface{}); ok {
  50. // Already a map, ensure timestamps are float64
  51. if ts, exists := dataMap["timestamp"]; exists {
  52. if tsInt, ok := ts.(int64); ok {
  53. dataMap["timestamp"] = float64(tsInt)
  54. }
  55. }
  56. result["eventData"] = dataMap
  57. } else {
  58. result["eventData"] = event.Data
  59. }
  60. case types.EventTypePresence:
  61. if presenceEvent, ok := event.Data.(types.PresenceEvent); ok {
  62. eventData := map[string]interface{}{
  63. "aimId": presenceEvent.AimID,
  64. "state": presenceEvent.State,
  65. "userType": presenceEvent.UserType,
  66. }
  67. // Convert timestamp fields to float64
  68. if presenceEvent.OnlineTime > 0 {
  69. eventData["onlineTime"] = float64(presenceEvent.OnlineTime)
  70. }
  71. // This branch flattens PresenceEvent through an explicit allowlist, so
  72. // buddyIcon must be added here or it never reaches an AMF3 client.
  73. if presenceEvent.BuddyIcon != "" {
  74. eventData["buddyIcon"] = presenceEvent.BuddyIcon
  75. }
  76. result["eventData"] = eventData
  77. } else if dataMap, ok := event.Data.(map[string]interface{}); ok {
  78. // Already a map, ensure timestamps are float64
  79. if ot, exists := dataMap["onlineTime"]; exists {
  80. if otInt, ok := ot.(int64); ok {
  81. dataMap["onlineTime"] = float64(otInt)
  82. }
  83. }
  84. result["eventData"] = dataMap
  85. } else {
  86. result["eventData"] = event.Data
  87. }
  88. case types.EventType("myInfo"):
  89. // MyInfo events often contain timestamps
  90. if dataMap, ok := event.Data.(map[string]interface{}); ok {
  91. // Convert any int64 timestamps to float64
  92. for key, val := range dataMap {
  93. if key == "onlineTime" || key == "memberSince" || key == "awayTime" || key == "statusTime" {
  94. if intVal, ok := val.(int64); ok {
  95. dataMap[key] = float64(intVal)
  96. }
  97. }
  98. }
  99. result["eventData"] = dataMap
  100. } else {
  101. result["eventData"] = event.Data
  102. }
  103. case types.EventTypeBuddyList:
  104. // Buddy list events are already converted to maps in FormatBuddyListEvent
  105. // Just pass through
  106. result["eventData"] = event.Data
  107. case types.EventTypeTyping:
  108. result["eventData"] = event.Data
  109. case types.EventTypeSentIM:
  110. if sentIMEvent, ok := event.Data.(types.SentIMEvent); ok {
  111. // Gromit expects both 'source' (sender) and 'dest' (recipient) for sentIM
  112. // The parseIM function needs source even for outgoing messages
  113. eventData := map[string]interface{}{
  114. "source": map[string]interface{}{
  115. "aimId": sentIMEvent.Sender.AimID,
  116. "displayId": sentIMEvent.Sender.DisplayID,
  117. "userType": sentIMEvent.Sender.UserType,
  118. "state": "online",
  119. },
  120. "dest": map[string]interface{}{
  121. "aimId": sentIMEvent.Dest.AimID,
  122. "displayId": sentIMEvent.Dest.DisplayID,
  123. "userType": sentIMEvent.Dest.UserType,
  124. "state": "online",
  125. },
  126. "message": sentIMEvent.Message,
  127. "timestamp": sentIMEvent.Timestamp, // Already float64
  128. "autoresponse": sentIMEvent.AutoResp,
  129. }
  130. if sentIMEvent.MsgID != "" {
  131. eventData["msgId"] = sentIMEvent.MsgID
  132. }
  133. result["eventData"] = eventData
  134. } else {
  135. result["eventData"] = event.Data
  136. }
  137. default:
  138. // For unknown types, check if data is a map and convert any int64 values
  139. if dataMap, ok := event.Data.(map[string]interface{}); ok {
  140. result["eventData"] = convertTimestampsInMap(dataMap)
  141. } else {
  142. result["eventData"] = event.Data
  143. }
  144. }
  145. return result
  146. }
  147. // convertTimestampsInMap recursively converts int64 values that look like timestamps to float64
  148. func convertTimestampsInMap(data map[string]interface{}) map[string]interface{} {
  149. result := make(map[string]interface{})
  150. for key, val := range data {
  151. // Check if key suggests it's a timestamp
  152. if isTimestampField(key) {
  153. if intVal, ok := val.(int64); ok {
  154. result[key] = float64(intVal)
  155. continue
  156. }
  157. }
  158. // Recursively process nested maps
  159. if nestedMap, ok := val.(map[string]interface{}); ok {
  160. result[key] = convertTimestampsInMap(nestedMap)
  161. } else if nestedSlice, ok := val.([]interface{}); ok {
  162. convertedSlice := make([]interface{}, len(nestedSlice))
  163. for i, item := range nestedSlice {
  164. if itemMap, ok := item.(map[string]interface{}); ok {
  165. convertedSlice[i] = convertTimestampsInMap(itemMap)
  166. } else {
  167. convertedSlice[i] = item
  168. }
  169. }
  170. result[key] = convertedSlice
  171. } else {
  172. result[key] = val
  173. }
  174. }
  175. return result
  176. }
  177. // isTimestampField checks if a field name suggests it contains a timestamp
  178. func isTimestampField(fieldName string) bool {
  179. timestampFields := []string{
  180. "timestamp", "Timestamp",
  181. "onlineTime", "OnlineTime",
  182. "memberSince", "MemberSince",
  183. "awayTime", "AwayTime",
  184. "statusTime", "StatusTime",
  185. "idleTime", "IdleTime",
  186. "loginTime", "LoginTime",
  187. "createdAt", "CreatedAt",
  188. "updatedAt", "UpdatedAt",
  189. }
  190. for _, tf := range timestampFields {
  191. if fieldName == tf {
  192. return true
  193. }
  194. }
  195. return false
  196. }
  197. // ConvertEventsForAMF3 converts a slice of WebAPIEvents for AMF3 encoding
  198. func ConvertEventsForAMF3(events []types.Event) []interface{} {
  199. result := make([]interface{}, len(events))
  200. for i, event := range events {
  201. result[i] = ConvertEventForAMF3(event)
  202. }
  203. return result
  204. }