mgmt_api.go 31 KB

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