preference.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. package handlers
  2. import (
  3. "context"
  4. "log/slog"
  5. "math/rand"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "github.com/mk6i/open-oscar-server/state"
  10. "github.com/mk6i/open-oscar-server/wire"
  11. )
  12. // PreferenceHandler handles Web AIM API preference-related endpoints.
  13. type PreferenceHandler struct {
  14. FeedbagService FeedbagService
  15. SessionManager *state.WebAPISessionManager
  16. PreferenceManager PreferenceManager
  17. Logger *slog.Logger
  18. }
  19. // PreferenceManager provides methods to manage user preferences.
  20. type PreferenceManager interface {
  21. SetPreferences(ctx context.Context, screenName state.IdentScreenName, prefs map[string]interface{}) error
  22. GetPreferences(ctx context.Context, screenName state.IdentScreenName) (map[string]interface{}, error)
  23. }
  24. // PermitDenyData contains permit/deny list information.
  25. type PermitDenyData struct {
  26. PDMode string `json:"pdMode" xml:"pdMode"`
  27. PermitList []string `json:"allows,omitempty" xml:"allows>user,omitempty"`
  28. DenyList []string `json:"blocks,omitempty" xml:"blocks>user,omitempty"`
  29. }
  30. // SetPreferences handles GET /preference/set requests to update user preferences.
  31. func (h *PreferenceHandler) SetPreferences(w http.ResponseWriter, r *http.Request) {
  32. ctx := r.Context()
  33. // Get session ID from parameters
  34. aimsid := r.URL.Query().Get("aimsid")
  35. if aimsid == "" {
  36. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  37. return
  38. }
  39. // Get session
  40. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  41. if err != nil {
  42. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  43. return
  44. }
  45. // Update session activity
  46. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  47. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  48. }
  49. // Parse preferences from query parameters
  50. prefs := make(map[string]interface{})
  51. // Common preference keys from the Web AIM API spec
  52. prefKeys := []string{
  53. "statusMsg", "awayMsg", "profileMsg", "buddyIcon",
  54. "soundsOn", "alertsOn", "typingStatus", "idleTime",
  55. "pdMode", "invisibleTo", "visibleTo", "blockList",
  56. "allowList", "language", "timeZone", "dateFormat",
  57. "showTimestamps", "fontSize", "fontFamily", "theme",
  58. "autoResponse", "saveHistory", "encryptMessages",
  59. }
  60. // Extract preferences from query parameters
  61. for _, key := range prefKeys {
  62. if val := r.URL.Query().Get(key); val != "" {
  63. // Try to parse as boolean
  64. if val == "true" || val == "false" {
  65. prefs[key] = val == "true"
  66. } else if num, err := strconv.Atoi(val); err == nil {
  67. // Try to parse as integer
  68. prefs[key] = num
  69. } else {
  70. // Store as string
  71. prefs[key] = val
  72. }
  73. }
  74. }
  75. // Allow any other parameters starting with "pref_" for extensibility
  76. for key, values := range r.URL.Query() {
  77. if strings.HasPrefix(key, "pref_") && len(values) > 0 {
  78. actualKey := strings.TrimPrefix(key, "pref_")
  79. prefs[actualKey] = values[0]
  80. }
  81. }
  82. // Save preferences
  83. if err := h.PreferenceManager.SetPreferences(ctx, session.ScreenName.IdentScreenName(), prefs); err != nil {
  84. h.Logger.ErrorContext(ctx, "failed to set preferences", "err", err.Error())
  85. h.sendError(w, http.StatusInternalServerError, "failed to save preferences")
  86. return
  87. }
  88. h.Logger.DebugContext(ctx, "preferences updated",
  89. "screenName", session.ScreenName.String(),
  90. "prefCount", len(prefs),
  91. )
  92. // Send success response
  93. response := BaseResponse{}
  94. response.Response.StatusCode = 200
  95. response.Response.StatusText = "OK"
  96. response.Response.Data = prefs
  97. SendResponse(w, r, response, h.Logger)
  98. }
  99. // GetPreferences handles GET /preference/get requests to retrieve user preferences.
  100. func (h *PreferenceHandler) GetPreferences(w http.ResponseWriter, r *http.Request) {
  101. ctx := r.Context()
  102. // Get session ID from parameters
  103. aimsid := r.URL.Query().Get("aimsid")
  104. if aimsid == "" {
  105. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  106. return
  107. }
  108. // Get session
  109. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  110. if err != nil {
  111. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  112. return
  113. }
  114. // Update session activity
  115. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  116. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  117. }
  118. // Get target user (optional, defaults to session user)
  119. targetUser := session.ScreenName.IdentScreenName()
  120. if t := r.URL.Query().Get("t"); t != "" {
  121. targetUser = state.NewIdentScreenName(t)
  122. }
  123. // Get all stored preferences or defaults
  124. allPrefs, err := h.PreferenceManager.GetPreferences(ctx, targetUser)
  125. if err != nil {
  126. h.Logger.ErrorContext(ctx, "failed to get preferences", "err", err.Error())
  127. allPrefs = h.getDefaultPreferences()
  128. }
  129. if len(allPrefs) == 0 {
  130. allPrefs = h.getDefaultPreferences()
  131. }
  132. // Check if specific preferences are being requested
  133. requestedPrefs := make(map[string]interface{})
  134. defaultPrefs := h.getDefaultPreferences()
  135. // Check each known preference key in the query parameters
  136. // When a preference appears in the query (e.g., playIMSound=1),
  137. // the client is requesting that specific preference value
  138. for key := range defaultPrefs {
  139. if r.URL.Query().Has(key) {
  140. // Client is requesting this specific preference
  141. if prefValue, exists := allPrefs[key]; exists {
  142. requestedPrefs[key] = prefValue
  143. } else {
  144. requestedPrefs[key] = defaultPrefs[key]
  145. }
  146. }
  147. }
  148. // If no specific preferences were requested, return all
  149. var prefs map[string]interface{}
  150. if len(requestedPrefs) > 0 {
  151. prefs = requestedPrefs
  152. } else {
  153. prefs = allPrefs
  154. }
  155. h.Logger.DebugContext(ctx, "preferences retrieved",
  156. "screenName", targetUser.String(),
  157. "prefCount", len(prefs),
  158. "requested", len(requestedPrefs) > 0,
  159. )
  160. // Check for AMF format to handle special Gromit compatibility requirements
  161. format := strings.ToLower(r.URL.Query().Get("f"))
  162. if format == "amf" || format == "amf3" {
  163. // Convert string "1"/"0" to numeric values for Gromit compatibility
  164. // Gromit expects numeric values for boolean preferences
  165. convertedPrefs := make(map[string]interface{})
  166. for key, val := range prefs {
  167. if strVal, ok := val.(string); ok {
  168. switch strVal {
  169. case "1":
  170. convertedPrefs[key] = 1
  171. case "0":
  172. convertedPrefs[key] = 0
  173. default:
  174. // Keep non-boolean values as strings
  175. convertedPrefs[key] = val
  176. }
  177. } else {
  178. convertedPrefs[key] = val
  179. }
  180. }
  181. prefs = convertedPrefs
  182. h.Logger.DebugContext(ctx, "AMF preference response",
  183. "prefs", prefs,
  184. "prefCount", len(prefs),
  185. "format", format,
  186. )
  187. // Ensure prefs is never nil or empty for Gromit
  188. if len(prefs) == 0 {
  189. // If no preferences found, at least return the requested ones with defaults
  190. if len(requestedPrefs) > 0 {
  191. prefs = requestedPrefs
  192. } else {
  193. // Return playIMSound as default if nothing else
  194. prefs = map[string]interface{}{
  195. "playIMSound": 1,
  196. }
  197. }
  198. }
  199. // For single preference requests, return directly for Gromit compatibility
  200. // For multiple preferences, wrap in jsonData
  201. if len(prefs) != 1 {
  202. prefs = map[string]interface{}{
  203. "jsonData": prefs,
  204. }
  205. }
  206. }
  207. // Send response in requested format
  208. response := BaseResponse{}
  209. response.Response.StatusCode = 200
  210. response.Response.StatusText = "OK"
  211. response.Response.Data = prefs
  212. SendResponse(w, r, response, h.Logger)
  213. }
  214. // SetPermitDeny handles GET /preference/setPermitDeny requests to update permit/deny settings.
  215. func (h *PreferenceHandler) SetPermitDeny(w http.ResponseWriter, r *http.Request) {
  216. ctx := r.Context()
  217. // Get session ID from parameters
  218. aimsid := r.URL.Query().Get("aimsid")
  219. if aimsid == "" {
  220. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  221. return
  222. }
  223. // Get session
  224. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  225. if err != nil {
  226. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  227. return
  228. }
  229. // Update session activity
  230. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  231. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  232. }
  233. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
  234. fb, err := h.FeedbagService.Query(r.Context(), session.OSCARSession, frame)
  235. if err != nil {
  236. h.sendError(w, http.StatusInternalServerError, "failed to retrieve feedbag")
  237. return
  238. }
  239. reply, ok := fb.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
  240. if !ok {
  241. h.sendError(w, http.StatusInternalServerError, "failed to retrieve feedbag")
  242. return
  243. }
  244. fl := state.NewFeedbagList(reply.Items, rand.Intn)
  245. // Get pdMode parameter
  246. pdModeStr := r.URL.Query().Get("pdMode")
  247. if pdModeStr != "" {
  248. switch pdModeStr { // todo: are the string ints possible inputs?
  249. case "permitAll", "1":
  250. fl.SetMode(uint8(wire.FeedbagPDModePermitAll))
  251. case "denyAll", "2":
  252. fl.SetMode(uint8(wire.FeedbagPDModeDenyAll))
  253. case "permitSome", "3":
  254. fl.SetMode(uint8(wire.FeedbagPDModePermitSome))
  255. case "denySome", "4":
  256. fl.SetMode(uint8(wire.FeedbagPDModeDenySome))
  257. case "permitOnList", "5":
  258. fl.SetMode(uint8(wire.FeedbagPDModePermitOnList))
  259. default:
  260. h.sendError(w, http.StatusBadRequest, "invalid pdMode value")
  261. return
  262. }
  263. }
  264. // Handle permit list updates
  265. if pdAllow := r.URL.Query().Get("pdAllow"); pdAllow != "" {
  266. users := strings.Split(pdAllow, ",")
  267. for _, user := range users {
  268. user = strings.TrimSpace(user)
  269. if user != "" {
  270. fl.PermitUser(user)
  271. }
  272. }
  273. }
  274. if pdAllowRemove := r.URL.Query().Get("pdAllowRemove"); pdAllowRemove != "" {
  275. users := strings.Split(pdAllowRemove, ",")
  276. for _, user := range users {
  277. user = strings.TrimSpace(user)
  278. if user != "" {
  279. fl.DeletePermit(user)
  280. }
  281. }
  282. }
  283. // Handle deny list updates
  284. if pdBlock := r.URL.Query().Get("pdBlock"); pdBlock != "" {
  285. users := strings.Split(pdBlock, ",")
  286. for _, user := range users {
  287. user = strings.TrimSpace(user)
  288. if user != "" {
  289. fl.DenyUser(user)
  290. }
  291. }
  292. }
  293. if pdBlockRemove := r.URL.Query().Get("pdBlockRemove"); pdBlockRemove != "" {
  294. users := strings.Split(pdBlockRemove, ",")
  295. for _, user := range users {
  296. user = strings.TrimSpace(user)
  297. if user != "" {
  298. fl.DeleteDeny(user)
  299. }
  300. }
  301. }
  302. if pending := fl.PendingUpdates(); len(pending) > 0 {
  303. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  304. if _, err := h.FeedbagService.UpsertItem(ctx, session.OSCARSession, frame, pending); err != nil {
  305. h.Logger.ErrorContext(ctx, "failed to set PD mode", "err", err.Error())
  306. h.sendError(w, http.StatusInternalServerError, "failed to update PD mode")
  307. return
  308. }
  309. }
  310. if pending := fl.PendingDeletes(); len(pending) > 0 {
  311. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem}
  312. body := wire.SNAC_0x13_0x0A_FeedbagDeleteItem{Items: pending}
  313. if _, err := h.FeedbagService.DeleteItem(ctx, session.OSCARSession, frame, body); err != nil {
  314. h.Logger.ErrorContext(ctx, "failed to set PD mode", "err", err.Error())
  315. h.sendError(w, http.StatusInternalServerError, "failed to update PD mode")
  316. return
  317. }
  318. }
  319. pdd := permitDenyData(fl.Items())
  320. h.Logger.DebugContext(ctx, "permit/deny settings updated",
  321. "screenName", session.ScreenName.String(),
  322. "pdMode", pdd.PDMode,
  323. "permitCount", len(pdd.PermitList),
  324. "denyCount", len(pdd.DenyList),
  325. )
  326. response := BaseResponse{}
  327. response.Response.StatusCode = 200
  328. response.Response.StatusText = "OK"
  329. response.Response.Data = pdd
  330. SendResponse(w, r, response, h.Logger)
  331. }
  332. func permitDenyData(fl []wire.FeedbagItem) PermitDenyData {
  333. pdd := PermitDenyData{}
  334. for _, item := range fl {
  335. switch item.ClassID {
  336. case wire.FeedbagClassIDDeny:
  337. pdd.DenyList = append(pdd.DenyList, item.Name)
  338. case wire.FeedbagClassIDPermit:
  339. pdd.PermitList = append(pdd.PermitList, item.Name)
  340. case wire.FeedbagClassIdPdinfo:
  341. mode, _ := item.Uint8(wire.FeedbagAttributesPdMode)
  342. switch wire.FeedbagPDMode(mode) {
  343. case wire.FeedbagPDModePermitAll:
  344. pdd.PDMode = "permitAll"
  345. case wire.FeedbagPDModeDenyAll:
  346. pdd.PDMode = "denyAll"
  347. case wire.FeedbagPDModePermitSome:
  348. pdd.PDMode = "permitSome"
  349. case wire.FeedbagPDModeDenySome:
  350. pdd.PDMode = "denySome"
  351. case wire.FeedbagPDModePermitOnList:
  352. pdd.PDMode = "permitOnList"
  353. }
  354. }
  355. }
  356. return pdd
  357. }
  358. // GetPermitDeny handles GET /preference/getPermitDeny requests to retrieve permit/deny settings.
  359. func (h *PreferenceHandler) GetPermitDeny(w http.ResponseWriter, r *http.Request) {
  360. ctx := r.Context()
  361. // Get session ID from parameters
  362. aimsid := r.URL.Query().Get("aimsid")
  363. if aimsid == "" {
  364. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  365. return
  366. }
  367. // Get session
  368. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  369. if err != nil {
  370. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  371. return
  372. }
  373. // Update session activity
  374. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  375. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  376. }
  377. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery}
  378. fb, err := h.FeedbagService.Query(r.Context(), session.OSCARSession, frame)
  379. if err != nil {
  380. h.sendError(w, http.StatusInternalServerError, "failed to retrieve feedbag")
  381. return
  382. }
  383. reply, ok := fb.Body.(wire.SNAC_0x13_0x06_FeedbagReply)
  384. if !ok {
  385. h.sendError(w, http.StatusInternalServerError, "failed to retrieve feedbag")
  386. return
  387. }
  388. pdd := permitDenyData(reply.Items)
  389. h.Logger.DebugContext(ctx, "permit/deny settings retrieved",
  390. "screenName", session.ScreenName.String(),
  391. "pdMode", pdd.PDMode,
  392. "permitCount", len(pdd.PermitList),
  393. "denyCount", len(pdd.DenyList),
  394. )
  395. response := BaseResponse{}
  396. response.Response.StatusCode = 200
  397. response.Response.StatusText = "OK"
  398. response.Response.Data = pdd
  399. SendResponse(w, r, response, h.Logger)
  400. }
  401. // sendError sends an error response in Web AIM API format.
  402. // getDefaultPreferences returns default preference values that clients expect.
  403. func (h *PreferenceHandler) getDefaultPreferences() map[string]interface{} {
  404. return map[string]interface{}{
  405. "autoPlay": "1",
  406. "playIMSound": "1",
  407. "playBuddySound": "1",
  408. "showTimestamps": "1",
  409. "showAdsFlag": "1",
  410. "soundSetting": "1",
  411. "awayMessageOn": "0",
  412. "awayMessage": "",
  413. "confirmSignOff": "0",
  414. "skipNavigator": "1",
  415. "displayIdleTime": "1",
  416. "repliesAnyone": "0",
  417. "repliesUsersOnline": "0",
  418. "repliesBuddies": "0",
  419. "replyMessage": "",
  420. "allowAccessPresence": "0",
  421. "blockIdleStatus": "0",
  422. "reportIdleTyping": "1",
  423. "smileysDisabled": "0",
  424. "sortBuddiesAlpha": "0",
  425. "statusMsg": "",
  426. "statusIcon": "",
  427. "skin": "default",
  428. }
  429. }
  430. func (h *PreferenceHandler) sendError(w http.ResponseWriter, statusCode int, message string) {
  431. // todo log
  432. SendError(w, statusCode, message)
  433. }