mgmt_api.go 32 KB

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