vanity.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. package handlers
  2. import (
  3. "log/slog"
  4. "net/http"
  5. "strings"
  6. "github.com/mk6i/open-oscar-server/state"
  7. )
  8. // VanityHandler handles Web AIM API vanity URL endpoints.
  9. type VanityHandler struct {
  10. SessionManager *state.WebAPISessionManager
  11. VanityManager *state.VanityURLManager
  12. Logger *slog.Logger
  13. }
  14. // GetVanityInfo handles GET /aim/getVanityInfo requests to retrieve vanity URL information.
  15. func (h *VanityHandler) GetVanityInfo(w http.ResponseWriter, r *http.Request) {
  16. ctx := r.Context()
  17. // According to spec, this endpoint requires signed request parameters
  18. // but we'll make them optional for compatibility
  19. ts := r.URL.Query().Get("ts")
  20. sig := r.URL.Query().Get("sig_sha256")
  21. // Validate timestamp if provided
  22. if ts != "" && sig == "" {
  23. SendError(w, http.StatusBadRequest, "signature required when timestamp provided")
  24. return
  25. }
  26. // Get authentication from either aimsid or token
  27. aimsid := r.URL.Query().Get("aimsid")
  28. _ = r.URL.Query().Get("a") // Token auth not fully implemented
  29. var screenName string
  30. if aimsid != "" {
  31. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  32. if err == nil {
  33. screenName = session.ScreenName.String()
  34. }
  35. }
  36. // If no explicit target, use authenticated user
  37. targetUser := r.URL.Query().Get("t")
  38. if targetUser == "" && screenName != "" {
  39. targetUser = screenName
  40. }
  41. if targetUser == "" {
  42. SendError(w, http.StatusBadRequest, "missing target user")
  43. return
  44. }
  45. h.Logger.DebugContext(ctx, "retrieving vanity info",
  46. "targetUser", targetUser,
  47. "authenticated", screenName,
  48. )
  49. // Lookup vanity info by screen name
  50. info, err := h.VanityManager.GetVanityInfoByScreenName(ctx, targetUser)
  51. // Handle error or no vanity URL found
  52. if err != nil || info == nil {
  53. if err != nil && !strings.Contains(err.Error(), "not found") {
  54. h.Logger.ErrorContext(ctx, "failed to get vanity info",
  55. "error", err,
  56. )
  57. SendError(w, http.StatusInternalServerError, "failed to retrieve vanity info")
  58. return
  59. }
  60. // No vanity URL configured - return not found
  61. response := BaseResponse{}
  62. response.Response.StatusCode = 200
  63. response.Response.StatusText = "OK"
  64. response.Response.Data = map[string]interface{}{
  65. "found": false,
  66. "screenName": targetUser,
  67. }
  68. SendResponse(w, r, response, h.Logger)
  69. return
  70. }
  71. // Build response
  72. responseData := map[string]interface{}{
  73. "found": true,
  74. "screenName": info.ScreenName,
  75. "vanityUrl": info.VanityURL,
  76. "profileUrl": info.ProfileURL,
  77. "isActive": info.IsActive,
  78. }
  79. // Add optional fields if present
  80. if info.DisplayName != "" {
  81. responseData["displayName"] = info.DisplayName
  82. }
  83. if info.Bio != "" {
  84. responseData["bio"] = info.Bio
  85. }
  86. if info.Location != "" {
  87. responseData["location"] = info.Location
  88. }
  89. if info.Website != "" {
  90. responseData["website"] = info.Website
  91. }
  92. // Add extra data if present
  93. if info.Extra != nil {
  94. for k, v := range info.Extra {
  95. responseData[k] = v
  96. }
  97. }
  98. response := BaseResponse{}
  99. response.Response.StatusCode = 200
  100. response.Response.StatusText = "OK"
  101. response.Response.Data = responseData
  102. SendResponse(w, r, response, h.Logger)
  103. }
  104. // SetVanityURL handles requests to set or update a vanity URL (requires authentication).
  105. func (h *VanityHandler) SetVanityURL(w http.ResponseWriter, r *http.Request) {
  106. ctx := r.Context()
  107. // Authentication required
  108. aimsid := r.URL.Query().Get("aimsid")
  109. if aimsid == "" {
  110. SendError(w, http.StatusBadRequest, "missing aimsid parameter")
  111. return
  112. }
  113. // Get session
  114. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  115. if err != nil {
  116. SendError(w, http.StatusUnauthorized, "invalid or expired session")
  117. return
  118. }
  119. // Update session activity
  120. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  121. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  122. }
  123. // Get vanity URL from parameters
  124. vanityURL := r.URL.Query().Get("vanityUrl")
  125. if vanityURL == "" {
  126. SendError(w, http.StatusBadRequest, "missing vanityUrl parameter")
  127. return
  128. }
  129. // Collect optional profile information
  130. info := make(map[string]interface{})
  131. if displayName := r.URL.Query().Get("displayName"); displayName != "" {
  132. info["displayName"] = displayName
  133. }
  134. if bio := r.URL.Query().Get("bio"); bio != "" {
  135. info["bio"] = bio
  136. }
  137. if location := r.URL.Query().Get("location"); location != "" {
  138. info["location"] = location
  139. }
  140. if website := r.URL.Query().Get("website"); website != "" {
  141. info["website"] = website
  142. }
  143. h.Logger.InfoContext(ctx, "setting vanity URL",
  144. "screenName", session.ScreenName.String(),
  145. "vanityUrl", vanityURL,
  146. )
  147. // Create or update the vanity URL
  148. if err := h.VanityManager.CreateOrUpdateVanityURL(ctx, session.ScreenName.String(), vanityURL, info); err != nil {
  149. h.Logger.ErrorContext(ctx, "failed to set vanity URL",
  150. "screenName", session.ScreenName.String(),
  151. "vanityUrl", vanityURL,
  152. "error", err,
  153. )
  154. // Check if it's a validation or duplicate error
  155. if strings.Contains(err.Error(), "reserved") ||
  156. strings.Contains(err.Error(), "already taken") ||
  157. strings.Contains(err.Error(), "must be") ||
  158. strings.Contains(err.Error(), "cannot") {
  159. SendError(w, http.StatusBadRequest, err.Error())
  160. return
  161. }
  162. SendError(w, http.StatusInternalServerError, "failed to set vanity URL")
  163. return
  164. }
  165. // Return success response with the new vanity info
  166. vanityInfo, _ := h.VanityManager.GetVanityInfoByScreenName(ctx, session.ScreenName.String())
  167. responseData := map[string]interface{}{
  168. "success": true,
  169. "screenName": session.ScreenName.String(),
  170. "vanityUrl": vanityURL,
  171. }
  172. if vanityInfo != nil {
  173. responseData["profileUrl"] = vanityInfo.ProfileURL
  174. }
  175. response := BaseResponse{}
  176. response.Response.StatusCode = 200
  177. response.Response.StatusText = "OK"
  178. response.Response.Data = responseData
  179. SendResponse(w, r, response, h.Logger)
  180. }
  181. // CheckAvailability handles requests to check if a vanity URL is available.
  182. func (h *VanityHandler) CheckAvailability(w http.ResponseWriter, r *http.Request) {
  183. ctx := r.Context()
  184. // Get vanity URL from parameters
  185. vanityURL := r.URL.Query().Get("vanityUrl")
  186. if vanityURL == "" {
  187. SendError(w, http.StatusBadRequest, "missing vanityUrl parameter")
  188. return
  189. }
  190. h.Logger.DebugContext(ctx, "checking vanity URL availability",
  191. "vanityUrl", vanityURL,
  192. )
  193. // Check availability
  194. available, err := h.VanityManager.CheckAvailability(ctx, vanityURL)
  195. if err != nil {
  196. // If it's a validation error, return it as a bad request
  197. if strings.Contains(err.Error(), "must be") ||
  198. strings.Contains(err.Error(), "cannot") ||
  199. strings.Contains(err.Error(), "can only") {
  200. response := BaseResponse{}
  201. response.Response.StatusCode = 200
  202. response.Response.StatusText = "OK"
  203. response.Response.Data = map[string]interface{}{
  204. "available": false,
  205. "reason": err.Error(),
  206. }
  207. SendResponse(w, r, response, h.Logger)
  208. return
  209. }
  210. h.Logger.ErrorContext(ctx, "failed to check availability",
  211. "vanityUrl", vanityURL,
  212. "error", err,
  213. )
  214. SendError(w, http.StatusInternalServerError, "failed to check availability")
  215. return
  216. }
  217. // Build response
  218. responseData := map[string]interface{}{
  219. "available": available,
  220. "vanityUrl": vanityURL,
  221. }
  222. if !available {
  223. responseData["reason"] = "This vanity URL is already taken or reserved"
  224. }
  225. response := BaseResponse{}
  226. response.Response.StatusCode = 200
  227. response.Response.StatusText = "OK"
  228. response.Response.Data = responseData
  229. SendResponse(w, r, response, h.Logger)
  230. }
  231. // DeleteVanityURL handles requests to delete a vanity URL (requires authentication).
  232. func (h *VanityHandler) DeleteVanityURL(w http.ResponseWriter, r *http.Request) {
  233. ctx := r.Context()
  234. // Authentication required
  235. aimsid := r.URL.Query().Get("aimsid")
  236. if aimsid == "" {
  237. SendError(w, http.StatusBadRequest, "missing aimsid parameter")
  238. return
  239. }
  240. // Get session
  241. session, err := h.SessionManager.GetSession(r.Context(), aimsid)
  242. if err != nil {
  243. SendError(w, http.StatusUnauthorized, "invalid or expired session")
  244. return
  245. }
  246. // Update session activity
  247. if err := h.SessionManager.TouchSession(r.Context(), aimsid); err != nil {
  248. h.Logger.WarnContext(ctx, "failed to touch session", "aimsid", aimsid, "error", err)
  249. }
  250. h.Logger.InfoContext(ctx, "deleting vanity URL",
  251. "screenName", session.ScreenName.String(),
  252. )
  253. // Delete the vanity URL
  254. if err := h.VanityManager.DeleteVanityURL(ctx, session.ScreenName.String()); err != nil {
  255. h.Logger.ErrorContext(ctx, "failed to delete vanity URL",
  256. "screenName", session.ScreenName.String(),
  257. "error", err,
  258. )
  259. SendError(w, http.StatusInternalServerError, "failed to delete vanity URL")
  260. return
  261. }
  262. // Return success response
  263. response := BaseResponse{}
  264. response.Response.StatusCode = 200
  265. response.Response.StatusText = "OK"
  266. response.Response.Data = map[string]interface{}{
  267. "success": true,
  268. "screenName": session.ScreenName.String(),
  269. "message": "Vanity URL deleted successfully",
  270. }
  271. SendResponse(w, r, response, h.Logger)
  272. }