buddylist_handler.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. package webapi
  2. import (
  3. "context"
  4. "log/slog"
  5. "math/rand"
  6. "net/http"
  7. "strings"
  8. "github.com/mk6i/open-oscar-server/state"
  9. "github.com/mk6i/open-oscar-server/wire"
  10. )
  11. // BuddyListHandler handles Web AIM API buddy list management endpoints.
  12. type BuddyListHandler struct {
  13. BuddyListManager *BuddyListManager
  14. Logger *slog.Logger
  15. FeedbagService FeedbagService
  16. }
  17. // AddBuddy handles GET /buddylist/addBuddy requests.
  18. func (h *BuddyListHandler) AddBuddy(w http.ResponseWriter, r *http.Request, session *Session) {
  19. ctx := r.Context()
  20. aimsid := r.URL.Query().Get("aimsid")
  21. buddyName := strings.TrimSpace(param(r, "buddy"))
  22. groupName := strings.TrimSpace(param(r, "group"))
  23. preAuthorized := isTrueParam(param(r, "preAuthorized"))
  24. authorizationMsg := strings.TrimSpace(param(r, "authorizationMsg"))
  25. if buddyName == "" {
  26. SendError(w, r, http.StatusBadRequest, "missing buddy parameter")
  27. return
  28. }
  29. if groupName == "" {
  30. groupName = "Buddies" // Default group
  31. }
  32. // Add buddy to feedbag
  33. resultCode := h.addBuddyToFeedbag(ctx, session, buddyName, groupName, preAuthorized, authorizationMsg)
  34. // Prepare response
  35. sendMutationResult(w, r, resultCode, h.Logger)
  36. h.Logger.InfoContext(ctx, "buddy added",
  37. "aimsid", aimsid,
  38. "buddy", buddyName,
  39. "group", groupName,
  40. "preAuthorized", preAuthorized,
  41. "result", resultCode,
  42. )
  43. }
  44. // AddGroup handles GET /buddylist/addGroup requests.
  45. func (h *BuddyListHandler) AddGroup(w http.ResponseWriter, r *http.Request, session *Session) {
  46. ctx := r.Context()
  47. aimsid := r.URL.Query().Get("aimsid")
  48. groupName := strings.TrimSpace(r.URL.Query().Get("group"))
  49. if groupName == "" {
  50. SendError(w, r, http.StatusBadRequest, "missing group parameter")
  51. return
  52. }
  53. resultCode := h.addGroupToFeedbag(ctx, session, groupName)
  54. sendMutationResult(w, r, resultCode, h.Logger)
  55. h.Logger.InfoContext(ctx, "buddy list group added",
  56. "aimsid", aimsid,
  57. "group", groupName,
  58. "result", resultCode,
  59. )
  60. }
  61. func (h *BuddyListHandler) addGroupToFeedbag(ctx context.Context, sess *Session, groupName string) string {
  62. defer sess.InvalidateFeedbag()
  63. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
  64. snac, err := h.FeedbagService.Query(ctx, sess.OSCARSession, frame)
  65. if err != nil {
  66. h.Logger.ErrorContext(ctx, "failed to retrieve feedbag", "err", err.Error())
  67. return "error"
  68. }
  69. reply, ok := snac.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
  70. if !ok {
  71. return "error"
  72. }
  73. fl := state.NewFeedbagList(reply.Items, rand.Intn)
  74. fl.AddGroup(groupName)
  75. pending := fl.PendingUpdates()
  76. if len(pending) == 0 {
  77. return "alreadyExists"
  78. }
  79. insertFrame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  80. if _, err := h.FeedbagService.UpsertItem(ctx, sess.OSCARSession, insertFrame, pending); err != nil {
  81. h.Logger.ErrorContext(ctx, "failed to add group", "err", err.Error())
  82. return "error"
  83. }
  84. return resultSuccess
  85. }
  86. // RemoveBuddy handles GET /buddylist/removeBuddy requests.
  87. func (h *BuddyListHandler) RemoveBuddy(w http.ResponseWriter, r *http.Request, session *Session) {
  88. ctx := r.Context()
  89. aimsid := r.URL.Query().Get("aimsid")
  90. buddyName := strings.TrimSpace(r.URL.Query().Get("buddy"))
  91. groupName := strings.TrimSpace(r.URL.Query().Get("group"))
  92. allGroupsParam := r.URL.Query().Get("allGroups")
  93. allGroups := allGroupsParam == "true" || allGroupsParam == "1"
  94. if buddyName == "" {
  95. SendError(w, r, http.StatusBadRequest, "missing buddy parameter")
  96. return
  97. }
  98. resultCode, rmErr := h.BuddyListManager.RemoveBuddyFromFeedbag(ctx, session, buddyName, groupName, allGroups)
  99. if rmErr != nil {
  100. h.Logger.ErrorContext(ctx, "remove buddy failed", "err", rmErr.Error())
  101. }
  102. sendMutationResult(w, r, resultCode, h.Logger)
  103. h.Logger.InfoContext(ctx, "buddy removed",
  104. "aimsid", aimsid,
  105. "buddy", buddyName,
  106. "group", groupName,
  107. "result", resultCode,
  108. )
  109. }
  110. // todo don't remove empty group?
  111. // RemoveGroup handles GET /buddylist/removeGroup requests.
  112. func (h *BuddyListHandler) RemoveGroup(w http.ResponseWriter, r *http.Request, session *Session) {
  113. ctx := r.Context()
  114. aimsid := r.URL.Query().Get("aimsid")
  115. groupName := strings.TrimSpace(r.URL.Query().Get("group"))
  116. if groupName == "" {
  117. SendError(w, r, http.StatusBadRequest, "missing group parameter")
  118. return
  119. }
  120. resultCode, rmErr := h.BuddyListManager.RemoveGroupFromFeedbag(ctx, session, groupName)
  121. if rmErr != nil {
  122. h.Logger.ErrorContext(ctx, "remove group failed", "err", rmErr.Error())
  123. }
  124. sendMutationResult(w, r, resultCode, h.Logger)
  125. h.Logger.InfoContext(ctx, "buddy list group removed",
  126. "aimsid", aimsid,
  127. "group", groupName,
  128. "result", resultCode,
  129. )
  130. }
  131. // addBuddyToFeedbag adds a buddy to the user's feedbag.
  132. func (h *BuddyListHandler) addBuddyToFeedbag(ctx context.Context, sess *Session, buddyName, groupName string, preAuthorized bool, authorizationMsg string) string {
  133. defer sess.InvalidateFeedbag()
  134. // Retrieve current feedbag
  135. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
  136. snac, err := h.FeedbagService.Query(ctx, sess.OSCARSession, frame)
  137. if err != nil {
  138. h.Logger.ErrorContext(ctx, "failed to retrieve feedbag", "err", err.Error())
  139. return "error"
  140. }
  141. reply, ok := snac.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
  142. if !ok {
  143. // todo what
  144. return "error"
  145. }
  146. fl := state.NewFeedbagList(reply.Items, rand.Intn)
  147. target := state.NewIdentScreenName(buddyName)
  148. fl.AddGroup(groupName)
  149. if pending := fl.PendingUpdates(); len(pending) > 0 {
  150. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  151. if _, err := h.FeedbagService.UpsertItem(ctx, sess.OSCARSession, frame, pending); err != nil {
  152. h.Logger.ErrorContext(ctx, "failed to add buddy", "err", err.Error())
  153. return "error"
  154. }
  155. }
  156. added, err := fl.AddBuddy(groupName, buddyName, "", "")
  157. if err != nil {
  158. h.Logger.ErrorContext(ctx, "failed to add buddy to feedbag", "err", err.Error())
  159. return "error"
  160. }
  161. if !added {
  162. return "alreadyExists"
  163. }
  164. if pending := fl.PendingUpdates(); len(pending) > 0 {
  165. buddyItems := make(map[uint16][]wire.FeedbagItem)
  166. for _, item := range pending {
  167. if item.ClassID == wire.FeedbagClassIdBuddy {
  168. if _, ok := buddyItems[item.GroupID]; !ok {
  169. buddyItems[item.GroupID] = nil
  170. }
  171. buddyItems[item.GroupID] = append(buddyItems[item.GroupID], item)
  172. }
  173. }
  174. if sess.OSCARSession.UIN() != 0 && target.UIN() != 0 {
  175. for _, buddies := range buddyItems {
  176. for i := range buddies {
  177. if state.NewIdentScreenName(buddies[i].Name) == target {
  178. buddies[i].Append(wire.NewTLVBE(wire.FeedbagAttributesPending, []byte{}))
  179. }
  180. }
  181. }
  182. }
  183. for _, buddies := range buddyItems {
  184. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  185. if _, err := h.FeedbagService.UpsertItem(ctx, sess.OSCARSession, frame, buddies); err != nil {
  186. h.Logger.ErrorContext(ctx, "failed to add buddy", "err", err.Error())
  187. return "error"
  188. }
  189. }
  190. for _, item := range pending { // todo why not filter buddies out of pending?
  191. if item.ClassID == wire.FeedbagClassIdGroup {
  192. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem}
  193. if _, err := h.FeedbagService.UpsertItem(ctx, sess.OSCARSession, frame, []wire.FeedbagItem{item}); err != nil {
  194. h.Logger.ErrorContext(ctx, "failed to add buddy", "err", err.Error())
  195. return "error"
  196. }
  197. }
  198. }
  199. }
  200. if preAuthorized {
  201. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagPreAuthorizeBuddy}
  202. body := wire.SNAC_0x13_0x14_FeedbagPreAuthorizeBuddy{
  203. ScreenName: buddyName,
  204. Message: authorizationMsg,
  205. }
  206. if _, err := h.FeedbagService.PreAuthorizeBuddy(ctx, sess.OSCARSession, frame, body); err != nil {
  207. h.Logger.ErrorContext(ctx, "failed to pre-authorize buddy",
  208. "buddy", buddyName, "err", err.Error())
  209. }
  210. }
  211. return resultSuccess
  212. }
  213. // RenameGroup handles GET /buddylist/renameGroup requests.
  214. //
  215. // The Web AIM client calls this with oldGroup (current group name) and newGroup
  216. // (the requested new name). This is a stub; it does not yet rename the group in
  217. // the feedbag.
  218. func (h *BuddyListHandler) RenameGroup(w http.ResponseWriter, r *http.Request, session *Session) {
  219. ctx := r.Context()
  220. aimsid := r.URL.Query().Get("aimsid")
  221. oldGroup := strings.TrimSpace(r.URL.Query().Get("oldGroup"))
  222. newGroup := strings.TrimSpace(r.URL.Query().Get("newGroup"))
  223. if oldGroup == "" || newGroup == "" {
  224. SendError(w, r, http.StatusBadRequest, "missing oldGroup or newGroup parameter")
  225. return
  226. }
  227. resultCode, rnErr := h.BuddyListManager.RenameGroupInFeedbag(ctx, session, oldGroup, newGroup)
  228. if rnErr != nil {
  229. h.Logger.ErrorContext(ctx, "rename group failed", "err", rnErr.Error())
  230. }
  231. sendMutationResult(w, r, resultCode, h.Logger)
  232. h.Logger.InfoContext(ctx, "buddy list group renamed",
  233. "aimsid", aimsid,
  234. "oldGroup", oldGroup,
  235. "newGroup", newGroup,
  236. "result", resultCode,
  237. )
  238. }
  239. // MoveBuddy handles GET /buddylist/moveBuddy requests.
  240. //
  241. // The Web AIM client calls this with buddy (the buddy to move), group (the
  242. // current group), and optionally newGroup (destination group) and beforeBuddy
  243. // (buddy to position it before). This is a stub; it does not yet move the buddy
  244. // in the feedbag.
  245. func (h *BuddyListHandler) MoveBuddy(w http.ResponseWriter, r *http.Request, session *Session) {
  246. ctx := r.Context()
  247. aimsid := r.URL.Query().Get("aimsid")
  248. buddyName := strings.TrimSpace(r.URL.Query().Get("buddy"))
  249. groupName := strings.TrimSpace(r.URL.Query().Get("group"))
  250. newGroup := strings.TrimSpace(r.URL.Query().Get("newGroup"))
  251. beforeBuddy := strings.TrimSpace(r.URL.Query().Get("beforeBuddy"))
  252. if buddyName == "" {
  253. SendError(w, r, http.StatusBadRequest, "missing buddy parameter")
  254. return
  255. }
  256. if groupName == "" {
  257. SendError(w, r, http.StatusBadRequest, "missing group parameter")
  258. return
  259. }
  260. resultCode, mvErr := h.BuddyListManager.MoveBuddyInFeedbag(ctx, session, buddyName, groupName, newGroup, beforeBuddy)
  261. if mvErr != nil {
  262. h.Logger.ErrorContext(ctx, "move buddy failed", "err", mvErr.Error())
  263. }
  264. sendMutationResult(w, r, resultCode, h.Logger)
  265. h.Logger.InfoContext(ctx, "buddy moved",
  266. "aimsid", aimsid,
  267. "buddy", buddyName,
  268. "group", groupName,
  269. "newGroup", newGroup,
  270. "beforeBuddy", beforeBuddy,
  271. "result", resultCode,
  272. )
  273. }
  274. // SetBuddyAttribute handles GET /buddylist/setBuddyAttribute requests.
  275. //
  276. // The Web AIM client calls this with t (the buddy) and friendly (the display
  277. // name / alias). This is a stub; it does not yet persist the attribute to the
  278. // feedbag.
  279. func (h *BuddyListHandler) SetBuddyAttribute(w http.ResponseWriter, r *http.Request, session *Session) {
  280. ctx := r.Context()
  281. aimsid := r.URL.Query().Get("aimsid")
  282. buddyName := strings.TrimSpace(r.URL.Query().Get("t"))
  283. friendly := strings.TrimSpace(r.URL.Query().Get("friendly"))
  284. if buddyName == "" {
  285. SendError(w, r, http.StatusBadRequest, "missing t parameter")
  286. return
  287. }
  288. resultCode, saErr := h.BuddyListManager.SetBuddyAttributeInFeedbag(ctx, session, buddyName, friendly)
  289. if saErr != nil {
  290. h.Logger.ErrorContext(ctx, "set buddy attribute failed", "err", saErr.Error())
  291. }
  292. sendMutationResult(w, r, resultCode, h.Logger)
  293. h.Logger.InfoContext(ctx, "buddy attribute set",
  294. "aimsid", aimsid,
  295. "buddy", buddyName,
  296. "friendly", friendly,
  297. "result", resultCode,
  298. )
  299. }
  300. // SetGroupAttribute handles GET /buddylist/setGroupAttribute requests.
  301. //
  302. // The Web AIM client calls this with collapsed (the group's collapsed state)
  303. // and, for named groups, group. The unnamed default group omits group. This is
  304. // a stub; it does not yet persist the attribute to the feedbag.
  305. func (h *BuddyListHandler) SetGroupAttribute(w http.ResponseWriter, r *http.Request, session *Session) {
  306. ctx := r.Context()
  307. aimsid := r.URL.Query().Get("aimsid")
  308. query := r.URL.Query()
  309. groupName := strings.TrimSpace(query.Get("group"))
  310. collapsedParam := strings.TrimSpace(query.Get("collapsed"))
  311. if collapsedParam == "" {
  312. SendError(w, r, http.StatusBadRequest, "missing collapsed parameter")
  313. return
  314. }
  315. collapsed := collapsedParam == "true" || collapsedParam == "1"
  316. resultCode, saErr := h.BuddyListManager.SetGroupAttributeInFeedbag(ctx, session, groupName, collapsed)
  317. if saErr != nil {
  318. h.Logger.ErrorContext(ctx, "set group attribute failed", "err", saErr.Error())
  319. }
  320. sendMutationResult(w, r, resultCode, h.Logger)
  321. h.Logger.InfoContext(ctx, "buddy list group attribute set",
  322. "aimsid", aimsid,
  323. "group", groupName,
  324. "collapsed", collapsed,
  325. "result", resultCode,
  326. )
  327. }
  328. // resultSuccess is the result code the buddy list methods no longer report; see
  329. // sendMutationResult.
  330. const resultSuccess = "success"
  331. func sendMutationResult(w http.ResponseWriter, r *http.Request, resultCode string, logger *slog.Logger) {
  332. if resultCode == resultSuccess {
  333. // Nil data renders as an empty "data":{} — no resultCode, no buddyInfo.
  334. SendOK(w, r, nil, logger)
  335. return
  336. }
  337. SendOK(w, r, &ResultCodeData{ResultCode: resultCode}, logger)
  338. }
  339. // ResultCodeData is the payload the buddy list editing methods answer with.
  340. //
  341. // The spec shows these methods returning an empty data; the Web AIM client
  342. // reads resultCode from it, so the server sends one.
  343. type ResultCodeData struct {
  344. ResultCode string `json:"resultCode" xml:"resultCode"`
  345. }