mgmt_api.go 39 KB

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