buddylist.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. package handlers
  2. import (
  3. "context"
  4. "log/slog"
  5. "math/rand"
  6. "net/http"
  7. "strings"
  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. // BuddyListHandler handles Web AIM API buddy list management endpoints.
  13. type BuddyListHandler struct {
  14. BuddyListManager *BuddyListManager
  15. Logger *slog.Logger
  16. FeedbagService FeedbagService
  17. }
  18. type FeedbagService interface {
  19. DeleteItem(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x0A_FeedbagDeleteItem) (*wire.SNACMessage, error)
  20. Query(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame) (wire.SNACMessage, error)
  21. QueryIfModified(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x05_FeedbagQueryIfModified) (wire.SNACMessage, error)
  22. RespondAuthorizeToHost(ctx context.Context, instance state.IdentScreenName, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x1A_FeedbagRespondAuthorizeToHost) error
  23. RightsQuery(ctx context.Context, inFrame wire.SNACFrame) wire.SNACMessage
  24. StartCluster(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x11_FeedbagStartCluster)
  25. EndCluster(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame) error
  26. UpsertItem(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, items []wire.FeedbagItem) (*wire.SNACMessage, error)
  27. Use(ctx context.Context, instance *state.SessionInstance) error
  28. }
  29. // AddBuddy handles GET /buddylist/addBuddy requests.
  30. func (h *BuddyListHandler) AddBuddy(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  31. ctx := r.Context()
  32. aimsid := r.URL.Query().Get("aimsid")
  33. buddyName := strings.TrimSpace(r.URL.Query().Get("buddy"))
  34. groupName := strings.TrimSpace(r.URL.Query().Get("group"))
  35. if buddyName == "" {
  36. h.sendError(w, http.StatusBadRequest, "missing buddy parameter")
  37. return
  38. }
  39. if groupName == "" {
  40. groupName = "Buddies" // Default group
  41. }
  42. // Add buddy to feedbag
  43. resultCode, buddyInfo := h.addBuddyToFeedbag(ctx, session, buddyName, groupName)
  44. // Prepare response
  45. responseData := map[string]any{
  46. "resultCode": resultCode,
  47. }
  48. if resultCode == "success" {
  49. responseData["buddyInfo"] = buddyInfo
  50. }
  51. resp := BaseResponse{}
  52. resp.Response.StatusCode = 200
  53. resp.Response.StatusText = "OK"
  54. resp.Response.Data = responseData
  55. SendResponse(w, r, resp, h.Logger)
  56. if resultCode == "success" {
  57. groups, err := h.BuddyListManager.GetBuddyListForUser(ctx, session)
  58. if err != nil {
  59. h.Logger.ErrorContext(ctx, "failed to get buddy list for event", "err", err.Error())
  60. } else {
  61. blPayload := map[string]any{"groups": groups}
  62. session.EventQueue.Push(types.EventTypeBuddyList, blPayload)
  63. }
  64. }
  65. h.Logger.InfoContext(ctx, "buddy added",
  66. "aimsid", aimsid,
  67. "buddy", buddyName,
  68. "group", groupName,
  69. "result", resultCode,
  70. )
  71. }
  72. // AddGroup handles GET /buddylist/addGroup requests.
  73. func (h *BuddyListHandler) AddGroup(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  74. ctx := r.Context()
  75. aimsid := r.URL.Query().Get("aimsid")
  76. groupName := strings.TrimSpace(r.URL.Query().Get("group"))
  77. if groupName == "" {
  78. h.sendError(w, http.StatusBadRequest, "missing group parameter")
  79. return
  80. }
  81. resultCode := h.addGroupToFeedbag(ctx, session, groupName)
  82. resp := BaseResponse{}
  83. resp.Response.StatusCode = 200
  84. resp.Response.StatusText = "OK"
  85. resp.Response.Data = map[string]any{
  86. "resultCode": resultCode,
  87. }
  88. SendResponse(w, r, resp, h.Logger)
  89. if resultCode == "success" {
  90. groups, err := h.BuddyListManager.GetBuddyListForUser(ctx, session)
  91. if err != nil {
  92. h.Logger.ErrorContext(ctx, "failed to get buddy list for event", "err", err.Error())
  93. } else {
  94. blPayload := map[string]any{"groups": groups}
  95. session.EventQueue.Push(types.EventTypeBuddyList, blPayload)
  96. }
  97. }
  98. h.Logger.InfoContext(ctx, "buddy list group added",
  99. "aimsid", aimsid,
  100. "group", groupName,
  101. "result", resultCode,
  102. )
  103. }
  104. func (h *BuddyListHandler) addGroupToFeedbag(ctx context.Context, sess *state.WebAPISession, groupName string) string {
  105. // A session sees no SNAC for its own feedbag writes, so it drops the alias
  106. // cache itself. See WebAPISession.InvalidateAliases.
  107. defer sess.InvalidateAliases()
  108. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
  109. snac, err := h.FeedbagService.Query(ctx, sess.OSCARSession, frame)
  110. if err != nil {
  111. h.Logger.ErrorContext(ctx, "failed to retrieve feedbag", "err", err.Error())
  112. return "error"
  113. }
  114. reply, ok := snac.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
  115. if !ok {
  116. return "error"
  117. }
  118. fl := state.NewFeedbagList(reply.Items, rand.Intn)
  119. fl.AddGroup(groupName)
  120. pending := fl.PendingUpdates()
  121. if len(pending) == 0 {
  122. return "alreadyExists"
  123. }
  124. insertFrame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  125. if _, err := h.FeedbagService.UpsertItem(ctx, sess.OSCARSession, insertFrame, pending); err != nil {
  126. h.Logger.ErrorContext(ctx, "failed to add group", "err", err.Error())
  127. return "error"
  128. }
  129. return "success"
  130. }
  131. // feedbagGroupMatchesRequested returns true if a feedbag group row matches the
  132. // group the Web client asked for. OSCAR often stores the default group with an
  133. // empty name; GetBuddyListForUser labels that as "Buddies", so addBuddy must
  134. // treat "" and "Buddies" as the same bucket when the client sends group=Buddies.
  135. func feedbagGroupMatchesRequested(storedName, requested string) bool {
  136. req := strings.TrimSpace(requested)
  137. st := strings.TrimSpace(storedName)
  138. if strings.EqualFold(st, req) {
  139. return true
  140. }
  141. if strings.EqualFold(strings.TrimSpace(req), "Buddies") && st == "" {
  142. return true
  143. }
  144. return false
  145. }
  146. // storedGroupNameForRequest returns the feedbag group row Name for a Web client group label.
  147. // Rows with GroupID 0 are the root order record, not a named buddy group.
  148. func storedGroupNameForRequest(items []wire.FeedbagItem, requested string) (string, bool) {
  149. for _, item := range items {
  150. if item.ClassID != wire.FeedbagClassIdGroup {
  151. continue
  152. }
  153. if item.GroupID == 0 {
  154. continue
  155. }
  156. if feedbagGroupMatchesRequested(item.Name, requested) {
  157. return item.Name, true
  158. }
  159. }
  160. return "", false
  161. }
  162. // RemoveBuddy handles GET /buddylist/removeBuddy requests.
  163. func (h *BuddyListHandler) RemoveBuddy(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  164. ctx := r.Context()
  165. aimsid := r.URL.Query().Get("aimsid")
  166. buddyName := strings.TrimSpace(r.URL.Query().Get("buddy"))
  167. groupName := strings.TrimSpace(r.URL.Query().Get("group"))
  168. allGroupsParam := r.URL.Query().Get("allGroups")
  169. allGroups := allGroupsParam == "true" || allGroupsParam == "1"
  170. if buddyName == "" {
  171. h.sendError(w, http.StatusBadRequest, "missing buddy parameter")
  172. return
  173. }
  174. resultCode, rmErr := h.BuddyListManager.RemoveBuddyFromFeedbag(ctx, session, buddyName, groupName, allGroups)
  175. if rmErr != nil {
  176. h.Logger.ErrorContext(ctx, "remove buddy failed", "err", rmErr.Error())
  177. }
  178. resp := BaseResponse{}
  179. resp.Response.StatusCode = 200
  180. resp.Response.StatusText = "OK"
  181. resp.Response.Data = map[string]any{
  182. "resultCode": resultCode,
  183. }
  184. SendResponse(w, r, resp, h.Logger)
  185. if resultCode == "success" {
  186. groups, err := h.BuddyListManager.GetBuddyListForUser(ctx, session)
  187. if err != nil {
  188. h.Logger.ErrorContext(ctx, "failed to get buddy list for event", "err", err.Error())
  189. } else {
  190. blPayload := map[string]any{"groups": groups}
  191. session.EventQueue.Push(types.EventTypeBuddyList, blPayload)
  192. }
  193. }
  194. h.Logger.InfoContext(ctx, "buddy removed",
  195. "aimsid", aimsid,
  196. "buddy", buddyName,
  197. "group", groupName,
  198. "result", resultCode,
  199. )
  200. }
  201. // todo don't remove empty group?
  202. // RemoveGroup handles GET /buddylist/removeGroup requests.
  203. func (h *BuddyListHandler) RemoveGroup(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  204. ctx := r.Context()
  205. aimsid := r.URL.Query().Get("aimsid")
  206. groupName := strings.TrimSpace(r.URL.Query().Get("group"))
  207. if groupName == "" {
  208. h.sendError(w, http.StatusBadRequest, "missing group parameter")
  209. return
  210. }
  211. resultCode, rmErr := h.BuddyListManager.RemoveGroupFromFeedbag(ctx, session, groupName)
  212. if rmErr != nil {
  213. h.Logger.ErrorContext(ctx, "remove group failed", "err", rmErr.Error())
  214. }
  215. resp := BaseResponse{}
  216. resp.Response.StatusCode = 200
  217. resp.Response.StatusText = "OK"
  218. resp.Response.Data = map[string]any{
  219. "resultCode": resultCode,
  220. }
  221. SendResponse(w, r, resp, h.Logger)
  222. if resultCode == "success" {
  223. groups, err := h.BuddyListManager.GetBuddyListForUser(ctx, session)
  224. if err != nil {
  225. h.Logger.ErrorContext(ctx, "failed to get buddy list for event", "err", err.Error())
  226. } else {
  227. blPayload := map[string]any{"groups": groups}
  228. session.EventQueue.Push(types.EventTypeBuddyList, blPayload)
  229. }
  230. }
  231. h.Logger.InfoContext(ctx, "buddy list group removed",
  232. "aimsid", aimsid,
  233. "group", groupName,
  234. "result", resultCode,
  235. )
  236. }
  237. // addBuddyToFeedbag adds a buddy to the user's feedbag.
  238. func (h *BuddyListHandler) addBuddyToFeedbag(ctx context.Context, sess *state.WebAPISession, buddyName, groupName string) (string, *BuddyPresenceInfo) {
  239. defer sess.InvalidateAliases()
  240. // Retrieve current feedbag
  241. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
  242. snac, err := h.FeedbagService.Query(ctx, sess.OSCARSession, frame)
  243. if err != nil {
  244. h.Logger.ErrorContext(ctx, "failed to retrieve feedbag", "err", err.Error())
  245. return "error", nil
  246. }
  247. reply, ok := snac.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
  248. if !ok {
  249. // todo what
  250. return "error", nil
  251. }
  252. fl := state.NewFeedbagList(reply.Items, rand.Intn)
  253. fl.AddGroup(groupName)
  254. if pending := fl.PendingUpdates(); len(pending) > 0 {
  255. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  256. if _, err := h.FeedbagService.UpsertItem(ctx, sess.OSCARSession, frame, pending); err != nil {
  257. h.Logger.ErrorContext(ctx, "failed to add buddy", "err", err.Error())
  258. return "error", nil
  259. }
  260. }
  261. added, err := fl.AddBuddy(groupName, buddyName, "", "")
  262. if err != nil {
  263. h.Logger.ErrorContext(ctx, "failed to add buddy to feedbag", "err", err.Error())
  264. return "error", nil
  265. }
  266. if !added {
  267. return "alreadyExists", nil
  268. }
  269. if pending := fl.PendingUpdates(); len(pending) > 0 {
  270. buddyItems := make(map[uint16][]wire.FeedbagItem)
  271. for _, item := range pending {
  272. if item.ClassID == wire.FeedbagClassIdBuddy {
  273. if _, ok := buddyItems[item.GroupID]; !ok {
  274. buddyItems[item.GroupID] = nil
  275. }
  276. buddyItems[item.GroupID] = append(buddyItems[item.GroupID], item)
  277. }
  278. }
  279. for _, buddies := range buddyItems {
  280. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  281. if _, err := h.FeedbagService.UpsertItem(ctx, sess.OSCARSession, frame, buddies); err != nil {
  282. h.Logger.ErrorContext(ctx, "failed to add buddy", "err", err.Error())
  283. return "error", nil
  284. }
  285. }
  286. for _, item := range pending { // todo why not filter buddies out of pending?
  287. if item.ClassID == wire.FeedbagClassIdGroup {
  288. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem}
  289. if _, err := h.FeedbagService.UpsertItem(ctx, sess.OSCARSession, frame, []wire.FeedbagItem{item}); err != nil {
  290. h.Logger.ErrorContext(ctx, "failed to add buddy", "err", err.Error())
  291. return "error", nil
  292. }
  293. }
  294. }
  295. }
  296. // Get current presence for the buddy
  297. buddyInfo := &BuddyPresenceInfo{
  298. AimID: state.NewIdentScreenName(buddyName).String(),
  299. DisplayID: buddyName,
  300. State: "offline", // Default to offline
  301. UserType: "aim",
  302. }
  303. // TODO: Check actual presence status and update buddyInfo accordingly
  304. return "success", buddyInfo
  305. }
  306. // AddTempBuddy handles GET /aim/addTempBuddy requests.
  307. // This adds temporary buddies to the session without persisting them to the feedbag.
  308. // The temporary buddies are only visible for the duration of the session.
  309. func (h *BuddyListHandler) AddTempBuddy(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  310. ctx := r.Context()
  311. aimsid := r.URL.Query().Get("aimsid")
  312. buddyNames := r.URL.Query()["t"]
  313. if len(buddyNames) == 0 {
  314. h.sendError(w, http.StatusBadRequest, "missing buddy names (t parameter)")
  315. return
  316. }
  317. // Store temporary buddies in the session
  318. // Note: These are not persisted to the feedbag database
  319. if session.TempBuddies == nil {
  320. session.TempBuddies = make(map[string]bool)
  321. }
  322. for _, buddyName := range buddyNames {
  323. buddyName = strings.TrimSpace(buddyName)
  324. if buddyName != "" {
  325. session.TempBuddies[buddyName] = true
  326. }
  327. }
  328. // Prepare response
  329. responseData := map[string]any{
  330. "resultCode": "success",
  331. "buddyNames": buddyNames,
  332. }
  333. resp := BaseResponse{}
  334. resp.Response.StatusCode = 200
  335. resp.Response.StatusText = "OK"
  336. resp.Response.Data = responseData
  337. SendResponse(w, r, resp, h.Logger)
  338. // Do not push buddylist events for temp buddies. The Web AIM client handles
  339. // addTempBuddy via the API response; a buddylist event without "groups" causes
  340. // the client to clear the entire contact list (zC always calls clear() first).
  341. h.Logger.InfoContext(ctx, "temporary buddies added",
  342. "aimsid", aimsid,
  343. "buddies", buddyNames,
  344. "count", len(buddyNames),
  345. )
  346. }
  347. // RemoveTempBuddy handles GET /aim/removeTempBuddy requests.
  348. // This removes temporary session buddies added via addTempBuddy.
  349. func (h *BuddyListHandler) RemoveTempBuddy(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  350. ctx := r.Context()
  351. aimsid := r.URL.Query().Get("aimsid")
  352. buddyNames := r.URL.Query()["t"]
  353. if len(buddyNames) == 0 {
  354. h.sendError(w, http.StatusBadRequest, "missing buddy names (t parameter)")
  355. return
  356. }
  357. removed := make([]string, 0, len(buddyNames))
  358. for _, buddyName := range buddyNames {
  359. buddyName = strings.TrimSpace(buddyName)
  360. if buddyName == "" {
  361. continue
  362. }
  363. if session.TempBuddies != nil {
  364. delete(session.TempBuddies, buddyName)
  365. }
  366. removed = append(removed, buddyName)
  367. }
  368. resp := BaseResponse{}
  369. resp.Response.StatusCode = 200
  370. resp.Response.StatusText = "OK"
  371. resp.Response.Data = map[string]any{
  372. "resultCode": "success",
  373. "buddyNames": removed,
  374. }
  375. SendResponse(w, r, resp, h.Logger)
  376. h.Logger.InfoContext(ctx, "temporary buddies removed",
  377. "aimsid", aimsid,
  378. "buddies", removed,
  379. "count", len(removed),
  380. )
  381. }
  382. // RenameGroup handles GET /buddylist/renameGroup requests.
  383. //
  384. // The Web AIM client calls this with oldGroup (current group name) and newGroup
  385. // (the requested new name). This is a stub; it does not yet rename the group in
  386. // the feedbag.
  387. func (h *BuddyListHandler) RenameGroup(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  388. ctx := r.Context()
  389. aimsid := r.URL.Query().Get("aimsid")
  390. oldGroup := strings.TrimSpace(r.URL.Query().Get("oldGroup"))
  391. newGroup := strings.TrimSpace(r.URL.Query().Get("newGroup"))
  392. if oldGroup == "" || newGroup == "" {
  393. h.sendError(w, http.StatusBadRequest, "missing oldGroup or newGroup parameter")
  394. return
  395. }
  396. resultCode, rnErr := h.BuddyListManager.RenameGroupInFeedbag(ctx, session, oldGroup, newGroup)
  397. if rnErr != nil {
  398. h.Logger.ErrorContext(ctx, "rename group failed", "err", rnErr.Error())
  399. }
  400. resp := BaseResponse{}
  401. resp.Response.StatusCode = 200
  402. resp.Response.StatusText = "OK"
  403. resp.Response.Data = map[string]any{
  404. "resultCode": resultCode,
  405. }
  406. SendResponse(w, r, resp, h.Logger)
  407. if resultCode == "success" {
  408. h.pushBuddyListEvent(ctx, session)
  409. }
  410. h.Logger.InfoContext(ctx, "buddy list group renamed",
  411. "aimsid", aimsid,
  412. "oldGroup", oldGroup,
  413. "newGroup", newGroup,
  414. "result", resultCode,
  415. )
  416. }
  417. // MoveBuddy handles GET /buddylist/moveBuddy requests.
  418. //
  419. // The Web AIM client calls this with buddy (the buddy to move), group (the
  420. // current group), and optionally newGroup (destination group) and beforeBuddy
  421. // (buddy to position it before). This is a stub; it does not yet move the buddy
  422. // in the feedbag.
  423. func (h *BuddyListHandler) MoveBuddy(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  424. ctx := r.Context()
  425. aimsid := r.URL.Query().Get("aimsid")
  426. buddyName := strings.TrimSpace(r.URL.Query().Get("buddy"))
  427. groupName := strings.TrimSpace(r.URL.Query().Get("group"))
  428. newGroup := strings.TrimSpace(r.URL.Query().Get("newGroup"))
  429. beforeBuddy := strings.TrimSpace(r.URL.Query().Get("beforeBuddy"))
  430. if buddyName == "" {
  431. h.sendError(w, http.StatusBadRequest, "missing buddy parameter")
  432. return
  433. }
  434. if groupName == "" {
  435. h.sendError(w, http.StatusBadRequest, "missing group parameter")
  436. return
  437. }
  438. resultCode, mvErr := h.BuddyListManager.MoveBuddyInFeedbag(ctx, session, buddyName, groupName, newGroup, beforeBuddy)
  439. if mvErr != nil {
  440. h.Logger.ErrorContext(ctx, "move buddy failed", "err", mvErr.Error())
  441. }
  442. resp := BaseResponse{}
  443. resp.Response.StatusCode = 200
  444. resp.Response.StatusText = "OK"
  445. resp.Response.Data = map[string]any{
  446. "resultCode": resultCode,
  447. }
  448. SendResponse(w, r, resp, h.Logger)
  449. if resultCode == "success" {
  450. h.pushBuddyListEvent(ctx, session)
  451. }
  452. h.Logger.InfoContext(ctx, "buddy moved",
  453. "aimsid", aimsid,
  454. "buddy", buddyName,
  455. "group", groupName,
  456. "newGroup", newGroup,
  457. "beforeBuddy", beforeBuddy,
  458. "result", resultCode,
  459. )
  460. }
  461. // SetBuddyAttribute handles GET /buddylist/setBuddyAttribute requests.
  462. //
  463. // The Web AIM client calls this with t (the buddy) and friendly (the display
  464. // name / alias). This is a stub; it does not yet persist the attribute to the
  465. // feedbag.
  466. func (h *BuddyListHandler) SetBuddyAttribute(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  467. ctx := r.Context()
  468. aimsid := r.URL.Query().Get("aimsid")
  469. buddyName := strings.TrimSpace(r.URL.Query().Get("t"))
  470. friendly := strings.TrimSpace(r.URL.Query().Get("friendly"))
  471. if buddyName == "" {
  472. h.sendError(w, http.StatusBadRequest, "missing t parameter")
  473. return
  474. }
  475. resultCode, saErr := h.BuddyListManager.SetBuddyAttributeInFeedbag(ctx, session, buddyName, friendly)
  476. if saErr != nil {
  477. h.Logger.ErrorContext(ctx, "set buddy attribute failed", "err", saErr.Error())
  478. }
  479. resp := BaseResponse{}
  480. resp.Response.StatusCode = 200
  481. resp.Response.StatusText = "OK"
  482. resp.Response.Data = map[string]any{
  483. "resultCode": resultCode,
  484. }
  485. SendResponse(w, r, resp, h.Logger)
  486. if resultCode == "success" {
  487. h.pushBuddyListEvent(ctx, session)
  488. }
  489. h.Logger.InfoContext(ctx, "buddy attribute set",
  490. "aimsid", aimsid,
  491. "buddy", buddyName,
  492. "friendly", friendly,
  493. "result", resultCode,
  494. )
  495. }
  496. // SetGroupAttribute handles GET /buddylist/setGroupAttribute requests.
  497. //
  498. // The Web AIM client calls this with collapsed (the group's collapsed state)
  499. // and, for named groups, group. The unnamed default group omits group. This is
  500. // a stub; it does not yet persist the attribute to the feedbag.
  501. func (h *BuddyListHandler) SetGroupAttribute(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  502. ctx := r.Context()
  503. aimsid := r.URL.Query().Get("aimsid")
  504. query := r.URL.Query()
  505. groupName := strings.TrimSpace(query.Get("group"))
  506. collapsedParam := strings.TrimSpace(query.Get("collapsed"))
  507. if collapsedParam == "" {
  508. h.sendError(w, http.StatusBadRequest, "missing collapsed parameter")
  509. return
  510. }
  511. collapsed := collapsedParam == "true" || collapsedParam == "1"
  512. resultCode, saErr := h.BuddyListManager.SetGroupAttributeInFeedbag(ctx, session, groupName, collapsed)
  513. if saErr != nil {
  514. h.Logger.ErrorContext(ctx, "set group attribute failed", "err", saErr.Error())
  515. }
  516. resp := BaseResponse{}
  517. resp.Response.StatusCode = 200
  518. resp.Response.StatusText = "OK"
  519. resp.Response.Data = map[string]any{
  520. "resultCode": resultCode,
  521. }
  522. SendResponse(w, r, resp, h.Logger)
  523. if resultCode == "success" {
  524. h.pushBuddyListEvent(ctx, session)
  525. }
  526. h.Logger.InfoContext(ctx, "buddy list group attribute set",
  527. "aimsid", aimsid,
  528. "group", groupName,
  529. "collapsed", collapsed,
  530. "result", resultCode,
  531. )
  532. }
  533. // pushBuddyListEvent refreshes the buddy list and pushes it to the session's
  534. // event queue so the Web client re-renders after a mutation.
  535. func (h *BuddyListHandler) pushBuddyListEvent(ctx context.Context, session *state.WebAPISession) {
  536. groups, err := h.BuddyListManager.GetBuddyListForUser(ctx, session)
  537. if err != nil {
  538. h.Logger.ErrorContext(ctx, "failed to get buddy list for event", "err", err.Error())
  539. return
  540. }
  541. blPayload := map[string]any{"groups": groups}
  542. session.EventQueue.Push(types.EventTypeBuddyList, blPayload)
  543. }
  544. // sendError is a convenience method that wraps the common SendError function.
  545. func (h *BuddyListHandler) sendError(w http.ResponseWriter, statusCode int, message string) {
  546. SendError(w, statusCode, message)
  547. }