buddy_list_manager.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. package handlers
  2. import (
  3. "context"
  4. "fmt"
  5. "log/slog"
  6. "time"
  7. "github.com/mk6i/open-oscar-server/state"
  8. "github.com/mk6i/open-oscar-server/wire"
  9. )
  10. // BuddyListManager handles the conversion of OSCAR feedbag data
  11. // to WebAPI buddy list format for web clients.
  12. type BuddyListManager struct {
  13. feedbagRetriever FeedbagRetriever
  14. sessionRetriever SessionRetriever
  15. logger *slog.Logger
  16. }
  17. // NewBuddyListManager creates a new instance of the buddy list manager.
  18. func NewBuddyListManager(feedbagRetriever FeedbagRetriever, sessionRetriever SessionRetriever, logger *slog.Logger) *BuddyListManager {
  19. return &BuddyListManager{
  20. feedbagRetriever: feedbagRetriever,
  21. sessionRetriever: sessionRetriever,
  22. logger: logger,
  23. }
  24. }
  25. // WebAPIBuddyGroup represents a group in the WebAPI buddy list format.
  26. type WebAPIBuddyGroup struct {
  27. Name string `json:"name"`
  28. Buddies []WebAPIBuddyInfo `json:"buddies"`
  29. Recent bool `json:"recent,omitempty"`
  30. Smart interface{} `json:"smart,omitempty"` // Can be null or number
  31. }
  32. // WebAPIBuddyInfo represents a buddy in the WebAPI format.
  33. type WebAPIBuddyInfo struct {
  34. AimID string `json:"aimId"`
  35. DisplayID string `json:"displayId"`
  36. State string `json:"state"` // "online", "offline", "away", "idle"
  37. StatusMsg string `json:"statusMsg,omitempty"`
  38. AwayMsg string `json:"awayMsg,omitempty"`
  39. OnlineTime int64 `json:"onlineTime,omitempty"`
  40. IdleTime int `json:"idleTime,omitempty"` // Minutes idle
  41. UserType string `json:"userType"` // "aim", "icq", "admin"
  42. Bot bool `json:"bot"`
  43. Service string `json:"service,omitempty"` // "aim", "icq"
  44. PresenceIcon string `json:"presenceIcon,omitempty"`
  45. BuddyIcon string `json:"buddyIcon,omitempty"`
  46. Capabilities []string `json:"capabilities,omitempty"`
  47. MemberSince int64 `json:"memberSince,omitempty"`
  48. }
  49. // GetBuddyListForUser retrieves and converts the buddy list for a user.
  50. func (m *BuddyListManager) GetBuddyListForUser(ctx context.Context, screenName state.IdentScreenName) ([]WebAPIBuddyGroup, error) {
  51. // Retrieve feedbag items
  52. items, err := m.feedbagRetriever.RetrieveFeedbag(ctx, screenName)
  53. if err != nil {
  54. return nil, fmt.Errorf("failed to retrieve feedbag: %w", err)
  55. }
  56. // Build group map
  57. groupMap := make(map[uint16]string)
  58. buddyGroupMap := make(map[uint16][]wire.FeedbagItem)
  59. for _, item := range items {
  60. switch item.ClassID {
  61. case wire.FeedbagClassIdGroup:
  62. // Store group name
  63. groupMap[item.ItemID] = item.Name
  64. buddyGroupMap[item.ItemID] = []wire.FeedbagItem{}
  65. case wire.FeedbagClassIdBuddy:
  66. // Add buddy to its group
  67. if _, exists := buddyGroupMap[item.GroupID]; !exists {
  68. // Create implicit group if it doesn't exist
  69. buddyGroupMap[item.GroupID] = []wire.FeedbagItem{}
  70. }
  71. buddyGroupMap[item.GroupID] = append(buddyGroupMap[item.GroupID], item)
  72. }
  73. }
  74. // Convert to WebAPI format
  75. var groups []WebAPIBuddyGroup
  76. // Add online group (virtual group for online buddies)
  77. onlineGroup := WebAPIBuddyGroup{
  78. Name: "Online",
  79. Buddies: []WebAPIBuddyInfo{},
  80. }
  81. // Process each group
  82. for groupID, buddyItems := range buddyGroupMap {
  83. groupName := groupMap[groupID]
  84. if groupName == "" {
  85. groupName = "Buddies" // Default group name
  86. }
  87. group := WebAPIBuddyGroup{
  88. Name: groupName,
  89. Buddies: []WebAPIBuddyInfo{},
  90. }
  91. // Process buddies in this group
  92. for _, buddyItem := range buddyItems {
  93. buddyInfo := m.getBuddyInfo(buddyItem.Name)
  94. // Add to online group if buddy is online
  95. if buddyInfo.State == "online" || buddyInfo.State == "away" || buddyInfo.State == "idle" {
  96. onlineGroup.Buddies = append(onlineGroup.Buddies, buddyInfo)
  97. }
  98. group.Buddies = append(group.Buddies, buddyInfo)
  99. }
  100. // Only add group if it has buddies
  101. if len(group.Buddies) > 0 {
  102. groups = append(groups, group)
  103. }
  104. }
  105. // Add online group at the beginning if it has buddies
  106. if len(onlineGroup.Buddies) > 0 {
  107. groups = append([]WebAPIBuddyGroup{onlineGroup}, groups...)
  108. }
  109. // Always add an "Offline" group at the end for offline buddies
  110. offlineGroup := WebAPIBuddyGroup{
  111. Name: "Offline",
  112. Buddies: []WebAPIBuddyInfo{},
  113. }
  114. // Collect all offline buddies
  115. for _, group := range groups {
  116. if group.Name != "Online" {
  117. for _, buddy := range group.Buddies {
  118. if buddy.State == "offline" {
  119. offlineGroup.Buddies = append(offlineGroup.Buddies, buddy)
  120. }
  121. }
  122. }
  123. }
  124. if len(offlineGroup.Buddies) > 0 {
  125. groups = append(groups, offlineGroup)
  126. }
  127. return groups, nil
  128. }
  129. // getBuddyInfo retrieves the current presence information for a buddy.
  130. func (m *BuddyListManager) getBuddyInfo(buddyName string) WebAPIBuddyInfo {
  131. // Default to offline
  132. info := WebAPIBuddyInfo{
  133. AimID: buddyName,
  134. DisplayID: buddyName,
  135. State: "offline",
  136. UserType: "aim",
  137. Bot: false,
  138. Service: "aim",
  139. }
  140. // Check if buddy is online
  141. buddyScreenName := state.NewIdentScreenName(buddyName)
  142. session := m.sessionRetriever.RetrieveSession(buddyScreenName)
  143. if session != nil {
  144. // Buddy is online
  145. info.State = "online"
  146. info.OnlineTime = session.SignonTime().Unix()
  147. // Check away status
  148. if session.Away() {
  149. info.State = "away"
  150. info.AwayMsg = session.AwayMessage()
  151. }
  152. // Check idle status
  153. if session.Idle() {
  154. idleDuration := time.Since(session.IdleTime())
  155. info.IdleTime = int(idleDuration.Minutes())
  156. if info.State == "online" {
  157. info.State = "idle"
  158. }
  159. }
  160. // Status messages not currently supported in SessionInstance
  161. // Set capabilities
  162. // Capabilities parsing not implemented
  163. info.Capabilities = []string{}
  164. }
  165. return info
  166. }
  167. // GetPresenceForBuddy retrieves presence information for a specific buddy.
  168. func (m *BuddyListManager) GetPresenceForBuddy(screenName string) WebAPIBuddyInfo {
  169. return m.getBuddyInfo(screenName)
  170. }
  171. // GetOnlineBuddies returns a list of all online buddies for a user.
  172. func (m *BuddyListManager) GetOnlineBuddies(ctx context.Context, userScreenName state.IdentScreenName) ([]WebAPIBuddyInfo, error) {
  173. // Get user's buddy list
  174. items, err := m.feedbagRetriever.RetrieveFeedbag(ctx, userScreenName)
  175. if err != nil {
  176. return nil, fmt.Errorf("failed to retrieve feedbag: %w", err)
  177. }
  178. var onlineBuddies []WebAPIBuddyInfo
  179. // Check each buddy's presence
  180. for _, item := range items {
  181. if item.ClassID == wire.FeedbagClassIdBuddy {
  182. buddyInfo := m.getBuddyInfo(item.Name)
  183. if buddyInfo.State != "offline" {
  184. onlineBuddies = append(onlineBuddies, buddyInfo)
  185. }
  186. }
  187. }
  188. return onlineBuddies, nil
  189. }
  190. // FormatBuddyListEvent formats a buddy list for an event.
  191. func (m *BuddyListManager) FormatBuddyListEvent(groups []WebAPIBuddyGroup) map[string]interface{} {
  192. // Convert groups to a format that AMF3 can properly encode
  193. // AMF3 has trouble with complex struct slices, so convert to maps
  194. groupMaps := make([]interface{}, len(groups))
  195. for i, group := range groups {
  196. buddyMaps := make([]interface{}, len(group.Buddies))
  197. for j, buddy := range group.Buddies {
  198. // Convert each buddy to a map
  199. buddyMap := map[string]interface{}{
  200. "aimId": buddy.AimID,
  201. "displayId": buddy.DisplayID,
  202. "state": buddy.State,
  203. "userType": buddy.UserType,
  204. "bot": buddy.Bot,
  205. "service": buddy.Service,
  206. }
  207. // Add optional fields if present
  208. if buddy.StatusMsg != "" {
  209. buddyMap["statusMsg"] = buddy.StatusMsg
  210. }
  211. if buddy.AwayMsg != "" {
  212. buddyMap["awayMsg"] = buddy.AwayMsg
  213. }
  214. if buddy.OnlineTime > 0 {
  215. buddyMap["onlineTime"] = float64(buddy.OnlineTime)
  216. }
  217. if buddy.IdleTime > 0 {
  218. buddyMap["idleTime"] = buddy.IdleTime
  219. }
  220. if buddy.PresenceIcon != "" {
  221. buddyMap["presenceIcon"] = buddy.PresenceIcon
  222. }
  223. if buddy.BuddyIcon != "" {
  224. buddyMap["buddyIcon"] = buddy.BuddyIcon
  225. }
  226. if len(buddy.Capabilities) > 0 {
  227. buddyMap["capabilities"] = buddy.Capabilities
  228. }
  229. if buddy.MemberSince > 0 {
  230. buddyMap["memberSince"] = float64(buddy.MemberSince)
  231. }
  232. buddyMaps[j] = buddyMap
  233. }
  234. // Convert group to a map
  235. groupMap := map[string]interface{}{
  236. "name": group.Name,
  237. "buddies": buddyMaps,
  238. }
  239. // Add optional group fields
  240. if group.Recent {
  241. groupMap["recent"] = group.Recent
  242. }
  243. if group.Smart != nil {
  244. groupMap["smart"] = group.Smart
  245. }
  246. groupMaps[i] = groupMap
  247. }
  248. return map[string]interface{}{
  249. "groups": groupMaps,
  250. }
  251. }