presence.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. package handlers
  2. import (
  3. "context"
  4. "log/slog"
  5. "net/http"
  6. "strings"
  7. "time"
  8. "github.com/mk6i/retro-aim-server/server/webapi/types"
  9. "github.com/mk6i/retro-aim-server/state"
  10. "github.com/mk6i/retro-aim-server/wire"
  11. )
  12. // PresenceHandler handles Web AIM API presence-related endpoints.
  13. type PresenceHandler struct {
  14. SessionManager *state.WebAPISessionManager
  15. SessionRetriever SessionRetriever
  16. FeedbagRetriever FeedbagRetriever
  17. BuddyBroadcaster BuddyBroadcaster
  18. ProfileManager ProfileManager
  19. RelationshipFetcher RelationshipFetcher
  20. Logger *slog.Logger
  21. }
  22. // BuddyBroadcaster broadcasts buddy presence updates
  23. type BuddyBroadcaster interface {
  24. BroadcastBuddyArrived(ctx context.Context, screenName state.IdentScreenName, userInfo wire.TLVUserInfo) error
  25. BroadcastBuddyDeparted(ctx context.Context, sess *state.Session) error
  26. }
  27. // ProfileManager manages user profiles
  28. type ProfileManager interface {
  29. SetProfile(ctx context.Context, screenName state.IdentScreenName, profile string) error
  30. Profile(ctx context.Context, screenName state.IdentScreenName) (string, error)
  31. }
  32. // PresenceData contains presence information.
  33. type PresenceData struct {
  34. Groups []BuddyGroupInfo `json:"groups,omitempty" xml:"groups>group,omitempty"`
  35. Users []BuddyPresenceInfo `json:"users,omitempty" xml:"users>user,omitempty"`
  36. }
  37. // BuddyGroupInfo represents a buddy group with its members.
  38. type BuddyGroupInfo struct {
  39. Name string `json:"name" xml:"name"`
  40. Buddies []BuddyPresenceInfo `json:"buddies" xml:"buddies>buddy"`
  41. }
  42. // BuddyPresenceInfo represents presence information for a buddy.
  43. type BuddyPresenceInfo struct {
  44. AimID string `json:"aimId" xml:"aimId"`
  45. State string `json:"state" xml:"state"` // "online", "offline", "away", "idle"
  46. StatusMsg string `json:"statusMsg,omitempty" xml:"statusMsg,omitempty"`
  47. AwayMsg string `json:"awayMsg,omitempty" xml:"awayMsg,omitempty"`
  48. IdleTime int `json:"idleTime,omitempty" xml:"idleTime,omitempty"`
  49. OnlineTime int64 `json:"onlineTime,omitempty" xml:"onlineTime,omitempty"`
  50. UserType string `json:"userType" xml:"userType"` // "aim", "icq", "admin"
  51. }
  52. // GetPresence handles GET /presence/get requests.
  53. func (h *PresenceHandler) GetPresence(w http.ResponseWriter, r *http.Request) {
  54. ctx := r.Context()
  55. // Get session ID from parameters
  56. aimsid := r.URL.Query().Get("aimsid")
  57. if aimsid == "" {
  58. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  59. return
  60. }
  61. // Get session
  62. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  63. if err != nil {
  64. if err == state.ErrNoWebAPISession {
  65. h.sendError(w, http.StatusNotFound, "session not found")
  66. } else if err == state.ErrWebAPISessionExpired {
  67. h.sendError(w, http.StatusGone, "session expired")
  68. } else {
  69. h.sendError(w, http.StatusInternalServerError, "internal server error")
  70. }
  71. return
  72. }
  73. // Touch the session
  74. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  75. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  76. }
  77. // Check if buddy list is requested
  78. getBuddyList := r.URL.Query().Get("bl") == "1"
  79. // Get target users if specified
  80. targetUsers := r.URL.Query().Get("t")
  81. // Prepare response
  82. resp := BaseResponse{}
  83. resp.Response.StatusCode = 200
  84. resp.Response.StatusText = "OK"
  85. // Create PresenceData struct to hold the response data
  86. presenceData := PresenceData{}
  87. if getBuddyList {
  88. // Retrieve buddy list from feedbag
  89. groups, err := h.getBuddyListGroups(ctx, session.ScreenName.IdentScreenName())
  90. if err != nil {
  91. h.Logger.ErrorContext(ctx, "failed to get buddy list", "err", err.Error())
  92. // Return empty buddy list on error instead of failing
  93. groups = []BuddyGroupInfo{}
  94. }
  95. presenceData.Groups = groups
  96. } else if targetUsers != "" {
  97. // Get presence for specific users
  98. users := strings.Split(targetUsers, ",")
  99. presenceList := make([]BuddyPresenceInfo, 0, len(users))
  100. for _, user := range users {
  101. user = strings.TrimSpace(user)
  102. if user == "" {
  103. continue
  104. }
  105. userScreenName := state.NewIdentScreenName(user)
  106. // Check blocking relationship (OSCAR compliant)
  107. rel, err := h.RelationshipFetcher.Relationship(ctx, session.ScreenName.IdentScreenName(), userScreenName)
  108. if err != nil {
  109. h.Logger.WarnContext(ctx, "failed to get relationship", "error", err)
  110. // On error, show as offline
  111. presence := BuddyPresenceInfo{
  112. AimID: user,
  113. State: "offline",
  114. UserType: "aim",
  115. }
  116. presenceList = append(presenceList, presence)
  117. continue
  118. }
  119. // OSCAR compliance: mutual invisibility when blocking
  120. if rel.YouBlock || rel.BlocksYou {
  121. presence := BuddyPresenceInfo{
  122. AimID: user,
  123. State: "offline",
  124. UserType: "aim",
  125. }
  126. presenceList = append(presenceList, presence)
  127. } else {
  128. presence := h.getUserPresence(userScreenName)
  129. presenceList = append(presenceList, presence)
  130. }
  131. }
  132. presenceData.Users = presenceList
  133. } else {
  134. // No specific request, return empty data
  135. presenceData.Groups = []BuddyGroupInfo{}
  136. }
  137. // Set the data to the response
  138. resp.Response.Data = presenceData
  139. // Send response in requested format
  140. SendResponse(w, r, resp, h.Logger)
  141. h.Logger.DebugContext(ctx, "presence retrieved",
  142. "aimsid", aimsid,
  143. "buddy_list", getBuddyList,
  144. "targets", targetUsers,
  145. )
  146. }
  147. // getBuddyListGroups retrieves the buddy list organized by groups.
  148. func (h *PresenceHandler) getBuddyListGroups(ctx context.Context, screenName state.IdentScreenName) ([]BuddyGroupInfo, error) {
  149. // Get feedbag items
  150. items, err := h.FeedbagRetriever.RetrieveFeedbag(ctx, screenName)
  151. if err != nil {
  152. return nil, err
  153. }
  154. // Organize items into groups
  155. groupMap := make(map[uint16]*BuddyGroupInfo)
  156. buddyToGroup := make(map[string]uint16)
  157. // First pass: identify groups
  158. for _, item := range items {
  159. if item.ClassID == wire.FeedbagClassIdGroup {
  160. name := item.Name
  161. if name == "" {
  162. name = "Buddies" // Default group name
  163. }
  164. groupMap[item.ItemID] = &BuddyGroupInfo{
  165. Name: name,
  166. Buddies: []BuddyPresenceInfo{},
  167. }
  168. }
  169. }
  170. // Second pass: add buddies to groups
  171. for _, item := range items {
  172. if item.ClassID == wire.FeedbagClassIdBuddy {
  173. // Get buddy screen name
  174. buddyName := item.Name
  175. if buddyName == "" {
  176. continue
  177. }
  178. // Find buddy's group
  179. groupID := item.GroupID
  180. buddyToGroup[buddyName] = groupID
  181. }
  182. }
  183. // If no groups exist, create a default one
  184. if len(groupMap) == 0 {
  185. groupMap[0] = &BuddyGroupInfo{
  186. Name: "Buddies",
  187. Buddies: []BuddyPresenceInfo{},
  188. }
  189. }
  190. // Add buddies to their groups with presence info
  191. for buddyName, groupID := range buddyToGroup {
  192. group, exists := groupMap[groupID]
  193. if !exists {
  194. // Put in first available group if group doesn't exist
  195. for _, g := range groupMap {
  196. group = g
  197. break
  198. }
  199. }
  200. buddyScreenName := state.NewIdentScreenName(buddyName)
  201. // Check blocking relationship (OSCAR compliant)
  202. rel, err := h.RelationshipFetcher.Relationship(ctx, screenName, buddyScreenName)
  203. if err != nil {
  204. h.Logger.WarnContext(ctx, "failed to get relationship", "error", err)
  205. // On error, include the buddy but they'll appear offline
  206. presence := BuddyPresenceInfo{
  207. AimID: buddyName,
  208. State: "offline",
  209. UserType: "aim",
  210. }
  211. group.Buddies = append(group.Buddies, presence)
  212. continue
  213. }
  214. // OSCAR compliance: mutual invisibility when blocking
  215. if rel.YouBlock || rel.BlocksYou {
  216. // Add them as offline to maintain buddy list structure
  217. presence := BuddyPresenceInfo{
  218. AimID: buddyName,
  219. State: "offline",
  220. UserType: "aim",
  221. }
  222. group.Buddies = append(group.Buddies, presence)
  223. } else {
  224. // Normal presence lookup
  225. presence := h.getUserPresence(buddyScreenName)
  226. group.Buddies = append(group.Buddies, presence)
  227. }
  228. }
  229. // Convert map to slice
  230. groups := make([]BuddyGroupInfo, 0, len(groupMap))
  231. for _, group := range groupMap {
  232. groups = append(groups, *group)
  233. }
  234. return groups, nil
  235. }
  236. // getUserPresence gets the current presence state for a user.
  237. func (h *PresenceHandler) getUserPresence(screenName state.IdentScreenName) BuddyPresenceInfo {
  238. // Default offline presence
  239. presence := BuddyPresenceInfo{
  240. AimID: screenName.String(),
  241. State: "offline",
  242. UserType: "aim",
  243. }
  244. // Check if user is online by looking for their OSCAR session
  245. if session := h.SessionRetriever.RetrieveSession(screenName); session != nil {
  246. presence.State = "online"
  247. // Check user status
  248. statusBitmask := session.UserStatusBitmask()
  249. if statusBitmask&wire.OServiceUserStatusAway != 0 {
  250. presence.State = "away"
  251. // TODO: Get away message from session
  252. } else if statusBitmask&wire.OServiceUserStatusDND != 0 {
  253. presence.State = "dnd"
  254. }
  255. // Check idle time
  256. if session.Idle() {
  257. presence.State = "idle"
  258. idleTime := time.Since(session.IdleTime())
  259. presence.IdleTime = int(idleTime.Minutes())
  260. }
  261. // Get online time
  262. presence.OnlineTime = session.SignonTime().Unix()
  263. // TODO: Get status message from profile
  264. }
  265. // Determine user type
  266. if strings.HasPrefix(screenName.String(), "admin") {
  267. presence.UserType = "admin"
  268. } else if isICQScreenName(screenName.String()) {
  269. presence.UserType = "icq"
  270. }
  271. return presence
  272. }
  273. // isICQScreenName checks if a screen name is an ICQ number.
  274. func isICQScreenName(screenName string) bool {
  275. if len(screenName) == 0 {
  276. return false
  277. }
  278. for _, r := range screenName {
  279. if r < '0' || r > '9' {
  280. return false
  281. }
  282. }
  283. return true
  284. }
  285. // sendError is a convenience method that wraps the common SendError function.
  286. func (h *PresenceHandler) sendError(w http.ResponseWriter, statusCode int, message string) {
  287. SendError(w, statusCode, message)
  288. }
  289. // SetState handles GET /presence/setState requests to update user's presence state.
  290. func (h *PresenceHandler) SetState(w http.ResponseWriter, r *http.Request) {
  291. ctx := r.Context()
  292. // Get session ID from parameters
  293. aimsid := r.URL.Query().Get("aimsid")
  294. if aimsid == "" {
  295. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  296. return
  297. }
  298. // Get session
  299. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  300. if err != nil {
  301. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  302. return
  303. }
  304. // Update session activity
  305. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  306. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  307. }
  308. // Get the requested state
  309. stateParam := r.URL.Query().Get("state")
  310. awayMsg := r.URL.Query().Get("awayMsg")
  311. // Get OSCAR session if available
  312. oscarSession := session.OSCARSession
  313. if oscarSession == nil {
  314. // For web-only sessions, we'll need to track state in the WebAPI session
  315. // For now, just store in event data
  316. h.Logger.WarnContext(ctx, "no OSCAR session for presence update", "aimsid", aimsid)
  317. // Still send success response
  318. response := BaseResponse{}
  319. response.Response.StatusCode = 200
  320. response.Response.StatusText = "OK"
  321. SendResponse(w, r, response, h.Logger)
  322. return
  323. }
  324. // Map web state to OSCAR status bits
  325. var statusBitmask uint32
  326. switch stateParam {
  327. case "online":
  328. statusBitmask = 0x0000 // Clear all status bits
  329. oscarSession.SetAwayMessage("")
  330. case "away":
  331. statusBitmask = wire.OServiceUserStatusAway
  332. if awayMsg != "" {
  333. oscarSession.SetAwayMessage(awayMsg)
  334. }
  335. case "invisible":
  336. statusBitmask = wire.OServiceUserStatusInvisible
  337. case "dnd":
  338. statusBitmask = wire.OServiceUserStatusDND
  339. default:
  340. h.sendError(w, http.StatusBadRequest, "invalid state parameter")
  341. return
  342. }
  343. // Update OSCAR session status
  344. oscarSession.SetUserStatusBitmask(statusBitmask)
  345. // Broadcast presence update
  346. if statusBitmask&wire.OServiceUserStatusInvisible != 0 {
  347. // User going invisible - broadcast departure
  348. if err := h.BuddyBroadcaster.BroadcastBuddyDeparted(ctx, oscarSession); err != nil {
  349. h.Logger.ErrorContext(ctx, "failed to broadcast buddy departed", "err", err.Error())
  350. }
  351. } else {
  352. // User visible - broadcast arrival/update
  353. if err := h.BuddyBroadcaster.BroadcastBuddyArrived(ctx, oscarSession.IdentScreenName(), oscarSession.TLVUserInfo()); err != nil {
  354. h.Logger.ErrorContext(ctx, "failed to broadcast buddy arrived", "err", err.Error())
  355. }
  356. }
  357. // Queue presence event for other WebAPI sessions watching this user
  358. h.broadcastPresenceEvent(session.ScreenName.IdentScreenName(), stateParam, awayMsg, "")
  359. h.Logger.InfoContext(ctx, "presence state updated",
  360. "screenName", session.ScreenName.String(),
  361. "state", stateParam,
  362. "hasAwayMsg", awayMsg != "",
  363. )
  364. // Send success response
  365. response := BaseResponse{}
  366. response.Response.StatusCode = 200
  367. response.Response.StatusText = "OK"
  368. SendResponse(w, r, response, h.Logger)
  369. }
  370. // SetStatus handles GET /presence/setStatus requests to update user's status message.
  371. func (h *PresenceHandler) SetStatus(w http.ResponseWriter, r *http.Request) {
  372. ctx := r.Context()
  373. // Get session ID from parameters
  374. aimsid := r.URL.Query().Get("aimsid")
  375. if aimsid == "" {
  376. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  377. return
  378. }
  379. // Get session
  380. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  381. if err != nil {
  382. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  383. return
  384. }
  385. // Update session activity
  386. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  387. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  388. }
  389. // Get the status message
  390. statusMsg := r.URL.Query().Get("statusMsg")
  391. statusCode := r.URL.Query().Get("statusCode")
  392. // Store status message in session (this would normally be stored in a profile/status service)
  393. // For now, we'll broadcast it as part of presence
  394. // Get OSCAR session if available
  395. if oscarSession := session.OSCARSession; oscarSession != nil {
  396. // In OSCAR, status messages are typically part of the profile
  397. // We'll need to extend this based on the actual implementation
  398. // Broadcast presence update with new status
  399. if err := h.BuddyBroadcaster.BroadcastBuddyArrived(ctx, oscarSession.IdentScreenName(), oscarSession.TLVUserInfo()); err != nil {
  400. h.Logger.ErrorContext(ctx, "failed to broadcast status update", "err", err.Error())
  401. }
  402. }
  403. // Queue status event for other WebAPI sessions
  404. h.broadcastPresenceEvent(session.ScreenName.IdentScreenName(), "", "", statusMsg)
  405. h.Logger.InfoContext(ctx, "status message updated",
  406. "screenName", session.ScreenName.String(),
  407. "statusMsg", statusMsg,
  408. "statusCode", statusCode,
  409. )
  410. // Send success response
  411. response := BaseResponse{}
  412. response.Response.StatusCode = 200
  413. response.Response.StatusText = "OK"
  414. SendResponse(w, r, response, h.Logger)
  415. }
  416. // SetProfile handles GET /presence/setProfile requests to update user's profile.
  417. func (h *PresenceHandler) SetProfile(w http.ResponseWriter, r *http.Request) {
  418. ctx := r.Context()
  419. // Get session ID from parameters
  420. aimsid := r.URL.Query().Get("aimsid")
  421. if aimsid == "" {
  422. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  423. return
  424. }
  425. // Get session
  426. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  427. if err != nil {
  428. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  429. return
  430. }
  431. // Update session activity
  432. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  433. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  434. }
  435. // Get the profile content
  436. profile := r.URL.Query().Get("profile")
  437. // Limit profile size (4KB max)
  438. if len(profile) > 4096 {
  439. h.sendError(w, http.StatusBadRequest, "profile too large (max 4KB)")
  440. return
  441. }
  442. // Save profile using ProfileManager
  443. if err := h.ProfileManager.SetProfile(ctx, session.ScreenName.IdentScreenName(), profile); err != nil {
  444. h.Logger.ErrorContext(ctx, "failed to set profile", "err", err.Error())
  445. h.sendError(w, http.StatusInternalServerError, "failed to save profile")
  446. return
  447. }
  448. h.Logger.InfoContext(ctx, "profile updated",
  449. "screenName", session.ScreenName.String(),
  450. "profileSize", len(profile),
  451. )
  452. // Send success response
  453. response := BaseResponse{}
  454. response.Response.StatusCode = 200
  455. response.Response.StatusText = "OK"
  456. SendResponse(w, r, response, h.Logger)
  457. }
  458. // GetProfile handles GET /presence/getProfile requests to retrieve user's profile.
  459. func (h *PresenceHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
  460. ctx := r.Context()
  461. // Get session ID from parameters
  462. aimsid := r.URL.Query().Get("aimsid")
  463. if aimsid == "" {
  464. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  465. return
  466. }
  467. // Get session
  468. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  469. if err != nil {
  470. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  471. return
  472. }
  473. // Update session activity
  474. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  475. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  476. }
  477. // Get target screen name (optional - defaults to self)
  478. targetSN := r.URL.Query().Get("sn")
  479. if targetSN == "" {
  480. targetSN = session.ScreenName.String()
  481. }
  482. // Retrieve profile using ProfileManager
  483. profile, err := h.ProfileManager.Profile(ctx, state.NewIdentScreenName(targetSN))
  484. if err != nil {
  485. h.Logger.WarnContext(ctx, "failed to get profile", "err", err.Error())
  486. // Return empty profile on error
  487. profile = ""
  488. }
  489. // Send response
  490. responseData := map[string]interface{}{
  491. "screenName": targetSN,
  492. "profile": profile,
  493. "lastUpdated": time.Now().Unix(),
  494. }
  495. response := BaseResponse{}
  496. response.Response.StatusCode = 200
  497. response.Response.StatusText = "OK"
  498. response.Response.Data = responseData
  499. SendResponse(w, r, response, h.Logger)
  500. }
  501. // Icon handles GET /presence/icon requests for presence icons.
  502. func (h *PresenceHandler) Icon(w http.ResponseWriter, r *http.Request) {
  503. // Get parameters
  504. name := r.URL.Query().Get("name")
  505. size := r.URL.Query().Get("size")
  506. iconType := r.URL.Query().Get("type")
  507. if name == "" {
  508. h.sendError(w, http.StatusBadRequest, "missing name parameter")
  509. return
  510. }
  511. // Default values
  512. if size == "" {
  513. size = "32"
  514. }
  515. if iconType == "" {
  516. iconType = "aim"
  517. }
  518. // For now, redirect to a placeholder icon
  519. // In production, this would redirect to actual icon storage/CDN
  520. iconURL := "/static/icons/default_" + iconType + "_" + size + ".png"
  521. // If it's an email lookup, extract username
  522. if strings.Contains(name, "@") {
  523. parts := strings.Split(name, "@")
  524. if len(parts) > 0 {
  525. name = parts[0]
  526. }
  527. }
  528. // Check if user is online and get their state
  529. screenName := state.NewIdentScreenName(name)
  530. if session := h.SessionRetriever.RetrieveSession(screenName); session != nil {
  531. statusBitmask := session.UserStatusBitmask()
  532. if statusBitmask&wire.OServiceUserStatusAway != 0 {
  533. iconURL = "/static/icons/away_" + iconType + "_" + size + ".png"
  534. } else if session.Idle() {
  535. iconURL = "/static/icons/idle_" + iconType + "_" + size + ".png"
  536. } else {
  537. iconURL = "/static/icons/online_" + iconType + "_" + size + ".png"
  538. }
  539. } else {
  540. iconURL = "/static/icons/offline_" + iconType + "_" + size + ".png"
  541. }
  542. // Redirect to icon URL
  543. http.Redirect(w, r, iconURL, http.StatusFound)
  544. }
  545. // broadcastPresenceEvent sends presence updates to all WebAPI sessions watching this user
  546. func (h *PresenceHandler) broadcastPresenceEvent(screenName state.IdentScreenName, stateStr, awayMsg, statusMsg string) {
  547. // Get all sessions that have this user in their buddy list
  548. // For now, we'll broadcast to all sessions (this should be optimized)
  549. // Using background context as this is an async broadcast operation
  550. for _, sess := range h.SessionManager.GetAllSessions(context.Background()) {
  551. if sess.EventQueue != nil && sess.Events != nil {
  552. // Check if session is subscribed to presence events
  553. for _, event := range sess.Events {
  554. if event == "presence" || event == "myInfo" {
  555. eventData := types.PresenceEvent{
  556. AimID: screenName.String(),
  557. State: stateStr,
  558. AwayMsg: awayMsg,
  559. StatusMsg: statusMsg,
  560. }
  561. sess.EventQueue.Push(types.EventTypePresence, eventData)
  562. break
  563. }
  564. }
  565. }
  566. }
  567. }