presence.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. package handlers
  2. import (
  3. "context"
  4. "log/slog"
  5. "net/http"
  6. "strings"
  7. "time"
  8. "github.com/mk6i/open-oscar-server/server/webapi/types"
  9. "github.com/mk6i/open-oscar-server/state"
  10. "github.com/mk6i/open-oscar-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, instance *state.SessionInstance) error
  26. }
  27. // ProfileManager manages user profiles (uses types.ProfileManager)
  28. type ProfileManager interface {
  29. SetProfile(ctx context.Context, screenName state.IdentScreenName, profile state.UserProfile) error
  30. Profile(ctx context.Context, screenName state.IdentScreenName) (state.UserProfile, 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. if session.Away() {
  249. presence.State = "away"
  250. // TODO: Get away message from session
  251. } else if session.AllUserStatusBitmask(wire.OServiceUserStatusDND) {
  252. presence.State = "dnd"
  253. }
  254. // Check idle time
  255. if session.Idle() {
  256. presence.State = "idle"
  257. idleTime := time.Since(session.IdleTime())
  258. presence.IdleTime = int(idleTime.Minutes())
  259. }
  260. // Get online time
  261. presence.OnlineTime = session.SignonTime().Unix()
  262. // TODO: Get status message from profile
  263. }
  264. // Determine user type
  265. if strings.HasPrefix(screenName.String(), "admin") {
  266. presence.UserType = "admin"
  267. } else if isICQScreenName(screenName.String()) {
  268. presence.UserType = "icq"
  269. }
  270. return presence
  271. }
  272. // isICQScreenName checks if a screen name is an ICQ number.
  273. func isICQScreenName(screenName string) bool {
  274. if len(screenName) == 0 {
  275. return false
  276. }
  277. for _, r := range screenName {
  278. if r < '0' || r > '9' {
  279. return false
  280. }
  281. }
  282. return true
  283. }
  284. // sendError is a convenience method that wraps the common SendError function.
  285. func (h *PresenceHandler) sendError(w http.ResponseWriter, statusCode int, message string) {
  286. SendError(w, statusCode, message)
  287. }
  288. // SetState handles GET /presence/setState requests to update user's presence state.
  289. func (h *PresenceHandler) SetState(w http.ResponseWriter, r *http.Request) {
  290. ctx := r.Context()
  291. // Get session ID from parameters
  292. aimsid := r.URL.Query().Get("aimsid")
  293. if aimsid == "" {
  294. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  295. return
  296. }
  297. // Get session
  298. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  299. if err != nil {
  300. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  301. return
  302. }
  303. // Update session activity
  304. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  305. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  306. }
  307. // Get the requested state
  308. stateParam := r.URL.Query().Get("state")
  309. awayMsg := r.URL.Query().Get("awayMsg")
  310. // Get OSCAR session if available
  311. oscarSession := session.OSCARSession
  312. if oscarSession == nil {
  313. // For web-only sessions, we'll need to track state in the WebAPI session
  314. // For now, just store in event data
  315. h.Logger.WarnContext(ctx, "no OSCAR session for presence update", "aimsid", aimsid)
  316. // Still send success response
  317. response := BaseResponse{}
  318. response.Response.StatusCode = 200
  319. response.Response.StatusText = "OK"
  320. SendResponse(w, r, response, h.Logger)
  321. return
  322. }
  323. // Map web state to OSCAR status bits
  324. var statusBitmask uint32
  325. switch stateParam {
  326. case "online":
  327. statusBitmask = 0x0000 // Clear all status bits
  328. oscarSession.SetAwayMessage("")
  329. case "away":
  330. statusBitmask = wire.OServiceUserStatusAway
  331. if awayMsg != "" {
  332. oscarSession.SetAwayMessage(awayMsg)
  333. }
  334. case "invisible":
  335. statusBitmask = wire.OServiceUserStatusInvisible
  336. case "dnd":
  337. statusBitmask = wire.OServiceUserStatusDND
  338. default:
  339. h.sendError(w, http.StatusBadRequest, "invalid state parameter")
  340. return
  341. }
  342. // Update OSCAR session status
  343. oscarSession.SetUserStatusBitmask(statusBitmask)
  344. // Broadcast presence update
  345. if statusBitmask&wire.OServiceUserStatusInvisible != 0 {
  346. // User going invisible - broadcast departure
  347. if err := h.BuddyBroadcaster.BroadcastBuddyDeparted(ctx, oscarSession); err != nil {
  348. h.Logger.ErrorContext(ctx, "failed to broadcast buddy departed", "err", err.Error())
  349. }
  350. } else {
  351. // User visible - broadcast arrival/update
  352. if err := h.BuddyBroadcaster.BroadcastBuddyArrived(ctx, oscarSession.IdentScreenName(), oscarSession.Session().TLVUserInfo()); err != nil {
  353. h.Logger.ErrorContext(ctx, "failed to broadcast buddy arrived", "err", err.Error())
  354. }
  355. }
  356. // Queue presence event for other WebAPI sessions watching this user
  357. h.broadcastPresenceEvent(session.ScreenName.IdentScreenName(), stateParam, awayMsg, "")
  358. h.Logger.InfoContext(ctx, "presence state updated",
  359. "screenName", session.ScreenName.String(),
  360. "state", stateParam,
  361. "hasAwayMsg", awayMsg != "",
  362. )
  363. // Send success response
  364. response := BaseResponse{}
  365. response.Response.StatusCode = 200
  366. response.Response.StatusText = "OK"
  367. SendResponse(w, r, response, h.Logger)
  368. }
  369. // SetStatus handles GET /presence/setStatus requests to update user's status message.
  370. func (h *PresenceHandler) SetStatus(w http.ResponseWriter, r *http.Request) {
  371. ctx := r.Context()
  372. // Get session ID from parameters
  373. aimsid := r.URL.Query().Get("aimsid")
  374. if aimsid == "" {
  375. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  376. return
  377. }
  378. // Get session
  379. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  380. if err != nil {
  381. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  382. return
  383. }
  384. // Update session activity
  385. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  386. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  387. }
  388. // Get the status message
  389. statusMsg := r.URL.Query().Get("statusMsg")
  390. statusCode := r.URL.Query().Get("statusCode")
  391. // Store status message in session (this would normally be stored in a profile/status service)
  392. // For now, we'll broadcast it as part of presence
  393. // Get OSCAR session if available
  394. if oscarSession := session.OSCARSession; oscarSession != nil {
  395. // In OSCAR, status messages are typically part of the profile
  396. // We'll need to extend this based on the actual implementation
  397. // Broadcast presence update with new status
  398. if err := h.BuddyBroadcaster.BroadcastBuddyArrived(ctx, oscarSession.IdentScreenName(), oscarSession.Session().TLVUserInfo()); err != nil {
  399. h.Logger.ErrorContext(ctx, "failed to broadcast status update", "err", err.Error())
  400. }
  401. }
  402. // Queue status event for other WebAPI sessions
  403. h.broadcastPresenceEvent(session.ScreenName.IdentScreenName(), "", "", statusMsg)
  404. h.Logger.InfoContext(ctx, "status message updated",
  405. "screenName", session.ScreenName.String(),
  406. "statusMsg", statusMsg,
  407. "statusCode", statusCode,
  408. )
  409. // Send success response
  410. response := BaseResponse{}
  411. response.Response.StatusCode = 200
  412. response.Response.StatusText = "OK"
  413. SendResponse(w, r, response, h.Logger)
  414. }
  415. // SetProfile handles GET /presence/setProfile requests to update user's profile.
  416. func (h *PresenceHandler) SetProfile(w http.ResponseWriter, r *http.Request) {
  417. ctx := r.Context()
  418. // Get session ID from parameters
  419. aimsid := r.URL.Query().Get("aimsid")
  420. if aimsid == "" {
  421. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  422. return
  423. }
  424. // Get session
  425. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  426. if err != nil {
  427. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  428. return
  429. }
  430. // Update session activity
  431. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  432. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  433. }
  434. // Get the profile content
  435. profileText := r.URL.Query().Get("profile")
  436. // Limit profile size (4KB max)
  437. if len(profileText) > 4096 {
  438. h.sendError(w, http.StatusBadRequest, "profile too large (max 4KB)")
  439. return
  440. }
  441. // Save profile using ProfileManager
  442. profile := state.UserProfile{
  443. ProfileText: profileText,
  444. UpdateTime: time.Now().UTC(),
  445. }
  446. if err := h.ProfileManager.SetProfile(ctx, session.ScreenName.IdentScreenName(), profile); err != nil {
  447. h.Logger.ErrorContext(ctx, "failed to set profile", "err", err.Error())
  448. h.sendError(w, http.StatusInternalServerError, "failed to save profile")
  449. return
  450. }
  451. h.Logger.InfoContext(ctx, "profile updated",
  452. "screenName", session.ScreenName.String(),
  453. "profileSize", len(profileText),
  454. )
  455. // Send success response
  456. response := BaseResponse{}
  457. response.Response.StatusCode = 200
  458. response.Response.StatusText = "OK"
  459. SendResponse(w, r, response, h.Logger)
  460. }
  461. // GetProfile handles GET /presence/getProfile requests to retrieve user's profile.
  462. func (h *PresenceHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
  463. ctx := r.Context()
  464. // Get session ID from parameters
  465. aimsid := r.URL.Query().Get("aimsid")
  466. if aimsid == "" {
  467. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  468. return
  469. }
  470. // Get session
  471. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  472. if err != nil {
  473. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  474. return
  475. }
  476. // Update session activity
  477. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  478. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  479. }
  480. // Get target screen name (optional - defaults to self)
  481. targetSN := r.URL.Query().Get("sn")
  482. if targetSN == "" {
  483. targetSN = session.ScreenName.String()
  484. }
  485. // Retrieve profile using ProfileManager
  486. profile, err := h.ProfileManager.Profile(ctx, state.NewIdentScreenName(targetSN))
  487. if err != nil {
  488. h.Logger.WarnContext(ctx, "failed to get profile", "err", err.Error())
  489. // Return empty profile on error
  490. profile = state.UserProfile{}
  491. }
  492. // Send response
  493. lastUpdated := int64(0)
  494. if !profile.UpdateTime.IsZero() {
  495. lastUpdated = profile.UpdateTime.Unix()
  496. }
  497. responseData := map[string]interface{}{
  498. "screenName": targetSN,
  499. "profile": profile.ProfileText,
  500. "lastUpdated": lastUpdated,
  501. }
  502. response := BaseResponse{}
  503. response.Response.StatusCode = 200
  504. response.Response.StatusText = "OK"
  505. response.Response.Data = responseData
  506. SendResponse(w, r, response, h.Logger)
  507. }
  508. // Icon handles GET /presence/icon requests for presence icons.
  509. func (h *PresenceHandler) Icon(w http.ResponseWriter, r *http.Request) {
  510. // Get parameters
  511. name := r.URL.Query().Get("name")
  512. size := r.URL.Query().Get("size")
  513. iconType := r.URL.Query().Get("type")
  514. if name == "" {
  515. h.sendError(w, http.StatusBadRequest, "missing name parameter")
  516. return
  517. }
  518. // Default values
  519. if size == "" {
  520. size = "32"
  521. }
  522. if iconType == "" {
  523. iconType = "aim"
  524. }
  525. // For now, redirect to a placeholder icon
  526. // In production, this would redirect to actual icon storage/CDN
  527. iconURL := "/static/icons/default_" + iconType + "_" + size + ".png"
  528. // If it's an email lookup, extract username
  529. if strings.Contains(name, "@") {
  530. parts := strings.Split(name, "@")
  531. if len(parts) > 0 {
  532. name = parts[0]
  533. }
  534. }
  535. // Check if user is online and get their state
  536. screenName := state.NewIdentScreenName(name)
  537. if session := h.SessionRetriever.RetrieveSession(screenName); session != nil {
  538. if session.Away() {
  539. iconURL = "/static/icons/away_" + iconType + "_" + size + ".png"
  540. } else if session.Idle() {
  541. iconURL = "/static/icons/idle_" + iconType + "_" + size + ".png"
  542. } else {
  543. iconURL = "/static/icons/online_" + iconType + "_" + size + ".png"
  544. }
  545. } else {
  546. iconURL = "/static/icons/offline_" + iconType + "_" + size + ".png"
  547. }
  548. // Redirect to icon URL
  549. http.Redirect(w, r, iconURL, http.StatusFound)
  550. }
  551. // broadcastPresenceEvent sends presence updates to all WebAPI sessions watching this user
  552. func (h *PresenceHandler) broadcastPresenceEvent(screenName state.IdentScreenName, stateStr, awayMsg, statusMsg string) {
  553. // Get all sessions that have this user in their buddy list
  554. // For now, we'll broadcast to all sessions (this should be optimized)
  555. // Using background context as this is an async broadcast operation
  556. for _, sess := range h.SessionManager.GetAllSessions(context.Background()) {
  557. if sess.EventQueue != nil && sess.Events != nil {
  558. // Check if session is subscribed to presence events
  559. for _, event := range sess.Events {
  560. if event == "presence" || event == "myInfo" {
  561. eventData := types.PresenceEvent{
  562. AimID: screenName.String(),
  563. State: stateStr,
  564. AwayMsg: awayMsg,
  565. StatusMsg: statusMsg,
  566. }
  567. sess.EventQueue.Push(types.EventTypePresence, eventData)
  568. break
  569. }
  570. }
  571. }
  572. }
  573. }