presence.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  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, screenName state.IdentScreenName) 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. switch err {
  65. case state.ErrNoWebAPISession:
  66. h.sendError(w, http.StatusNotFound, "session not found")
  67. case state.ErrWebAPISessionExpired:
  68. h.sendError(w, http.StatusGone, "session expired")
  69. default:
  70. h.sendError(w, http.StatusInternalServerError, "internal server error")
  71. }
  72. return
  73. }
  74. // Touch the session
  75. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  76. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  77. }
  78. // Check if buddy list is requested
  79. getBuddyList := r.URL.Query().Get("bl") == "1"
  80. // Get target users if specified
  81. targetUsers := r.URL.Query().Get("t")
  82. // Prepare response
  83. resp := BaseResponse{}
  84. resp.Response.StatusCode = 200
  85. resp.Response.StatusText = "OK"
  86. // Create PresenceData struct to hold the response data
  87. presenceData := PresenceData{}
  88. if getBuddyList {
  89. // Retrieve buddy list from feedbag
  90. groups, err := h.getBuddyListGroups(ctx, session.ScreenName.IdentScreenName())
  91. if err != nil {
  92. h.Logger.ErrorContext(ctx, "failed to get buddy list", "err", err.Error())
  93. // Return empty buddy list on error instead of failing
  94. groups = []BuddyGroupInfo{}
  95. }
  96. presenceData.Groups = groups
  97. } else if targetUsers != "" {
  98. // Get presence for specific users
  99. users := strings.Split(targetUsers, ",")
  100. presenceList := make([]BuddyPresenceInfo, 0, len(users))
  101. for _, user := range users {
  102. user = strings.TrimSpace(user)
  103. if user == "" {
  104. continue
  105. }
  106. userScreenName := state.NewIdentScreenName(user)
  107. // Check blocking relationship (OSCAR compliant)
  108. rel, err := h.RelationshipFetcher.Relationship(ctx, session.ScreenName.IdentScreenName(), userScreenName)
  109. if err != nil {
  110. h.Logger.WarnContext(ctx, "failed to get relationship", "error", err)
  111. // On error, show as offline
  112. presence := BuddyPresenceInfo{
  113. AimID: user,
  114. State: "offline",
  115. UserType: "aim",
  116. }
  117. presenceList = append(presenceList, presence)
  118. continue
  119. }
  120. // OSCAR compliance: mutual invisibility when blocking
  121. if rel.YouBlock || rel.BlocksYou {
  122. presence := BuddyPresenceInfo{
  123. AimID: user,
  124. State: "offline",
  125. UserType: "aim",
  126. }
  127. presenceList = append(presenceList, presence)
  128. } else {
  129. presence := h.getUserPresence(userScreenName)
  130. presenceList = append(presenceList, presence)
  131. }
  132. }
  133. presenceData.Users = presenceList
  134. } else {
  135. // No specific request, return empty data
  136. presenceData.Groups = []BuddyGroupInfo{}
  137. }
  138. // Set the data to the response
  139. resp.Response.Data = presenceData
  140. // Send response in requested format
  141. SendResponse(w, r, resp, h.Logger)
  142. h.Logger.DebugContext(ctx, "presence retrieved",
  143. "aimsid", aimsid,
  144. "buddy_list", getBuddyList,
  145. "targets", targetUsers,
  146. )
  147. }
  148. // getBuddyListGroups retrieves the buddy list organized by groups.
  149. func (h *PresenceHandler) getBuddyListGroups(ctx context.Context, screenName state.IdentScreenName) ([]BuddyGroupInfo, error) {
  150. // Get feedbag items
  151. items, err := h.FeedbagRetriever.RetrieveFeedbag(ctx, screenName)
  152. if err != nil {
  153. return nil, err
  154. }
  155. // Organize items into groups
  156. groupMap := make(map[uint16]*BuddyGroupInfo)
  157. buddyToGroup := make(map[string]uint16)
  158. // First pass: identify groups
  159. for _, item := range items {
  160. if item.ClassID == wire.FeedbagClassIdGroup {
  161. name := item.Name
  162. if name == "" {
  163. name = "Buddies" // Default group name
  164. }
  165. groupMap[item.ItemID] = &BuddyGroupInfo{
  166. Name: name,
  167. Buddies: []BuddyPresenceInfo{},
  168. }
  169. }
  170. }
  171. // Second pass: add buddies to groups
  172. for _, item := range items {
  173. if item.ClassID == wire.FeedbagClassIdBuddy {
  174. // Get buddy screen name
  175. buddyName := item.Name
  176. if buddyName == "" {
  177. continue
  178. }
  179. // Find buddy's group
  180. groupID := item.GroupID
  181. buddyToGroup[buddyName] = groupID
  182. }
  183. }
  184. // If no groups exist, create a default one
  185. if len(groupMap) == 0 {
  186. groupMap[0] = &BuddyGroupInfo{
  187. Name: "Buddies",
  188. Buddies: []BuddyPresenceInfo{},
  189. }
  190. }
  191. // Add buddies to their groups with presence info
  192. for buddyName, groupID := range buddyToGroup {
  193. group, exists := groupMap[groupID]
  194. if !exists {
  195. // Put in first available group if group doesn't exist
  196. for _, g := range groupMap {
  197. group = g
  198. break
  199. }
  200. }
  201. buddyScreenName := state.NewIdentScreenName(buddyName)
  202. // Check blocking relationship (OSCAR compliant)
  203. rel, err := h.RelationshipFetcher.Relationship(ctx, screenName, buddyScreenName)
  204. if err != nil {
  205. h.Logger.WarnContext(ctx, "failed to get relationship", "error", err)
  206. // On error, include the buddy but they'll appear offline
  207. presence := BuddyPresenceInfo{
  208. AimID: buddyName,
  209. State: "offline",
  210. UserType: "aim",
  211. }
  212. group.Buddies = append(group.Buddies, presence)
  213. continue
  214. }
  215. // OSCAR compliance: mutual invisibility when blocking
  216. if rel.YouBlock || rel.BlocksYou {
  217. // Add them as offline to maintain buddy list structure
  218. presence := BuddyPresenceInfo{
  219. AimID: buddyName,
  220. State: "offline",
  221. UserType: "aim",
  222. }
  223. group.Buddies = append(group.Buddies, presence)
  224. } else {
  225. // Normal presence lookup
  226. presence := h.getUserPresence(buddyScreenName)
  227. group.Buddies = append(group.Buddies, presence)
  228. }
  229. }
  230. // Convert map to slice
  231. groups := make([]BuddyGroupInfo, 0, len(groupMap))
  232. for _, group := range groupMap {
  233. groups = append(groups, *group)
  234. }
  235. return groups, nil
  236. }
  237. // getUserPresence gets the current presence state for a user.
  238. func (h *PresenceHandler) getUserPresence(screenName state.IdentScreenName) BuddyPresenceInfo {
  239. // Default offline presence
  240. presence := BuddyPresenceInfo{
  241. AimID: screenName.String(),
  242. State: "offline",
  243. UserType: "aim",
  244. }
  245. // Check if user is online by looking for their OSCAR session
  246. if session := h.SessionRetriever.RetrieveSession(screenName); session != nil {
  247. presence.State = "online"
  248. // Check user status
  249. if session.Away() {
  250. presence.State = "away"
  251. // TODO: Get away message from session
  252. } else if session.AllUserStatusBitmask(wire.OServiceUserStatusDND) {
  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.IdentScreenName()); 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.Session().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.Session().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. profileText := r.URL.Query().Get("profile")
  437. // Limit profile size (4KB max)
  438. if len(profileText) > 4096 {
  439. h.sendError(w, http.StatusBadRequest, "profile too large (max 4KB)")
  440. return
  441. }
  442. // Save profile using ProfileManager
  443. profile := state.UserProfile{
  444. ProfileText: profileText,
  445. UpdateTime: time.Now().UTC(),
  446. }
  447. if err := h.ProfileManager.SetProfile(ctx, session.ScreenName.IdentScreenName(), profile); err != nil {
  448. h.Logger.ErrorContext(ctx, "failed to set profile", "err", err.Error())
  449. h.sendError(w, http.StatusInternalServerError, "failed to save profile")
  450. return
  451. }
  452. h.Logger.InfoContext(ctx, "profile updated",
  453. "screenName", session.ScreenName.String(),
  454. "profileSize", len(profileText),
  455. )
  456. // Send success response
  457. response := BaseResponse{}
  458. response.Response.StatusCode = 200
  459. response.Response.StatusText = "OK"
  460. SendResponse(w, r, response, h.Logger)
  461. }
  462. // GetProfile handles GET /presence/getProfile requests to retrieve user's profile.
  463. func (h *PresenceHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
  464. ctx := r.Context()
  465. // Get session ID from parameters
  466. aimsid := r.URL.Query().Get("aimsid")
  467. if aimsid == "" {
  468. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  469. return
  470. }
  471. // Get session
  472. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  473. if err != nil {
  474. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  475. return
  476. }
  477. // Update session activity
  478. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  479. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  480. }
  481. // Get target screen name (optional - defaults to self)
  482. targetSN := r.URL.Query().Get("sn")
  483. if targetSN == "" {
  484. targetSN = session.ScreenName.String()
  485. }
  486. // Retrieve profile using ProfileManager
  487. profile, err := h.ProfileManager.Profile(ctx, state.NewIdentScreenName(targetSN))
  488. if err != nil {
  489. h.Logger.WarnContext(ctx, "failed to get profile", "err", err.Error())
  490. // Return empty profile on error
  491. profile = state.UserProfile{}
  492. }
  493. // Send response
  494. lastUpdated := int64(0)
  495. if !profile.UpdateTime.IsZero() {
  496. lastUpdated = profile.UpdateTime.Unix()
  497. }
  498. responseData := map[string]interface{}{
  499. "screenName": targetSN,
  500. "profile": profile.ProfileText,
  501. "lastUpdated": lastUpdated,
  502. }
  503. response := BaseResponse{}
  504. response.Response.StatusCode = 200
  505. response.Response.StatusText = "OK"
  506. response.Response.Data = responseData
  507. SendResponse(w, r, response, h.Logger)
  508. }
  509. // Icon handles GET /presence/icon requests for presence icons.
  510. func (h *PresenceHandler) Icon(w http.ResponseWriter, r *http.Request) {
  511. // Get parameters
  512. name := r.URL.Query().Get("name")
  513. size := r.URL.Query().Get("size")
  514. iconType := r.URL.Query().Get("type")
  515. if name == "" {
  516. h.sendError(w, http.StatusBadRequest, "missing name parameter")
  517. return
  518. }
  519. // Default values
  520. if size == "" {
  521. size = "32"
  522. }
  523. if iconType == "" {
  524. iconType = "aim"
  525. }
  526. // For now, redirect to a placeholder icon
  527. // In production, this would redirect to actual icon storage/CDN
  528. var iconURL string
  529. // If it's an email lookup, extract username
  530. if strings.Contains(name, "@") {
  531. parts := strings.Split(name, "@")
  532. if len(parts) > 0 {
  533. name = parts[0]
  534. }
  535. }
  536. // Check if user is online and get their state
  537. screenName := state.NewIdentScreenName(name)
  538. if session := h.SessionRetriever.RetrieveSession(screenName); session != nil {
  539. if session.Away() {
  540. iconURL = "/static/icons/away_" + iconType + "_" + size + ".png"
  541. } else if session.Idle() {
  542. iconURL = "/static/icons/idle_" + iconType + "_" + size + ".png"
  543. } else {
  544. iconURL = "/static/icons/online_" + iconType + "_" + size + ".png"
  545. }
  546. } else {
  547. iconURL = "/static/icons/offline_" + iconType + "_" + size + ".png"
  548. }
  549. // Redirect to icon URL
  550. http.Redirect(w, r, iconURL, http.StatusFound)
  551. }
  552. // broadcastPresenceEvent sends presence updates to all WebAPI sessions watching this user
  553. func (h *PresenceHandler) broadcastPresenceEvent(screenName state.IdentScreenName, stateStr, awayMsg, statusMsg string) {
  554. // Get all sessions that have this user in their buddy list
  555. // For now, we'll broadcast to all sessions (this should be optimized)
  556. // Using background context as this is an async broadcast operation
  557. for _, sess := range h.SessionManager.GetAllSessions(context.Background()) {
  558. if sess.EventQueue != nil && sess.Events != nil {
  559. // Check if session is subscribed to presence events
  560. for _, event := range sess.Events {
  561. if event == "presence" || event == "myInfo" {
  562. eventData := types.PresenceEvent{
  563. AimID: screenName.String(),
  564. State: stateStr,
  565. AwayMsg: awayMsg,
  566. StatusMsg: statusMsg,
  567. }
  568. sess.EventQueue.Push(types.EventTypePresence, eventData)
  569. break
  570. }
  571. }
  572. }
  573. }
  574. }