mgmt_api.go 27 KB

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