webapi_event_converter.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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.OfflineIMEvent); ok {
  44. eventData := map[string]interface{}{
  45. "aimId": imEvent.AimID,
  46. "message": imEvent.Message,
  47. "timestamp": imEvent.Timestamp, // Already float64
  48. "autoresponse": imEvent.AutoResp,
  49. }
  50. // The client keys its conversation list and chat-log cache by msgId, so
  51. // an event without one collides with every other offline message.
  52. if imEvent.MsgID != "" {
  53. eventData["msgId"] = imEvent.MsgID
  54. }
  55. result["eventData"] = eventData
  56. } else if dataMap, ok := event.Data.(map[string]interface{}); ok {
  57. // Already a map, ensure timestamps are float64
  58. if ts, exists := dataMap["timestamp"]; exists {
  59. if tsInt, ok := ts.(int64); ok {
  60. dataMap["timestamp"] = float64(tsInt)
  61. }
  62. }
  63. result["eventData"] = dataMap
  64. } else {
  65. result["eventData"] = event.Data
  66. }
  67. case types.EventTypePresence:
  68. if presenceEvent, ok := event.Data.(types.PresenceEvent); ok {
  69. eventData := map[string]interface{}{
  70. "aimId": presenceEvent.AimID,
  71. "state": presenceEvent.State,
  72. "userType": presenceEvent.UserType,
  73. }
  74. // Convert timestamp fields to float64
  75. if presenceEvent.OnlineTime > 0 {
  76. eventData["onlineTime"] = float64(presenceEvent.OnlineTime)
  77. }
  78. // This branch flattens PresenceEvent through an explicit allowlist, so
  79. // buddyIcon must be added here or it never reaches an AMF3 client.
  80. if presenceEvent.BuddyIcon != "" {
  81. eventData["buddyIcon"] = presenceEvent.BuddyIcon
  82. }
  83. result["eventData"] = eventData
  84. } else if dataMap, ok := event.Data.(map[string]interface{}); ok {
  85. // Already a map, ensure timestamps are float64
  86. if ot, exists := dataMap["onlineTime"]; exists {
  87. if otInt, ok := ot.(int64); ok {
  88. dataMap["onlineTime"] = float64(otInt)
  89. }
  90. }
  91. result["eventData"] = dataMap
  92. } else {
  93. result["eventData"] = event.Data
  94. }
  95. case types.EventType("myInfo"):
  96. // MyInfo events often contain timestamps
  97. if dataMap, ok := event.Data.(map[string]interface{}); ok {
  98. // Convert any int64 timestamps to float64
  99. for key, val := range dataMap {
  100. if key == "onlineTime" || key == "memberSince" || key == "awayTime" || key == "statusTime" {
  101. if intVal, ok := val.(int64); ok {
  102. dataMap[key] = float64(intVal)
  103. }
  104. }
  105. }
  106. result["eventData"] = dataMap
  107. } else {
  108. result["eventData"] = event.Data
  109. }
  110. case types.EventTypeBuddyList:
  111. // Just pass through
  112. result["eventData"] = event.Data
  113. case types.EventTypeTyping:
  114. result["eventData"] = event.Data
  115. case types.EventTypeSentIM:
  116. if sentIMEvent, ok := event.Data.(types.SentIMEvent); ok {
  117. // Gromit expects both 'source' (sender) and 'dest' (recipient) for sentIM
  118. // The parseIM function needs source even for outgoing messages
  119. eventData := map[string]interface{}{
  120. "source": map[string]interface{}{
  121. "aimId": sentIMEvent.Sender.AimID,
  122. "displayId": sentIMEvent.Sender.DisplayID,
  123. "userType": sentIMEvent.Sender.UserType,
  124. "state": "online",
  125. },
  126. "dest": map[string]interface{}{
  127. "aimId": sentIMEvent.Dest.AimID,
  128. "displayId": sentIMEvent.Dest.DisplayID,
  129. "userType": sentIMEvent.Dest.UserType,
  130. "state": "online",
  131. },
  132. "message": sentIMEvent.Message,
  133. "timestamp": sentIMEvent.Timestamp, // Already float64
  134. "autoresponse": sentIMEvent.AutoResp,
  135. }
  136. if sentIMEvent.MsgID != "" {
  137. eventData["msgId"] = sentIMEvent.MsgID
  138. }
  139. result["eventData"] = eventData
  140. } else {
  141. result["eventData"] = event.Data
  142. }
  143. default:
  144. // For unknown types, check if data is a map and convert any int64 values
  145. if dataMap, ok := event.Data.(map[string]interface{}); ok {
  146. result["eventData"] = convertTimestampsInMap(dataMap)
  147. } else {
  148. result["eventData"] = event.Data
  149. }
  150. }
  151. return result
  152. }
  153. // convertTimestampsInMap recursively converts int64 values that look like timestamps to float64
  154. func convertTimestampsInMap(data map[string]interface{}) map[string]interface{} {
  155. result := make(map[string]interface{})
  156. for key, val := range data {
  157. // Check if key suggests it's a timestamp
  158. if isTimestampField(key) {
  159. if intVal, ok := val.(int64); ok {
  160. result[key] = float64(intVal)
  161. continue
  162. }
  163. }
  164. // Recursively process nested maps
  165. if nestedMap, ok := val.(map[string]interface{}); ok {
  166. result[key] = convertTimestampsInMap(nestedMap)
  167. } else if nestedSlice, ok := val.([]interface{}); ok {
  168. convertedSlice := make([]interface{}, len(nestedSlice))
  169. for i, item := range nestedSlice {
  170. if itemMap, ok := item.(map[string]interface{}); ok {
  171. convertedSlice[i] = convertTimestampsInMap(itemMap)
  172. } else {
  173. convertedSlice[i] = item
  174. }
  175. }
  176. result[key] = convertedSlice
  177. } else {
  178. result[key] = val
  179. }
  180. }
  181. return result
  182. }
  183. // isTimestampField checks if a field name suggests it contains a timestamp
  184. func isTimestampField(fieldName string) bool {
  185. timestampFields := []string{
  186. "timestamp", "Timestamp",
  187. "onlineTime", "OnlineTime",
  188. "memberSince", "MemberSince",
  189. "awayTime", "AwayTime",
  190. "statusTime", "StatusTime",
  191. "idleTime", "IdleTime",
  192. "loginTime", "LoginTime",
  193. "createdAt", "CreatedAt",
  194. "updatedAt", "UpdatedAt",
  195. }
  196. for _, tf := range timestampFields {
  197. if fieldName == tf {
  198. return true
  199. }
  200. }
  201. return false
  202. }
  203. // ConvertEventsForAMF3 converts a slice of WebAPIEvents for AMF3 encoding
  204. func ConvertEventsForAMF3(events []types.Event) []interface{} {
  205. result := make([]interface{}, len(events))
  206. for i, event := range events {
  207. result[i] = ConvertEventForAMF3(event)
  208. }
  209. return result
  210. }