preference.go 15 KB

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