preference.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  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. if strVal == "1" {
  179. convertedPrefs[key] = 1
  180. } else if strVal == "0" {
  181. convertedPrefs[key] = 0
  182. } else {
  183. // Keep non-boolean values as strings
  184. convertedPrefs[key] = val
  185. }
  186. } else {
  187. convertedPrefs[key] = val
  188. }
  189. }
  190. prefs = convertedPrefs
  191. h.Logger.DebugContext(ctx, "AMF preference response",
  192. "prefs", prefs,
  193. "prefCount", len(prefs),
  194. "format", format,
  195. )
  196. // Ensure prefs is never nil or empty for Gromit
  197. if len(prefs) == 0 {
  198. // If no preferences found, at least return the requested ones with defaults
  199. if len(requestedPrefs) > 0 {
  200. prefs = requestedPrefs
  201. } else {
  202. // Return playIMSound as default if nothing else
  203. prefs = map[string]interface{}{
  204. "playIMSound": 1,
  205. }
  206. }
  207. }
  208. // For single preference requests, return directly for Gromit compatibility
  209. // For multiple preferences, wrap in jsonData
  210. if len(prefs) != 1 {
  211. prefs = map[string]interface{}{
  212. "jsonData": prefs,
  213. }
  214. }
  215. }
  216. // Send response in requested format
  217. response := BaseResponse{}
  218. response.Response.StatusCode = 200
  219. response.Response.StatusText = "OK"
  220. response.Response.Data = prefs
  221. SendResponse(w, r, response, h.Logger)
  222. }
  223. // SetPermitDeny handles GET /preference/setPermitDeny requests to update permit/deny settings.
  224. func (h *PreferenceHandler) SetPermitDeny(w http.ResponseWriter, r *http.Request) {
  225. ctx := r.Context()
  226. // Get session ID from parameters
  227. aimsid := r.URL.Query().Get("aimsid")
  228. if aimsid == "" {
  229. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  230. return
  231. }
  232. // Get session
  233. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  234. if err != nil {
  235. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  236. return
  237. }
  238. // Update session activity
  239. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  240. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  241. }
  242. // Get pdMode parameter
  243. pdModeStr := r.URL.Query().Get("pdMode")
  244. if pdModeStr != "" {
  245. pdMode, err := strconv.Atoi(pdModeStr)
  246. if err != nil || pdMode < 0 || pdMode > 5 {
  247. h.sendError(w, http.StatusBadRequest, "invalid pdMode value (must be 0-5)")
  248. return
  249. }
  250. // Set the PD mode
  251. if err := h.PermitDenyManager.SetPDMode(ctx, session.ScreenName.IdentScreenName(), wire.FeedbagPDMode(pdMode)); err != nil {
  252. h.Logger.ErrorContext(ctx, "failed to set PD mode", "err", err.Error())
  253. h.sendError(w, http.StatusInternalServerError, "failed to update PD mode")
  254. return
  255. }
  256. }
  257. // Handle permit list updates
  258. if permitAdd := r.URL.Query().Get("permitAdd"); permitAdd != "" {
  259. users := strings.Split(permitAdd, ",")
  260. for _, user := range users {
  261. user = strings.TrimSpace(user)
  262. if user != "" {
  263. targetSN := state.NewIdentScreenName(user)
  264. if err := h.PermitDenyManager.AddPermitBuddy(ctx, session.ScreenName.IdentScreenName(), targetSN); err != nil {
  265. h.Logger.ErrorContext(ctx, "failed to add to permit list", "user", user, "err", err.Error())
  266. }
  267. }
  268. }
  269. }
  270. if permitRemove := r.URL.Query().Get("permitRemove"); permitRemove != "" {
  271. users := strings.Split(permitRemove, ",")
  272. for _, user := range users {
  273. user = strings.TrimSpace(user)
  274. if user != "" {
  275. targetSN := state.NewIdentScreenName(user)
  276. if err := h.PermitDenyManager.RemovePermitBuddy(ctx, session.ScreenName.IdentScreenName(), targetSN); err != nil {
  277. h.Logger.ErrorContext(ctx, "failed to remove from permit list", "user", user, "err", err.Error())
  278. }
  279. }
  280. }
  281. }
  282. // Handle deny list updates
  283. if denyAdd := r.URL.Query().Get("denyAdd"); denyAdd != "" {
  284. users := strings.Split(denyAdd, ",")
  285. for _, user := range users {
  286. user = strings.TrimSpace(user)
  287. if user != "" {
  288. targetSN := state.NewIdentScreenName(user)
  289. if err := h.PermitDenyManager.AddDenyBuddy(ctx, session.ScreenName.IdentScreenName(), targetSN); err != nil {
  290. h.Logger.ErrorContext(ctx, "failed to add to deny list", "user", user, "err", err.Error())
  291. }
  292. }
  293. }
  294. }
  295. if denyRemove := r.URL.Query().Get("denyRemove"); denyRemove != "" {
  296. users := strings.Split(denyRemove, ",")
  297. for _, user := range users {
  298. user = strings.TrimSpace(user)
  299. if user != "" {
  300. targetSN := state.NewIdentScreenName(user)
  301. if err := h.PermitDenyManager.RemoveDenyBuddy(ctx, session.ScreenName.IdentScreenName(), targetSN); err != nil {
  302. h.Logger.ErrorContext(ctx, "failed to remove from deny list", "user", user, "err", err.Error())
  303. }
  304. }
  305. }
  306. }
  307. // Get updated PD data
  308. pdMode, _ := h.PermitDenyManager.GetPDMode(ctx, session.ScreenName.IdentScreenName())
  309. permitList, _ := h.PermitDenyManager.GetPermitList(ctx, session.ScreenName.IdentScreenName())
  310. denyList, _ := h.PermitDenyManager.GetDenyList(ctx, session.ScreenName.IdentScreenName())
  311. // Convert to string arrays
  312. permitUsers := make([]string, len(permitList))
  313. for i, u := range permitList {
  314. permitUsers[i] = u.String()
  315. }
  316. denyUsers := make([]string, len(denyList))
  317. for i, u := range denyList {
  318. denyUsers[i] = u.String()
  319. }
  320. h.Logger.DebugContext(ctx, "permit/deny settings updated",
  321. "screenName", session.ScreenName.String(),
  322. "pdMode", pdMode,
  323. "permitCount", len(permitUsers),
  324. "denyCount", len(denyUsers),
  325. )
  326. // Note: We don't broadcast immediate presence changes here.
  327. // The blocking relationship is now in the database and will be respected
  328. // by all future presence checks and message routing.
  329. // The blocked users will appear offline to each other on the next presence update.
  330. // Send response
  331. permitDenyData := PermitDenyData{
  332. PDMode: int(pdMode),
  333. PermitList: permitUsers,
  334. DenyList: denyUsers,
  335. }
  336. response := BaseResponse{}
  337. response.Response.StatusCode = 200
  338. response.Response.StatusText = "OK"
  339. response.Response.Data = permitDenyData
  340. SendResponse(w, r, response, h.Logger)
  341. }
  342. // GetPermitDeny handles GET /preference/getPermitDeny requests to retrieve permit/deny settings.
  343. func (h *PreferenceHandler) GetPermitDeny(w http.ResponseWriter, r *http.Request) {
  344. ctx := r.Context()
  345. // Get session ID from parameters
  346. aimsid := r.URL.Query().Get("aimsid")
  347. if aimsid == "" {
  348. h.sendError(w, http.StatusBadRequest, "missing aimsid parameter")
  349. return
  350. }
  351. // Get session
  352. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  353. if err != nil {
  354. h.sendError(w, http.StatusUnauthorized, "invalid or expired session")
  355. return
  356. }
  357. // Update session activity
  358. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  359. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  360. }
  361. // Get PD data
  362. pdMode, _ := h.PermitDenyManager.GetPDMode(ctx, session.ScreenName.IdentScreenName())
  363. permitList, _ := h.PermitDenyManager.GetPermitList(ctx, session.ScreenName.IdentScreenName())
  364. denyList, _ := h.PermitDenyManager.GetDenyList(ctx, session.ScreenName.IdentScreenName())
  365. // Convert to string arrays
  366. permitUsers := make([]string, len(permitList))
  367. for i, u := range permitList {
  368. permitUsers[i] = u.String()
  369. }
  370. denyUsers := make([]string, len(denyList))
  371. for i, u := range denyList {
  372. denyUsers[i] = u.String()
  373. }
  374. h.Logger.DebugContext(ctx, "permit/deny settings retrieved",
  375. "screenName", session.ScreenName.String(),
  376. "pdMode", pdMode,
  377. "permitCount", len(permitUsers),
  378. "denyCount", len(denyUsers),
  379. )
  380. // Send response
  381. permitDenyData := PermitDenyData{
  382. PDMode: int(pdMode),
  383. PermitList: permitUsers,
  384. DenyList: denyUsers,
  385. }
  386. response := BaseResponse{}
  387. response.Response.StatusCode = 200
  388. response.Response.StatusText = "OK"
  389. response.Response.Data = permitDenyData
  390. SendResponse(w, r, response, h.Logger)
  391. }
  392. // sendError sends an error response in Web AIM API format.
  393. // getDefaultPreferences returns default preference values that clients expect.
  394. func (h *PreferenceHandler) getDefaultPreferences() map[string]interface{} {
  395. return map[string]interface{}{
  396. "autoPlay": "1",
  397. "playIMSound": "1",
  398. "playBuddySound": "1",
  399. "showTimestamps": "1",
  400. "showAdsFlag": "1",
  401. "soundSetting": "1",
  402. "awayMessageOn": "0",
  403. "awayMessage": "",
  404. "confirmSignOff": "0",
  405. "skipNavigator": "1",
  406. "displayIdleTime": "1",
  407. "repliesAnyone": "0",
  408. "repliesUsersOnline": "0",
  409. "repliesBuddies": "0",
  410. "replyMessage": "",
  411. "allowAccessPresence": "0",
  412. "blockIdleStatus": "0",
  413. "reportIdleTyping": "1",
  414. "smileysDisabled": "0",
  415. "sortBuddiesAlpha": "0",
  416. "statusMsg": "",
  417. "statusIcon": "",
  418. "skin": "default",
  419. }
  420. }
  421. func (h *PreferenceHandler) sendError(w http.ResponseWriter, statusCode int, message string) {
  422. SendError(w, statusCode, message)
  423. }