mgmt_api.go 26 KB

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