mgmt_api.go 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556
  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. "math"
  13. "math/rand"
  14. "net/http"
  15. "strconv"
  16. "strings"
  17. "time"
  18. "github.com/google/uuid"
  19. "github.com/mk6i/open-oscar-server/config"
  20. "github.com/mk6i/open-oscar-server/state"
  21. "github.com/mk6i/open-oscar-server/wire"
  22. )
  23. func NewManagementAPI(bld config.Build, listener string, userManager UserManager, sessionRetriever SessionRetriever, buddyBroadcaster BuddyBroadcaster, chatRoomRetriever ChatRoomRetriever, chatRoomCreator ChatRoomCreator, chatRoomDeleter ChatRoomDeleter, chatSessionRetriever ChatSessionRetriever, directoryManager DirectoryManager, messageRelayer MessageRelayer, bartAssetManager BARTAssetManager, feedbagRetriever FeedBagRetriever, feedbagManager FeedbagManager, accountManager AccountManager, profileRetriever ProfileRetriever, webAPIKeyManager WebAPIKeyManager, createAccount state.CreateAccountFunc, logger *slog.Logger) *Server {
  24. mux := http.NewServeMux()
  25. // Handlers for '/user' route
  26. mux.HandleFunc("DELETE /user", func(w http.ResponseWriter, r *http.Request) {
  27. deleteUserHandler(w, r, userManager, logger)
  28. })
  29. mux.HandleFunc("GET /user", func(w http.ResponseWriter, r *http.Request) {
  30. getUserHandler(w, r, userManager, logger)
  31. })
  32. mux.HandleFunc("POST /user", func(w http.ResponseWriter, r *http.Request) {
  33. postUserHandler(w, r, createAccount, logger)
  34. })
  35. // Handlers for '/user/password' route
  36. mux.HandleFunc("PUT /user/password", func(w http.ResponseWriter, r *http.Request) {
  37. putUserPasswordHandler(w, r, userManager, logger)
  38. })
  39. // Handlers for '/user/login' route
  40. mux.HandleFunc("GET /user/login", func(w http.ResponseWriter, r *http.Request) {
  41. getUserLoginHandler(w, r, userManager, logger)
  42. })
  43. // Handlers for '/user/{screenname}/account' route
  44. mux.HandleFunc("GET /user/{screenname}/account", func(w http.ResponseWriter, r *http.Request) {
  45. getUserAccountHandler(w, r, userManager, accountManager, profileRetriever, logger)
  46. })
  47. mux.HandleFunc("PATCH /user/{screenname}/account", func(w http.ResponseWriter, r *http.Request) {
  48. patchUserAccountHandler(w, r, userManager, accountManager, logger)
  49. })
  50. // Handlers for '/user/{screenname}/icon' route
  51. mux.HandleFunc("GET /user/{screenname}/icon", func(w http.ResponseWriter, r *http.Request) {
  52. getUserBuddyIconHandler(w, r, userManager, feedbagRetriever, bartAssetManager, logger)
  53. })
  54. // Handlers for '/session' route
  55. mux.HandleFunc("GET /session", func(w http.ResponseWriter, r *http.Request) {
  56. getSessionHandler(w, r, sessionRetriever, time.Now)
  57. })
  58. // Handlers for '/session/{screenname}' route
  59. mux.HandleFunc("GET /session/{screenname}", func(w http.ResponseWriter, r *http.Request) {
  60. getSessionHandler(w, r, sessionRetriever, time.Now)
  61. })
  62. mux.HandleFunc("DELETE /session/{screenname}", func(w http.ResponseWriter, r *http.Request) {
  63. deleteSessionHandler(w, r, sessionRetriever)
  64. })
  65. // Handlers for '/chat/room/public' route
  66. mux.HandleFunc("GET /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  67. getPublicChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  68. })
  69. mux.HandleFunc("POST /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  70. postPublicChatHandler(w, r, chatRoomCreator, logger)
  71. })
  72. mux.HandleFunc("DELETE /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  73. deletePublicChatHandler(w, r, chatRoomDeleter, logger)
  74. })
  75. // Handlers for '/chat/room/private' route
  76. mux.HandleFunc("GET /chat/room/private", func(w http.ResponseWriter, r *http.Request) {
  77. getPrivateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  78. })
  79. // Handlers for '/instant-message' route
  80. mux.HandleFunc("POST /instant-message", func(w http.ResponseWriter, r *http.Request) {
  81. postInstantMessageHandler(w, r, messageRelayer, logger)
  82. })
  83. // Handlers for '/version' route
  84. mux.HandleFunc("GET /version", func(w http.ResponseWriter, r *http.Request) {
  85. getVersionHandler(w, bld)
  86. })
  87. // Handlers for '/admin/webapi/keys' route - Web API key management
  88. mux.HandleFunc("POST /admin/webapi/keys", func(w http.ResponseWriter, r *http.Request) {
  89. postWebAPIKeyHandler(w, r, webAPIKeyManager, uuid.New, logger)
  90. })
  91. mux.HandleFunc("GET /admin/webapi/keys", func(w http.ResponseWriter, r *http.Request) {
  92. getWebAPIKeysHandler(w, r, webAPIKeyManager, logger)
  93. })
  94. mux.HandleFunc("GET /admin/webapi/keys/{id}", func(w http.ResponseWriter, r *http.Request) {
  95. getWebAPIKeyHandler(w, r, webAPIKeyManager, logger)
  96. })
  97. mux.HandleFunc("PUT /admin/webapi/keys/{id}", func(w http.ResponseWriter, r *http.Request) {
  98. putWebAPIKeyHandler(w, r, webAPIKeyManager, logger)
  99. })
  100. mux.HandleFunc("DELETE /admin/webapi/keys/{id}", func(w http.ResponseWriter, r *http.Request) {
  101. deleteWebAPIKeyHandler(w, r, webAPIKeyManager, logger)
  102. })
  103. // Handlers for '/directory/category' route
  104. mux.HandleFunc("GET /directory/category", func(w http.ResponseWriter, r *http.Request) {
  105. getDirectoryCategoryHandler(w, r, directoryManager, logger)
  106. })
  107. mux.HandleFunc("POST /directory/category", func(w http.ResponseWriter, r *http.Request) {
  108. postDirectoryCategoryHandler(w, r, directoryManager, logger)
  109. })
  110. // Handlers for '/directory/category/{id}' route
  111. mux.HandleFunc("DELETE /directory/category/{id}", func(w http.ResponseWriter, r *http.Request) {
  112. deleteDirectoryCategoryHandler(w, r, directoryManager, logger)
  113. })
  114. // Handlers for '/directory/category/{id}/keyword' route
  115. mux.HandleFunc("GET /directory/category/{id}/keyword", func(w http.ResponseWriter, r *http.Request) {
  116. getDirectoryCategoryKeywordHandler(w, r, directoryManager, logger)
  117. })
  118. // Handlers for '/directory/keyword' route
  119. mux.HandleFunc("POST /directory/keyword", func(w http.ResponseWriter, r *http.Request) {
  120. postDirectoryKeywordHandler(w, r, directoryManager, logger)
  121. })
  122. // Handlers for '/directory/keyword/{id}' route
  123. mux.HandleFunc("DELETE /directory/keyword/{id}", func(w http.ResponseWriter, r *http.Request) {
  124. deleteDirectoryKeywordHandler(w, r, directoryManager, logger)
  125. })
  126. // Handlers for '/bart' route
  127. mux.HandleFunc("GET /bart", func(w http.ResponseWriter, r *http.Request) {
  128. getBARTByTypeHandler(w, r, bartAssetManager, logger)
  129. })
  130. // Handlers for '/bart/{hash}' route
  131. mux.HandleFunc("GET /bart/{hash}", func(w http.ResponseWriter, r *http.Request) {
  132. getBARTHandler(w, r, bartAssetManager, logger)
  133. })
  134. mux.HandleFunc("POST /bart/{hash}", func(w http.ResponseWriter, r *http.Request) {
  135. postBARTHandler(w, r, bartAssetManager, logger)
  136. })
  137. mux.HandleFunc("DELETE /bart/{hash}", func(w http.ResponseWriter, r *http.Request) {
  138. deleteBARTHandler(w, r, bartAssetManager, logger)
  139. })
  140. // Handlers for '/feedbag/{screen_name}/group' route
  141. mux.HandleFunc("GET /feedbag/{screen_name}/group", func(w http.ResponseWriter, r *http.Request) {
  142. getFeedbagBuddyHandler(w, r, feedbagManager, logger)
  143. })
  144. // Handlers for '/feedbag/{screen_name}/group/{group_id}/buddy/{buddy_screen_name}' route
  145. mux.HandleFunc("PUT /feedbag/{screen_name}/group/{group_id}/buddy/{buddy_screen_name}", func(w http.ResponseWriter, r *http.Request) {
  146. putFeedbagBuddyHandler(w, r, buddyBroadcaster, feedbagManager, sessionRetriever, messageRelayer, logger, rand.Intn)
  147. })
  148. mux.HandleFunc("DELETE /feedbag/{screen_name}/group/{group_id}/buddy/{buddy_screen_name}", func(w http.ResponseWriter, r *http.Request) {
  149. deleteFeedbagBuddyHandler(w, r, buddyBroadcaster, feedbagManager, sessionRetriever, messageRelayer, logger)
  150. })
  151. return &Server{
  152. server: http.Server{
  153. Addr: listener,
  154. Handler: mux,
  155. },
  156. logger: logger,
  157. }
  158. }
  159. type Server struct {
  160. server http.Server
  161. logger *slog.Logger
  162. }
  163. func (s *Server) ListenAndServe() error {
  164. s.logger.Info("starting server", "addr", s.server.Addr)
  165. if err := s.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
  166. return fmt.Errorf("unable to start management API server: %w", err)
  167. }
  168. return nil
  169. }
  170. func (s *Server) Shutdown(ctx context.Context) error {
  171. defer s.logger.Info("shutdown complete")
  172. return s.server.Shutdown(ctx)
  173. }
  174. // deleteUserHandler handles the DELETE /user endpoint.
  175. func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManager, logger *slog.Logger) {
  176. user, err := userFromBody(r)
  177. if err != nil {
  178. http.Error(w, err.Error(), http.StatusBadRequest)
  179. return
  180. }
  181. err = manager.DeleteUser(r.Context(), state.NewIdentScreenName(user.ScreenName))
  182. switch {
  183. case errors.Is(err, state.ErrNoUser):
  184. http.Error(w, "user does not exist", http.StatusNotFound)
  185. return
  186. case err != nil:
  187. logger.Error("error deleting user DELETE /user", "err", err.Error())
  188. http.Error(w, "internal server error", http.StatusInternalServerError)
  189. return
  190. }
  191. w.WriteHeader(http.StatusNoContent)
  192. _, _ = fmt.Fprintln(w, "User account successfully deleted.")
  193. }
  194. // putUserPasswordHandler handles the PUT /user/password endpoint.
  195. func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  196. input, err := userFromBody(r)
  197. if err != nil {
  198. http.Error(w, err.Error(), http.StatusBadRequest)
  199. return
  200. }
  201. sn := state.NewIdentScreenName(input.ScreenName)
  202. if err := userManager.SetUserPassword(r.Context(), sn, input.Password); err != nil {
  203. switch {
  204. case errors.Is(err, state.ErrNoUser):
  205. http.Error(w, "user does not exist", http.StatusNotFound)
  206. return
  207. case errors.Is(err, state.ErrPasswordInvalid):
  208. http.Error(w, err.Error(), http.StatusBadRequest)
  209. return
  210. default:
  211. logger.Error("error updating user password PUT /user/password", "err", err.Error())
  212. http.Error(w, "internal server error", http.StatusInternalServerError)
  213. return
  214. }
  215. }
  216. w.WriteHeader(http.StatusNoContent)
  217. _, _ = fmt.Fprintln(w, "Password successfully reset.")
  218. }
  219. // getSessionHandler handles GET /session
  220. func getSessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever, nowFn func() time.Time) {
  221. w.Header().Set("Content-Type", "application/json")
  222. var allSessions []*state.Session
  223. if screenName := r.PathValue("screenname"); screenName != "" {
  224. session := sessionRetriever.RetrieveSession(state.NewIdentScreenName(screenName))
  225. if session == nil {
  226. http.Error(w, "session not found", http.StatusNotFound)
  227. return
  228. }
  229. allSessions = append(allSessions, session)
  230. } else {
  231. // AllSessions returns all sessions
  232. allSessions = sessionRetriever.AllSessions()
  233. }
  234. ou := onlineUsers{
  235. Count: len(allSessions),
  236. Sessions: make([]sessionHandle, len(allSessions)),
  237. }
  238. for i, s := range allSessions {
  239. instances := s.Instances()
  240. instanceHandles := make([]instanceHandle, len(instances))
  241. for j, inst := range instances {
  242. instanceIdleSeconds := 0
  243. if inst.Idle() {
  244. instanceIdleSeconds = int(nowFn().Sub(inst.IdleTime()).Seconds())
  245. }
  246. awayMsg, _ := inst.AwayMessage()
  247. instanceHandles[j] = instanceHandle{
  248. Num: int(inst.Num()),
  249. IdleSeconds: instanceIdleSeconds,
  250. IsAway: inst.Away(),
  251. AwayMessage: awayMsg,
  252. IsInvisible: inst.Invisible(),
  253. }
  254. ra := inst.RemoteAddr()
  255. if ra != nil {
  256. instanceHandles[j].RemoteAddr = ra.Addr().String()
  257. instanceHandles[j].RemotePort = int(ra.Port())
  258. }
  259. }
  260. sessionIdleSeconds := 0
  261. if s.Idle() {
  262. sessionIdleSeconds = int(nowFn().Sub(s.IdleTime()).Seconds())
  263. }
  264. allAway := s.Away()
  265. awayMessage := ""
  266. if allAway {
  267. awayMessage = s.AwayMessage()
  268. }
  269. ou.Sessions[i] = sessionHandle{
  270. ID: s.IdentScreenName().String(),
  271. ScreenName: s.DisplayScreenName().String(),
  272. OnlineSeconds: int(nowFn().Sub(s.SignonTime()).Seconds()),
  273. IsAway: allAway,
  274. AwayMessage: awayMessage,
  275. IdleSeconds: sessionIdleSeconds,
  276. IsInvisible: s.Invisible(),
  277. IsICQ: s.UIN() > 0,
  278. InstanceCount: s.InstanceCount(),
  279. Instances: instanceHandles,
  280. }
  281. }
  282. if err := json.NewEncoder(w).Encode(ou); err != nil {
  283. http.Error(w, err.Error(), http.StatusInternalServerError)
  284. return
  285. }
  286. }
  287. // deleteSessionHandler handles DELETE /session/{screenname}
  288. func deleteSessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever) {
  289. w.Header().Set("Content-Type", "application/json")
  290. if screenName := r.PathValue("screenname"); screenName != "" {
  291. session := sessionRetriever.RetrieveSession(state.NewIdentScreenName(screenName))
  292. if session == nil {
  293. errorMsg(w, "session not found", http.StatusNotFound)
  294. return
  295. }
  296. session.CloseSession()
  297. }
  298. w.WriteHeader(http.StatusNoContent)
  299. }
  300. // getUserHandler handles the GET /user endpoint.
  301. func getUserHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  302. w.Header().Set("Content-Type", "application/json")
  303. users, err := userManager.AllUsers(r.Context())
  304. if err != nil {
  305. logger.Error("error in GET /user", "err", err.Error())
  306. http.Error(w, "internal server error", http.StatusInternalServerError)
  307. return
  308. }
  309. out := make([]userHandle, len(users))
  310. for i, u := range users {
  311. suspendedStatus, err := getSuspendedStatusErrCodeToText(u.SuspendedStatus)
  312. if err != nil {
  313. logger.Error("error getting suspended status in GET /user", "err", err.Error())
  314. http.Error(w, "internal server error", http.StatusInternalServerError)
  315. return
  316. }
  317. out[i] = userHandle{
  318. ID: u.IdentScreenName.String(),
  319. ScreenName: u.DisplayScreenName.String(),
  320. IsICQ: u.IsICQ,
  321. SuspendedStatus: suspendedStatus,
  322. IsBot: u.IsBot,
  323. }
  324. }
  325. if err := json.NewEncoder(w).Encode(out); err != nil {
  326. http.Error(w, err.Error(), http.StatusInternalServerError)
  327. return
  328. }
  329. }
  330. // postUserHandler handles the POST /user endpoint.
  331. func postUserHandler(w http.ResponseWriter, r *http.Request, createAccount state.CreateAccountFunc, logger *slog.Logger) {
  332. input, err := userFromBody(r)
  333. if err != nil {
  334. http.Error(w, err.Error(), http.StatusBadRequest)
  335. return
  336. }
  337. sn := state.DisplayScreenName(input.ScreenName)
  338. err = createAccount(r.Context(), sn, input.Password)
  339. switch {
  340. case errors.Is(err, state.ErrDupUser):
  341. http.Error(w, "user already exists", http.StatusConflict)
  342. return
  343. case errors.Is(err, state.ErrAIMHandleInvalidFormat), errors.Is(err, state.ErrAIMHandleLength):
  344. http.Error(w, fmt.Sprintf("invalid screen name: %s", err), http.StatusBadRequest)
  345. return
  346. case errors.Is(err, state.ErrICQUINInvalidFormat):
  347. http.Error(w, fmt.Sprintf("invalid uin: %s", err), http.StatusBadRequest)
  348. return
  349. case errors.Is(err, state.ErrPasswordInvalid):
  350. http.Error(w, fmt.Sprintf("invalid password: %s", err), http.StatusBadRequest)
  351. return
  352. case err != nil:
  353. logger.Error("error inserting user POST /user", "err", err.Error())
  354. http.Error(w, "internal server error", http.StatusInternalServerError)
  355. return
  356. }
  357. w.WriteHeader(http.StatusCreated)
  358. _, _ = fmt.Fprintln(w, "User account created successfully.")
  359. }
  360. func userFromBody(r *http.Request) (userWithPassword, error) {
  361. user := userWithPassword{}
  362. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  363. return userWithPassword{}, errors.New("malformed input")
  364. }
  365. return user, nil
  366. }
  367. // getUserLoginHandler is a temporary endpoint for validating user credentials.
  368. // do not rely on this endpoint, as it will be eventually removed.
  369. func getUserLoginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  370. authHeader := r.Header.Get("Authorization")
  371. if authHeader == "" {
  372. // No authentication header found
  373. w.WriteHeader(http.StatusUnauthorized)
  374. w.Header().Set("WWW-Authenticate", `Basic realm="User Login"`)
  375. _, _ = w.Write([]byte("401 Unauthorized\n"))
  376. return
  377. }
  378. auth := strings.SplitN(authHeader, " ", 2)
  379. if len(auth) != 2 || auth[0] != "Basic" {
  380. w.WriteHeader(http.StatusUnauthorized)
  381. _, _ = w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
  382. return
  383. }
  384. payload, err := base64.StdEncoding.DecodeString(auth[1])
  385. if err != nil {
  386. w.WriteHeader(http.StatusUnauthorized)
  387. _, _ = w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
  388. return
  389. }
  390. pair := strings.SplitN(string(payload), ":", 2)
  391. if len(pair) != 2 {
  392. w.WriteHeader(http.StatusUnauthorized)
  393. _, _ = w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
  394. return
  395. }
  396. username, password := state.NewIdentScreenName(pair[0]), pair[1]
  397. user, err := userManager.User(r.Context(), username)
  398. if err != nil {
  399. w.WriteHeader(http.StatusInternalServerError)
  400. _, _ = w.Write([]byte("500 InternalServerError\n"))
  401. logger.Error("error getting user", "err", err.Error())
  402. return
  403. }
  404. if user == nil || !user.ValidateHash(wire.StrongMD5PasswordHash(password, user.AuthKey)) {
  405. w.WriteHeader(http.StatusUnauthorized)
  406. _, _ = w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
  407. return
  408. }
  409. // Successfully authenticated
  410. w.WriteHeader(http.StatusOK)
  411. _, _ = w.Write([]byte("200 OK: Successfully Authenticated\n"))
  412. }
  413. // getPublicChatHandler handles the GET /chat/room/public endpoint.
  414. func getPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  415. w.Header().Set("Content-Type", "application/json")
  416. rooms, err := chatRoomRetriever.AllChatRooms(r.Context(), state.PublicExchange)
  417. if err != nil {
  418. logger.Error("error in GET /chat/rooms/public", "err", err.Error())
  419. http.Error(w, "internal server error", http.StatusInternalServerError)
  420. return
  421. }
  422. out := make([]chatRoom, len(rooms))
  423. for i, room := range rooms {
  424. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  425. cr := chatRoom{
  426. CreateTime: room.CreateTime(),
  427. Name: room.Name(),
  428. Participants: make([]aimChatUserHandle, len(sessions)),
  429. URL: room.URL().String(),
  430. }
  431. for j, sess := range sessions {
  432. cr.Participants[j] = aimChatUserHandle{
  433. ID: sess.IdentScreenName().String(),
  434. ScreenName: sess.DisplayScreenName().String(),
  435. }
  436. }
  437. out[i] = cr
  438. }
  439. writeUnescapeChatURL(w, out)
  440. }
  441. // postPublicChatHandler handles the POST /chat/room/public endpoint.
  442. func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreator ChatRoomCreator, logger *slog.Logger) {
  443. input := chatRoomCreate{}
  444. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  445. http.Error(w, "invalid input", http.StatusBadRequest)
  446. return
  447. }
  448. input.Name = strings.TrimSpace(input.Name)
  449. if input.Name == "" || len(input.Name) > 50 {
  450. http.Error(w, "chat room name must be between 1 and 50 characters", http.StatusBadRequest)
  451. return
  452. }
  453. cr := state.NewChatRoom(input.Name, state.NewIdentScreenName("system"), state.PublicExchange)
  454. err := chatRoomCreator.CreateChatRoom(r.Context(), &cr)
  455. switch {
  456. case errors.Is(err, state.ErrDupChatRoom):
  457. http.Error(w, "Chat room already exists.", http.StatusConflict)
  458. return
  459. case err != nil:
  460. logger.Error("error inserting chat room POST /chat/room/public", "err", err.Error())
  461. http.Error(w, "internal server error", http.StatusInternalServerError)
  462. return
  463. }
  464. w.WriteHeader(http.StatusCreated)
  465. _, _ = fmt.Fprintln(w, "Chat room created successfully.")
  466. }
  467. // getPrivateChatHandler handles the GET /chat/room/private endpoint.
  468. func getPrivateChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  469. w.Header().Set("Content-Type", "application/json")
  470. rooms, err := chatRoomRetriever.AllChatRooms(r.Context(), state.PrivateExchange)
  471. if err != nil {
  472. logger.Error("error in GET /chat/rooms/private", "err", err.Error())
  473. http.Error(w, "internal server error", http.StatusInternalServerError)
  474. return
  475. }
  476. out := make([]chatRoom, len(rooms))
  477. for i, room := range rooms {
  478. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  479. cr := chatRoom{
  480. CreateTime: room.CreateTime(),
  481. CreatorID: room.Creator().String(),
  482. Name: room.Name(),
  483. Participants: make([]aimChatUserHandle, len(sessions)),
  484. URL: room.URL().String(),
  485. }
  486. for j, sess := range sessions {
  487. cr.Participants[j] = aimChatUserHandle{
  488. ID: sess.IdentScreenName().String(),
  489. ScreenName: sess.DisplayScreenName().String(),
  490. }
  491. }
  492. out[i] = cr
  493. }
  494. writeUnescapeChatURL(w, out)
  495. }
  496. // deletePublicChatHandler handles the DELETE /chat/room/public endpoint.
  497. func deletePublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomDeleter ChatRoomDeleter, logger *slog.Logger) {
  498. input := chatRoomDelete{}
  499. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  500. http.Error(w, "malformed input", http.StatusBadRequest)
  501. return
  502. }
  503. if len(input.Names) == 0 {
  504. http.Error(w, "no chat room names provided", http.StatusBadRequest)
  505. return
  506. }
  507. err := chatRoomDeleter.DeleteChatRooms(r.Context(), state.PublicExchange, input.Names)
  508. if err != nil {
  509. logger.Error("error deleting public chat rooms DELETE /chat/room/public", "err", err.Error())
  510. http.Error(w, "internal server error", http.StatusInternalServerError)
  511. return
  512. }
  513. w.WriteHeader(http.StatusNoContent)
  514. _, _ = fmt.Fprintln(w, "Chat rooms deleted successfully.")
  515. }
  516. // writeUnescapeChatURL writes a JSON-encoded list of chat rooms with unescaped
  517. // ampersands preceding the exchange query param.
  518. //
  519. // before: aim:gochat?roomname=Office+Hijinks\u0026exchange=5
  520. // after: aim:gochat?roomname=Office+Hijinks&exchange=5
  521. //
  522. // This makes it easier to copy the gochat URL into AIM, which does not
  523. // recognize the ampersand unicode character \u0026.
  524. func writeUnescapeChatURL(w http.ResponseWriter, out []chatRoom) {
  525. buf := &bytes.Buffer{}
  526. if err := json.NewEncoder(buf).Encode(out); err != nil {
  527. http.Error(w, err.Error(), http.StatusInternalServerError)
  528. return
  529. }
  530. b := bytes.ReplaceAll(buf.Bytes(), []byte(`\u0026exchange`), []byte(`&exchange`))
  531. if _, err := w.Write(b); err != nil {
  532. http.Error(w, err.Error(), http.StatusInternalServerError)
  533. return
  534. }
  535. }
  536. // postIMHandler handles the POST /instant-message endpoint.
  537. func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) {
  538. input := instantMessage{}
  539. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  540. http.Error(w, "malformed input", http.StatusBadRequest)
  541. return
  542. }
  543. tlv, err := wire.ICBMFragmentList(input.Text)
  544. if err != nil {
  545. logger.Error("error sending message POST /instant-message", "err", err.Error())
  546. http.Error(w, "internal server error", http.StatusInternalServerError)
  547. return
  548. }
  549. msg := wire.SNACMessage{
  550. Frame: wire.SNACFrame{
  551. FoodGroup: wire.ICBM,
  552. SubGroup: wire.ICBMChannelMsgToClient,
  553. },
  554. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  555. ChannelID: 1,
  556. TLVUserInfo: wire.TLVUserInfo{
  557. ScreenName: input.From,
  558. },
  559. TLVRestBlock: wire.TLVRestBlock{
  560. TLVList: wire.TLVList{
  561. wire.NewTLVBE(wire.ICBMTLVAOLIMData, tlv),
  562. },
  563. },
  564. },
  565. }
  566. messageRelayer.RelayToScreenName(context.Background(), state.NewIdentScreenName(input.To), msg)
  567. w.WriteHeader(http.StatusOK)
  568. _, _ = fmt.Fprintln(w, "Message sent successfully.")
  569. }
  570. // getUserBuddyIconHandler handles the GET /user/{screenname}/icon endpoint.
  571. func getUserBuddyIconHandler(w http.ResponseWriter, r *http.Request, u UserManager, f FeedBagRetriever, b BARTAssetManager, logger *slog.Logger) {
  572. screenName := state.NewIdentScreenName(r.PathValue("screenname"))
  573. user, err := u.User(r.Context(), screenName)
  574. if err != nil {
  575. logger.Error("error retrieving user", "err", err.Error())
  576. http.Error(w, "internal server error", http.StatusInternalServerError)
  577. return
  578. }
  579. if user == nil {
  580. http.Error(w, "user not found", http.StatusNotFound)
  581. return
  582. }
  583. iconRef, err := f.BuddyIconMetadata(r.Context(), screenName)
  584. if err != nil {
  585. logger.Error("error retrieving buddy icon ref", "err", err.Error())
  586. http.Error(w, "internal server error", http.StatusInternalServerError)
  587. return
  588. }
  589. if iconRef == nil || iconRef.HasClearIconHash() {
  590. http.Error(w, "icon not found", http.StatusNotFound)
  591. return
  592. }
  593. icon, err := b.BARTItem(r.Context(), iconRef.Hash)
  594. if err != nil {
  595. logger.Error("error retrieving buddy icon bart item", "err", err.Error())
  596. http.Error(w, "internal server error", http.StatusInternalServerError)
  597. return
  598. }
  599. w.Header().Set("Content-Type", http.DetectContentType(icon))
  600. w.Write(icon)
  601. }
  602. // getUserAccountHandler handles the GET /user/{screenname}/account endpoint.
  603. func getUserAccountHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, a AccountManager, p ProfileRetriever, logger *slog.Logger) {
  604. w.Header().Set("Content-Type", "application/json")
  605. screenName := r.PathValue("screenname")
  606. user, err := userManager.User(r.Context(), state.NewIdentScreenName(screenName))
  607. if err != nil {
  608. logger.Error("error in GET /user/{screenname}/account", "err", err.Error())
  609. http.Error(w, "internal server error", http.StatusInternalServerError)
  610. return
  611. }
  612. if user == nil {
  613. http.Error(w, "user not found", http.StatusNotFound)
  614. return
  615. }
  616. emailAddress := ""
  617. email, err := a.EmailAddress(r.Context(), user.IdentScreenName)
  618. if err != nil {
  619. emailAddress = ""
  620. } else {
  621. emailAddress = email.String()
  622. }
  623. regStatus, err := a.RegStatus(r.Context(), user.IdentScreenName)
  624. if err != nil {
  625. logger.Error("error in GET /user/*/account RegStatus", "err", err.Error())
  626. http.Error(w, "internal server error", http.StatusInternalServerError)
  627. return
  628. }
  629. confirmStatus, err := a.ConfirmStatus(r.Context(), user.IdentScreenName)
  630. if err != nil {
  631. logger.Error("error in GET /user/*/account ConfirmStatus", "err", err.Error())
  632. http.Error(w, "internal server error", http.StatusInternalServerError)
  633. return
  634. }
  635. profile, err := p.Profile(r.Context(), user.IdentScreenName)
  636. if err != nil {
  637. logger.Error("error in GET /user/*/account Profile", "err", err.Error())
  638. http.Error(w, "internal server error", http.StatusInternalServerError)
  639. return
  640. }
  641. suspendedStatusText, err := getSuspendedStatusErrCodeToText(user.SuspendedStatus)
  642. if err != nil {
  643. logger.Error("error in GET /user/{screenname}/account", "err", err.Error())
  644. http.Error(w, "internal server error", http.StatusInternalServerError)
  645. }
  646. out := userAccountHandle{
  647. ID: user.IdentScreenName.String(),
  648. ScreenName: user.DisplayScreenName.String(),
  649. EmailAddress: emailAddress,
  650. RegStatus: regStatus,
  651. Confirmed: confirmStatus,
  652. Profile: profile.ProfileText,
  653. IsICQ: user.IsICQ,
  654. SuspendedStatus: suspendedStatusText,
  655. IsBot: user.IsBot,
  656. }
  657. if err := json.NewEncoder(w).Encode(out); err != nil {
  658. http.Error(w, err.Error(), http.StatusInternalServerError)
  659. return
  660. }
  661. }
  662. // patchUserAccountHandler handles the PATCH /user/{screenname}/account endpoint.
  663. func patchUserAccountHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, a AccountManager, logger *slog.Logger) {
  664. w.Header().Set("Content-Type", "application/json")
  665. screenName := r.PathValue("screenname")
  666. user, err := userManager.User(r.Context(), state.NewIdentScreenName(screenName))
  667. if err != nil {
  668. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  669. http.Error(w, "internal server error", http.StatusInternalServerError)
  670. return
  671. }
  672. if user == nil {
  673. http.Error(w, "user not found", http.StatusNotFound)
  674. return
  675. }
  676. input := userAccountPatch{}
  677. d := json.NewDecoder(r.Body)
  678. d.DisallowUnknownFields()
  679. if err := d.Decode(&input); err != nil {
  680. errorMsg(w, err.Error(), http.StatusBadRequest)
  681. return
  682. }
  683. modifiedUser := false
  684. if input.SuspendedStatusText != nil {
  685. switch *input.SuspendedStatusText {
  686. case
  687. "", "deleted", "expired",
  688. "suspended", "suspended_age":
  689. suspendedStatus, err := getSuspendedStatusTextToErrCode(*input.SuspendedStatusText)
  690. if err != nil {
  691. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  692. http.Error(w, "internal server error", http.StatusInternalServerError)
  693. return
  694. }
  695. if suspendedStatus != user.SuspendedStatus {
  696. if err := a.UpdateSuspendedStatus(r.Context(), suspendedStatus, user.IdentScreenName); err != nil {
  697. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  698. http.Error(w, "internal server error", http.StatusInternalServerError)
  699. return
  700. }
  701. modifiedUser = true
  702. }
  703. default:
  704. errorMsg(w, "suspended_status must be empty str or one of deleted,expired,suspended,suspended_age", http.StatusBadRequest)
  705. return
  706. }
  707. }
  708. if input.IsBot != nil && user.IsBot != *input.IsBot {
  709. if err := a.SetBotStatus(r.Context(), *input.IsBot, user.IdentScreenName); err != nil {
  710. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  711. http.Error(w, "internal server error", http.StatusInternalServerError)
  712. return
  713. }
  714. modifiedUser = true
  715. }
  716. if !modifiedUser {
  717. w.WriteHeader(http.StatusNotModified)
  718. return
  719. }
  720. w.WriteHeader(http.StatusNoContent)
  721. }
  722. // getSuspendedStatusTextToErrCode maps the given suspendedStatusText to
  723. // the appropriate error code, or 0x0 for none.
  724. func getSuspendedStatusTextToErrCode(suspendedStatusText string) (uint16, error) {
  725. suspendedStatusTextMap := map[string]uint16{
  726. "": 0x0,
  727. "deleted": wire.LoginErrDeletedAccount,
  728. "expired": wire.LoginErrExpiredAccount,
  729. "suspended": wire.LoginErrSuspendedAccount,
  730. "suspended_age": wire.LoginErrSuspendedAccountAge,
  731. }
  732. suspendedStatus, ok := suspendedStatusTextMap[suspendedStatusText]
  733. if !ok {
  734. return 0x0, errors.New("unable to map suspendedText to error code")
  735. }
  736. return suspendedStatus, nil
  737. }
  738. // getSuspendedStatusErrCodeToText maps the given suspendedStatus to
  739. // the appropriate text, or "" for none.
  740. func getSuspendedStatusErrCodeToText(suspendedStatus uint16) (string, error) {
  741. suspendedStatusTextMap := map[uint16]string{
  742. 0x0: "",
  743. wire.LoginErrDeletedAccount: "deleted",
  744. wire.LoginErrExpiredAccount: "expired",
  745. wire.LoginErrSuspendedAccount: "suspended",
  746. wire.LoginErrSuspendedAccountAge: "suspended_age",
  747. }
  748. st, ok := suspendedStatusTextMap[suspendedStatus]
  749. if !ok {
  750. return "", errors.New("unable to map error code to suspendedText")
  751. }
  752. return st, nil
  753. }
  754. // getVersionHandler handles the GET /version endpoint.
  755. func getVersionHandler(w http.ResponseWriter, bld config.Build) {
  756. w.Header().Set("Content-Type", "application/json")
  757. if err := json.NewEncoder(w).Encode(bld); err != nil {
  758. http.Error(w, err.Error(), http.StatusInternalServerError)
  759. return
  760. }
  761. }
  762. // getDirectoryCategoryHandler handles the GET /directory/category endpoint.
  763. func getDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  764. w.Header().Set("Content-Type", "application/json")
  765. categories, err := manager.Categories(r.Context())
  766. if err != nil {
  767. logger.Error("error in GET /directory/category", "err", err.Error())
  768. errorMsg(w, "internal server error", http.StatusInternalServerError)
  769. return
  770. }
  771. out := make([]directoryCategory, len(categories))
  772. for i, category := range categories {
  773. out[i] = directoryCategory{
  774. ID: category.ID,
  775. Name: category.Name,
  776. }
  777. }
  778. if err := json.NewEncoder(w).Encode(out); err != nil {
  779. errorMsg(w, err.Error(), http.StatusInternalServerError)
  780. }
  781. }
  782. // postDirectoryCategoryHandler handles the POST /directory/category endpoint.
  783. func postDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  784. input := directoryCategoryCreate{}
  785. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  786. errorMsg(w, "malformed input", http.StatusBadRequest)
  787. return
  788. }
  789. category, err := manager.CreateCategory(r.Context(), input.Name)
  790. if err != nil {
  791. if errors.Is(err, state.ErrKeywordCategoryExists) {
  792. errorMsg(w, "category already exists", http.StatusConflict)
  793. } else {
  794. logger.Error("error in POST /directory/category", "err", err.Error())
  795. errorMsg(w, "internal server error", http.StatusInternalServerError)
  796. }
  797. return
  798. }
  799. w.WriteHeader(http.StatusCreated)
  800. dc := directoryCategory{
  801. ID: category.ID,
  802. Name: category.Name,
  803. }
  804. if err := json.NewEncoder(w).Encode(dc); err != nil {
  805. errorMsg(w, err.Error(), http.StatusBadRequest)
  806. }
  807. }
  808. // deleteDirectoryCategoryHandler handles the DELETE /directory/category/{id} endpoint.
  809. func deleteDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  810. categoryID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  811. if err != nil {
  812. http.Error(w, "invalid category ID", http.StatusBadRequest)
  813. return
  814. }
  815. if err := manager.DeleteCategory(r.Context(), uint8(categoryID)); err != nil {
  816. switch {
  817. case errors.Is(err, state.ErrKeywordCategoryNotFound):
  818. errorMsg(w, "category not found", http.StatusNotFound)
  819. return
  820. case errors.Is(err, state.ErrKeywordInUse):
  821. errorMsg(w, "can't delete because category in use by a user", http.StatusConflict)
  822. return
  823. default:
  824. logger.Error("error in DELETE /directory/category/{id}", "err", err.Error())
  825. errorMsg(w, "internal server error", http.StatusInternalServerError)
  826. return
  827. }
  828. }
  829. w.WriteHeader(http.StatusNoContent)
  830. }
  831. // getDirectoryCategoryKeywordHandler handles the GET /directory/category/{id}/keyword endpoint.
  832. func getDirectoryCategoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  833. w.Header().Set("Content-Type", "application/json")
  834. categoryID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  835. if err != nil {
  836. errorMsg(w, "invalid category ID", http.StatusBadRequest)
  837. return
  838. }
  839. categories, err := manager.KeywordsByCategory(r.Context(), uint8(categoryID))
  840. if err != nil {
  841. if errors.Is(err, state.ErrKeywordCategoryNotFound) {
  842. errorMsg(w, "category not found", http.StatusNotFound)
  843. } else {
  844. logger.Error("error in GET /directory/category/{id}/keyword", "err", err.Error())
  845. errorMsg(w, "internal server error", http.StatusInternalServerError)
  846. }
  847. return
  848. }
  849. out := make([]directoryCategory, len(categories))
  850. for i, category := range categories {
  851. out[i] = directoryCategory{
  852. ID: category.ID,
  853. Name: category.Name,
  854. }
  855. }
  856. if err := json.NewEncoder(w).Encode(out); err != nil {
  857. errorMsg(w, err.Error(), http.StatusInternalServerError)
  858. }
  859. }
  860. // postDirectoryKeywordHandler handles the POST /directory/keyword endpoint.
  861. func postDirectoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  862. w.Header().Set("Content-Type", "application/json")
  863. input := directoryKeywordCreate{}
  864. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  865. errorMsg(w, "malformed input", http.StatusBadRequest)
  866. return
  867. }
  868. kw, err := manager.CreateKeyword(r.Context(), input.Name, input.CategoryID)
  869. if err != nil {
  870. switch {
  871. case errors.Is(err, state.ErrKeywordCategoryNotFound):
  872. errorMsg(w, "category not found", http.StatusNotFound)
  873. return
  874. case errors.Is(err, state.ErrKeywordExists):
  875. errorMsg(w, "keyword already exists", http.StatusConflict)
  876. return
  877. default:
  878. logger.Error("error in POST /directory/keyword", "err", err.Error())
  879. errorMsg(w, "internal server error", http.StatusInternalServerError)
  880. return
  881. }
  882. }
  883. w.WriteHeader(http.StatusCreated)
  884. dc := directoryKeyword{
  885. ID: kw.ID,
  886. Name: kw.Name,
  887. }
  888. if err := json.NewEncoder(w).Encode(dc); err != nil {
  889. errorMsg(w, err.Error(), http.StatusBadRequest)
  890. }
  891. }
  892. // deleteDirectoryKeywordHandler handles the DELETE /directory/keyword/{id} endpoint.
  893. func deleteDirectoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  894. keywordID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  895. if err != nil {
  896. errorMsg(w, "invalid keyword ID", http.StatusBadRequest)
  897. return
  898. }
  899. if err := manager.DeleteKeyword(r.Context(), uint8(keywordID)); err != nil {
  900. switch {
  901. case errors.Is(err, state.ErrKeywordInUse):
  902. errorMsg(w, "can't delete because category in use by a user", http.StatusConflict)
  903. return
  904. case errors.Is(err, state.ErrKeywordNotFound):
  905. errorMsg(w, "keyword not found", http.StatusNotFound)
  906. return
  907. default:
  908. logger.Error("error in DELETE /directory/keyword/{id}", "err", err.Error())
  909. errorMsg(w, "internal server error", http.StatusInternalServerError)
  910. return
  911. }
  912. }
  913. w.WriteHeader(http.StatusNoContent)
  914. }
  915. // errorMsg sends an error response message and code.
  916. func errorMsg(w http.ResponseWriter, error string, code int) {
  917. msg := messageBody{Message: error}
  918. w.WriteHeader(code)
  919. if err := json.NewEncoder(w).Encode(msg); err != nil {
  920. http.Error(w, err.Error(), http.StatusInternalServerError)
  921. }
  922. }
  923. // BARTAsset represents a BART asset entry.
  924. type BARTAsset struct {
  925. Hash string `json:"hash"`
  926. Type uint16 `json:"type"`
  927. }
  928. // getBARTByTypeHandler handles the GET /bart endpoint.
  929. func getBARTByTypeHandler(w http.ResponseWriter, r *http.Request, bartAssetManager BARTAssetManager, logger *slog.Logger) {
  930. w.Header().Set("Content-Type", "application/json")
  931. // Get type from query parameter (required)
  932. typeStr := r.URL.Query().Get("type")
  933. if typeStr == "" {
  934. errorMsg(w, "type query parameter is required", http.StatusBadRequest)
  935. return
  936. }
  937. typeVal, err := strconv.ParseUint(typeStr, 10, 16)
  938. if err != nil {
  939. errorMsg(w, "invalid type ID", http.StatusBadRequest)
  940. return
  941. }
  942. itemType := uint16(typeVal)
  943. // Get BART items, filtered by type
  944. items, err := bartAssetManager.ListBARTItems(r.Context(), itemType)
  945. if err != nil {
  946. logger.Error("error listing BART items", "err", err.Error())
  947. errorMsg(w, "internal server error", http.StatusInternalServerError)
  948. return
  949. }
  950. // Convert to BARTAsset format
  951. assets := make([]BARTAsset, 0, len(items))
  952. for _, item := range items {
  953. assets = append(assets, BARTAsset{
  954. Hash: item.Hash,
  955. Type: item.Type,
  956. })
  957. }
  958. if err := json.NewEncoder(w).Encode(assets); err != nil {
  959. logger.Error("error encoding response", "err", err.Error())
  960. }
  961. }
  962. // getBARTHandler handles the GET /bart/{hash} endpoint.
  963. func getBARTHandler(w http.ResponseWriter, r *http.Request, bartAssetManager BARTAssetManager, logger *slog.Logger) {
  964. hashStr := r.PathValue("hash")
  965. if hashStr == "" {
  966. errorMsg(w, "hash is required", http.StatusBadRequest)
  967. return
  968. }
  969. hashBytes, err := hex.DecodeString(hashStr)
  970. if err != nil {
  971. errorMsg(w, "invalid hash format", http.StatusBadRequest)
  972. return
  973. }
  974. body, err := bartAssetManager.BARTItem(r.Context(), hashBytes)
  975. if err != nil {
  976. logger.Error("error retrieving BART asset", "err", err.Error())
  977. errorMsg(w, "internal server error", http.StatusInternalServerError)
  978. return
  979. }
  980. if len(body) == 0 {
  981. errorMsg(w, "BART asset not found", http.StatusNotFound)
  982. return
  983. }
  984. w.Header().Set("Content-Type", "application/octet-stream")
  985. w.Write(body)
  986. }
  987. // postBARTHandler handles the POST /bart endpoint.
  988. func postBARTHandler(w http.ResponseWriter, r *http.Request, bartAssetManager BARTAssetManager, logger *slog.Logger) {
  989. w.Header().Set("Content-Type", "application/json")
  990. // Extract hash from URL path
  991. hashStr := r.PathValue("hash")
  992. if hashStr == "" {
  993. errorMsg(w, "hash path parameter is required", http.StatusBadRequest)
  994. return
  995. }
  996. hashBytes, err := hex.DecodeString(hashStr)
  997. if err != nil {
  998. errorMsg(w, "invalid hash format", http.StatusBadRequest)
  999. return
  1000. }
  1001. typeStr := r.URL.Query().Get("type")
  1002. if typeStr == "" {
  1003. errorMsg(w, "type query parameter is required", http.StatusBadRequest)
  1004. return
  1005. }
  1006. typeVal, err := strconv.ParseUint(typeStr, 10, 16)
  1007. if err != nil {
  1008. errorMsg(w, "invalid type ID", http.StatusBadRequest)
  1009. return
  1010. }
  1011. bartType := uint16(typeVal)
  1012. data, err := io.ReadAll(r.Body)
  1013. if err != nil {
  1014. errorMsg(w, "failed to read request body", http.StatusBadRequest)
  1015. return
  1016. }
  1017. if err := bartAssetManager.InsertBARTItem(r.Context(), hashBytes, data, bartType); err != nil {
  1018. if errors.Is(err, state.ErrBARTItemExists) {
  1019. errorMsg(w, "BART asset already exists", http.StatusConflict)
  1020. return
  1021. }
  1022. logger.Error("error in POST /bart", "err", err.Error())
  1023. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1024. return
  1025. }
  1026. w.WriteHeader(http.StatusCreated)
  1027. response := BARTAsset{
  1028. Hash: hex.EncodeToString(hashBytes),
  1029. Type: bartType,
  1030. }
  1031. json.NewEncoder(w).Encode(response)
  1032. }
  1033. // deleteBARTHandler handles the DELETE /bart/{hash} endpoint.
  1034. func deleteBARTHandler(w http.ResponseWriter, r *http.Request, bartAssetManager BARTAssetManager, logger *slog.Logger) {
  1035. w.Header().Set("Content-Type", "application/json")
  1036. // Extract hash from URL path
  1037. hashStr := r.PathValue("hash")
  1038. if hashStr == "" {
  1039. errorMsg(w, "hash path parameter is required", http.StatusBadRequest)
  1040. return
  1041. }
  1042. hashBytes, err := hex.DecodeString(hashStr)
  1043. if err != nil {
  1044. errorMsg(w, "invalid hash format", http.StatusBadRequest)
  1045. return
  1046. }
  1047. if err := bartAssetManager.DeleteBARTItem(r.Context(), hashBytes); err != nil {
  1048. if errors.Is(err, state.ErrBARTItemNotFound) {
  1049. errorMsg(w, "BART asset not found", http.StatusNotFound)
  1050. return
  1051. }
  1052. logger.Error("error in DELETE /bart", "err", err.Error())
  1053. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1054. return
  1055. }
  1056. msg := messageBody{Message: "BART asset deleted successfully."}
  1057. json.NewEncoder(w).Encode(msg)
  1058. }
  1059. // getFeedbagBuddyHandler handles the GET /feedbag/{screen_name}/group endpoint.
  1060. func getFeedbagBuddyHandler(w http.ResponseWriter, r *http.Request, feedbagManager FeedbagManager, logger *slog.Logger) {
  1061. w.Header().Set("Content-Type", "application/json")
  1062. screenName := r.PathValue("screen_name")
  1063. if screenName == "" {
  1064. errorMsg(w, "screen_name is required", http.StatusBadRequest)
  1065. return
  1066. }
  1067. items, err := feedbagManager.Feedbag(r.Context(), state.NewIdentScreenName(screenName))
  1068. if err != nil {
  1069. logger.Error("error retrieving feedbag", "err", err.Error())
  1070. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1071. return
  1072. }
  1073. if len(items) == 0 {
  1074. errorMsg(w, "feedbag not found", http.StatusNotFound)
  1075. return
  1076. }
  1077. buddyMap := make(map[uint16][]*wire.FeedbagItem)
  1078. for _, item := range items {
  1079. switch item.ClassID {
  1080. case wire.FeedbagClassIdBuddy:
  1081. buddyMap[item.GroupID] = append(buddyMap[item.GroupID], &item)
  1082. }
  1083. }
  1084. type buddyItem struct {
  1085. Name string `json:"name"`
  1086. ItemID uint16 `json:"item_id"`
  1087. }
  1088. type groupItem struct {
  1089. GroupID uint16 `json:"group_id"`
  1090. GroupName string `json:"group_name"`
  1091. Buddies []buddyItem `json:"buddies"`
  1092. }
  1093. response := make([]groupItem, 0)
  1094. for _, item := range items {
  1095. switch item.ClassID {
  1096. case wire.FeedbagClassIdGroup:
  1097. if item.GroupID == 0 {
  1098. // can't add buddies to the root group
  1099. continue
  1100. }
  1101. group := groupItem{
  1102. GroupID: item.GroupID,
  1103. GroupName: item.Name,
  1104. Buddies: make([]buddyItem, 0, len(buddyMap[item.GroupID])),
  1105. }
  1106. for _, buddy := range buddyMap[item.GroupID] {
  1107. group.Buddies = append(group.Buddies, buddyItem{
  1108. Name: buddy.Name,
  1109. ItemID: buddy.ItemID,
  1110. })
  1111. }
  1112. response = append(response, group)
  1113. }
  1114. }
  1115. if err := json.NewEncoder(w).Encode(response); err != nil {
  1116. logger.Error("error encoding response", "err", err.Error())
  1117. }
  1118. }
  1119. // putFeedbagBuddyHandler handles the PUT /feedbag/{screen_name}/group/{group_id}/buddy/{buddy_screen_name} endpoint.
  1120. func putFeedbagBuddyHandler(w http.ResponseWriter, r *http.Request, buddyBroadcaster BuddyBroadcaster, feedbagManager FeedbagManager, sessionRetriever SessionRetriever, messageRelayer MessageRelayer, logger *slog.Logger, randInt func(n int) int) {
  1121. w.Header().Set("Content-Type", "application/json")
  1122. gid, err := strconv.ParseUint(r.PathValue("group_id"), 10, 16)
  1123. if err != nil {
  1124. errorMsg(w, "invalid group_id", http.StatusBadRequest)
  1125. return
  1126. }
  1127. groupID := uint16(gid)
  1128. if groupID == 0 {
  1129. errorMsg(w, "can't add buddies to root group", http.StatusBadRequest)
  1130. return
  1131. }
  1132. screenName := r.PathValue("screen_name")
  1133. if screenName == "" {
  1134. errorMsg(w, "screen_name is required", http.StatusBadRequest)
  1135. return
  1136. }
  1137. me := state.NewIdentScreenName(screenName)
  1138. buddyScreenName := r.PathValue("buddy_screen_name")
  1139. if buddyScreenName == "" {
  1140. errorMsg(w, "buddy_screen_name is required", http.StatusBadRequest)
  1141. return
  1142. }
  1143. newBuddy := state.DisplayScreenName(buddyScreenName)
  1144. if newBuddy.IsUIN() {
  1145. if err := newBuddy.ValidateUIN(); err != nil {
  1146. errorMsg(w, fmt.Sprintf("invalid uin: %s", err), http.StatusBadRequest)
  1147. return
  1148. }
  1149. } else {
  1150. if err := newBuddy.ValidateAIMHandle(); err != nil {
  1151. errorMsg(w, fmt.Sprintf("invalid screen name: %s", err), http.StatusBadRequest)
  1152. return
  1153. }
  1154. }
  1155. items, err := feedbagManager.Feedbag(r.Context(), me)
  1156. if err != nil {
  1157. logger.Error("error retrieving feedbag", "err", err.Error())
  1158. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1159. return
  1160. }
  1161. var group *wire.FeedbagItem
  1162. count := 0
  1163. for _, item := range items {
  1164. switch {
  1165. case item.ClassID == wire.FeedbagClassIdGroup && item.GroupID == groupID:
  1166. group = &item
  1167. case item.ClassID == wire.FeedbagClassIdBuddy && item.GroupID == groupID:
  1168. count++
  1169. if item.Name == newBuddy.IdentScreenName().String() {
  1170. response := struct {
  1171. Name string `json:"name"`
  1172. GroupID uint16 `json:"group_id"`
  1173. ItemID uint16 `json:"item_id"`
  1174. }{
  1175. Name: buddyScreenName,
  1176. GroupID: groupID,
  1177. ItemID: item.ItemID,
  1178. }
  1179. w.WriteHeader(http.StatusOK)
  1180. if err := json.NewEncoder(w).Encode(response); err != nil {
  1181. logger.Error("error encoding response", "err", err.Error())
  1182. }
  1183. return
  1184. }
  1185. }
  1186. }
  1187. if count >= 30 {
  1188. errorMsg(w, "too many buddies in group. max: 30", http.StatusBadRequest)
  1189. return
  1190. }
  1191. if group == nil {
  1192. errorMsg(w, "group not found", http.StatusNotFound)
  1193. return
  1194. }
  1195. buddyItem := wire.FeedbagItem{
  1196. Name: buddyScreenName,
  1197. GroupID: groupID,
  1198. ItemID: randItemID(randInt, items),
  1199. ClassID: wire.FeedbagClassIdBuddy,
  1200. }
  1201. if buddyItem.ItemID == 0 {
  1202. errorMsg(w, "maximum items reached", http.StatusConflict)
  1203. return
  1204. }
  1205. group.AppendOrderMembers(buddyItem.ItemID)
  1206. updates := []wire.FeedbagItem{
  1207. buddyItem,
  1208. *group,
  1209. }
  1210. if err := feedbagManager.FeedbagUpsert(r.Context(), me, updates); err != nil {
  1211. logger.Error("error inserting feedbag item", "err", err.Error())
  1212. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1213. return
  1214. }
  1215. session := sessionRetriever.RetrieveSession(me)
  1216. if session != nil {
  1217. messageRelayer.RelayToScreenName(r.Context(), me, wire.SNACMessage{
  1218. Frame: wire.SNACFrame{
  1219. FoodGroup: wire.Feedbag,
  1220. SubGroup: wire.FeedbagInsertItem,
  1221. RequestID: wire.ReqIDFromServer,
  1222. },
  1223. Body: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  1224. Items: []wire.FeedbagItem{buddyItem},
  1225. },
  1226. })
  1227. messageRelayer.RelayToScreenName(r.Context(), me, wire.SNACMessage{
  1228. Frame: wire.SNACFrame{
  1229. FoodGroup: wire.Feedbag,
  1230. SubGroup: wire.FeedbagUpdateItem,
  1231. RequestID: wire.ReqIDFromServer,
  1232. },
  1233. Body: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  1234. Items: []wire.FeedbagItem{*group},
  1235. },
  1236. })
  1237. instances := session.Instances()
  1238. if len(instances) > 0 {
  1239. if err := buddyBroadcaster.BroadcastVisibility(r.Context(), instances[0], []state.IdentScreenName{newBuddy.IdentScreenName()}, false); err != nil {
  1240. logger.Error("error broadcasting visibility", "err", err.Error())
  1241. }
  1242. }
  1243. }
  1244. response := struct {
  1245. Name string `json:"name"`
  1246. GroupID uint16 `json:"group_id"`
  1247. ItemID uint16 `json:"item_id"`
  1248. }{
  1249. Name: buddyItem.Name,
  1250. GroupID: buddyItem.GroupID,
  1251. ItemID: buddyItem.ItemID,
  1252. }
  1253. w.WriteHeader(http.StatusOK)
  1254. if err := json.NewEncoder(w).Encode(response); err != nil {
  1255. logger.Error("error encoding response", "err", err.Error())
  1256. }
  1257. }
  1258. func randItemID(randInt func(n int) int, items []wire.FeedbagItem) uint16 {
  1259. num := uint16(randInt(math.MaxUint16))
  1260. for itemID := num; itemID != num-1; itemID++ {
  1261. if itemID == 0 {
  1262. continue
  1263. }
  1264. exists := false
  1265. for _, item := range items {
  1266. if item.GroupID == itemID || item.ItemID == itemID {
  1267. exists = true
  1268. break
  1269. }
  1270. }
  1271. if !exists {
  1272. return itemID
  1273. }
  1274. }
  1275. return 0
  1276. }
  1277. // deleteFeedbagBuddyHandler handles the DELETE /feedbag/{screen_name}/group/{group_id}/buddy/{buddy_screen_name} endpoint.
  1278. func deleteFeedbagBuddyHandler(w http.ResponseWriter, r *http.Request, buddyBroadcaster BuddyBroadcaster, feedbagManager FeedbagManager, sessionRetriever SessionRetriever, messageRelayer MessageRelayer, logger *slog.Logger) {
  1279. gid, err := strconv.ParseUint(r.PathValue("group_id"), 10, 16)
  1280. if err != nil {
  1281. errorMsg(w, "invalid group_id", http.StatusBadRequest)
  1282. return
  1283. }
  1284. groupID := uint16(gid)
  1285. if groupID == 0 {
  1286. errorMsg(w, "can't add buddies to root group", http.StatusBadRequest)
  1287. return
  1288. }
  1289. screenName := r.PathValue("screen_name")
  1290. if screenName == "" {
  1291. errorMsg(w, "screen_name is required", http.StatusBadRequest)
  1292. return
  1293. }
  1294. me := state.NewIdentScreenName(screenName)
  1295. buddyScreenName := r.PathValue("buddy_screen_name")
  1296. if buddyScreenName == "" {
  1297. errorMsg(w, "buddy_screen_name is required", http.StatusBadRequest)
  1298. return
  1299. }
  1300. deleteBuddy := state.NewIdentScreenName(buddyScreenName)
  1301. items, err := feedbagManager.Feedbag(r.Context(), me)
  1302. if err != nil {
  1303. logger.Error("error retrieving feedbag", "err", err.Error())
  1304. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1305. return
  1306. }
  1307. var itemToDelete *wire.FeedbagItem
  1308. var parentGroup *wire.FeedbagItem
  1309. for _, item := range items {
  1310. switch {
  1311. case item.ClassID == wire.FeedbagClassIdGroup && item.GroupID == groupID:
  1312. parentGroup = &item
  1313. case item.ClassID == wire.FeedbagClassIdBuddy && item.Name == buddyScreenName && item.GroupID == groupID:
  1314. itemToDelete = &item
  1315. }
  1316. }
  1317. switch {
  1318. case parentGroup == nil:
  1319. errorMsg(w, "group not found", http.StatusNotFound)
  1320. return
  1321. case itemToDelete == nil:
  1322. errorMsg(w, "buddy not found", http.StatusNotFound)
  1323. return
  1324. }
  1325. // Remove the buddy from the parent group's order TLV (same as TOC feedbag_list.DeleteBuddy).
  1326. parentGroup.RemoveOrderMembers(itemToDelete.ItemID)
  1327. if err := feedbagManager.FeedbagUpsert(r.Context(), me, []wire.FeedbagItem{*parentGroup}); err != nil {
  1328. logger.Error("error updating feedbag group order", "err", err.Error())
  1329. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1330. return
  1331. }
  1332. if err := feedbagManager.FeedbagDelete(r.Context(), me, []wire.FeedbagItem{*itemToDelete}); err != nil {
  1333. logger.Error("error deleting feedbag item", "err", err.Error())
  1334. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1335. return
  1336. }
  1337. session := sessionRetriever.RetrieveSession(me)
  1338. if session != nil {
  1339. messageRelayer.RelayToScreenName(r.Context(), me, wire.SNACMessage{
  1340. Frame: wire.SNACFrame{
  1341. FoodGroup: wire.Feedbag,
  1342. SubGroup: wire.FeedbagDeleteItem,
  1343. RequestID: wire.ReqIDFromServer,
  1344. },
  1345. Body: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1346. Items: []wire.FeedbagItem{*itemToDelete},
  1347. },
  1348. })
  1349. messageRelayer.RelayToScreenName(r.Context(), me, wire.SNACMessage{
  1350. Frame: wire.SNACFrame{
  1351. FoodGroup: wire.Feedbag,
  1352. SubGroup: wire.FeedbagUpdateItem,
  1353. RequestID: wire.ReqIDFromServer,
  1354. },
  1355. Body: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  1356. Items: []wire.FeedbagItem{*parentGroup},
  1357. },
  1358. })
  1359. instances := session.Instances()
  1360. if len(instances) > 0 {
  1361. if err := buddyBroadcaster.BroadcastVisibility(r.Context(), instances[0], []state.IdentScreenName{deleteBuddy}, true); err != nil {
  1362. logger.Error("error broadcasting visibility", "err", err.Error())
  1363. }
  1364. }
  1365. }
  1366. w.WriteHeader(http.StatusNoContent)
  1367. }