mgmt_api.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988
  1. package http
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/base64"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "log/slog"
  10. "net"
  11. "net/http"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "github.com/google/uuid"
  16. "github.com/mk6i/retro-aim-server/config"
  17. "github.com/mk6i/retro-aim-server/state"
  18. "github.com/mk6i/retro-aim-server/wire"
  19. )
  20. func NewManagementAPI(
  21. bld config.Build,
  22. cfg config.Config,
  23. userManager UserManager,
  24. sessionRetriever SessionRetriever,
  25. chatRoomRetriever ChatRoomRetriever,
  26. chatRoomCreator ChatRoomCreator,
  27. chatSessionRetriever ChatSessionRetriever,
  28. directoryManager DirectoryManager,
  29. messageRelayer MessageRelayer,
  30. bartRetriever BuddyIconRetriever,
  31. feedbagRetriever FeedBagRetriever,
  32. accountManager AccountManager,
  33. profileRetriever ProfileRetriever,
  34. logger *slog.Logger,
  35. ) *Server {
  36. mux := http.NewServeMux()
  37. // Handlers for '/user' route
  38. mux.HandleFunc("DELETE /user", func(w http.ResponseWriter, r *http.Request) {
  39. deleteUserHandler(w, r, userManager, logger)
  40. })
  41. mux.HandleFunc("GET /user", func(w http.ResponseWriter, r *http.Request) {
  42. getUserHandler(w, r, userManager, logger)
  43. })
  44. mux.HandleFunc("POST /user", func(w http.ResponseWriter, r *http.Request) {
  45. postUserHandler(w, r, userManager, uuid.New, logger)
  46. })
  47. // Handlers for '/user/password' route
  48. mux.HandleFunc("PUT /user/password", func(w http.ResponseWriter, r *http.Request) {
  49. putUserPasswordHandler(w, r, userManager, logger)
  50. })
  51. // Handlers for '/user/login' route
  52. mux.HandleFunc("GET /user/login", func(w http.ResponseWriter, r *http.Request) {
  53. getUserLoginHandler(w, r, userManager, logger)
  54. })
  55. // Handlers for '/user/{screenname}/account' route
  56. mux.HandleFunc("GET /user/{screenname}/account", func(w http.ResponseWriter, r *http.Request) {
  57. getUserAccountHandler(w, r, userManager, accountManager, profileRetriever, logger)
  58. })
  59. mux.HandleFunc("PATCH /user/{screenname}/account", func(w http.ResponseWriter, r *http.Request) {
  60. patchUserAccountHandler(w, r, userManager, accountManager, logger)
  61. })
  62. // Handlers for '/user/{screenname}/icon' route
  63. mux.HandleFunc("GET /user/{screenname}/icon", func(w http.ResponseWriter, r *http.Request) {
  64. getUserBuddyIconHandler(w, r, userManager, feedbagRetriever, bartRetriever, logger)
  65. })
  66. // Handlers for '/session' route
  67. mux.HandleFunc("GET /session", func(w http.ResponseWriter, r *http.Request) {
  68. getSessionHandler(w, r, sessionRetriever, time.Since)
  69. })
  70. // Handlers for '/session/{screenname}' route
  71. mux.HandleFunc("GET /session/{screenname}", func(w http.ResponseWriter, r *http.Request) {
  72. getSessionHandler(w, r, sessionRetriever, time.Since)
  73. })
  74. mux.HandleFunc("DELETE /session/{screenname}", func(w http.ResponseWriter, r *http.Request) {
  75. deleteSessionHandler(w, r, sessionRetriever)
  76. })
  77. // Handlers for '/chat/room/public' route
  78. mux.HandleFunc("GET /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  79. getPublicChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  80. })
  81. mux.HandleFunc("POST /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  82. postPublicChatHandler(w, r, chatRoomCreator, logger)
  83. })
  84. // Handlers for '/chat/room/private' route
  85. mux.HandleFunc("GET /chat/room/private", func(w http.ResponseWriter, r *http.Request) {
  86. getPrivateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  87. })
  88. // Handlers for '/instant-message' route
  89. mux.HandleFunc("POST /instant-message", func(w http.ResponseWriter, r *http.Request) {
  90. postInstantMessageHandler(w, r, messageRelayer, logger)
  91. })
  92. // Handlers for '/version' route
  93. mux.HandleFunc("GET /version", func(w http.ResponseWriter, r *http.Request) {
  94. getVersionHandler(w, bld)
  95. })
  96. // Handlers for '/directory/category' route
  97. mux.HandleFunc("GET /directory/category", func(w http.ResponseWriter, r *http.Request) {
  98. getDirectoryCategoryHandler(w, r, directoryManager, logger)
  99. })
  100. mux.HandleFunc("POST /directory/category", func(w http.ResponseWriter, r *http.Request) {
  101. postDirectoryCategoryHandler(w, r, directoryManager, logger)
  102. })
  103. // Handlers for '/directory/category/{id}' route
  104. mux.HandleFunc("DELETE /directory/category/{id}", func(w http.ResponseWriter, r *http.Request) {
  105. deleteDirectoryCategoryHandler(w, r, directoryManager, logger)
  106. })
  107. // Handlers for '/directory/category/{id}/keyword' route
  108. mux.HandleFunc("GET /directory/category/{id}/keyword", func(w http.ResponseWriter, r *http.Request) {
  109. getDirectoryCategoryKeywordHandler(w, r, directoryManager, logger)
  110. })
  111. // Handlers for '/directory/keyword' route
  112. mux.HandleFunc("POST /directory/keyword", func(w http.ResponseWriter, r *http.Request) {
  113. postDirectoryKeywordHandler(w, r, directoryManager, logger)
  114. })
  115. // Handlers for '/directory/keyword/{id}' route
  116. mux.HandleFunc("DELETE /directory/keyword/{id}", func(w http.ResponseWriter, r *http.Request) {
  117. deleteDirectoryKeywordHandler(w, r, directoryManager, logger)
  118. })
  119. return &Server{
  120. Server: http.Server{
  121. Addr: net.JoinHostPort(cfg.ApiHost, cfg.ApiPort),
  122. Handler: mux,
  123. },
  124. Logger: logger,
  125. }
  126. }
  127. type Server struct {
  128. http.Server
  129. Logger *slog.Logger
  130. }
  131. func (s *Server) Start(ctx context.Context) error {
  132. ch := make(chan error)
  133. go func() {
  134. s.Logger.Info("starting management API server", "addr", s.Addr)
  135. if err := s.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
  136. ch <- fmt.Errorf("unable to start management API server: %w", err)
  137. }
  138. }()
  139. select {
  140. case <-ctx.Done():
  141. case err := <-ch:
  142. return err
  143. }
  144. shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  145. defer cancel()
  146. if err := s.Shutdown(shutdownCtx); err != nil {
  147. s.Logger.Error("unable to shutdown management API server", "err", err.Error())
  148. }
  149. return nil
  150. }
  151. // deleteUserHandler handles the DELETE /user endpoint.
  152. func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManager, logger *slog.Logger) {
  153. user, err := userFromBody(r)
  154. if err != nil {
  155. http.Error(w, err.Error(), http.StatusBadRequest)
  156. return
  157. }
  158. err = manager.DeleteUser(r.Context(), state.NewIdentScreenName(user.ScreenName))
  159. switch {
  160. case errors.Is(err, state.ErrNoUser):
  161. http.Error(w, "user does not exist", http.StatusNotFound)
  162. return
  163. case err != nil:
  164. logger.Error("error deleting user DELETE /user", "err", err.Error())
  165. http.Error(w, "internal server error", http.StatusInternalServerError)
  166. return
  167. }
  168. w.WriteHeader(http.StatusNoContent)
  169. _, _ = fmt.Fprintln(w, "User account successfully deleted.")
  170. }
  171. // putUserPasswordHandler handles the PUT /user/password endpoint.
  172. func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  173. input, err := userFromBody(r)
  174. if err != nil {
  175. http.Error(w, err.Error(), http.StatusBadRequest)
  176. return
  177. }
  178. sn := state.NewIdentScreenName(input.ScreenName)
  179. if err := userManager.SetUserPassword(r.Context(), sn, input.Password); err != nil {
  180. switch {
  181. case errors.Is(err, state.ErrNoUser):
  182. http.Error(w, "user does not exist", http.StatusNotFound)
  183. return
  184. case errors.Is(err, state.ErrPasswordInvalid):
  185. http.Error(w, err.Error(), http.StatusBadRequest)
  186. return
  187. default:
  188. logger.Error("error updating user password PUT /user/password", "err", err.Error())
  189. http.Error(w, "internal server error", http.StatusInternalServerError)
  190. return
  191. }
  192. }
  193. w.WriteHeader(http.StatusNoContent)
  194. _, _ = fmt.Fprintln(w, "Password successfully reset.")
  195. }
  196. // getSessionHandler handles GET /session
  197. func getSessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever, funcTimeSince func(t time.Time) time.Duration) {
  198. w.Header().Set("Content-Type", "application/json")
  199. var allUsers []*state.Session
  200. if screenName := r.PathValue("screenname"); screenName != "" {
  201. session := sessionRetriever.RetrieveSession(state.NewIdentScreenName(screenName))
  202. if session == nil {
  203. http.Error(w, "session not found", http.StatusNotFound)
  204. return
  205. }
  206. allUsers = append(allUsers, session)
  207. } else {
  208. allUsers = sessionRetriever.AllSessions()
  209. }
  210. ou := onlineUsers{
  211. Count: len(allUsers),
  212. Sessions: make([]sessionHandle, len(allUsers)),
  213. }
  214. for i, s := range allUsers {
  215. // report 0 if the user is not idle
  216. idleSeconds := funcTimeSince(s.IdleTime()).Seconds()
  217. if !s.Idle() {
  218. idleSeconds = 0
  219. }
  220. onlineSeconds := funcTimeSince(s.SignonTime()).Seconds()
  221. ou.Sessions[i] = sessionHandle{
  222. ID: s.IdentScreenName().String(),
  223. ScreenName: s.DisplayScreenName().String(),
  224. OnlineSeconds: onlineSeconds,
  225. AwayMessage: s.AwayMessage(),
  226. IdleSeconds: idleSeconds,
  227. IsICQ: s.UIN() > 0,
  228. }
  229. ra := s.RemoteAddr()
  230. if ra != nil {
  231. ou.Sessions[i].RemoteAddr = ra.Addr().String()
  232. ou.Sessions[i].RemotePort = ra.Port()
  233. }
  234. }
  235. if err := json.NewEncoder(w).Encode(ou); err != nil {
  236. http.Error(w, err.Error(), http.StatusInternalServerError)
  237. return
  238. }
  239. }
  240. // deleteSessionHandler handles DELETE /session/{screenname}
  241. func deleteSessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever) {
  242. w.Header().Set("Content-Type", "application/json")
  243. if screenName := r.PathValue("screenname"); screenName != "" {
  244. session := sessionRetriever.RetrieveSession(state.NewIdentScreenName(screenName))
  245. if session == nil {
  246. errorMsg(w, "session not found", http.StatusNotFound)
  247. return
  248. }
  249. session.Close()
  250. }
  251. w.WriteHeader(http.StatusNoContent)
  252. }
  253. // getUserHandler handles the GET /user endpoint.
  254. func getUserHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  255. w.Header().Set("Content-Type", "application/json")
  256. users, err := userManager.AllUsers(r.Context())
  257. if err != nil {
  258. logger.Error("error in GET /user", "err", err.Error())
  259. http.Error(w, "internal server error", http.StatusInternalServerError)
  260. return
  261. }
  262. out := make([]userHandle, len(users))
  263. for i, u := range users {
  264. suspendedStatus, err := getSuspendedStatusErrCodeToText(u.SuspendedStatus)
  265. if err != nil {
  266. logger.Error("error getting suspended status in GET /user", "err", err.Error())
  267. http.Error(w, "internal server error", http.StatusInternalServerError)
  268. return
  269. }
  270. out[i] = userHandle{
  271. ID: u.IdentScreenName.String(),
  272. ScreenName: u.DisplayScreenName.String(),
  273. IsICQ: u.IsICQ,
  274. SuspendedStatus: suspendedStatus,
  275. IsBot: u.IsBot,
  276. }
  277. }
  278. if err := json.NewEncoder(w).Encode(out); err != nil {
  279. http.Error(w, err.Error(), http.StatusInternalServerError)
  280. return
  281. }
  282. }
  283. // postUserHandler handles the POST /user endpoint.
  284. func postUserHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  285. input, err := userFromBody(r)
  286. if err != nil {
  287. http.Error(w, err.Error(), http.StatusBadRequest)
  288. return
  289. }
  290. sn := state.DisplayScreenName(input.ScreenName)
  291. if sn.IsUIN() {
  292. if err := sn.ValidateUIN(); err != nil {
  293. http.Error(w, fmt.Sprintf("invalid uin: %s", err), http.StatusBadRequest)
  294. return
  295. }
  296. } else {
  297. if err := sn.ValidateAIMHandle(); err != nil {
  298. http.Error(w, fmt.Sprintf("invalid screen name: %s", err), http.StatusBadRequest)
  299. return
  300. }
  301. }
  302. user := state.User{
  303. AuthKey: newUUID().String(),
  304. DisplayScreenName: sn,
  305. IdentScreenName: sn.IdentScreenName(),
  306. IsICQ: sn.IsUIN(),
  307. }
  308. if err := user.HashPassword(input.Password); err != nil {
  309. http.Error(w, fmt.Sprintf("invalid password: %s", err), http.StatusBadRequest)
  310. return
  311. }
  312. err = userManager.InsertUser(r.Context(), user)
  313. switch {
  314. case errors.Is(err, state.ErrDupUser):
  315. http.Error(w, "user already exists", http.StatusConflict)
  316. return
  317. case err != nil:
  318. logger.Error("error inserting user POST /user", "err", err.Error())
  319. http.Error(w, "internal server error", http.StatusInternalServerError)
  320. return
  321. }
  322. w.WriteHeader(http.StatusCreated)
  323. _, _ = fmt.Fprintln(w, "User account created successfully.")
  324. }
  325. func userFromBody(r *http.Request) (userWithPassword, error) {
  326. user := userWithPassword{}
  327. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  328. return userWithPassword{}, errors.New("malformed input")
  329. }
  330. return user, nil
  331. }
  332. // getUserLoginHandler is a temporary endpoint for validating user credentials for
  333. // chivanet. do not rely on this endpoint, as it will be eventually removed.
  334. func getUserLoginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  335. authHeader := r.Header.Get("Authorization")
  336. if authHeader == "" {
  337. // No authentication header found
  338. w.WriteHeader(http.StatusUnauthorized)
  339. w.Header().Set("WWW-Authenticate", `Basic realm="User Login"`)
  340. _, _ = w.Write([]byte("401 Unauthorized\n"))
  341. return
  342. }
  343. auth := strings.SplitN(authHeader, " ", 2)
  344. if len(auth) != 2 || auth[0] != "Basic" {
  345. w.WriteHeader(http.StatusUnauthorized)
  346. _, _ = w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
  347. return
  348. }
  349. payload, err := base64.StdEncoding.DecodeString(auth[1])
  350. if err != nil {
  351. w.WriteHeader(http.StatusUnauthorized)
  352. _, _ = w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
  353. return
  354. }
  355. pair := strings.SplitN(string(payload), ":", 2)
  356. if len(pair) != 2 {
  357. w.WriteHeader(http.StatusUnauthorized)
  358. _, _ = w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
  359. return
  360. }
  361. username, password := state.NewIdentScreenName(pair[0]), pair[1]
  362. user, err := userManager.User(r.Context(), username)
  363. if err != nil {
  364. w.WriteHeader(http.StatusInternalServerError)
  365. _, _ = w.Write([]byte("500 InternalServerError\n"))
  366. logger.Error("error getting user", "err", err.Error())
  367. return
  368. }
  369. if user == nil || !user.ValidateHash(wire.StrongMD5PasswordHash(password, user.AuthKey)) {
  370. w.WriteHeader(http.StatusUnauthorized)
  371. _, _ = w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
  372. return
  373. }
  374. // Successfully authenticated
  375. w.WriteHeader(http.StatusOK)
  376. _, _ = w.Write([]byte("200 OK: Successfully Authenticated\n"))
  377. }
  378. // getPublicChatHandler handles the GET /chat/room/public endpoint.
  379. func getPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  380. w.Header().Set("Content-Type", "application/json")
  381. rooms, err := chatRoomRetriever.AllChatRooms(r.Context(), state.PublicExchange)
  382. if err != nil {
  383. logger.Error("error in GET /chat/rooms/public", "err", err.Error())
  384. http.Error(w, "internal server error", http.StatusInternalServerError)
  385. return
  386. }
  387. out := make([]chatRoom, len(rooms))
  388. for i, room := range rooms {
  389. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  390. cr := chatRoom{
  391. CreateTime: room.CreateTime(),
  392. Name: room.Name(),
  393. Participants: make([]aimChatUserHandle, len(sessions)),
  394. URL: room.URL().String(),
  395. }
  396. for j, sess := range sessions {
  397. cr.Participants[j] = aimChatUserHandle{
  398. ID: sess.IdentScreenName().String(),
  399. ScreenName: sess.DisplayScreenName().String(),
  400. }
  401. }
  402. out[i] = cr
  403. }
  404. writeUnescapeChatURL(w, out)
  405. }
  406. // postPublicChatHandler handles the POST /chat/room/public endpoint.
  407. func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreator ChatRoomCreator, logger *slog.Logger) {
  408. input := chatRoomCreate{}
  409. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  410. http.Error(w, "invalid input", http.StatusBadRequest)
  411. return
  412. }
  413. input.Name = strings.TrimSpace(input.Name)
  414. if input.Name == "" || len(input.Name) > 50 {
  415. http.Error(w, "chat room name must be between 1 and 50 characters", http.StatusBadRequest)
  416. return
  417. }
  418. cr := state.NewChatRoom(input.Name, state.NewIdentScreenName("system"), state.PublicExchange)
  419. err := chatRoomCreator.CreateChatRoom(r.Context(), &cr)
  420. switch {
  421. case errors.Is(err, state.ErrDupChatRoom):
  422. http.Error(w, "Chat room already exists.", http.StatusConflict)
  423. return
  424. case err != nil:
  425. logger.Error("error inserting chat room POST /chat/room/public", "err", err.Error())
  426. http.Error(w, "internal server error", http.StatusInternalServerError)
  427. return
  428. }
  429. w.WriteHeader(http.StatusCreated)
  430. _, _ = fmt.Fprintln(w, "Chat room created successfully.")
  431. }
  432. // getPrivateChatHandler handles the GET /chat/room/private endpoint.
  433. func getPrivateChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  434. w.Header().Set("Content-Type", "application/json")
  435. rooms, err := chatRoomRetriever.AllChatRooms(r.Context(), state.PrivateExchange)
  436. if err != nil {
  437. logger.Error("error in GET /chat/rooms/private", "err", err.Error())
  438. http.Error(w, "internal server error", http.StatusInternalServerError)
  439. return
  440. }
  441. out := make([]chatRoom, len(rooms))
  442. for i, room := range rooms {
  443. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  444. cr := chatRoom{
  445. CreateTime: room.CreateTime(),
  446. CreatorID: room.Creator().String(),
  447. Name: room.Name(),
  448. Participants: make([]aimChatUserHandle, len(sessions)),
  449. URL: room.URL().String(),
  450. }
  451. for j, sess := range sessions {
  452. cr.Participants[j] = aimChatUserHandle{
  453. ID: sess.IdentScreenName().String(),
  454. ScreenName: sess.DisplayScreenName().String(),
  455. }
  456. }
  457. out[i] = cr
  458. }
  459. writeUnescapeChatURL(w, out)
  460. }
  461. // writeUnescapeChatURL writes a JSON-encoded list of chat rooms with unescaped
  462. // ampersands preceding the exchange query param.
  463. //
  464. // before: aim:gochat?roomname=Office+Hijinks\u0026exchange=5
  465. // after: aim:gochat?roomname=Office+Hijinks&exchange=5
  466. //
  467. // This makes it easier to copy the gochat URL into AIM, which does not
  468. // recognize the ampersand unicode character \u0026.
  469. func writeUnescapeChatURL(w http.ResponseWriter, out []chatRoom) {
  470. buf := &bytes.Buffer{}
  471. if err := json.NewEncoder(buf).Encode(out); err != nil {
  472. http.Error(w, err.Error(), http.StatusInternalServerError)
  473. return
  474. }
  475. b := bytes.ReplaceAll(buf.Bytes(), []byte(`\u0026exchange`), []byte(`&exchange`))
  476. if _, err := w.Write(b); err != nil {
  477. http.Error(w, err.Error(), http.StatusInternalServerError)
  478. return
  479. }
  480. }
  481. // postIMHandler handles the POST /instant-message endpoint.
  482. func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) {
  483. input := instantMessage{}
  484. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  485. http.Error(w, "malformed input", http.StatusBadRequest)
  486. return
  487. }
  488. tlv, err := wire.ICBMFragmentList(input.Text)
  489. if err != nil {
  490. logger.Error("error sending message POST /instant-message", "err", err.Error())
  491. http.Error(w, "internal server error", http.StatusInternalServerError)
  492. return
  493. }
  494. msg := wire.SNACMessage{
  495. Frame: wire.SNACFrame{
  496. FoodGroup: wire.ICBM,
  497. SubGroup: wire.ICBMChannelMsgToClient,
  498. },
  499. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  500. ChannelID: 1,
  501. TLVUserInfo: wire.TLVUserInfo{
  502. ScreenName: input.From,
  503. },
  504. TLVRestBlock: wire.TLVRestBlock{
  505. TLVList: wire.TLVList{
  506. wire.NewTLVBE(wire.ICBMTLVAOLIMData, tlv),
  507. },
  508. },
  509. },
  510. }
  511. messageRelayer.RelayToScreenName(context.Background(), state.NewIdentScreenName(input.To), msg)
  512. w.WriteHeader(http.StatusOK)
  513. _, _ = fmt.Fprintln(w, "Message sent successfully.")
  514. }
  515. // getUserBuddyIconHandler handles the GET /user/{screenname}/icon endpoint.
  516. func getUserBuddyIconHandler(w http.ResponseWriter, r *http.Request, u UserManager, f FeedBagRetriever, b BuddyIconRetriever, logger *slog.Logger) {
  517. screenName := state.NewIdentScreenName(r.PathValue("screenname"))
  518. user, err := u.User(r.Context(), screenName)
  519. if err != nil {
  520. logger.Error("error retrieving user", "err", err.Error())
  521. http.Error(w, "internal server error", http.StatusInternalServerError)
  522. return
  523. }
  524. if user == nil {
  525. http.Error(w, "user not found", http.StatusNotFound)
  526. return
  527. }
  528. iconRef, err := f.BuddyIconMetadata(r.Context(), screenName)
  529. if err != nil {
  530. logger.Error("error retrieving buddy icon ref", "err", err.Error())
  531. http.Error(w, "internal server error", http.StatusInternalServerError)
  532. return
  533. }
  534. if iconRef == nil || iconRef.HasClearIconHash() {
  535. http.Error(w, "icon not found", http.StatusNotFound)
  536. return
  537. }
  538. icon, err := b.BuddyIcon(r.Context(), iconRef.Hash)
  539. if err != nil {
  540. logger.Error("error retrieving buddy icon bart item", "err", err.Error())
  541. http.Error(w, "internal server error", http.StatusInternalServerError)
  542. return
  543. }
  544. w.Header().Set("Content-Type", http.DetectContentType(icon))
  545. w.Write(icon)
  546. }
  547. // getUserAccountHandler handles the GET /user/{screenname}/account endpoint.
  548. func getUserAccountHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, a AccountManager, p ProfileRetriever, logger *slog.Logger) {
  549. w.Header().Set("Content-Type", "application/json")
  550. screenName := r.PathValue("screenname")
  551. user, err := userManager.User(r.Context(), state.NewIdentScreenName(screenName))
  552. if err != nil {
  553. logger.Error("error in GET /user/{screenname}/account", "err", err.Error())
  554. http.Error(w, "internal server error", http.StatusInternalServerError)
  555. return
  556. }
  557. if user == nil {
  558. http.Error(w, "user not found", http.StatusNotFound)
  559. return
  560. }
  561. emailAddress := ""
  562. email, err := a.EmailAddress(r.Context(), user.IdentScreenName)
  563. if err != nil {
  564. emailAddress = ""
  565. } else {
  566. emailAddress = email.String()
  567. }
  568. regStatus, err := a.RegStatus(r.Context(), user.IdentScreenName)
  569. if err != nil {
  570. logger.Error("error in GET /user/*/account RegStatus", "err", err.Error())
  571. http.Error(w, "internal server error", http.StatusInternalServerError)
  572. return
  573. }
  574. confirmStatus, err := a.ConfirmStatus(r.Context(), user.IdentScreenName)
  575. if err != nil {
  576. logger.Error("error in GET /user/*/account ConfirmStatus", "err", err.Error())
  577. http.Error(w, "internal server error", http.StatusInternalServerError)
  578. return
  579. }
  580. profile, err := p.Profile(r.Context(), user.IdentScreenName)
  581. if err != nil {
  582. logger.Error("error in GET /user/*/account Profile", "err", err.Error())
  583. http.Error(w, "internal server error", http.StatusInternalServerError)
  584. return
  585. }
  586. suspendedStatusText, err := getSuspendedStatusErrCodeToText(user.SuspendedStatus)
  587. if err != nil {
  588. logger.Error("error in GET /user/{screenname}/account", "err", err.Error())
  589. http.Error(w, "internal server error", http.StatusInternalServerError)
  590. }
  591. out := userAccountHandle{
  592. ID: user.IdentScreenName.String(),
  593. ScreenName: user.DisplayScreenName.String(),
  594. EmailAddress: emailAddress,
  595. RegStatus: regStatus,
  596. Confirmed: confirmStatus,
  597. Profile: profile,
  598. IsICQ: user.IsICQ,
  599. SuspendedStatus: suspendedStatusText,
  600. IsBot: user.IsBot,
  601. }
  602. if err := json.NewEncoder(w).Encode(out); err != nil {
  603. http.Error(w, err.Error(), http.StatusInternalServerError)
  604. return
  605. }
  606. }
  607. // patchUserAccountHandler handles the PATCH /user/{screenname}/account endpoint.
  608. func patchUserAccountHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, a AccountManager, logger *slog.Logger) {
  609. w.Header().Set("Content-Type", "application/json")
  610. screenName := r.PathValue("screenname")
  611. user, err := userManager.User(r.Context(), state.NewIdentScreenName(screenName))
  612. if err != nil {
  613. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  614. http.Error(w, "internal server error", http.StatusInternalServerError)
  615. return
  616. }
  617. if user == nil {
  618. http.Error(w, "user not found", http.StatusNotFound)
  619. return
  620. }
  621. input := userAccountPatch{}
  622. d := json.NewDecoder(r.Body)
  623. d.DisallowUnknownFields()
  624. if err := d.Decode(&input); err != nil {
  625. errorMsg(w, err.Error(), http.StatusBadRequest)
  626. return
  627. }
  628. modifiedUser := false
  629. if input.SuspendedStatusText != nil {
  630. switch *input.SuspendedStatusText {
  631. case
  632. "", "deleted", "expired",
  633. "suspended", "suspended_age":
  634. suspendedStatus, err := getSuspendedStatusTextToErrCode(*input.SuspendedStatusText)
  635. if err != nil {
  636. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  637. http.Error(w, "internal server error", http.StatusInternalServerError)
  638. return
  639. }
  640. if suspendedStatus != user.SuspendedStatus {
  641. if err := a.UpdateSuspendedStatus(r.Context(), suspendedStatus, user.IdentScreenName); err != nil {
  642. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  643. http.Error(w, "internal server error", http.StatusInternalServerError)
  644. return
  645. }
  646. modifiedUser = true
  647. }
  648. default:
  649. errorMsg(w, "suspended_status must be empty str or one of deleted,expired,suspended,suspended_age", http.StatusBadRequest)
  650. return
  651. }
  652. }
  653. if input.IsBot != nil && user.IsBot != *input.IsBot {
  654. if err := a.SetBotStatus(r.Context(), *input.IsBot, user.IdentScreenName); err != nil {
  655. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  656. http.Error(w, "internal server error", http.StatusInternalServerError)
  657. return
  658. }
  659. modifiedUser = true
  660. }
  661. if !modifiedUser {
  662. w.WriteHeader(http.StatusNotModified)
  663. return
  664. }
  665. w.WriteHeader(http.StatusNoContent)
  666. }
  667. // getSuspendedStatusTextToErrCode maps the given suspendedStatusText to
  668. // the appropriate error code, or 0x0 for none.
  669. func getSuspendedStatusTextToErrCode(suspendedStatusText string) (uint16, error) {
  670. suspendedStatusTextMap := map[string]uint16{
  671. "": 0x0,
  672. "deleted": wire.LoginErrDeletedAccount,
  673. "expired": wire.LoginErrExpiredAccount,
  674. "suspended": wire.LoginErrSuspendedAccount,
  675. "suspended_age": wire.LoginErrSuspendedAccountAge,
  676. }
  677. suspendedStatus, ok := suspendedStatusTextMap[suspendedStatusText]
  678. if !ok {
  679. return 0x0, errors.New("unable to map suspendedText to error code")
  680. }
  681. return suspendedStatus, nil
  682. }
  683. // getSuspendedStatusErrCodeToText maps the given suspendedStatus to
  684. // the appropriate text, or "" for none.
  685. func getSuspendedStatusErrCodeToText(suspendedStatus uint16) (string, error) {
  686. suspendedStatusTextMap := map[uint16]string{
  687. 0x0: "",
  688. wire.LoginErrDeletedAccount: "deleted",
  689. wire.LoginErrExpiredAccount: "expired",
  690. wire.LoginErrSuspendedAccount: "suspended",
  691. wire.LoginErrSuspendedAccountAge: "suspended_age",
  692. }
  693. st, ok := suspendedStatusTextMap[suspendedStatus]
  694. if !ok {
  695. return "", errors.New("unable to map error code to suspendedText")
  696. }
  697. return st, nil
  698. }
  699. // getVersionHandler handles the GET /version endpoint.
  700. func getVersionHandler(w http.ResponseWriter, bld config.Build) {
  701. w.Header().Set("Content-Type", "application/json")
  702. if err := json.NewEncoder(w).Encode(bld); err != nil {
  703. http.Error(w, err.Error(), http.StatusInternalServerError)
  704. return
  705. }
  706. }
  707. // getDirectoryCategoryHandler handles the GET /directory/category endpoint.
  708. func getDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  709. w.Header().Set("Content-Type", "application/json")
  710. categories, err := manager.Categories(r.Context())
  711. if err != nil {
  712. logger.Error("error in GET /directory/category", "err", err.Error())
  713. errorMsg(w, "internal server error", http.StatusInternalServerError)
  714. return
  715. }
  716. out := make([]directoryCategory, len(categories))
  717. for i, category := range categories {
  718. out[i] = directoryCategory{
  719. ID: category.ID,
  720. Name: category.Name,
  721. }
  722. }
  723. if err := json.NewEncoder(w).Encode(out); err != nil {
  724. errorMsg(w, err.Error(), http.StatusInternalServerError)
  725. }
  726. }
  727. // postDirectoryCategoryHandler handles the POST /directory/category endpoint.
  728. func postDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  729. input := directoryCategoryCreate{}
  730. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  731. errorMsg(w, "malformed input", http.StatusBadRequest)
  732. return
  733. }
  734. category, err := manager.CreateCategory(r.Context(), input.Name)
  735. if err != nil {
  736. if errors.Is(err, state.ErrKeywordCategoryExists) {
  737. errorMsg(w, "category already exists", http.StatusConflict)
  738. } else {
  739. logger.Error("error in POST /directory/category", "err", err.Error())
  740. errorMsg(w, "internal server error", http.StatusInternalServerError)
  741. }
  742. return
  743. }
  744. w.WriteHeader(http.StatusCreated)
  745. dc := directoryCategory{
  746. ID: category.ID,
  747. Name: category.Name,
  748. }
  749. if err := json.NewEncoder(w).Encode(dc); err != nil {
  750. errorMsg(w, err.Error(), http.StatusBadRequest)
  751. }
  752. }
  753. // deleteDirectoryCategoryHandler handles the DELETE /directory/category/{id} endpoint.
  754. func deleteDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  755. categoryID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  756. if err != nil {
  757. http.Error(w, "invalid category ID", http.StatusBadRequest)
  758. return
  759. }
  760. if err := manager.DeleteCategory(r.Context(), uint8(categoryID)); err != nil {
  761. switch {
  762. case errors.Is(err, state.ErrKeywordCategoryNotFound):
  763. errorMsg(w, "category not found", http.StatusNotFound)
  764. return
  765. case errors.Is(err, state.ErrKeywordInUse):
  766. errorMsg(w, "can't delete because category in use by a user", http.StatusConflict)
  767. return
  768. default:
  769. logger.Error("error in DELETE /directory/category/{id}", "err", err.Error())
  770. errorMsg(w, "internal server error", http.StatusInternalServerError)
  771. return
  772. }
  773. }
  774. w.WriteHeader(http.StatusNoContent)
  775. }
  776. // getDirectoryCategoryKeywordHandler handles the GET /directory/category/{id}/keyword endpoint.
  777. func getDirectoryCategoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  778. w.Header().Set("Content-Type", "application/json")
  779. categoryID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  780. if err != nil {
  781. errorMsg(w, "invalid category ID", http.StatusBadRequest)
  782. return
  783. }
  784. categories, err := manager.KeywordsByCategory(r.Context(), uint8(categoryID))
  785. if err != nil {
  786. if errors.Is(err, state.ErrKeywordCategoryNotFound) {
  787. errorMsg(w, "category not found", http.StatusNotFound)
  788. } else {
  789. logger.Error("error in GET /directory/category/{id}/keyword", "err", err.Error())
  790. errorMsg(w, "internal server error", http.StatusInternalServerError)
  791. }
  792. return
  793. }
  794. out := make([]directoryCategory, len(categories))
  795. for i, category := range categories {
  796. out[i] = directoryCategory{
  797. ID: category.ID,
  798. Name: category.Name,
  799. }
  800. }
  801. if err := json.NewEncoder(w).Encode(out); err != nil {
  802. errorMsg(w, err.Error(), http.StatusInternalServerError)
  803. }
  804. }
  805. // postDirectoryKeywordHandler handles the POST /directory/keyword endpoint.
  806. func postDirectoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  807. w.Header().Set("Content-Type", "application/json")
  808. input := directoryKeywordCreate{}
  809. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  810. errorMsg(w, "malformed input", http.StatusBadRequest)
  811. return
  812. }
  813. kw, err := manager.CreateKeyword(r.Context(), input.Name, input.CategoryID)
  814. if err != nil {
  815. switch {
  816. case errors.Is(err, state.ErrKeywordCategoryNotFound):
  817. errorMsg(w, "category not found", http.StatusNotFound)
  818. return
  819. case errors.Is(err, state.ErrKeywordExists):
  820. errorMsg(w, "keyword already exists", http.StatusConflict)
  821. return
  822. default:
  823. logger.Error("error in POST /directory/keyword", "err", err.Error())
  824. errorMsg(w, "internal server error", http.StatusInternalServerError)
  825. return
  826. }
  827. }
  828. w.WriteHeader(http.StatusCreated)
  829. dc := directoryKeyword{
  830. ID: kw.ID,
  831. Name: kw.Name,
  832. }
  833. if err := json.NewEncoder(w).Encode(dc); err != nil {
  834. errorMsg(w, err.Error(), http.StatusBadRequest)
  835. }
  836. }
  837. // deleteDirectoryKeywordHandler handles the DELETE /directory/keyword/{id} endpoint.
  838. func deleteDirectoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  839. keywordID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  840. if err != nil {
  841. errorMsg(w, "invalid keyword ID", http.StatusBadRequest)
  842. return
  843. }
  844. if err := manager.DeleteKeyword(r.Context(), uint8(keywordID)); err != nil {
  845. switch {
  846. case errors.Is(err, state.ErrKeywordInUse):
  847. errorMsg(w, "can't delete because category in use by a user", http.StatusConflict)
  848. return
  849. case errors.Is(err, state.ErrKeywordNotFound):
  850. errorMsg(w, "keyword not found", http.StatusNotFound)
  851. return
  852. default:
  853. logger.Error("error in DELETE /directory/keyword/{id}", "err", err.Error())
  854. errorMsg(w, "internal server error", http.StatusInternalServerError)
  855. return
  856. }
  857. }
  858. w.WriteHeader(http.StatusNoContent)
  859. }
  860. // errorMsg sends an error response message and code.
  861. func errorMsg(w http.ResponseWriter, error string, code int) {
  862. msg := messageBody{Message: error}
  863. w.WriteHeader(code)
  864. if err := json.NewEncoder(w).Encode(msg); err != nil {
  865. http.Error(w, err.Error(), http.StatusInternalServerError)
  866. }
  867. }