mgmt_api.go 72 KB

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