mgmt_api.go 71 KB

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