mgmt_api.go 65 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916
  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, icqProfileManager ICQProfileManager, 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 '/user/{screenname}/icq' route
  55. mux.HandleFunc("GET /user/{screenname}/icq", func(w http.ResponseWriter, r *http.Request) {
  56. getICQProfileHandler(w, r, icqProfileManager, logger)
  57. })
  58. mux.HandleFunc("PUT /user/{screenname}/icq", func(w http.ResponseWriter, r *http.Request) {
  59. putICQProfileHandler(w, r, icqProfileManager, logger)
  60. })
  61. // Handlers for '/session' route
  62. mux.HandleFunc("GET /session", func(w http.ResponseWriter, r *http.Request) {
  63. getSessionHandler(w, r, sessionRetriever, time.Now)
  64. })
  65. // Handlers for '/session/{screenname}' route
  66. mux.HandleFunc("GET /session/{screenname}", func(w http.ResponseWriter, r *http.Request) {
  67. getSessionHandler(w, r, sessionRetriever, time.Now)
  68. })
  69. mux.HandleFunc("DELETE /session/{screenname}", func(w http.ResponseWriter, r *http.Request) {
  70. deleteSessionHandler(w, r, sessionRetriever)
  71. })
  72. // Handlers for '/chat/room/public' route
  73. mux.HandleFunc("GET /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  74. getPublicChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  75. })
  76. mux.HandleFunc("POST /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  77. postPublicChatHandler(w, r, chatRoomCreator, logger)
  78. })
  79. mux.HandleFunc("DELETE /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  80. deletePublicChatHandler(w, r, chatRoomDeleter, logger)
  81. })
  82. // Handlers for '/chat/room/private' route
  83. mux.HandleFunc("GET /chat/room/private", func(w http.ResponseWriter, r *http.Request) {
  84. getPrivateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  85. })
  86. // Handlers for '/instant-message' route
  87. mux.HandleFunc("POST /instant-message", func(w http.ResponseWriter, r *http.Request) {
  88. postInstantMessageHandler(w, r, messageRelayer, logger)
  89. })
  90. // Handlers for '/version' route
  91. mux.HandleFunc("GET /version", func(w http.ResponseWriter, r *http.Request) {
  92. getVersionHandler(w, bld)
  93. })
  94. // Handlers for '/admin/webapi/keys' route - Web API key management
  95. mux.HandleFunc("POST /admin/webapi/keys", func(w http.ResponseWriter, r *http.Request) {
  96. postWebAPIKeyHandler(w, r, webAPIKeyManager, uuid.New, logger)
  97. })
  98. mux.HandleFunc("GET /admin/webapi/keys", func(w http.ResponseWriter, r *http.Request) {
  99. getWebAPIKeysHandler(w, r, webAPIKeyManager, logger)
  100. })
  101. mux.HandleFunc("GET /admin/webapi/keys/{id}", func(w http.ResponseWriter, r *http.Request) {
  102. getWebAPIKeyHandler(w, r, webAPIKeyManager, logger)
  103. })
  104. mux.HandleFunc("PUT /admin/webapi/keys/{id}", func(w http.ResponseWriter, r *http.Request) {
  105. putWebAPIKeyHandler(w, r, webAPIKeyManager, logger)
  106. })
  107. mux.HandleFunc("DELETE /admin/webapi/keys/{id}", func(w http.ResponseWriter, r *http.Request) {
  108. deleteWebAPIKeyHandler(w, r, webAPIKeyManager, logger)
  109. })
  110. // Handlers for '/directory/category' route
  111. mux.HandleFunc("GET /directory/category", func(w http.ResponseWriter, r *http.Request) {
  112. getDirectoryCategoryHandler(w, r, directoryManager, logger)
  113. })
  114. mux.HandleFunc("POST /directory/category", func(w http.ResponseWriter, r *http.Request) {
  115. postDirectoryCategoryHandler(w, r, directoryManager, logger)
  116. })
  117. // Handlers for '/directory/category/{id}' route
  118. mux.HandleFunc("DELETE /directory/category/{id}", func(w http.ResponseWriter, r *http.Request) {
  119. deleteDirectoryCategoryHandler(w, r, directoryManager, logger)
  120. })
  121. // Handlers for '/directory/category/{id}/keyword' route
  122. mux.HandleFunc("GET /directory/category/{id}/keyword", func(w http.ResponseWriter, r *http.Request) {
  123. getDirectoryCategoryKeywordHandler(w, r, directoryManager, logger)
  124. })
  125. // Handlers for '/directory/keyword' route
  126. mux.HandleFunc("POST /directory/keyword", func(w http.ResponseWriter, r *http.Request) {
  127. postDirectoryKeywordHandler(w, r, directoryManager, logger)
  128. })
  129. // Handlers for '/directory/keyword/{id}' route
  130. mux.HandleFunc("DELETE /directory/keyword/{id}", func(w http.ResponseWriter, r *http.Request) {
  131. deleteDirectoryKeywordHandler(w, r, directoryManager, logger)
  132. })
  133. // Handlers for '/bart' route
  134. mux.HandleFunc("GET /bart", func(w http.ResponseWriter, r *http.Request) {
  135. getBARTByTypeHandler(w, r, bartAssetManager, logger)
  136. })
  137. // Handlers for '/bart/{hash}' route
  138. mux.HandleFunc("GET /bart/{hash}", func(w http.ResponseWriter, r *http.Request) {
  139. getBARTHandler(w, r, bartAssetManager, logger)
  140. })
  141. mux.HandleFunc("POST /bart/{hash}", func(w http.ResponseWriter, r *http.Request) {
  142. postBARTHandler(w, r, bartAssetManager, logger)
  143. })
  144. mux.HandleFunc("DELETE /bart/{hash}", func(w http.ResponseWriter, r *http.Request) {
  145. deleteBARTHandler(w, r, bartAssetManager, logger)
  146. })
  147. // Handlers for '/feedbag/{screen_name}/group' route
  148. mux.HandleFunc("GET /feedbag/{screen_name}/group", func(w http.ResponseWriter, r *http.Request) {
  149. getFeedbagBuddyHandler(w, r, feedbagManager, logger)
  150. })
  151. // Handlers for '/feedbag/{screen_name}/group/{group_id}/buddy/{buddy_screen_name}' route
  152. mux.HandleFunc("PUT /feedbag/{screen_name}/group/{group_id}/buddy/{buddy_screen_name}", func(w http.ResponseWriter, r *http.Request) {
  153. putFeedbagBuddyHandler(w, r, buddyBroadcaster, feedbagManager, sessionRetriever, messageRelayer, logger, rand.Intn)
  154. })
  155. mux.HandleFunc("DELETE /feedbag/{screen_name}/group/{group_id}/buddy/{buddy_screen_name}", func(w http.ResponseWriter, r *http.Request) {
  156. deleteFeedbagBuddyHandler(w, r, buddyBroadcaster, feedbagManager, sessionRetriever, messageRelayer, logger)
  157. })
  158. return &Server{
  159. server: http.Server{
  160. Addr: listener,
  161. Handler: mux,
  162. },
  163. logger: logger,
  164. }
  165. }
  166. type Server struct {
  167. server http.Server
  168. logger *slog.Logger
  169. }
  170. func (s *Server) ListenAndServe() error {
  171. s.logger.Info("starting server", "addr", s.server.Addr)
  172. if err := s.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
  173. return fmt.Errorf("unable to start management API server: %w", err)
  174. }
  175. return nil
  176. }
  177. func (s *Server) Shutdown(ctx context.Context) error {
  178. defer s.logger.Info("shutdown complete")
  179. return s.server.Shutdown(ctx)
  180. }
  181. // deleteUserHandler handles the DELETE /user endpoint.
  182. func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManager, logger *slog.Logger) {
  183. user, err := userFromBody(r)
  184. if err != nil {
  185. http.Error(w, err.Error(), http.StatusBadRequest)
  186. return
  187. }
  188. err = manager.DeleteUser(r.Context(), state.NewIdentScreenName(user.ScreenName))
  189. switch {
  190. case errors.Is(err, state.ErrNoUser):
  191. http.Error(w, "user does not exist", http.StatusNotFound)
  192. return
  193. case err != nil:
  194. logger.Error("error deleting user DELETE /user", "err", err.Error())
  195. http.Error(w, "internal server error", http.StatusInternalServerError)
  196. return
  197. }
  198. w.WriteHeader(http.StatusNoContent)
  199. _, _ = fmt.Fprintln(w, "User account successfully deleted.")
  200. }
  201. // putUserPasswordHandler handles the PUT /user/password endpoint.
  202. func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  203. input, err := userFromBody(r)
  204. if err != nil {
  205. http.Error(w, err.Error(), http.StatusBadRequest)
  206. return
  207. }
  208. sn := state.NewIdentScreenName(input.ScreenName)
  209. if err := userManager.SetUserPassword(r.Context(), sn, input.Password); err != nil {
  210. switch {
  211. case errors.Is(err, state.ErrNoUser):
  212. http.Error(w, "user does not exist", http.StatusNotFound)
  213. return
  214. case errors.Is(err, state.ErrPasswordInvalid):
  215. http.Error(w, err.Error(), http.StatusBadRequest)
  216. return
  217. default:
  218. logger.Error("error updating user password PUT /user/password", "err", err.Error())
  219. http.Error(w, "internal server error", http.StatusInternalServerError)
  220. return
  221. }
  222. }
  223. w.WriteHeader(http.StatusNoContent)
  224. _, _ = fmt.Fprintln(w, "Password successfully reset.")
  225. }
  226. // getSessionHandler handles GET /session
  227. func getSessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever, nowFn func() time.Time) {
  228. w.Header().Set("Content-Type", "application/json")
  229. var allSessions []*state.Session
  230. if screenName := r.PathValue("screenname"); screenName != "" {
  231. session := sessionRetriever.RetrieveSession(state.NewIdentScreenName(screenName))
  232. if session == nil {
  233. http.Error(w, "session not found", http.StatusNotFound)
  234. return
  235. }
  236. allSessions = append(allSessions, session)
  237. } else {
  238. // AllSessions returns all sessions
  239. allSessions = sessionRetriever.AllSessions()
  240. }
  241. ou := onlineUsers{
  242. Count: len(allSessions),
  243. Sessions: make([]sessionHandle, len(allSessions)),
  244. }
  245. for i, s := range allSessions {
  246. instances := s.Instances()
  247. instanceHandles := make([]instanceHandle, len(instances))
  248. for j, inst := range instances {
  249. instanceIdleSeconds := 0
  250. if inst.Idle() {
  251. instanceIdleSeconds = int(nowFn().Sub(inst.IdleTime()).Seconds())
  252. }
  253. awayMsg, _ := inst.AwayMessage()
  254. instanceHandles[j] = instanceHandle{
  255. Num: int(inst.Num()),
  256. IdleSeconds: instanceIdleSeconds,
  257. IsAway: inst.Away(),
  258. AwayMessage: awayMsg,
  259. IsInvisible: inst.Invisible(),
  260. }
  261. ra := inst.RemoteAddr()
  262. if ra != nil {
  263. instanceHandles[j].RemoteAddr = ra.Addr().String()
  264. instanceHandles[j].RemotePort = int(ra.Port())
  265. }
  266. }
  267. sessionIdleSeconds := 0
  268. if s.Idle() {
  269. sessionIdleSeconds = int(nowFn().Sub(s.IdleTime()).Seconds())
  270. }
  271. allAway := s.Away()
  272. awayMessage := ""
  273. if allAway {
  274. awayMessage = s.AwayMessage()
  275. }
  276. ou.Sessions[i] = sessionHandle{
  277. ID: s.IdentScreenName().String(),
  278. ScreenName: s.DisplayScreenName().String(),
  279. OnlineSeconds: int(nowFn().Sub(s.SignonTime()).Seconds()),
  280. IsAway: allAway,
  281. AwayMessage: awayMessage,
  282. IdleSeconds: sessionIdleSeconds,
  283. IsInvisible: s.Invisible(),
  284. IsICQ: s.UIN() > 0,
  285. InstanceCount: s.InstanceCount(),
  286. Instances: instanceHandles,
  287. }
  288. }
  289. if err := json.NewEncoder(w).Encode(ou); err != nil {
  290. http.Error(w, err.Error(), http.StatusInternalServerError)
  291. return
  292. }
  293. }
  294. // deleteSessionHandler handles DELETE /session/{screenname}
  295. func deleteSessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever) {
  296. w.Header().Set("Content-Type", "application/json")
  297. if screenName := r.PathValue("screenname"); screenName != "" {
  298. session := sessionRetriever.RetrieveSession(state.NewIdentScreenName(screenName))
  299. if session == nil {
  300. errorMsg(w, "session not found", http.StatusNotFound)
  301. return
  302. }
  303. session.CloseSession()
  304. }
  305. w.WriteHeader(http.StatusNoContent)
  306. }
  307. // getUserHandler handles the GET /user endpoint.
  308. func getUserHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  309. w.Header().Set("Content-Type", "application/json")
  310. users, err := userManager.AllUsers(r.Context())
  311. if err != nil {
  312. logger.Error("error in GET /user", "err", err.Error())
  313. http.Error(w, "internal server error", http.StatusInternalServerError)
  314. return
  315. }
  316. out := make([]userHandle, len(users))
  317. for i, u := range users {
  318. suspendedStatus, err := getSuspendedStatusErrCodeToText(u.SuspendedStatus)
  319. if err != nil {
  320. logger.Error("error getting suspended status in GET /user", "err", err.Error())
  321. http.Error(w, "internal server error", http.StatusInternalServerError)
  322. return
  323. }
  324. out[i] = userHandle{
  325. ID: u.IdentScreenName.String(),
  326. ScreenName: u.DisplayScreenName.String(),
  327. IsICQ: u.IsICQ,
  328. SuspendedStatus: suspendedStatus,
  329. IsBot: u.IsBot,
  330. }
  331. }
  332. if err := json.NewEncoder(w).Encode(out); err != nil {
  333. http.Error(w, err.Error(), http.StatusInternalServerError)
  334. return
  335. }
  336. }
  337. // postUserHandler handles the POST /user endpoint.
  338. func postUserHandler(w http.ResponseWriter, r *http.Request, createAccount state.CreateAccountFunc, logger *slog.Logger) {
  339. input, err := userFromBody(r)
  340. if err != nil {
  341. http.Error(w, err.Error(), http.StatusBadRequest)
  342. return
  343. }
  344. sn := state.DisplayScreenName(input.ScreenName)
  345. err = createAccount(r.Context(), sn, input.Password)
  346. switch {
  347. case errors.Is(err, state.ErrDupUser):
  348. http.Error(w, "user already exists", http.StatusConflict)
  349. return
  350. case errors.Is(err, state.ErrAIMHandleInvalidFormat), errors.Is(err, state.ErrAIMHandleLength):
  351. http.Error(w, fmt.Sprintf("invalid screen name: %s", err), http.StatusBadRequest)
  352. return
  353. case errors.Is(err, state.ErrICQUINInvalidFormat):
  354. http.Error(w, fmt.Sprintf("invalid uin: %s", err), http.StatusBadRequest)
  355. return
  356. case errors.Is(err, state.ErrPasswordInvalid):
  357. http.Error(w, fmt.Sprintf("invalid password: %s", err), http.StatusBadRequest)
  358. return
  359. case err != nil:
  360. logger.Error("error inserting user POST /user", "err", err.Error())
  361. http.Error(w, "internal server error", http.StatusInternalServerError)
  362. return
  363. }
  364. w.WriteHeader(http.StatusCreated)
  365. _, _ = fmt.Fprintln(w, "User account created successfully.")
  366. }
  367. func userFromBody(r *http.Request) (userWithPassword, error) {
  368. user := userWithPassword{}
  369. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  370. return userWithPassword{}, errors.New("malformed input")
  371. }
  372. return user, nil
  373. }
  374. // getUserLoginHandler is a temporary endpoint for validating user credentials.
  375. // do not rely on this endpoint, as it will be eventually removed.
  376. func getUserLoginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  377. authHeader := r.Header.Get("Authorization")
  378. if authHeader == "" {
  379. // No authentication header found
  380. w.WriteHeader(http.StatusUnauthorized)
  381. w.Header().Set("WWW-Authenticate", `Basic realm="User Login"`)
  382. _, _ = w.Write([]byte("401 Unauthorized\n"))
  383. return
  384. }
  385. auth := strings.SplitN(authHeader, " ", 2)
  386. if len(auth) != 2 || auth[0] != "Basic" {
  387. w.WriteHeader(http.StatusUnauthorized)
  388. _, _ = w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
  389. return
  390. }
  391. payload, err := base64.StdEncoding.DecodeString(auth[1])
  392. if err != nil {
  393. w.WriteHeader(http.StatusUnauthorized)
  394. _, _ = w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
  395. return
  396. }
  397. pair := strings.SplitN(string(payload), ":", 2)
  398. if len(pair) != 2 {
  399. w.WriteHeader(http.StatusUnauthorized)
  400. _, _ = w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
  401. return
  402. }
  403. username, password := state.NewIdentScreenName(pair[0]), pair[1]
  404. user, err := userManager.User(r.Context(), username)
  405. if err != nil {
  406. w.WriteHeader(http.StatusInternalServerError)
  407. _, _ = w.Write([]byte("500 InternalServerError\n"))
  408. logger.Error("error getting user", "err", err.Error())
  409. return
  410. }
  411. if user == nil || !user.ValidateHash(wire.StrongMD5PasswordHash(password, user.AuthKey)) {
  412. w.WriteHeader(http.StatusUnauthorized)
  413. _, _ = w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
  414. return
  415. }
  416. // Successfully authenticated
  417. w.WriteHeader(http.StatusOK)
  418. _, _ = w.Write([]byte("200 OK: Successfully Authenticated\n"))
  419. }
  420. // getPublicChatHandler handles the GET /chat/room/public endpoint.
  421. func getPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  422. w.Header().Set("Content-Type", "application/json")
  423. rooms, err := chatRoomRetriever.AllChatRooms(r.Context(), state.PublicExchange)
  424. if err != nil {
  425. logger.Error("error in GET /chat/rooms/public", "err", err.Error())
  426. http.Error(w, "internal server error", http.StatusInternalServerError)
  427. return
  428. }
  429. out := make([]chatRoom, len(rooms))
  430. for i, room := range rooms {
  431. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  432. cr := chatRoom{
  433. CreateTime: room.CreateTime(),
  434. Name: room.Name(),
  435. Participants: make([]aimChatUserHandle, len(sessions)),
  436. URL: room.URL().String(),
  437. }
  438. for j, sess := range sessions {
  439. cr.Participants[j] = aimChatUserHandle{
  440. ID: sess.IdentScreenName().String(),
  441. ScreenName: sess.DisplayScreenName().String(),
  442. }
  443. }
  444. out[i] = cr
  445. }
  446. writeUnescapeChatURL(w, out)
  447. }
  448. // postPublicChatHandler handles the POST /chat/room/public endpoint.
  449. func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreator ChatRoomCreator, logger *slog.Logger) {
  450. input := chatRoomCreate{}
  451. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  452. http.Error(w, "invalid input", http.StatusBadRequest)
  453. return
  454. }
  455. input.Name = strings.TrimSpace(input.Name)
  456. if input.Name == "" || len(input.Name) > 50 {
  457. http.Error(w, "chat room name must be between 1 and 50 characters", http.StatusBadRequest)
  458. return
  459. }
  460. cr := state.NewChatRoom(input.Name, state.NewIdentScreenName("system"), state.PublicExchange)
  461. err := chatRoomCreator.CreateChatRoom(r.Context(), &cr)
  462. switch {
  463. case errors.Is(err, state.ErrDupChatRoom):
  464. http.Error(w, "Chat room already exists.", http.StatusConflict)
  465. return
  466. case err != nil:
  467. logger.Error("error inserting chat room POST /chat/room/public", "err", err.Error())
  468. http.Error(w, "internal server error", http.StatusInternalServerError)
  469. return
  470. }
  471. w.WriteHeader(http.StatusCreated)
  472. _, _ = fmt.Fprintln(w, "Chat room created successfully.")
  473. }
  474. // getPrivateChatHandler handles the GET /chat/room/private endpoint.
  475. func getPrivateChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  476. w.Header().Set("Content-Type", "application/json")
  477. rooms, err := chatRoomRetriever.AllChatRooms(r.Context(), state.PrivateExchange)
  478. if err != nil {
  479. logger.Error("error in GET /chat/rooms/private", "err", err.Error())
  480. http.Error(w, "internal server error", http.StatusInternalServerError)
  481. return
  482. }
  483. out := make([]chatRoom, len(rooms))
  484. for i, room := range rooms {
  485. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  486. cr := chatRoom{
  487. CreateTime: room.CreateTime(),
  488. CreatorID: room.Creator().String(),
  489. Name: room.Name(),
  490. Participants: make([]aimChatUserHandle, len(sessions)),
  491. URL: room.URL().String(),
  492. }
  493. for j, sess := range sessions {
  494. cr.Participants[j] = aimChatUserHandle{
  495. ID: sess.IdentScreenName().String(),
  496. ScreenName: sess.DisplayScreenName().String(),
  497. }
  498. }
  499. out[i] = cr
  500. }
  501. writeUnescapeChatURL(w, out)
  502. }
  503. // deletePublicChatHandler handles the DELETE /chat/room/public endpoint.
  504. func deletePublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomDeleter ChatRoomDeleter, logger *slog.Logger) {
  505. input := chatRoomDelete{}
  506. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  507. http.Error(w, "malformed input", http.StatusBadRequest)
  508. return
  509. }
  510. if len(input.Names) == 0 {
  511. http.Error(w, "no chat room names provided", http.StatusBadRequest)
  512. return
  513. }
  514. err := chatRoomDeleter.DeleteChatRooms(r.Context(), state.PublicExchange, input.Names)
  515. if err != nil {
  516. logger.Error("error deleting public chat rooms DELETE /chat/room/public", "err", err.Error())
  517. http.Error(w, "internal server error", http.StatusInternalServerError)
  518. return
  519. }
  520. w.WriteHeader(http.StatusNoContent)
  521. _, _ = fmt.Fprintln(w, "Chat rooms deleted successfully.")
  522. }
  523. // writeUnescapeChatURL writes a JSON-encoded list of chat rooms with unescaped
  524. // ampersands preceding the exchange query param.
  525. //
  526. // before: aim:gochat?roomname=Office+Hijinks\u0026exchange=5
  527. // after: aim:gochat?roomname=Office+Hijinks&exchange=5
  528. //
  529. // This makes it easier to copy the gochat URL into AIM, which does not
  530. // recognize the ampersand unicode character \u0026.
  531. func writeUnescapeChatURL(w http.ResponseWriter, out []chatRoom) {
  532. buf := &bytes.Buffer{}
  533. if err := json.NewEncoder(buf).Encode(out); err != nil {
  534. http.Error(w, err.Error(), http.StatusInternalServerError)
  535. return
  536. }
  537. b := bytes.ReplaceAll(buf.Bytes(), []byte(`\u0026exchange`), []byte(`&exchange`))
  538. if _, err := w.Write(b); err != nil {
  539. http.Error(w, err.Error(), http.StatusInternalServerError)
  540. return
  541. }
  542. }
  543. // postIMHandler handles the POST /instant-message endpoint.
  544. func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) {
  545. input := instantMessage{}
  546. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  547. http.Error(w, "malformed input", http.StatusBadRequest)
  548. return
  549. }
  550. tlv, err := wire.ICBMFragmentList(input.Text)
  551. if err != nil {
  552. logger.Error("error sending message POST /instant-message", "err", err.Error())
  553. http.Error(w, "internal server error", http.StatusInternalServerError)
  554. return
  555. }
  556. msg := wire.SNACMessage{
  557. Frame: wire.SNACFrame{
  558. FoodGroup: wire.ICBM,
  559. SubGroup: wire.ICBMChannelMsgToClient,
  560. },
  561. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  562. ChannelID: 1,
  563. TLVUserInfo: wire.TLVUserInfo{
  564. ScreenName: input.From,
  565. },
  566. TLVRestBlock: wire.TLVRestBlock{
  567. TLVList: wire.TLVList{
  568. wire.NewTLVBE(wire.ICBMTLVAOLIMData, tlv),
  569. },
  570. },
  571. },
  572. }
  573. messageRelayer.RelayToScreenName(context.Background(), state.NewIdentScreenName(input.To), msg)
  574. w.WriteHeader(http.StatusOK)
  575. _, _ = fmt.Fprintln(w, "Message sent successfully.")
  576. }
  577. // getUserBuddyIconHandler handles the GET /user/{screenname}/icon endpoint.
  578. func getUserBuddyIconHandler(w http.ResponseWriter, r *http.Request, u UserManager, f FeedBagRetriever, b BARTAssetManager, logger *slog.Logger) {
  579. screenName := state.NewIdentScreenName(r.PathValue("screenname"))
  580. user, err := u.User(r.Context(), screenName)
  581. if err != nil {
  582. logger.Error("error retrieving user", "err", err.Error())
  583. http.Error(w, "internal server error", http.StatusInternalServerError)
  584. return
  585. }
  586. if user == nil {
  587. http.Error(w, "user not found", http.StatusNotFound)
  588. return
  589. }
  590. iconRef, err := f.BuddyIconMetadata(r.Context(), screenName)
  591. if err != nil {
  592. logger.Error("error retrieving buddy icon ref", "err", err.Error())
  593. http.Error(w, "internal server error", http.StatusInternalServerError)
  594. return
  595. }
  596. if iconRef == nil || iconRef.HasClearIconHash() {
  597. http.Error(w, "icon not found", http.StatusNotFound)
  598. return
  599. }
  600. icon, err := b.BARTItem(r.Context(), iconRef.Hash)
  601. if err != nil {
  602. logger.Error("error retrieving buddy icon bart item", "err", err.Error())
  603. http.Error(w, "internal server error", http.StatusInternalServerError)
  604. return
  605. }
  606. w.Header().Set("Content-Type", http.DetectContentType(icon))
  607. _, _ = w.Write(icon)
  608. }
  609. // getUserAccountHandler handles the GET /user/{screenname}/account endpoint.
  610. func getUserAccountHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, a AccountManager, p ProfileRetriever, logger *slog.Logger) {
  611. w.Header().Set("Content-Type", "application/json")
  612. screenName := r.PathValue("screenname")
  613. user, err := userManager.User(r.Context(), state.NewIdentScreenName(screenName))
  614. if err != nil {
  615. logger.Error("error in GET /user/{screenname}/account", "err", err.Error())
  616. http.Error(w, "internal server error", http.StatusInternalServerError)
  617. return
  618. }
  619. if user == nil {
  620. http.Error(w, "user not found", http.StatusNotFound)
  621. return
  622. }
  623. emailAddress := ""
  624. email, err := a.EmailAddress(r.Context(), user.IdentScreenName)
  625. if err != nil {
  626. emailAddress = ""
  627. } else {
  628. emailAddress = email.String()
  629. }
  630. regStatus, err := a.RegStatus(r.Context(), user.IdentScreenName)
  631. if err != nil {
  632. logger.Error("error in GET /user/*/account RegStatus", "err", err.Error())
  633. http.Error(w, "internal server error", http.StatusInternalServerError)
  634. return
  635. }
  636. confirmStatus, err := a.ConfirmStatus(r.Context(), user.IdentScreenName)
  637. if err != nil {
  638. logger.Error("error in GET /user/*/account ConfirmStatus", "err", err.Error())
  639. http.Error(w, "internal server error", http.StatusInternalServerError)
  640. return
  641. }
  642. profile, err := p.Profile(r.Context(), user.IdentScreenName)
  643. if err != nil {
  644. logger.Error("error in GET /user/*/account Profile", "err", err.Error())
  645. http.Error(w, "internal server error", http.StatusInternalServerError)
  646. return
  647. }
  648. suspendedStatusText, err := getSuspendedStatusErrCodeToText(user.SuspendedStatus)
  649. if err != nil {
  650. logger.Error("error in GET /user/{screenname}/account", "err", err.Error())
  651. http.Error(w, "internal server error", http.StatusInternalServerError)
  652. }
  653. out := userAccountHandle{
  654. ID: user.IdentScreenName.String(),
  655. ScreenName: user.DisplayScreenName.String(),
  656. EmailAddress: emailAddress,
  657. RegStatus: regStatus,
  658. Confirmed: confirmStatus,
  659. Profile: profile.ProfileText,
  660. IsICQ: user.IsICQ,
  661. SuspendedStatus: suspendedStatusText,
  662. IsBot: user.IsBot,
  663. }
  664. if err := json.NewEncoder(w).Encode(out); err != nil {
  665. http.Error(w, err.Error(), http.StatusInternalServerError)
  666. return
  667. }
  668. }
  669. // patchUserAccountHandler handles the PATCH /user/{screenname}/account endpoint.
  670. func patchUserAccountHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, a AccountManager, logger *slog.Logger) {
  671. w.Header().Set("Content-Type", "application/json")
  672. screenName := r.PathValue("screenname")
  673. user, err := userManager.User(r.Context(), state.NewIdentScreenName(screenName))
  674. if err != nil {
  675. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  676. http.Error(w, "internal server error", http.StatusInternalServerError)
  677. return
  678. }
  679. if user == nil {
  680. http.Error(w, "user not found", http.StatusNotFound)
  681. return
  682. }
  683. input := userAccountPatch{}
  684. d := json.NewDecoder(r.Body)
  685. d.DisallowUnknownFields()
  686. if err := d.Decode(&input); err != nil {
  687. errorMsg(w, err.Error(), http.StatusBadRequest)
  688. return
  689. }
  690. modifiedUser := false
  691. if input.SuspendedStatusText != nil {
  692. switch *input.SuspendedStatusText {
  693. case
  694. "", "deleted", "expired",
  695. "suspended", "suspended_age":
  696. suspendedStatus, err := getSuspendedStatusTextToErrCode(*input.SuspendedStatusText)
  697. if err != nil {
  698. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  699. http.Error(w, "internal server error", http.StatusInternalServerError)
  700. return
  701. }
  702. if suspendedStatus != user.SuspendedStatus {
  703. if err := a.UpdateSuspendedStatus(r.Context(), suspendedStatus, user.IdentScreenName); err != nil {
  704. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  705. http.Error(w, "internal server error", http.StatusInternalServerError)
  706. return
  707. }
  708. modifiedUser = true
  709. }
  710. default:
  711. errorMsg(w, "suspended_status must be empty str or one of deleted,expired,suspended,suspended_age", http.StatusBadRequest)
  712. return
  713. }
  714. }
  715. if input.IsBot != nil && user.IsBot != *input.IsBot {
  716. if err := a.SetBotStatus(r.Context(), *input.IsBot, user.IdentScreenName); err != nil {
  717. logger.Error("error in PATCH /user/{screenname}/account", "err", err.Error())
  718. http.Error(w, "internal server error", http.StatusInternalServerError)
  719. return
  720. }
  721. modifiedUser = true
  722. }
  723. if !modifiedUser {
  724. w.WriteHeader(http.StatusNotModified)
  725. return
  726. }
  727. w.WriteHeader(http.StatusNoContent)
  728. }
  729. // getSuspendedStatusTextToErrCode maps the given suspendedStatusText to
  730. // the appropriate error code, or 0x0 for none.
  731. func getSuspendedStatusTextToErrCode(suspendedStatusText string) (uint16, error) {
  732. suspendedStatusTextMap := map[string]uint16{
  733. "": 0x0,
  734. "deleted": wire.LoginErrDeletedAccount,
  735. "expired": wire.LoginErrExpiredAccount,
  736. "suspended": wire.LoginErrSuspendedAccount,
  737. "suspended_age": wire.LoginErrSuspendedAccountAge,
  738. }
  739. suspendedStatus, ok := suspendedStatusTextMap[suspendedStatusText]
  740. if !ok {
  741. return 0x0, errors.New("unable to map suspendedText to error code")
  742. }
  743. return suspendedStatus, nil
  744. }
  745. // getSuspendedStatusErrCodeToText maps the given suspendedStatus to
  746. // the appropriate text, or "" for none.
  747. func getSuspendedStatusErrCodeToText(suspendedStatus uint16) (string, error) {
  748. suspendedStatusTextMap := map[uint16]string{
  749. 0x0: "",
  750. wire.LoginErrDeletedAccount: "deleted",
  751. wire.LoginErrExpiredAccount: "expired",
  752. wire.LoginErrSuspendedAccount: "suspended",
  753. wire.LoginErrSuspendedAccountAge: "suspended_age",
  754. }
  755. st, ok := suspendedStatusTextMap[suspendedStatus]
  756. if !ok {
  757. return "", errors.New("unable to map error code to suspendedText")
  758. }
  759. return st, nil
  760. }
  761. // getVersionHandler handles the GET /version endpoint.
  762. func getVersionHandler(w http.ResponseWriter, bld config.Build) {
  763. w.Header().Set("Content-Type", "application/json")
  764. if err := json.NewEncoder(w).Encode(bld); err != nil {
  765. http.Error(w, err.Error(), http.StatusInternalServerError)
  766. return
  767. }
  768. }
  769. // getDirectoryCategoryHandler handles the GET /directory/category endpoint.
  770. func getDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  771. w.Header().Set("Content-Type", "application/json")
  772. categories, err := manager.Categories(r.Context())
  773. if err != nil {
  774. logger.Error("error in GET /directory/category", "err", err.Error())
  775. errorMsg(w, "internal server error", http.StatusInternalServerError)
  776. return
  777. }
  778. out := make([]directoryCategory, len(categories))
  779. for i, category := range categories {
  780. out[i] = directoryCategory{
  781. ID: category.ID,
  782. Name: category.Name,
  783. }
  784. }
  785. if err := json.NewEncoder(w).Encode(out); err != nil {
  786. errorMsg(w, err.Error(), http.StatusInternalServerError)
  787. }
  788. }
  789. // postDirectoryCategoryHandler handles the POST /directory/category endpoint.
  790. func postDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  791. input := directoryCategoryCreate{}
  792. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  793. errorMsg(w, "malformed input", http.StatusBadRequest)
  794. return
  795. }
  796. category, err := manager.CreateCategory(r.Context(), input.Name)
  797. if err != nil {
  798. if errors.Is(err, state.ErrKeywordCategoryExists) {
  799. errorMsg(w, "category already exists", http.StatusConflict)
  800. } else {
  801. logger.Error("error in POST /directory/category", "err", err.Error())
  802. errorMsg(w, "internal server error", http.StatusInternalServerError)
  803. }
  804. return
  805. }
  806. w.WriteHeader(http.StatusCreated)
  807. dc := directoryCategory{
  808. ID: category.ID,
  809. Name: category.Name,
  810. }
  811. if err := json.NewEncoder(w).Encode(dc); err != nil {
  812. errorMsg(w, err.Error(), http.StatusBadRequest)
  813. }
  814. }
  815. // deleteDirectoryCategoryHandler handles the DELETE /directory/category/{id} endpoint.
  816. func deleteDirectoryCategoryHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  817. categoryID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  818. if err != nil {
  819. http.Error(w, "invalid category ID", http.StatusBadRequest)
  820. return
  821. }
  822. if err := manager.DeleteCategory(r.Context(), uint8(categoryID)); err != nil {
  823. switch {
  824. case errors.Is(err, state.ErrKeywordCategoryNotFound):
  825. errorMsg(w, "category not found", http.StatusNotFound)
  826. return
  827. case errors.Is(err, state.ErrKeywordInUse):
  828. errorMsg(w, "can't delete because category in use by a user", http.StatusConflict)
  829. return
  830. default:
  831. logger.Error("error in DELETE /directory/category/{id}", "err", err.Error())
  832. errorMsg(w, "internal server error", http.StatusInternalServerError)
  833. return
  834. }
  835. }
  836. w.WriteHeader(http.StatusNoContent)
  837. }
  838. // getDirectoryCategoryKeywordHandler handles the GET /directory/category/{id}/keyword endpoint.
  839. func getDirectoryCategoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  840. w.Header().Set("Content-Type", "application/json")
  841. categoryID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  842. if err != nil {
  843. errorMsg(w, "invalid category ID", http.StatusBadRequest)
  844. return
  845. }
  846. categories, err := manager.KeywordsByCategory(r.Context(), uint8(categoryID))
  847. if err != nil {
  848. if errors.Is(err, state.ErrKeywordCategoryNotFound) {
  849. errorMsg(w, "category not found", http.StatusNotFound)
  850. } else {
  851. logger.Error("error in GET /directory/category/{id}/keyword", "err", err.Error())
  852. errorMsg(w, "internal server error", http.StatusInternalServerError)
  853. }
  854. return
  855. }
  856. out := make([]directoryCategory, len(categories))
  857. for i, category := range categories {
  858. out[i] = directoryCategory{
  859. ID: category.ID,
  860. Name: category.Name,
  861. }
  862. }
  863. if err := json.NewEncoder(w).Encode(out); err != nil {
  864. errorMsg(w, err.Error(), http.StatusInternalServerError)
  865. }
  866. }
  867. // postDirectoryKeywordHandler handles the POST /directory/keyword endpoint.
  868. func postDirectoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  869. w.Header().Set("Content-Type", "application/json")
  870. input := directoryKeywordCreate{}
  871. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  872. errorMsg(w, "malformed input", http.StatusBadRequest)
  873. return
  874. }
  875. kw, err := manager.CreateKeyword(r.Context(), input.Name, input.CategoryID)
  876. if err != nil {
  877. switch {
  878. case errors.Is(err, state.ErrKeywordCategoryNotFound):
  879. errorMsg(w, "category not found", http.StatusNotFound)
  880. return
  881. case errors.Is(err, state.ErrKeywordExists):
  882. errorMsg(w, "keyword already exists", http.StatusConflict)
  883. return
  884. default:
  885. logger.Error("error in POST /directory/keyword", "err", err.Error())
  886. errorMsg(w, "internal server error", http.StatusInternalServerError)
  887. return
  888. }
  889. }
  890. w.WriteHeader(http.StatusCreated)
  891. dc := directoryKeyword{
  892. ID: kw.ID,
  893. Name: kw.Name,
  894. }
  895. if err := json.NewEncoder(w).Encode(dc); err != nil {
  896. errorMsg(w, err.Error(), http.StatusBadRequest)
  897. }
  898. }
  899. // deleteDirectoryKeywordHandler handles the DELETE /directory/keyword/{id} endpoint.
  900. func deleteDirectoryKeywordHandler(w http.ResponseWriter, r *http.Request, manager DirectoryManager, logger *slog.Logger) {
  901. keywordID, err := strconv.ParseUint(r.PathValue("id"), 10, 8)
  902. if err != nil {
  903. errorMsg(w, "invalid keyword ID", http.StatusBadRequest)
  904. return
  905. }
  906. if err := manager.DeleteKeyword(r.Context(), uint8(keywordID)); err != nil {
  907. switch {
  908. case errors.Is(err, state.ErrKeywordInUse):
  909. errorMsg(w, "can't delete because category in use by a user", http.StatusConflict)
  910. return
  911. case errors.Is(err, state.ErrKeywordNotFound):
  912. errorMsg(w, "keyword not found", http.StatusNotFound)
  913. return
  914. default:
  915. logger.Error("error in DELETE /directory/keyword/{id}", "err", err.Error())
  916. errorMsg(w, "internal server error", http.StatusInternalServerError)
  917. return
  918. }
  919. }
  920. w.WriteHeader(http.StatusNoContent)
  921. }
  922. // getICQProfileHandler handles the GET /user/{screenname}/icq endpoint.
  923. func getICQProfileHandler(w http.ResponseWriter, r *http.Request, mgr ICQProfileManager, logger *slog.Logger) {
  924. w.Header().Set("Content-Type", "application/json")
  925. screenName := r.PathValue("screenname")
  926. user, err := mgr.User(r.Context(), state.NewIdentScreenName(screenName))
  927. if err != nil {
  928. logger.Error("error in GET /user/{screenname}/icq", "err", err.Error())
  929. http.Error(w, "internal server error", http.StatusInternalServerError)
  930. return
  931. }
  932. if user == nil {
  933. http.Error(w, "user not found", http.StatusNotFound)
  934. return
  935. }
  936. if !user.IsICQ {
  937. http.Error(w, "user is not an ICQ account", http.StatusBadRequest)
  938. return
  939. }
  940. out := icqProfileHandle{
  941. UIN: user.IdentScreenName.UIN(),
  942. BasicInfo: icqBasicInfoHandle{
  943. Nickname: user.ICQInfo.Basic.Nickname,
  944. FirstName: user.ICQInfo.Basic.FirstName,
  945. LastName: user.ICQInfo.Basic.LastName,
  946. EmailAddress: user.ICQInfo.Basic.EmailAddress,
  947. City: user.ICQInfo.Basic.City,
  948. State: user.ICQInfo.Basic.State,
  949. Phone: user.ICQInfo.Basic.Phone,
  950. Fax: user.ICQInfo.Basic.Fax,
  951. Address: user.ICQInfo.Basic.Address,
  952. CellPhone: user.ICQInfo.Basic.CellPhone,
  953. ZIPCode: user.ICQInfo.Basic.ZIPCode,
  954. CountryCode: user.ICQInfo.Basic.CountryCode,
  955. GMTOffset: user.ICQInfo.Basic.GMTOffset,
  956. PublishEmail: user.ICQInfo.Basic.PublishEmail,
  957. OriginCity: user.ICQInfo.Basic.OriginallyFromCity,
  958. OriginState: user.ICQInfo.Basic.OriginallyFromState,
  959. OriginCountryCode: user.ICQInfo.Basic.OriginallyFromCountryCode,
  960. },
  961. MoreInfo: icqMoreInfoHandle{
  962. Gender: user.ICQInfo.More.Gender,
  963. HomePageAddr: user.ICQInfo.More.HomePageAddr,
  964. BirthYear: user.ICQInfo.More.BirthYear,
  965. BirthMonth: user.ICQInfo.More.BirthMonth,
  966. BirthDay: user.ICQInfo.More.BirthDay,
  967. Lang1: user.ICQInfo.More.Lang1,
  968. Lang2: user.ICQInfo.More.Lang2,
  969. Lang3: user.ICQInfo.More.Lang3,
  970. },
  971. WorkInfo: icqWorkInfoHandle{
  972. Company: user.ICQInfo.Work.Company,
  973. Department: user.ICQInfo.Work.Department,
  974. Position: user.ICQInfo.Work.Position,
  975. OccupationCode: user.ICQInfo.Work.OccupationCode,
  976. Address: user.ICQInfo.Work.Address,
  977. City: user.ICQInfo.Work.City,
  978. State: user.ICQInfo.Work.State,
  979. ZIPCode: user.ICQInfo.Work.ZIPCode,
  980. CountryCode: user.ICQInfo.Work.CountryCode,
  981. Phone: user.ICQInfo.Work.Phone,
  982. Fax: user.ICQInfo.Work.Fax,
  983. WebPage: user.ICQInfo.Work.WebPage,
  984. },
  985. Notes: user.ICQInfo.Notes.Notes,
  986. Interests: icqInterestsHandle{
  987. Code1: user.ICQInfo.Interests.Code1,
  988. Keyword1: user.ICQInfo.Interests.Keyword1,
  989. Code2: user.ICQInfo.Interests.Code2,
  990. Keyword2: user.ICQInfo.Interests.Keyword2,
  991. Code3: user.ICQInfo.Interests.Code3,
  992. Keyword3: user.ICQInfo.Interests.Keyword3,
  993. Code4: user.ICQInfo.Interests.Code4,
  994. Keyword4: user.ICQInfo.Interests.Keyword4,
  995. },
  996. Affiliations: icqAffiliationsHandle{
  997. PastCode1: user.ICQInfo.Affiliations.PastCode1,
  998. PastKeyword1: user.ICQInfo.Affiliations.PastKeyword1,
  999. PastCode2: user.ICQInfo.Affiliations.PastCode2,
  1000. PastKeyword2: user.ICQInfo.Affiliations.PastKeyword2,
  1001. PastCode3: user.ICQInfo.Affiliations.PastCode3,
  1002. PastKeyword3: user.ICQInfo.Affiliations.PastKeyword3,
  1003. CurrentCode1: user.ICQInfo.Affiliations.CurrentCode1,
  1004. CurrentKeyword1: user.ICQInfo.Affiliations.CurrentKeyword1,
  1005. CurrentCode2: user.ICQInfo.Affiliations.CurrentCode2,
  1006. CurrentKeyword2: user.ICQInfo.Affiliations.CurrentKeyword2,
  1007. CurrentCode3: user.ICQInfo.Affiliations.CurrentCode3,
  1008. CurrentKeyword3: user.ICQInfo.Affiliations.CurrentKeyword3,
  1009. },
  1010. Permissions: icqPermissionsHandle{
  1011. AuthRequired: user.ICQInfo.Permissions.AuthRequired,
  1012. WebAware: user.ICQInfo.Permissions.WebAware,
  1013. AllowSpam: user.ICQInfo.Permissions.AllowSpam,
  1014. },
  1015. }
  1016. if err := json.NewEncoder(w).Encode(out); err != nil {
  1017. http.Error(w, err.Error(), http.StatusInternalServerError)
  1018. }
  1019. }
  1020. // putICQProfileHandler handles the PUT /user/{screenname}/icq endpoint.
  1021. func putICQProfileHandler(w http.ResponseWriter, r *http.Request, mgr ICQProfileManager, logger *slog.Logger) {
  1022. w.Header().Set("Content-Type", "application/json")
  1023. screenName := r.PathValue("screenname")
  1024. user, err := mgr.User(r.Context(), state.NewIdentScreenName(screenName))
  1025. if err != nil {
  1026. logger.Error("error in PUT /user/{screenname}/icq", "err", err.Error())
  1027. http.Error(w, "internal server error", http.StatusInternalServerError)
  1028. return
  1029. }
  1030. if user == nil {
  1031. http.Error(w, "user not found", http.StatusNotFound)
  1032. return
  1033. }
  1034. if !user.IsICQ {
  1035. http.Error(w, "user is not an ICQ account", http.StatusBadRequest)
  1036. return
  1037. }
  1038. var input icqProfileHandle
  1039. d := json.NewDecoder(r.Body)
  1040. if err := d.Decode(&input); err != nil {
  1041. errorMsg(w, "malformed input: "+err.Error(), http.StatusBadRequest)
  1042. return
  1043. }
  1044. // Validate field lengths. ICQ clients typically handle:
  1045. // - String fields: max 127 chars (some fields shorter)
  1046. // - Nickname: max 20 chars
  1047. // - Phone/fax: max 30 chars
  1048. // - Email: max 64 chars
  1049. // - Address/city/state: max 64 chars
  1050. // - Country codes (basic_info.country_code, basic_info.origin_country_code,
  1051. // work_info.country_code): uint16 in JSON, not string length checks
  1052. // - ZIP: max 12 chars
  1053. // - Homepage: max 127 chars
  1054. // - Notes: max 450 chars (v5 limit)
  1055. // - Interest/affiliation keywords: max 64 chars
  1056. // - Company/department/position: max 64 chars
  1057. // - Web page: max 127 chars
  1058. type fieldCheck struct {
  1059. name string
  1060. val string
  1061. max int
  1062. }
  1063. checks := []fieldCheck{
  1064. {"basic_info.nickname", input.BasicInfo.Nickname, 20},
  1065. {"basic_info.first_name", input.BasicInfo.FirstName, 64},
  1066. {"basic_info.last_name", input.BasicInfo.LastName, 64},
  1067. {"basic_info.email", input.BasicInfo.EmailAddress, 64},
  1068. {"basic_info.city", input.BasicInfo.City, 64},
  1069. {"basic_info.state", input.BasicInfo.State, 64},
  1070. {"basic_info.phone", input.BasicInfo.Phone, 30},
  1071. {"basic_info.fax", input.BasicInfo.Fax, 30},
  1072. {"basic_info.address", input.BasicInfo.Address, 64},
  1073. {"basic_info.cell_phone", input.BasicInfo.CellPhone, 30},
  1074. {"basic_info.zip", input.BasicInfo.ZIPCode, 12},
  1075. {"basic_info.origin_city", input.BasicInfo.OriginCity, 64},
  1076. {"basic_info.origin_state", input.BasicInfo.OriginState, 64},
  1077. // basic_info.origin_country_code is uint16 (like basic_info.country_code); validated by JSON binding only.
  1078. {"more_info.homepage", input.MoreInfo.HomePageAddr, 127},
  1079. {"work_info.company", input.WorkInfo.Company, 64},
  1080. {"work_info.department", input.WorkInfo.Department, 64},
  1081. {"work_info.position", input.WorkInfo.Position, 64},
  1082. {"work_info.address", input.WorkInfo.Address, 64},
  1083. {"work_info.city", input.WorkInfo.City, 64},
  1084. {"work_info.state", input.WorkInfo.State, 64},
  1085. {"work_info.zip", input.WorkInfo.ZIPCode, 12},
  1086. {"work_info.phone", input.WorkInfo.Phone, 30},
  1087. {"work_info.fax", input.WorkInfo.Fax, 30},
  1088. {"work_info.web_page", input.WorkInfo.WebPage, 127},
  1089. {"notes", input.Notes, 450},
  1090. {"interests.keyword1", input.Interests.Keyword1, 64},
  1091. {"interests.keyword2", input.Interests.Keyword2, 64},
  1092. {"interests.keyword3", input.Interests.Keyword3, 64},
  1093. {"interests.keyword4", input.Interests.Keyword4, 64},
  1094. {"affiliations.past_keyword1", input.Affiliations.PastKeyword1, 64},
  1095. {"affiliations.past_keyword2", input.Affiliations.PastKeyword2, 64},
  1096. {"affiliations.past_keyword3", input.Affiliations.PastKeyword3, 64},
  1097. {"affiliations.current_keyword1", input.Affiliations.CurrentKeyword1, 64},
  1098. {"affiliations.current_keyword2", input.Affiliations.CurrentKeyword2, 64},
  1099. {"affiliations.current_keyword3", input.Affiliations.CurrentKeyword3, 64},
  1100. }
  1101. var validationErrors []string
  1102. for _, c := range checks {
  1103. if len(c.val) > c.max {
  1104. msg := fmt.Sprintf("field %s exceeds max length of %d (got %d)", c.name, c.max, len(c.val))
  1105. validationErrors = append(validationErrors, msg)
  1106. logger.Warn("ICQ profile field exceeds max length",
  1107. "screenname", screenName,
  1108. "field", c.name,
  1109. "max", c.max,
  1110. "got", len(c.val),
  1111. )
  1112. }
  1113. }
  1114. // Validate gender (0=not specified, 1=female, 2=male)
  1115. if input.MoreInfo.Gender > 2 {
  1116. validationErrors = append(validationErrors, "more_info.gender must be 0 (unspecified), 1 (female), or 2 (male)")
  1117. logger.Warn("ICQ profile invalid gender value", "screenname", screenName, "gender", input.MoreInfo.Gender)
  1118. }
  1119. // Validate birth date
  1120. if input.MoreInfo.BirthMonth > 12 {
  1121. validationErrors = append(validationErrors, "more_info.birth_month must be 0-12")
  1122. logger.Warn("ICQ profile invalid birth month", "screenname", screenName, "birth_month", input.MoreInfo.BirthMonth)
  1123. }
  1124. if input.MoreInfo.BirthDay > 31 {
  1125. validationErrors = append(validationErrors, "more_info.birth_day must be 0-31")
  1126. logger.Warn("ICQ profile invalid birth day", "screenname", screenName, "birth_day", input.MoreInfo.BirthDay)
  1127. }
  1128. if len(validationErrors) > 0 {
  1129. errorMsg(w, strings.Join(validationErrors, "; "), http.StatusBadRequest)
  1130. return
  1131. }
  1132. sn := user.IdentScreenName
  1133. if err := mgr.SetBasicInfo(r.Context(), sn, state.ICQBasicInfo{
  1134. Nickname: input.BasicInfo.Nickname,
  1135. FirstName: input.BasicInfo.FirstName,
  1136. LastName: input.BasicInfo.LastName,
  1137. EmailAddress: input.BasicInfo.EmailAddress,
  1138. City: input.BasicInfo.City,
  1139. State: input.BasicInfo.State,
  1140. Phone: input.BasicInfo.Phone,
  1141. Fax: input.BasicInfo.Fax,
  1142. Address: input.BasicInfo.Address,
  1143. CellPhone: input.BasicInfo.CellPhone,
  1144. ZIPCode: input.BasicInfo.ZIPCode,
  1145. CountryCode: input.BasicInfo.CountryCode,
  1146. GMTOffset: input.BasicInfo.GMTOffset,
  1147. PublishEmail: input.BasicInfo.PublishEmail,
  1148. }); err != nil {
  1149. logger.Error("error setting basic info", "err", err.Error())
  1150. http.Error(w, "internal server error", http.StatusInternalServerError)
  1151. return
  1152. }
  1153. if err := mgr.SetMoreInfo(r.Context(), sn, state.ICQMoreInfo{
  1154. Gender: input.MoreInfo.Gender,
  1155. HomePageAddr: input.MoreInfo.HomePageAddr,
  1156. BirthYear: input.MoreInfo.BirthYear,
  1157. BirthMonth: input.MoreInfo.BirthMonth,
  1158. BirthDay: input.MoreInfo.BirthDay,
  1159. Lang1: input.MoreInfo.Lang1,
  1160. Lang2: input.MoreInfo.Lang2,
  1161. Lang3: input.MoreInfo.Lang3,
  1162. }); err != nil {
  1163. logger.Error("error setting more info", "err", err.Error())
  1164. http.Error(w, "internal server error", http.StatusInternalServerError)
  1165. return
  1166. }
  1167. if err := mgr.SetWorkInfo(r.Context(), sn, state.ICQWorkInfo{
  1168. Company: input.WorkInfo.Company,
  1169. Department: input.WorkInfo.Department,
  1170. Position: input.WorkInfo.Position,
  1171. OccupationCode: input.WorkInfo.OccupationCode,
  1172. Address: input.WorkInfo.Address,
  1173. City: input.WorkInfo.City,
  1174. State: input.WorkInfo.State,
  1175. ZIPCode: input.WorkInfo.ZIPCode,
  1176. CountryCode: input.WorkInfo.CountryCode,
  1177. Phone: input.WorkInfo.Phone,
  1178. Fax: input.WorkInfo.Fax,
  1179. WebPage: input.WorkInfo.WebPage,
  1180. }); err != nil {
  1181. logger.Error("error setting work info", "err", err.Error())
  1182. http.Error(w, "internal server error", http.StatusInternalServerError)
  1183. return
  1184. }
  1185. if err := mgr.SetUserNotes(r.Context(), sn, state.ICQUserNotes{
  1186. Notes: input.Notes,
  1187. }); err != nil {
  1188. logger.Error("error setting notes", "err", err.Error())
  1189. http.Error(w, "internal server error", http.StatusInternalServerError)
  1190. return
  1191. }
  1192. if err := mgr.SetInterests(r.Context(), sn, state.ICQInterests{
  1193. Code1: input.Interests.Code1,
  1194. Keyword1: input.Interests.Keyword1,
  1195. Code2: input.Interests.Code2,
  1196. Keyword2: input.Interests.Keyword2,
  1197. Code3: input.Interests.Code3,
  1198. Keyword3: input.Interests.Keyword3,
  1199. Code4: input.Interests.Code4,
  1200. Keyword4: input.Interests.Keyword4,
  1201. }); err != nil {
  1202. logger.Error("error setting interests", "err", err.Error())
  1203. http.Error(w, "internal server error", http.StatusInternalServerError)
  1204. return
  1205. }
  1206. if err := mgr.SetAffiliations(r.Context(), sn, state.ICQAffiliations{
  1207. PastCode1: input.Affiliations.PastCode1,
  1208. PastKeyword1: input.Affiliations.PastKeyword1,
  1209. PastCode2: input.Affiliations.PastCode2,
  1210. PastKeyword2: input.Affiliations.PastKeyword2,
  1211. PastCode3: input.Affiliations.PastCode3,
  1212. PastKeyword3: input.Affiliations.PastKeyword3,
  1213. CurrentCode1: input.Affiliations.CurrentCode1,
  1214. CurrentKeyword1: input.Affiliations.CurrentKeyword1,
  1215. CurrentCode2: input.Affiliations.CurrentCode2,
  1216. CurrentKeyword2: input.Affiliations.CurrentKeyword2,
  1217. CurrentCode3: input.Affiliations.CurrentCode3,
  1218. CurrentKeyword3: input.Affiliations.CurrentKeyword3,
  1219. }); err != nil {
  1220. logger.Error("error setting affiliations", "err", err.Error())
  1221. http.Error(w, "internal server error", http.StatusInternalServerError)
  1222. return
  1223. }
  1224. if err := mgr.SetPermissions(r.Context(), sn, state.ICQPermissions{
  1225. AuthRequired: input.Permissions.AuthRequired,
  1226. WebAware: input.Permissions.WebAware,
  1227. AllowSpam: input.Permissions.AllowSpam,
  1228. }); err != nil {
  1229. logger.Error("error setting permissions", "err", err.Error())
  1230. http.Error(w, "internal server error", http.StatusInternalServerError)
  1231. return
  1232. }
  1233. merged, err := mgr.User(r.Context(), sn)
  1234. if err != nil {
  1235. logger.Error("error reloading user after ICQ profile update", "err", err.Error())
  1236. http.Error(w, "internal server error", http.StatusInternalServerError)
  1237. return
  1238. }
  1239. if merged == nil {
  1240. http.Error(w, "user not found", http.StatusNotFound)
  1241. return
  1242. }
  1243. merged.ICQInfo.Basic.OriginallyFromCity = input.BasicInfo.OriginCity
  1244. merged.ICQInfo.Basic.OriginallyFromState = input.BasicInfo.OriginState
  1245. merged.ICQInfo.Basic.OriginallyFromCountryCode = input.BasicInfo.OriginCountryCode
  1246. if err := mgr.SetICQInfo(r.Context(), sn, merged.ICQInfo); err != nil {
  1247. logger.Error("error setting ICQ info (origin fields)", "err", err.Error())
  1248. http.Error(w, "internal server error", http.StatusInternalServerError)
  1249. return
  1250. }
  1251. w.WriteHeader(http.StatusNoContent)
  1252. }
  1253. // errorMsg sends an error response message and code.
  1254. func errorMsg(w http.ResponseWriter, error string, code int) {
  1255. msg := messageBody{Message: error}
  1256. w.WriteHeader(code)
  1257. if err := json.NewEncoder(w).Encode(msg); err != nil {
  1258. http.Error(w, err.Error(), http.StatusInternalServerError)
  1259. }
  1260. }
  1261. // BARTAsset represents a BART asset entry.
  1262. type BARTAsset struct {
  1263. Hash string `json:"hash"`
  1264. Type uint16 `json:"type"`
  1265. }
  1266. // getBARTByTypeHandler handles the GET /bart endpoint.
  1267. func getBARTByTypeHandler(w http.ResponseWriter, r *http.Request, bartAssetManager BARTAssetManager, logger *slog.Logger) {
  1268. w.Header().Set("Content-Type", "application/json")
  1269. // Get type from query parameter (required)
  1270. typeStr := r.URL.Query().Get("type")
  1271. if typeStr == "" {
  1272. errorMsg(w, "type query parameter is required", http.StatusBadRequest)
  1273. return
  1274. }
  1275. typeVal, err := strconv.ParseUint(typeStr, 10, 16)
  1276. if err != nil {
  1277. errorMsg(w, "invalid type ID", http.StatusBadRequest)
  1278. return
  1279. }
  1280. itemType := uint16(typeVal)
  1281. // Get BART items, filtered by type
  1282. items, err := bartAssetManager.ListBARTItems(r.Context(), itemType)
  1283. if err != nil {
  1284. logger.Error("error listing BART items", "err", err.Error())
  1285. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1286. return
  1287. }
  1288. // Convert to BARTAsset format
  1289. assets := make([]BARTAsset, 0, len(items))
  1290. for _, item := range items {
  1291. assets = append(assets, BARTAsset{
  1292. Hash: item.Hash,
  1293. Type: item.Type,
  1294. })
  1295. }
  1296. if err := json.NewEncoder(w).Encode(assets); err != nil {
  1297. logger.Error("error encoding response", "err", err.Error())
  1298. }
  1299. }
  1300. // getBARTHandler handles the GET /bart/{hash} endpoint.
  1301. func getBARTHandler(w http.ResponseWriter, r *http.Request, bartAssetManager BARTAssetManager, logger *slog.Logger) {
  1302. hashStr := r.PathValue("hash")
  1303. if hashStr == "" {
  1304. errorMsg(w, "hash is required", http.StatusBadRequest)
  1305. return
  1306. }
  1307. hashBytes, err := hex.DecodeString(hashStr)
  1308. if err != nil {
  1309. errorMsg(w, "invalid hash format", http.StatusBadRequest)
  1310. return
  1311. }
  1312. body, err := bartAssetManager.BARTItem(r.Context(), hashBytes)
  1313. if err != nil {
  1314. logger.Error("error retrieving BART asset", "err", err.Error())
  1315. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1316. return
  1317. }
  1318. if len(body) == 0 {
  1319. errorMsg(w, "BART asset not found", http.StatusNotFound)
  1320. return
  1321. }
  1322. w.Header().Set("Content-Type", "application/octet-stream")
  1323. _, _ = w.Write(body)
  1324. }
  1325. // postBARTHandler handles the POST /bart endpoint.
  1326. func postBARTHandler(w http.ResponseWriter, r *http.Request, bartAssetManager BARTAssetManager, logger *slog.Logger) {
  1327. w.Header().Set("Content-Type", "application/json")
  1328. // Extract hash from URL path
  1329. hashStr := r.PathValue("hash")
  1330. if hashStr == "" {
  1331. errorMsg(w, "hash path parameter is required", http.StatusBadRequest)
  1332. return
  1333. }
  1334. hashBytes, err := hex.DecodeString(hashStr)
  1335. if err != nil {
  1336. errorMsg(w, "invalid hash format", http.StatusBadRequest)
  1337. return
  1338. }
  1339. typeStr := r.URL.Query().Get("type")
  1340. if typeStr == "" {
  1341. errorMsg(w, "type query parameter is required", http.StatusBadRequest)
  1342. return
  1343. }
  1344. typeVal, err := strconv.ParseUint(typeStr, 10, 16)
  1345. if err != nil {
  1346. errorMsg(w, "invalid type ID", http.StatusBadRequest)
  1347. return
  1348. }
  1349. bartType := uint16(typeVal)
  1350. data, err := io.ReadAll(r.Body)
  1351. if err != nil {
  1352. errorMsg(w, "failed to read request body", http.StatusBadRequest)
  1353. return
  1354. }
  1355. if err := bartAssetManager.InsertBARTItem(r.Context(), hashBytes, data, bartType); err != nil {
  1356. if errors.Is(err, state.ErrBARTItemExists) {
  1357. errorMsg(w, "BART asset already exists", http.StatusConflict)
  1358. return
  1359. }
  1360. logger.Error("error in POST /bart", "err", err.Error())
  1361. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1362. return
  1363. }
  1364. w.WriteHeader(http.StatusCreated)
  1365. response := BARTAsset{
  1366. Hash: hex.EncodeToString(hashBytes),
  1367. Type: bartType,
  1368. }
  1369. _ = json.NewEncoder(w).Encode(response)
  1370. }
  1371. // deleteBARTHandler handles the DELETE /bart/{hash} endpoint.
  1372. func deleteBARTHandler(w http.ResponseWriter, r *http.Request, bartAssetManager BARTAssetManager, logger *slog.Logger) {
  1373. w.Header().Set("Content-Type", "application/json")
  1374. // Extract hash from URL path
  1375. hashStr := r.PathValue("hash")
  1376. if hashStr == "" {
  1377. errorMsg(w, "hash path parameter is required", http.StatusBadRequest)
  1378. return
  1379. }
  1380. hashBytes, err := hex.DecodeString(hashStr)
  1381. if err != nil {
  1382. errorMsg(w, "invalid hash format", http.StatusBadRequest)
  1383. return
  1384. }
  1385. if err := bartAssetManager.DeleteBARTItem(r.Context(), hashBytes); err != nil {
  1386. if errors.Is(err, state.ErrBARTItemNotFound) {
  1387. errorMsg(w, "BART asset not found", http.StatusNotFound)
  1388. return
  1389. }
  1390. logger.Error("error in DELETE /bart", "err", err.Error())
  1391. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1392. return
  1393. }
  1394. msg := messageBody{Message: "BART asset deleted successfully."}
  1395. _ = json.NewEncoder(w).Encode(msg)
  1396. }
  1397. // getFeedbagBuddyHandler handles the GET /feedbag/{screen_name}/group endpoint.
  1398. func getFeedbagBuddyHandler(w http.ResponseWriter, r *http.Request, feedbagManager FeedbagManager, logger *slog.Logger) {
  1399. w.Header().Set("Content-Type", "application/json")
  1400. screenName := r.PathValue("screen_name")
  1401. if screenName == "" {
  1402. errorMsg(w, "screen_name is required", http.StatusBadRequest)
  1403. return
  1404. }
  1405. items, err := feedbagManager.Feedbag(r.Context(), state.NewIdentScreenName(screenName))
  1406. if err != nil {
  1407. logger.Error("error retrieving feedbag", "err", err.Error())
  1408. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1409. return
  1410. }
  1411. if len(items) == 0 {
  1412. errorMsg(w, "feedbag not found", http.StatusNotFound)
  1413. return
  1414. }
  1415. buddyMap := make(map[uint16][]*wire.FeedbagItem)
  1416. for _, item := range items {
  1417. switch item.ClassID {
  1418. case wire.FeedbagClassIdBuddy:
  1419. buddyMap[item.GroupID] = append(buddyMap[item.GroupID], &item)
  1420. }
  1421. }
  1422. type buddyItem struct {
  1423. Name string `json:"name"`
  1424. ItemID uint16 `json:"item_id"`
  1425. }
  1426. type groupItem struct {
  1427. GroupID uint16 `json:"group_id"`
  1428. GroupName string `json:"group_name"`
  1429. Buddies []buddyItem `json:"buddies"`
  1430. }
  1431. response := make([]groupItem, 0)
  1432. for _, item := range items {
  1433. switch item.ClassID {
  1434. case wire.FeedbagClassIdGroup:
  1435. if item.GroupID == 0 {
  1436. // can't add buddies to the root group
  1437. continue
  1438. }
  1439. group := groupItem{
  1440. GroupID: item.GroupID,
  1441. GroupName: item.Name,
  1442. Buddies: make([]buddyItem, 0, len(buddyMap[item.GroupID])),
  1443. }
  1444. for _, buddy := range buddyMap[item.GroupID] {
  1445. group.Buddies = append(group.Buddies, buddyItem{
  1446. Name: buddy.Name,
  1447. ItemID: buddy.ItemID,
  1448. })
  1449. }
  1450. response = append(response, group)
  1451. }
  1452. }
  1453. if err := json.NewEncoder(w).Encode(response); err != nil {
  1454. logger.Error("error encoding response", "err", err.Error())
  1455. }
  1456. }
  1457. // putFeedbagBuddyHandler handles the PUT /feedbag/{screen_name}/group/{group_id}/buddy/{buddy_screen_name} endpoint.
  1458. func putFeedbagBuddyHandler(w http.ResponseWriter, r *http.Request, buddyBroadcaster BuddyBroadcaster, feedbagManager FeedbagManager, sessionRetriever SessionRetriever, messageRelayer MessageRelayer, logger *slog.Logger, randInt func(n int) int) {
  1459. w.Header().Set("Content-Type", "application/json")
  1460. gid, err := strconv.ParseUint(r.PathValue("group_id"), 10, 16)
  1461. if err != nil {
  1462. errorMsg(w, "invalid group_id", http.StatusBadRequest)
  1463. return
  1464. }
  1465. groupID := uint16(gid)
  1466. if groupID == 0 {
  1467. errorMsg(w, "can't add buddies to root group", http.StatusBadRequest)
  1468. return
  1469. }
  1470. screenName := r.PathValue("screen_name")
  1471. if screenName == "" {
  1472. errorMsg(w, "screen_name is required", http.StatusBadRequest)
  1473. return
  1474. }
  1475. me := state.NewIdentScreenName(screenName)
  1476. buddyScreenName := r.PathValue("buddy_screen_name")
  1477. if buddyScreenName == "" {
  1478. errorMsg(w, "buddy_screen_name is required", http.StatusBadRequest)
  1479. return
  1480. }
  1481. newBuddy := state.DisplayScreenName(buddyScreenName)
  1482. if newBuddy.IsUIN() {
  1483. if err := newBuddy.ValidateUIN(); err != nil {
  1484. errorMsg(w, fmt.Sprintf("invalid uin: %s", err), http.StatusBadRequest)
  1485. return
  1486. }
  1487. } else {
  1488. if err := newBuddy.ValidateAIMHandle(); err != nil {
  1489. errorMsg(w, fmt.Sprintf("invalid screen name: %s", err), http.StatusBadRequest)
  1490. return
  1491. }
  1492. }
  1493. items, err := feedbagManager.Feedbag(r.Context(), me)
  1494. if err != nil {
  1495. logger.Error("error retrieving feedbag", "err", err.Error())
  1496. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1497. return
  1498. }
  1499. var group *wire.FeedbagItem
  1500. count := 0
  1501. for _, item := range items {
  1502. switch {
  1503. case item.ClassID == wire.FeedbagClassIdGroup && item.GroupID == groupID:
  1504. group = &item
  1505. case item.ClassID == wire.FeedbagClassIdBuddy && item.GroupID == groupID:
  1506. count++
  1507. if item.Name == newBuddy.IdentScreenName().String() {
  1508. response := struct {
  1509. Name string `json:"name"`
  1510. GroupID uint16 `json:"group_id"`
  1511. ItemID uint16 `json:"item_id"`
  1512. }{
  1513. Name: buddyScreenName,
  1514. GroupID: groupID,
  1515. ItemID: item.ItemID,
  1516. }
  1517. w.WriteHeader(http.StatusOK)
  1518. if err := json.NewEncoder(w).Encode(response); err != nil {
  1519. logger.Error("error encoding response", "err", err.Error())
  1520. }
  1521. return
  1522. }
  1523. }
  1524. }
  1525. if count >= 30 {
  1526. errorMsg(w, "too many buddies in group. max: 30", http.StatusBadRequest)
  1527. return
  1528. }
  1529. if group == nil {
  1530. errorMsg(w, "group not found", http.StatusNotFound)
  1531. return
  1532. }
  1533. buddyItem := wire.FeedbagItem{
  1534. Name: buddyScreenName,
  1535. GroupID: groupID,
  1536. ItemID: randItemID(randInt, items),
  1537. ClassID: wire.FeedbagClassIdBuddy,
  1538. }
  1539. if buddyItem.ItemID == 0 {
  1540. errorMsg(w, "maximum items reached", http.StatusConflict)
  1541. return
  1542. }
  1543. group.AppendOrderMembers(buddyItem.ItemID)
  1544. updates := []wire.FeedbagItem{
  1545. buddyItem,
  1546. *group,
  1547. }
  1548. if err := feedbagManager.FeedbagUpsert(r.Context(), me, updates); err != nil {
  1549. logger.Error("error inserting feedbag item", "err", err.Error())
  1550. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1551. return
  1552. }
  1553. session := sessionRetriever.RetrieveSession(me)
  1554. if session != nil {
  1555. messageRelayer.RelayToScreenName(r.Context(), me, wire.SNACMessage{
  1556. Frame: wire.SNACFrame{
  1557. FoodGroup: wire.Feedbag,
  1558. SubGroup: wire.FeedbagInsertItem,
  1559. RequestID: wire.ReqIDFromServer,
  1560. },
  1561. Body: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  1562. Items: []wire.FeedbagItem{buddyItem},
  1563. },
  1564. })
  1565. messageRelayer.RelayToScreenName(r.Context(), me, wire.SNACMessage{
  1566. Frame: wire.SNACFrame{
  1567. FoodGroup: wire.Feedbag,
  1568. SubGroup: wire.FeedbagUpdateItem,
  1569. RequestID: wire.ReqIDFromServer,
  1570. },
  1571. Body: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  1572. Items: []wire.FeedbagItem{*group},
  1573. },
  1574. })
  1575. instances := session.Instances()
  1576. if len(instances) > 0 {
  1577. if err := buddyBroadcaster.BroadcastVisibility(r.Context(), instances[0], []state.IdentScreenName{newBuddy.IdentScreenName()}, false); err != nil {
  1578. logger.Error("error broadcasting visibility", "err", err.Error())
  1579. }
  1580. }
  1581. }
  1582. response := struct {
  1583. Name string `json:"name"`
  1584. GroupID uint16 `json:"group_id"`
  1585. ItemID uint16 `json:"item_id"`
  1586. }{
  1587. Name: buddyItem.Name,
  1588. GroupID: buddyItem.GroupID,
  1589. ItemID: buddyItem.ItemID,
  1590. }
  1591. w.WriteHeader(http.StatusOK)
  1592. if err := json.NewEncoder(w).Encode(response); err != nil {
  1593. logger.Error("error encoding response", "err", err.Error())
  1594. }
  1595. }
  1596. func randItemID(randInt func(n int) int, items []wire.FeedbagItem) uint16 {
  1597. num := uint16(randInt(math.MaxUint16))
  1598. for itemID := num; itemID != num-1; itemID++ {
  1599. if itemID == 0 {
  1600. continue
  1601. }
  1602. exists := false
  1603. for _, item := range items {
  1604. if item.GroupID == itemID || item.ItemID == itemID {
  1605. exists = true
  1606. break
  1607. }
  1608. }
  1609. if !exists {
  1610. return itemID
  1611. }
  1612. }
  1613. return 0
  1614. }
  1615. // deleteFeedbagBuddyHandler handles the DELETE /feedbag/{screen_name}/group/{group_id}/buddy/{buddy_screen_name} endpoint.
  1616. func deleteFeedbagBuddyHandler(w http.ResponseWriter, r *http.Request, buddyBroadcaster BuddyBroadcaster, feedbagManager FeedbagManager, sessionRetriever SessionRetriever, messageRelayer MessageRelayer, logger *slog.Logger) {
  1617. gid, err := strconv.ParseUint(r.PathValue("group_id"), 10, 16)
  1618. if err != nil {
  1619. errorMsg(w, "invalid group_id", http.StatusBadRequest)
  1620. return
  1621. }
  1622. groupID := uint16(gid)
  1623. if groupID == 0 {
  1624. errorMsg(w, "can't add buddies to root group", http.StatusBadRequest)
  1625. return
  1626. }
  1627. screenName := r.PathValue("screen_name")
  1628. if screenName == "" {
  1629. errorMsg(w, "screen_name is required", http.StatusBadRequest)
  1630. return
  1631. }
  1632. me := state.NewIdentScreenName(screenName)
  1633. buddyScreenName := r.PathValue("buddy_screen_name")
  1634. if buddyScreenName == "" {
  1635. errorMsg(w, "buddy_screen_name is required", http.StatusBadRequest)
  1636. return
  1637. }
  1638. deleteBuddy := state.NewIdentScreenName(buddyScreenName)
  1639. items, err := feedbagManager.Feedbag(r.Context(), me)
  1640. if err != nil {
  1641. logger.Error("error retrieving feedbag", "err", err.Error())
  1642. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1643. return
  1644. }
  1645. var itemToDelete *wire.FeedbagItem
  1646. var parentGroup *wire.FeedbagItem
  1647. for _, item := range items {
  1648. switch {
  1649. case item.ClassID == wire.FeedbagClassIdGroup && item.GroupID == groupID:
  1650. parentGroup = &item
  1651. case item.ClassID == wire.FeedbagClassIdBuddy && item.Name == buddyScreenName && item.GroupID == groupID:
  1652. itemToDelete = &item
  1653. }
  1654. }
  1655. switch {
  1656. case parentGroup == nil:
  1657. errorMsg(w, "group not found", http.StatusNotFound)
  1658. return
  1659. case itemToDelete == nil:
  1660. errorMsg(w, "buddy not found", http.StatusNotFound)
  1661. return
  1662. }
  1663. // Remove the buddy from the parent group's order TLV (same as TOC feedbag_list.DeleteBuddy).
  1664. parentGroup.RemoveOrderMembers(itemToDelete.ItemID)
  1665. if err := feedbagManager.FeedbagUpsert(r.Context(), me, []wire.FeedbagItem{*parentGroup}); err != nil {
  1666. logger.Error("error updating feedbag group order", "err", err.Error())
  1667. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1668. return
  1669. }
  1670. if err := feedbagManager.FeedbagDelete(r.Context(), me, []wire.FeedbagItem{*itemToDelete}); err != nil {
  1671. logger.Error("error deleting feedbag item", "err", err.Error())
  1672. errorMsg(w, "internal server error", http.StatusInternalServerError)
  1673. return
  1674. }
  1675. session := sessionRetriever.RetrieveSession(me)
  1676. if session != nil {
  1677. messageRelayer.RelayToScreenName(r.Context(), me, wire.SNACMessage{
  1678. Frame: wire.SNACFrame{
  1679. FoodGroup: wire.Feedbag,
  1680. SubGroup: wire.FeedbagDeleteItem,
  1681. RequestID: wire.ReqIDFromServer,
  1682. },
  1683. Body: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1684. Items: []wire.FeedbagItem{*itemToDelete},
  1685. },
  1686. })
  1687. messageRelayer.RelayToScreenName(r.Context(), me, wire.SNACMessage{
  1688. Frame: wire.SNACFrame{
  1689. FoodGroup: wire.Feedbag,
  1690. SubGroup: wire.FeedbagUpdateItem,
  1691. RequestID: wire.ReqIDFromServer,
  1692. },
  1693. Body: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  1694. Items: []wire.FeedbagItem{*parentGroup},
  1695. },
  1696. })
  1697. instances := session.Instances()
  1698. if len(instances) > 0 {
  1699. if err := buddyBroadcaster.BroadcastVisibility(r.Context(), instances[0], []state.IdentScreenName{deleteBuddy}, true); err != nil {
  1700. logger.Error("error broadcasting visibility", "err", err.Error())
  1701. }
  1702. }
  1703. }
  1704. w.WriteHeader(http.StatusNoContent)
  1705. }