mgmt_api.go 74 KB

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