4
0

preference.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. package handlers
  2. import (
  3. "context"
  4. "fmt"
  5. "log/slog"
  6. "math/rand"
  7. "net/http"
  8. "strings"
  9. "github.com/mk6i/open-oscar-server/server/webapi/types"
  10. "github.com/mk6i/open-oscar-server/state"
  11. "github.com/mk6i/open-oscar-server/wire"
  12. )
  13. // PreferenceHandler handles Web AIM API preference-related endpoints.
  14. type PreferenceHandler struct {
  15. FeedbagService FeedbagService
  16. SessionManager *state.WebAPISessionManager
  17. Logger *slog.Logger
  18. }
  19. // buddyPref maps a Web AIM API preference to its OSCAR buddy-pref bit number
  20. // and the default value defined by the Web API spec.
  21. type buddyPref struct {
  22. num uint16
  23. def bool
  24. }
  25. // webBuddyPrefs maps Web AIM API preference names to OSCAR buddy prefs, which
  26. // are stored as a bitmask in the user's feedbag (see wire.BuddyPref). Pref
  27. // numbers 0x07, 0x13, and 0x17 are reserved/unused and intentionally absent.
  28. var webBuddyPrefs = map[string]buddyPref{
  29. "displayLogin": {wire.FeedbagBuddyPrefsDisplayLogin, true},
  30. "displayEBuddy": {wire.FeedbagBuddyPrefsDisplayEBuddy, true},
  31. "playEnter": {wire.FeedbagBuddyPrefsPlayEnter, true},
  32. "playExit": {wire.FeedbagBuddyPrefsPlayExit, true},
  33. "viewIMTimestamps": {wire.FeedbagBuddyPrefsViewIMStamp, true},
  34. "viewSmilies": {wire.FeedbagBuddyPrefsViewSmileys, true},
  35. "acceptIcons": {wire.FeedbagBuddyPrefsAcceptIcons, true},
  36. "knockNonAOLIMs": {wire.FeedbagBuddyPrefsKnockNonAOLIMs, true},
  37. "knockNonListIMs": {wire.FeedbagBuddyPrefsKnockNonListIMs, true},
  38. "discloseIdle": {wire.FeedbagBuddyPrefsDiscloseIdle, true},
  39. "acceptCustomBart": {wire.FeedbagBuddyPrefsAcceptCustomBart, false},
  40. "acceptNonListBart": {wire.FeedbagBuddyPrefsAcceptNonListBart, false},
  41. "acceptBgs": {wire.FeedbagBuddyPrefsAcceptBgs, true},
  42. "acceptChromes": {wire.FeedbagBuddyPrefsAcceptChromes, true},
  43. "acceptBLSounds": {wire.FeedbagBuddyPrefsAcceptBLSounds, true},
  44. "acceptIMsounds": {wire.FeedbagBuddyPrefsAcceptIMSounds, true},
  45. "noSeeRecentBuddies": {wire.FeedbagBuddyPrefsNoSeeRecentBuddies, false},
  46. "acceptSMSLegal": {wire.FeedbagBuddyPrefsAcceptSMSLegal, false},
  47. "enterDoesCRLF": {wire.FeedbagBuddyPrefsEnterDoesCRLF, false},
  48. "playIMSound": {wire.FeedbagBuddyPrefsPlayIMSound, true},
  49. "discloseTyping": {wire.FeedbagBuddyPrefsDiscloseTyping, true},
  50. "acceptSuperIcons": {wire.FeedbagBuddyPrefsAcceptSuperIcons, true},
  51. "acceptBLRichText": {wire.FeedbagBuddyPrefsAcceptBLRichText, true},
  52. "reduceIMSound": {wire.FeedbagBuddyPrefsReduceIMSound, true},
  53. "confirmDirectIM": {wire.FeedbagBuddyPrefsConfirmDirectIM, true},
  54. "oneTabbedIMWindow": {wire.FeedbagBuddyPrefsOneTabbedIMWindow, true},
  55. "buddyInfoOnMouseover": {wire.FeedbagBuddyPrefsBuddyInfoOnMouseover, true},
  56. "discloseBuddyMatches": {wire.FeedbagBuddyPrefsDiscloseBuddyMatches, true},
  57. "catchIMs": {wire.FeedbagBuddyPrefsCatchIMs, false},
  58. "showFriendlyName": {wire.FeedbagBuddyPrefsShowFriendlyName, true},
  59. "discloseRadio": {wire.FeedbagBuddyPrefsDiscloseRadio, true},
  60. "showCapabilities": {wire.FeedbagBuddyPrefsShowCapabilities, true},
  61. "showBuddyListFilter": {wire.FeedbagBuddyPrefsShowBuddyListFilter, true},
  62. "showAwayIdle": {wire.FeedbagBuddyPrefsShowAwayIdle, true},
  63. "showMobile": {wire.FeedbagBuddyPrefsShowMobile, true},
  64. "sortBuddyList": {wire.FeedbagBuddyPrefsSortBuddyList, false},
  65. "catchIMsForClient": {wire.FeedbagBuddyPrefsCatchIMsForClient, false},
  66. "newMessageSmallNotification": {wire.FeedbagBuddyPrefsNewMessageSmallNotify, true},
  67. "noFrequentBuddies": {wire.FeedbagBuddyPrefsNoFrequentBuddies, false},
  68. "blogAwayMessages": {wire.FeedbagBuddyPrefsBlogAwayMessages, false},
  69. "blogAIMSigMessages": {wire.FeedbagBuddyPrefsBlogAIMSigMessages, false},
  70. "blogNoComments": {wire.FeedbagBuddyPrefsBlogNoComments, false},
  71. "friendOfFriend": {wire.FeedbagBuddyPrefsFriendOfFriend, false},
  72. "friendGetContactList": {wire.FeedbagBuddyPrefsFriendGetContactList, false},
  73. "compadInit": {wire.FeedbagBuddyPrefsCompadInit, false},
  74. "sendBuddyFeed": {wire.FeedbagBuddyPrefsSendBuddyFeed, true},
  75. "blkSendIMWhileAway": {wire.FeedbagBuddyPrefsBlkSendIMWhileAway, false},
  76. "showBuddyFeed": {wire.FeedbagBuddyPrefsShowBuddyFeed, true},
  77. "noSaveVanityInfo": {wire.FeedbagBuddyPrefsNoSaveVanityInfo, false},
  78. "acceptOffLineIM": {wire.FeedbagBuddyPrefsAcceptOfflineIM, true},
  79. "showGroups": {wire.FeedbagBuddyPrefsShowGroups, false},
  80. "sortGroup": {wire.FeedbagBuddyPrefsSortGroup, true},
  81. "showOffLineBuddies": {wire.FeedbagBuddyPrefsShowOfflineBuddies, true},
  82. "expandBuddies": {wire.FeedbagBuddyPrefsExpandBuddies, false},
  83. "thirdPartyFeeds": {wire.FeedbagBuddyPrefsThirdPartyFeeds, false},
  84. "notifyReceivedInvite": {wire.FeedbagBuddyPrefsNotifyReceivedInvite, true},
  85. "apfAutoAccept": {wire.FeedbagBuddyPrefsApfAutoAccept, false},
  86. "apfAutoAcceptBuddy": {wire.FeedbagBuddyPrefsApfAutoAcceptBuddy, false},
  87. "blockAwayMsgFeed": {wire.FeedbagBuddyPrefsBlockAwayMsgFeed, false},
  88. "blockAIMProfileFeed": {wire.FeedbagBuddyPrefsBlockAIMProfileFeed, false},
  89. "blockAIMPagesFeed": {wire.FeedbagBuddyPrefsBlockAIMPagesFeed, false},
  90. "blockJournalsFeed": {wire.FeedbagBuddyPrefsBlockJournalsFeed, false},
  91. "blockLocationFeed": {wire.FeedbagBuddyPrefsBlockLocationFeed, false},
  92. "blockStickiesFeed": {wire.FeedbagBuddyPrefsBlockStickiesFeed, false},
  93. "blockUncutFeed": {wire.FeedbagBuddyPrefsBlockUncutFeed, false},
  94. "blockLinksFeed": {wire.FeedbagBuddyPrefsBlockLinksFeed, false},
  95. "blockAIMBulletinFeed": {wire.FeedbagBuddyPrefsBlockAIMBulletinFeed, false},
  96. "saveStatusMsg": {wire.FeedbagBuddyPrefsSaveStatusMsg, true},
  97. // Not in the spec Preferences enum, but sent by the web client.
  98. "apfNotifyReceivedInviteByEmail": {wire.FeedbagBuddyPrefsApfNotifyReceivedByEmail, false},
  99. "showOfflineGrp": {wire.FeedbagBuddyPrefsShowOfflineGrp, true},
  100. "offlineGrpCollapsed": {wire.FeedbagBuddyPrefsOfflineGrpCollapsed, false},
  101. "firstImSoundOnly": {wire.FeedbagBuddyPrefsFirstIMSoundOnly, false},
  102. "imblastInviteNotify": {wire.FeedbagBuddyPrefsImblastInviteNotify, true},
  103. // Web-client-only preferences with no OSCAR buddy-pref equivalent. OSCAR
  104. // defines prefs through 0x4B, so we persist these in the same feedbag
  105. // buddy-prefs bitmask at positions above that range; no real OSCAR client
  106. // reads or writes these bits.
  107. "viewIMsInBubbles": {wire.FeedbagBuddyPrefsViewIMsInBubbles, true},
  108. "viewIMTimestampsRelative": {wire.FeedbagBuddyPrefsViewIMTimestampsRelative, false},
  109. "globalOTR": {wire.FeedbagBuddyPrefsGlobalOTR, false},
  110. "imblastInviteFromBuddyOnly": {wire.FeedbagBuddyPrefsImblastInviteFromBuddyOnly, false},
  111. }
  112. // PermitDenyData contains permit/deny list information.
  113. type PermitDenyData struct {
  114. PDMode string `json:"pdMode" xml:"pdMode"`
  115. PermitList []string `json:"allows,omitempty" xml:"allows>user,omitempty"`
  116. DenyList []string `json:"blocks,omitempty" xml:"blocks>user,omitempty"`
  117. }
  118. // SetPreferences handles GET /preference/set requests to update user preferences.
  119. func (h *PreferenceHandler) SetPreferences(w http.ResponseWriter, r *http.Request) {
  120. ctx := r.Context()
  121. // Get session ID from parameters
  122. aimsid := r.URL.Query().Get("aimsid")
  123. if aimsid == "" {
  124. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  125. return
  126. }
  127. // Get session
  128. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  129. if err != nil {
  130. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  131. return
  132. }
  133. // Update session activity
  134. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  135. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  136. }
  137. // Preferences are stored as OSCAR buddy prefs in the feedbag, which requires
  138. // an OSCAR session to act on behalf of.
  139. instance := session.OSCARSession
  140. if instance == nil {
  141. h.sendError(w, http.StatusBadRequest, "no OSCAR session")
  142. return
  143. }
  144. // Read-modify-write the buddy-prefs item so bits the web client doesn't
  145. // manage (e.g. the typing-events bit consumed by the OSCAR session) survive.
  146. item, err := buddyPrefsItem(ctx, h.FeedbagService, instance)
  147. if err != nil {
  148. h.Logger.ErrorContext(ctx, "failed to retrieve feedbag", "err", err.Error())
  149. h.sendError(w, http.StatusInternalServerError, "failed to retrieve feedbag")
  150. return
  151. }
  152. applied := make(map[string]interface{})
  153. for name, pref := range webBuddyPrefs {
  154. val := r.URL.Query().Get(name)
  155. if val == "" {
  156. continue
  157. }
  158. on := parseBoolPref(val)
  159. item.TLVList = wire.SetBuddyPref(item.TLVList, pref.num, on)
  160. applied[name] = boolToPrefInt(on)
  161. }
  162. if len(applied) > 0 {
  163. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  164. if _, err := h.FeedbagService.UpsertItem(ctx, instance, frame, []wire.FeedbagItem{item}); err != nil {
  165. h.Logger.ErrorContext(ctx, "failed to set preferences", "err", err.Error())
  166. h.sendError(w, http.StatusInternalServerError, "failed to save preferences")
  167. return
  168. }
  169. // Notify the client's open windows via the event stream so display
  170. // changes (e.g. bubbles/classic) take effect immediately without a
  171. // browser refresh.
  172. session.EventQueue.Push(types.EventTypePreference, applied)
  173. }
  174. h.Logger.DebugContext(ctx, "preferences updated",
  175. "screenName", session.ScreenName.String(),
  176. "prefCount", len(applied),
  177. )
  178. // Send success response
  179. response := BaseResponse{}
  180. response.Response.StatusCode = 200
  181. response.Response.StatusText = "OK"
  182. response.Response.Data = applied
  183. SendResponse(w, r, response, h.Logger)
  184. }
  185. // GetPreferences handles GET /preference/get requests to retrieve user preferences.
  186. func (h *PreferenceHandler) GetPreferences(w http.ResponseWriter, r *http.Request) {
  187. ctx := r.Context()
  188. // Get session ID from parameters
  189. aimsid := r.URL.Query().Get("aimsid")
  190. if aimsid == "" {
  191. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  192. return
  193. }
  194. // Get session
  195. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  196. if err != nil {
  197. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  198. return
  199. }
  200. // Update session activity
  201. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  202. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  203. }
  204. // Load the buddy-prefs bitmask from the feedbag. Absent prefs fall back to
  205. // the spec default. Web-only sessions resolve entirely to defaults.
  206. var prefsList wire.TLVList
  207. if instance := session.OSCARSession; instance != nil {
  208. if item, err := buddyPrefsItem(ctx, h.FeedbagService, instance); err != nil {
  209. h.Logger.WarnContext(ctx, "failed to get preferences", "err", err.Error())
  210. } else {
  211. prefsList = item.TLVList
  212. }
  213. }
  214. // When specific preferences are named in the query (e.g. playIMSound=1), the
  215. // client is selecting those; otherwise return all preferences.
  216. requestedPrefs := make(map[string]interface{})
  217. for name, pref := range webBuddyPrefs {
  218. if r.URL.Query().Has(name) {
  219. requestedPrefs[name] = effectivePrefValue(prefsList, pref)
  220. }
  221. }
  222. var prefs map[string]interface{}
  223. if len(requestedPrefs) > 0 {
  224. prefs = requestedPrefs
  225. } else {
  226. prefs = make(map[string]interface{}, len(webBuddyPrefs))
  227. for name, pref := range webBuddyPrefs {
  228. prefs[name] = effectivePrefValue(prefsList, pref)
  229. }
  230. }
  231. h.Logger.DebugContext(ctx, "preferences retrieved",
  232. "screenName", session.ScreenName.String(),
  233. "prefCount", len(prefs),
  234. "requested", len(requestedPrefs) > 0,
  235. )
  236. // AMF clients (e.g. Gromit) expect the payload shaped a specific way. Pref
  237. // values are already numeric 0/1, which is what these clients expect.
  238. format := strings.ToLower(r.URL.Query().Get("f"))
  239. if format == "amf" || format == "amf3" {
  240. // Ensure prefs is never empty for Gromit.
  241. if len(prefs) == 0 {
  242. prefs = map[string]interface{}{"playIMSound": 1}
  243. }
  244. // A single preference is returned directly; multiple are wrapped in
  245. // jsonData for Gromit compatibility.
  246. if len(prefs) != 1 {
  247. prefs = map[string]interface{}{"jsonData": prefs}
  248. }
  249. h.Logger.DebugContext(ctx, "AMF preference response",
  250. "prefCount", len(prefs),
  251. "format", format,
  252. )
  253. }
  254. // Send response in requested format
  255. response := BaseResponse{}
  256. response.Response.StatusCode = 200
  257. response.Response.StatusText = "OK"
  258. response.Response.Data = prefs
  259. SendResponse(w, r, response, h.Logger)
  260. }
  261. // buddyPrefsItem returns the user's buddy-prefs feedbag item, creating a fresh
  262. // (empty) one if the feedbag does not have it yet.
  263. func buddyPrefsItem(ctx context.Context, fs FeedbagService, instance *state.SessionInstance) (wire.FeedbagItem, error) {
  264. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
  265. fb, err := fs.Query(ctx, instance, frame)
  266. if err != nil {
  267. return wire.FeedbagItem{}, err
  268. }
  269. reply, ok := fb.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
  270. if !ok {
  271. return wire.FeedbagItem{}, fmt.Errorf("unexpected feedbag reply type")
  272. }
  273. for _, item := range reply.Items {
  274. if item.ClassID == wire.FeedbagClassIdBuddyPrefs {
  275. return item, nil
  276. }
  277. }
  278. // No buddy-prefs item yet; create one. Only a single item of this class is
  279. // allowed, with an empty name and no group.
  280. return wire.FeedbagItem{
  281. ClassID: wire.FeedbagClassIdBuddyPrefs,
  282. ItemID: uint16(rand.Intn(0xFFFF)),
  283. }, nil
  284. }
  285. // validBuddyPrefs returns the 0/1 values of the preferences that are actually
  286. // present (valid) in the feedbag buddy-prefs bitmask. Preferences the user has
  287. // never set are omitted, so callers can distinguish "unset" from a default
  288. // value.
  289. func validBuddyPrefs(list wire.TLVList) map[string]interface{} {
  290. prefs := make(map[string]interface{})
  291. for name, pref := range webBuddyPrefs {
  292. if valid, val := wire.BuddyPref(list, pref.num); valid {
  293. prefs[name] = boolToPrefInt(val)
  294. }
  295. }
  296. return prefs
  297. }
  298. // effectivePrefValue returns the 0/1 value for pref, using the feedbag value
  299. // when present and the spec default otherwise. Values are emitted as numbers
  300. // (not "1"/"0" strings) because the web client evaluates them with JavaScript
  301. // truthiness/numeric comparisons, where the string "0" is truthy.
  302. func effectivePrefValue(list wire.TLVList, pref buddyPref) int {
  303. valid, val := wire.BuddyPref(list, pref.num)
  304. if !valid {
  305. val = pref.def
  306. }
  307. return boolToPrefInt(val)
  308. }
  309. // parseBoolPref interprets a Web API preference query value as a boolean.
  310. func parseBoolPref(v string) bool {
  311. switch strings.ToLower(v) {
  312. case "1", "true", "yes", "on":
  313. return true
  314. default:
  315. return false
  316. }
  317. }
  318. func boolToPrefInt(b bool) int {
  319. if b {
  320. return 1
  321. }
  322. return 0
  323. }
  324. // SetPermitDeny handles GET /preference/setPermitDeny requests to update permit/deny settings.
  325. func (h *PreferenceHandler) SetPermitDeny(w http.ResponseWriter, r *http.Request) {
  326. ctx := r.Context()
  327. // Get session ID from parameters
  328. aimsid := r.URL.Query().Get("aimsid")
  329. if aimsid == "" {
  330. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  331. return
  332. }
  333. // Get session
  334. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  335. if err != nil {
  336. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  337. return
  338. }
  339. // Update session activity
  340. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  341. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  342. }
  343. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
  344. fb, err := h.FeedbagService.Query(r.Context(), session.OSCARSession, frame)
  345. if err != nil {
  346. h.sendError(w, http.StatusInternalServerError, "failed to retrieve feedbag")
  347. return
  348. }
  349. reply, ok := fb.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
  350. if !ok {
  351. h.sendError(w, http.StatusInternalServerError, "failed to retrieve feedbag")
  352. return
  353. }
  354. fl := state.NewFeedbagList(reply.Items, rand.Intn)
  355. // Get pdMode parameter
  356. pdModeStr := r.URL.Query().Get("pdMode")
  357. if pdModeStr != "" {
  358. switch pdModeStr { // todo: are the string ints possible inputs?
  359. case "permitAll", "1":
  360. fl.SetMode(uint8(wire.FeedbagPDModePermitAll))
  361. case "denyAll", "2":
  362. fl.SetMode(uint8(wire.FeedbagPDModeDenyAll))
  363. case "permitSome", "3":
  364. fl.SetMode(uint8(wire.FeedbagPDModePermitSome))
  365. case "denySome", "4":
  366. fl.SetMode(uint8(wire.FeedbagPDModeDenySome))
  367. case "permitOnList", "5":
  368. fl.SetMode(uint8(wire.FeedbagPDModePermitOnList))
  369. default:
  370. h.sendError(w, http.StatusBadRequest, "invalid pdMode value")
  371. return
  372. }
  373. }
  374. // Handle permit list updates
  375. if pdAllow := r.URL.Query().Get("pdAllow"); pdAllow != "" {
  376. users := strings.Split(pdAllow, ",")
  377. for _, user := range users {
  378. user = strings.TrimSpace(user)
  379. if user != "" {
  380. fl.PermitUser(user)
  381. }
  382. }
  383. }
  384. if pdAllowRemove := r.URL.Query().Get("pdAllowRemove"); pdAllowRemove != "" {
  385. users := strings.Split(pdAllowRemove, ",")
  386. for _, user := range users {
  387. user = strings.TrimSpace(user)
  388. if user != "" {
  389. fl.DeletePermit(user)
  390. }
  391. }
  392. }
  393. // Handle deny list updates
  394. if pdBlock := r.URL.Query().Get("pdBlock"); pdBlock != "" {
  395. users := strings.Split(pdBlock, ",")
  396. for _, user := range users {
  397. user = strings.TrimSpace(user)
  398. if user != "" {
  399. fl.DenyUser(user)
  400. }
  401. }
  402. }
  403. if pdBlockRemove := r.URL.Query().Get("pdBlockRemove"); pdBlockRemove != "" {
  404. users := strings.Split(pdBlockRemove, ",")
  405. for _, user := range users {
  406. user = strings.TrimSpace(user)
  407. if user != "" {
  408. fl.DeleteDeny(user)
  409. }
  410. }
  411. }
  412. if pending := fl.PendingUpdates(); len(pending) > 0 {
  413. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  414. if _, err := h.FeedbagService.UpsertItem(ctx, session.OSCARSession, frame, pending); err != nil {
  415. h.Logger.ErrorContext(ctx, "failed to set PD mode", "err", err.Error())
  416. h.sendError(w, http.StatusInternalServerError, "failed to update PD mode")
  417. return
  418. }
  419. }
  420. if pending := fl.PendingDeletes(); len(pending) > 0 {
  421. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem}
  422. body := wire.SNAC_0x13_0x0A_FeedbagDeleteItem{Items: pending}
  423. if _, err := h.FeedbagService.DeleteItem(ctx, session.OSCARSession, frame, body); err != nil {
  424. h.Logger.ErrorContext(ctx, "failed to set PD mode", "err", err.Error())
  425. h.sendError(w, http.StatusInternalServerError, "failed to update PD mode")
  426. return
  427. }
  428. }
  429. pdd := permitDenyData(fl.Items())
  430. h.Logger.DebugContext(ctx, "permit/deny settings updated",
  431. "screenName", session.ScreenName.String(),
  432. "pdMode", pdd.PDMode,
  433. "permitCount", len(pdd.PermitList),
  434. "denyCount", len(pdd.DenyList),
  435. )
  436. response := BaseResponse{}
  437. response.Response.StatusCode = 200
  438. response.Response.StatusText = "OK"
  439. response.Response.Data = pdd
  440. SendResponse(w, r, response, h.Logger)
  441. }
  442. func permitDenyData(fl []wire.FeedbagItem) PermitDenyData {
  443. pdd := PermitDenyData{}
  444. for _, item := range fl {
  445. switch item.ClassID {
  446. case wire.FeedbagClassIDDeny:
  447. pdd.DenyList = append(pdd.DenyList, item.Name)
  448. case wire.FeedbagClassIDPermit:
  449. pdd.PermitList = append(pdd.PermitList, item.Name)
  450. case wire.FeedbagClassIdPdinfo:
  451. mode, _ := item.Uint8(wire.FeedbagAttributesPdMode)
  452. switch wire.FeedbagPDMode(mode) {
  453. case wire.FeedbagPDModePermitAll:
  454. pdd.PDMode = "permitAll"
  455. case wire.FeedbagPDModeDenyAll:
  456. pdd.PDMode = "denyAll"
  457. case wire.FeedbagPDModePermitSome:
  458. pdd.PDMode = "permitSome"
  459. case wire.FeedbagPDModeDenySome:
  460. pdd.PDMode = "denySome"
  461. case wire.FeedbagPDModePermitOnList:
  462. pdd.PDMode = "permitOnList"
  463. }
  464. }
  465. }
  466. return pdd
  467. }
  468. // GetPermitDeny handles GET /preference/getPermitDeny requests to retrieve permit/deny settings.
  469. func (h *PreferenceHandler) GetPermitDeny(w http.ResponseWriter, r *http.Request) {
  470. ctx := r.Context()
  471. // Get session ID from parameters
  472. aimsid := r.URL.Query().Get("aimsid")
  473. if aimsid == "" {
  474. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  475. return
  476. }
  477. // Get session
  478. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  479. if err != nil {
  480. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  481. return
  482. }
  483. // Update session activity
  484. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  485. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  486. }
  487. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
  488. fb, err := h.FeedbagService.Query(r.Context(), session.OSCARSession, frame)
  489. if err != nil {
  490. h.sendError(w, http.StatusInternalServerError, "failed to retrieve feedbag")
  491. return
  492. }
  493. reply, ok := fb.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
  494. if !ok {
  495. h.sendError(w, http.StatusInternalServerError, "failed to retrieve feedbag")
  496. return
  497. }
  498. pdd := permitDenyData(reply.Items)
  499. h.Logger.DebugContext(ctx, "permit/deny settings retrieved",
  500. "screenName", session.ScreenName.String(),
  501. "pdMode", pdd.PDMode,
  502. "permitCount", len(pdd.PermitList),
  503. "denyCount", len(pdd.DenyList),
  504. )
  505. response := BaseResponse{}
  506. response.Response.StatusCode = 200
  507. response.Response.StatusText = "OK"
  508. response.Response.Data = pdd
  509. SendResponse(w, r, response, h.Logger)
  510. }
  511. func (h *PreferenceHandler) sendError(w http.ResponseWriter, statusCode int, message string) {
  512. // todo log
  513. SendError(w, statusCode, message)
  514. }