events.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. package types
  2. import (
  3. "context"
  4. "sync"
  5. "sync/atomic"
  6. "time"
  7. )
  8. // EventType defines the type of WebAPI event.
  9. type EventType string
  10. const (
  11. EventTypeBuddyList EventType = "buddylist"
  12. EventTypeConversation EventType = "conversation"
  13. EventTypeIM EventType = "im"
  14. EventTypeOfflineIM EventType = "offlineIM"
  15. EventTypePreference EventType = "preference"
  16. EventTypePresence EventType = "presence"
  17. EventTypeRateLimit EventType = "rateLimit"
  18. EventTypeSentIM EventType = "sentIM"
  19. EventTypeSessionEnded EventType = "sessionEnded"
  20. EventTypeStatus EventType = "status"
  21. EventTypeTyping EventType = "typing"
  22. EventTypePermitDeny EventType = "permitDeny"
  23. EventTypeImserv EventType = "imserv"
  24. )
  25. // Event represents an event to be delivered to a web client.
  26. type Event struct {
  27. Type EventType `json:"type"`
  28. SeqNum uint64 `json:"seqNum"`
  29. Timestamp int64 `json:"timestamp"`
  30. Data interface{} `json:"eventData"`
  31. }
  32. // PresenceEvent represents a presence change event.
  33. // Friendly repeats the viewer's alias for the user. The client's merge deletes any
  34. // alias it already holds, so a presence update that omits it silently renames the
  35. // buddy back to their screen name. See UserInfo.
  36. type PresenceEvent struct {
  37. AimID string `json:"aimId"`
  38. Friendly string `json:"friendly,omitempty"`
  39. State string `json:"state"` // "online", "offline", "away", "idle"
  40. StatusMsg string `json:"statusMsg,omitempty"`
  41. AwayMsg string `json:"awayMsg,omitempty"`
  42. IdleTime int `json:"idleTime,omitempty"` // Minutes idle
  43. OnlineTime int64 `json:"onlineTime,omitempty"` // Unix timestamp
  44. UserType string `json:"userType"` // "aim", "icq", "admin"
  45. BuddyIcon string `json:"buddyIcon,omitempty"` // Absolute icon URL; empty preserves the client's current icon, the placeholder URL clears it
  46. }
  47. // IMEvent represents an instant message event.
  48. type IMEvent struct {
  49. Source UserInfo `json:"source"`
  50. Message string `json:"message"`
  51. MsgID string `json:"msgId,omitempty"`
  52. Timestamp float64 `json:"timestamp"` // float64 for AMF3 encoding
  53. AutoResp bool `json:"autoresponse,omitempty"`
  54. }
  55. // RoomIMEvent represents a group-chat (imserv) room line delivered as an `im`
  56. // event. The web client keys the conversation by Imserv (the room id) via
  57. // Source.AimID, but reads the individual speaker and text from
  58. // SpecialData.ImFromImserv — see the client's qv/Fg/kl parsers. SpecialIM must be
  59. // "imservMsg" for the client to treat the line as a room message.
  60. type RoomIMEvent struct {
  61. Source UserInfo `json:"source"`
  62. Imserv string `json:"imserv"`
  63. SpecialIM string `json:"specialIM"`
  64. SpecialData RoomSpecialData `json:"specialData"`
  65. Message string `json:"message"`
  66. MsgID string `json:"msgId,omitempty"`
  67. Timestamp float64 `json:"timestamp"` // float64 for AMF3 encoding
  68. }
  69. // RoomSpecialData carries the imserv-specific payload of a RoomIMEvent.
  70. type RoomSpecialData struct {
  71. ImFromImserv RoomImFrom `json:"imFromImserv"`
  72. }
  73. // RoomInviteEvent represents a group-chat invitation, delivered (like a room
  74. // line) as an `im` event. The presence of SpecialData.Invitation is what flags
  75. // the event as an invite to the client (predicate `no()`); Imserv is the room id
  76. // the client passes verbatim to imserv/join to accept.
  77. type RoomInviteEvent struct {
  78. Source UserInfo `json:"source"`
  79. Imserv string `json:"imserv"`
  80. Message string `json:"message,omitempty"`
  81. MsgID string `json:"msgId,omitempty"`
  82. Timestamp float64 `json:"timestamp"` // float64 for AMF3 encoding
  83. SpecialData RoomInviteSpecial `json:"specialData"`
  84. }
  85. // RoomInviteSpecial wraps the invitation object.
  86. type RoomInviteSpecial struct {
  87. Invitation RoomInvitation `json:"invitation"`
  88. }
  89. // RoomInvitation identifies who invited the user and to which room. From is the
  90. // inviter's screen name (the client renders "<From> has invited you…");
  91. // GroupName is the room's friendly name.
  92. type RoomInvitation struct {
  93. From string `json:"from"`
  94. GroupName string `json:"groupName"`
  95. }
  96. // RoomImFrom identifies the speaker of a room line. The client prefers OrigSender
  97. // and falls back to Sender; both are set to the speaker's aimId. Text is the line
  98. // text (the client renders this in preference to the top-level Message).
  99. type RoomImFrom struct {
  100. OrigSender string `json:"origSender"`
  101. Sender string `json:"sender"`
  102. Text string `json:"text"`
  103. }
  104. // ImservEvent carries live group-chat activity (member join/leave) as its own
  105. // `imserv` event type. The client (parser `rB`) reads RecentActivities and, per
  106. // room, applies each activity to the roster (`fC`). See RoomActivity.
  107. type ImservEvent struct {
  108. RecentActivities []ImservRoomActivity `json:"recentActivities"`
  109. }
  110. // ImservRoomActivity groups a room's activity records under its room id.
  111. type ImservRoomActivity struct {
  112. Imserv string `json:"imserv"`
  113. Activities []ImservActivity `json:"activities"`
  114. }
  115. // ImservActivity is a single room activity record. Action is the discriminator
  116. // ("memberJoin" / "memberLeft"); the client takes the affected member from
  117. // Member1 for both. Member2 mirrors Member1 for a self join/leave. The friendly
  118. // name is optional and falls back to the screen name client-side.
  119. type ImservActivity struct {
  120. Action string `json:"action"`
  121. Member1 string `json:"member1"`
  122. Member2 string `json:"member2"`
  123. Member1FriendlyName string `json:"member1FriendlyName,omitempty"`
  124. Timestamp float64 `json:"timestamp"` // float64 for AMF3 encoding
  125. }
  126. // SentIMEvent represents a sent instant message event.
  127. type SentIMEvent struct {
  128. Sender UserInfo `json:"sender"` // Sender user info
  129. Dest UserInfo `json:"dest"` // Destination user info
  130. Message string `json:"message"`
  131. MsgID string `json:"msgId,omitempty"`
  132. Timestamp float64 `json:"timestamp"` // float64 for AMF3 encoding
  133. AutoResp bool `json:"autoResponse,omitempty"`
  134. }
  135. // UserInfo represents basic user information in events.
  136. // AimID is the normalized screen name the client keys users by. DisplayID is the
  137. // screen name as its owner formatted it. Friendly is the viewer's private alias for
  138. // that user, and takes precedence over DisplayID when the client renders a name.
  139. //
  140. // The client merges every user map it receives onto the single user object it holds
  141. // per aimId, and that merge deletes friendly before applying the map. An alias
  142. // therefore has to be repeated on every user map, or it is lost.
  143. type UserInfo struct {
  144. AimID string `json:"aimId"`
  145. DisplayID string `json:"displayId,omitempty"`
  146. Friendly string `json:"friendly,omitempty"`
  147. UserType string `json:"userType,omitempty"`
  148. State string `json:"state,omitempty"`
  149. OnlineTime float64 `json:"onlineTime,omitempty"` // float64 for AMF3 encoding
  150. }
  151. // TypingEvent represents a typing notification event.
  152. type TypingEvent struct {
  153. AimID string `json:"aimId"`
  154. TypingStatus string `json:"typingStatus"`
  155. }
  156. // EventQueue manages a queue of events for a WebAPI session.
  157. type EventQueue struct {
  158. events []Event
  159. seqNum uint64
  160. maxSize int
  161. mu sync.RWMutex
  162. waitChan chan struct{}
  163. closeChan chan struct{}
  164. closeOnce sync.Once
  165. }
  166. // isClosed reports whether Close has been called.
  167. func (q *EventQueue) isClosed() bool {
  168. select {
  169. case <-q.closeChan:
  170. return true
  171. default:
  172. return false
  173. }
  174. }
  175. // NewEventQueue creates a new event queue with the specified maximum size.
  176. func NewEventQueue(maxSize int) *EventQueue {
  177. return &EventQueue{
  178. events: make([]Event, 0),
  179. maxSize: maxSize,
  180. waitChan: make(chan struct{}, 1),
  181. closeChan: make(chan struct{}),
  182. }
  183. }
  184. // Push adds an event to the queue.
  185. func (q *EventQueue) Push(eventType EventType, data interface{}) {
  186. if q.isClosed() {
  187. return
  188. }
  189. q.mu.Lock()
  190. defer q.mu.Unlock()
  191. // Increment sequence number atomically
  192. seqNum := atomic.AddUint64(&q.seqNum, 1)
  193. event := Event{
  194. Type: eventType,
  195. SeqNum: seqNum,
  196. Timestamp: time.Now().Unix(),
  197. Data: data,
  198. }
  199. // Add event to queue
  200. q.events = append(q.events, event)
  201. // If queue exceeds max size, remove oldest events
  202. if len(q.events) > q.maxSize {
  203. // Keep only the most recent maxSize events
  204. q.events = q.events[len(q.events)-q.maxSize:]
  205. }
  206. // Signal any waiting fetchers
  207. select {
  208. case q.waitChan <- struct{}{}:
  209. default:
  210. // Channel already has a signal
  211. }
  212. }
  213. // Fetch retrieves events from the queue, optionally waiting for new events.
  214. func (q *EventQueue) Fetch(ctx context.Context, lastSeqNum uint64, timeout time.Duration) ([]Event, error) {
  215. if q.isClosed() {
  216. return []Event{}, nil
  217. }
  218. // First, check if we have any events newer than lastSeqNum
  219. q.mu.RLock()
  220. events := q.getEventsAfter(lastSeqNum)
  221. q.mu.RUnlock()
  222. if len(events) > 0 {
  223. return events, nil
  224. }
  225. // No events available, wait for new ones or timeout
  226. timeoutChan := time.After(timeout)
  227. for {
  228. select {
  229. case <-q.closeChan:
  230. return []Event{}, nil
  231. case <-q.waitChan:
  232. // New events may be available
  233. q.mu.RLock()
  234. events = q.getEventsAfter(lastSeqNum)
  235. q.mu.RUnlock()
  236. if len(events) > 0 {
  237. return events, nil
  238. }
  239. // False alarm, keep waiting
  240. case <-timeoutChan:
  241. // Timeout reached, return empty array
  242. return []Event{}, nil
  243. case <-ctx.Done():
  244. // Context cancelled
  245. return nil, ctx.Err()
  246. }
  247. }
  248. }
  249. // getEventsAfter returns all events with sequence number greater than the specified value.
  250. // Must be called with at least a read lock held.
  251. func (q *EventQueue) getEventsAfter(seqNum uint64) []Event {
  252. var result []Event
  253. for _, event := range q.events {
  254. if event.SeqNum > seqNum {
  255. result = append(result, event)
  256. }
  257. }
  258. return result
  259. }
  260. // GetAllEvents returns all events in the queue (for debugging).
  261. func (q *EventQueue) GetAllEvents() []Event {
  262. q.mu.RLock()
  263. defer q.mu.RUnlock()
  264. result := make([]Event, len(q.events))
  265. copy(result, q.events)
  266. return result
  267. }
  268. // Close closes the event queue, unblocking any waiting fetchers. Safe to call
  269. // more than once.
  270. func (q *EventQueue) Close() {
  271. q.closeOnce.Do(func() {
  272. close(q.closeChan)
  273. })
  274. }