mgmt_api.go 73 KB

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