feedbag.go 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082
  1. package foodgroup
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/mk6i/open-oscar-server/state"
  12. "github.com/mk6i/open-oscar-server/wire"
  13. )
  14. // NewFeedbagService creates a new instance of FeedbagService.
  15. func NewFeedbagService(
  16. logger *slog.Logger,
  17. messageRelayer MessageRelayer,
  18. feedbagManager FeedbagManager,
  19. bartItemManager BARTItemManager,
  20. relationshipFetcher RelationshipFetcher,
  21. sessionRetriever SessionRetriever,
  22. contactPreAuthorizer ContactPreAuthorizer,
  23. userManager UserManager,
  24. buddyAddedNotifierDeduper BuddyAddedNotifierDeduper,
  25. ) *FeedbagService {
  26. return &FeedbagService{
  27. bartItemManager: bartItemManager,
  28. buddyBroadcaster: newBuddyNotifier(bartItemManager, relationshipFetcher, messageRelayer, sessionRetriever),
  29. buddyAddedNotifierDeduper: buddyAddedNotifierDeduper,
  30. feedbagManager: feedbagManager,
  31. logger: logger,
  32. messageRelayer: messageRelayer,
  33. relationshipFetcher: relationshipFetcher,
  34. sessionRetriever: sessionRetriever,
  35. contactPreAuthorizer: contactPreAuthorizer,
  36. userManager: userManager,
  37. icbmSender: func(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*wire.SNACMessage, error) {
  38. return nil, errors.New("icbmSender not implemented")
  39. },
  40. }
  41. }
  42. // FeedbagService provides functionality for the Feedbag food group, which
  43. // handles buddy list management.
  44. type FeedbagService struct {
  45. bartItemManager BARTItemManager
  46. buddyBroadcaster buddyBroadcaster
  47. buddyAddedNotifierDeduper BuddyAddedNotifierDeduper
  48. feedbagManager FeedbagManager
  49. logger *slog.Logger
  50. messageRelayer MessageRelayer
  51. relationshipFetcher RelationshipFetcher
  52. sessionRetriever SessionRetriever
  53. contactPreAuthorizer ContactPreAuthorizer
  54. userManager UserManager
  55. icbmSender func(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x04_0x06_ICBMChannelMsgToHost) (*wire.SNACMessage, error)
  56. }
  57. // BridgeICBMService enables the FeedbagService to send instant messages via the
  58. // ICBM service.
  59. func (s *FeedbagService) BridgeICBMService(service *ICBMService) {
  60. s.icbmSender = service.ChannelMsgToHost
  61. }
  62. // RightsQuery returns SNAC wire.FeedbagRightsReply, which contains Feedbag
  63. // food group settings for the current user. The values within the SNAC are not
  64. // well understood but seem to make the AIM client happy.
  65. func (s *FeedbagService) RightsQuery(_ context.Context, inFrame wire.SNACFrame) wire.SNACMessage {
  66. // maxItemsByClass defines per-type item limits. Types not listed here are
  67. // 0 by default. The slice size is equal to the maximum "enum" value+1.
  68. maxItemsByClass := make([]uint16, wire.FeedbagClassIdAlInfo+1)
  69. maxItemsByClass[wire.FeedbagClassIdBuddy] = 61
  70. maxItemsByClass[wire.FeedbagClassIdGroup] = 61
  71. maxItemsByClass[wire.FeedbagClassIDPermit] = 100
  72. maxItemsByClass[wire.FeedbagClassIDDeny] = 100
  73. maxItemsByClass[wire.FeedbagClassIdPdinfo] = 1
  74. maxItemsByClass[wire.FeedbagClassIdBuddyPrefs] = 1
  75. maxItemsByClass[wire.FeedbagClassIdNonbuddy] = 50
  76. maxItemsByClass[wire.FeedbagClassIdClientPrefs] = 3
  77. maxItemsByClass[wire.FeedbagClassIdWatchList] = 128
  78. maxItemsByClass[wire.FeedbagClassIdIgnoreList] = 255
  79. maxItemsByClass[wire.FeedbagClassIdDateTime] = 20
  80. maxItemsByClass[wire.FeedbagClassIdExternalUser] = 200
  81. maxItemsByClass[wire.FeedbagClassIdRootCreator] = 1
  82. maxItemsByClass[wire.FeedbagClassIdImportTimestamp] = 1
  83. maxItemsByClass[wire.FeedbagClassIdBart] = 200
  84. maxItemsByClass[wire.FeedbagClassIdAlInfo] = 4
  85. return wire.SNACMessage{
  86. Frame: wire.SNACFrame{
  87. FoodGroup: wire.Feedbag,
  88. SubGroup: wire.FeedbagRightsReply,
  89. RequestID: inFrame.RequestID,
  90. },
  91. Body: wire.SNAC_0x13_0x03_FeedbagRightsReply{
  92. TLVRestBlock: wire.TLVRestBlock{
  93. TLVList: wire.TLVList{
  94. wire.NewTLVBE(wire.FeedbagRightsMaxItemAttrs, uint16(200)),
  95. wire.NewTLVBE(wire.FeedbagRightsMaxItemsByClass, maxItemsByClass),
  96. wire.NewTLVBE(wire.FeedbagRightsMaxClientItems, uint16(200)),
  97. wire.NewTLVBE(wire.FeedbagRightsMaxItemNameLen, uint16(200)),
  98. wire.NewTLVBE(wire.FeedbagRightsMaxRecentBuddies, uint16(200)),
  99. wire.NewTLVBE(wire.FeedbagRightsInteractionBuddies, uint16(200)),
  100. wire.NewTLVBE(wire.FeedbagRightsInteractionHalfLife, uint16(200)),
  101. wire.NewTLVBE(wire.FeedbagRightsInteractionMaxScore, uint16(200)),
  102. wire.NewTLVBE(wire.FeedbagRightsMaxBuddiesPerGroup, uint16(200)),
  103. wire.NewTLVBE(wire.FeedbagRightsMaxMegaBots, uint16(200)),
  104. wire.NewTLVBE(wire.FeedbagRightsMaxSmartGroups, uint16(100)),
  105. },
  106. },
  107. },
  108. }
  109. }
  110. // Query fetches the user's feedbag (aka buddy list). It returns
  111. // wire.FeedbagReply, which contains feedbag entries.
  112. func (s *FeedbagService) Query(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame) (wire.SNACMessage, error) {
  113. fb, err := s.feedbagManager.Feedbag(ctx, instance.IdentScreenName())
  114. if err != nil {
  115. return wire.SNACMessage{}, err
  116. }
  117. lm := time.UnixMilli(0)
  118. if len(fb) > 0 {
  119. lm, err = s.feedbagManager.FeedbagLastModified(ctx, instance.IdentScreenName())
  120. if err != nil {
  121. return wire.SNACMessage{}, err
  122. }
  123. }
  124. return wire.SNACMessage{
  125. Frame: wire.SNACFrame{
  126. FoodGroup: wire.Feedbag,
  127. SubGroup: wire.FeedbagReply,
  128. RequestID: inFrame.RequestID,
  129. },
  130. Body: wire.SNAC_0x13_0x06_FeedbagReply{
  131. Version: 0,
  132. Items: fb,
  133. LastUpdate: uint32(lm.Unix()),
  134. },
  135. }, nil
  136. }
  137. // QueryIfModified fetches the user's feedbag (aka buddy list). It returns
  138. // wire.FeedbagReplyNotModified if the feedbag was last modified before
  139. // inBody.LastUpdate, else return wire.FeedbagReply, which contains feedbag
  140. // entries.
  141. func (s *FeedbagService) QueryIfModified(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x05_FeedbagQueryIfModified) (wire.SNACMessage, error) {
  142. fb, err := s.feedbagManager.Feedbag(ctx, instance.IdentScreenName())
  143. if err != nil {
  144. return wire.SNACMessage{}, err
  145. }
  146. lm := time.UnixMilli(0)
  147. if len(fb) > 0 {
  148. lm, err = s.feedbagManager.FeedbagLastModified(ctx, instance.IdentScreenName())
  149. if err != nil {
  150. return wire.SNACMessage{}, err
  151. }
  152. if lm.Before(time.Unix(int64(inBody.LastUpdate), 0)) {
  153. return wire.SNACMessage{
  154. Frame: wire.SNACFrame{
  155. FoodGroup: wire.Feedbag,
  156. SubGroup: wire.FeedbagReplyNotModified,
  157. RequestID: inFrame.RequestID,
  158. },
  159. Body: wire.SNAC_0x13_0x05_FeedbagQueryIfModified{
  160. LastUpdate: uint32(lm.Unix()),
  161. Count: uint8(len(fb)),
  162. },
  163. }, nil
  164. }
  165. }
  166. return wire.SNACMessage{
  167. Frame: wire.SNACFrame{
  168. FoodGroup: wire.Feedbag,
  169. SubGroup: wire.FeedbagReply,
  170. RequestID: inFrame.RequestID,
  171. },
  172. Body: wire.SNAC_0x13_0x06_FeedbagReply{
  173. Version: 0,
  174. Items: fb,
  175. LastUpdate: uint32(lm.Unix()),
  176. },
  177. }, nil
  178. }
  179. // UpsertItem updates items in the user's feedbag (aka buddy list). Sends user
  180. // buddy arrival notifications for each online & visible buddy added to the
  181. // feedbag. Sends a buddy departure notification to blocked buddies if current
  182. // user is visible. It returns wire.FeedbagStatus, which contains insert
  183. // confirmation.
  184. // UpdateItem updates items in the user's feedbag (aka buddy list). Sends user
  185. // buddy arrival notifications for each online & visible buddy added to the
  186. // feedbag. It returns wire.FeedbagStatus, which contains update confirmation.
  187. func (s *FeedbagService) UpsertItem(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, items []wire.FeedbagItem) (*wire.SNACMessage, error) {
  188. for _, item := range items {
  189. switch item.ClassID {
  190. case wire.FeedbagClassIDDeny:
  191. // don't let users block themselves, it causes the AIM client to go
  192. // into a weird state.
  193. if state.NewIdentScreenName(item.Name) == instance.IdentScreenName() {
  194. return &wire.SNACMessage{
  195. Frame: wire.SNACFrame{
  196. FoodGroup: wire.Feedbag,
  197. SubGroup: wire.FeedbagErr,
  198. RequestID: inFrame.RequestID,
  199. },
  200. Body: wire.SNACError{
  201. Code: wire.ErrorCodeNotSupportedByHost,
  202. },
  203. }, nil
  204. }
  205. case wire.FeedbagClassIdAlInfo:
  206. // don't let users add arbitrary linked accounts. they can only be
  207. // added via the management API.
  208. return &wire.SNACMessage{
  209. Frame: wire.SNACFrame{
  210. FoodGroup: wire.Feedbag,
  211. SubGroup: wire.FeedbagErr,
  212. RequestID: inFrame.RequestID,
  213. },
  214. Body: wire.SNACError{
  215. Code: wire.ErrorCodeInsufficientRights,
  216. },
  217. }, nil
  218. }
  219. }
  220. // Check which buddy items require authorization. Items that need auth
  221. // are NOT inserted into the feedbag — the client must retry with the
  222. // FeedbagAttributesPending flag after receiving error 0x000E.
  223. var toUpsert []wire.FeedbagItem
  224. authRequired := make(map[string]bool)
  225. for _, item := range items {
  226. if item.ClassID == wire.FeedbagClassIdAlInfo {
  227. continue
  228. }
  229. if item.ClassID == wire.FeedbagClassIdBuddy {
  230. sn := state.NewIdentScreenName(item.Name)
  231. switch {
  232. case instance.UIN() != 0 && sn.UIN() == 0:
  233. // sender: icq, recipient:aim
  234. // Automatically authorize AIM users when adding to ICQ buddy
  235. // list since AIM does not support the ICQ authorization flow.
  236. if err := s.contactPreAuthorizer.RecordPreAuth(ctx, instance.IdentScreenName(), sn); err != nil {
  237. return nil, fmt.Errorf("contactPreAuthorizer.RecordPreAuth: %w", err)
  238. }
  239. case instance.UIN() != 0 && sn.UIN() != 0:
  240. // sender: icq, recipient:icq
  241. // Perform ICQ authorization flow.
  242. blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, sn, instance.IdentScreenName())
  243. if err != nil {
  244. return nil, fmt.Errorf("contactPreAuthorizer.RequiresAuthorization: %w", err)
  245. }
  246. hasPending := item.HasTag(wire.FeedbagAttributesPending)
  247. if blocked && !hasPending {
  248. authRequired[item.Name] = true
  249. continue
  250. } else if hasPending && !blocked {
  251. // Authorization has since been granted (IServerd strips
  252. // SSI_TLV_AUTH before inserting when auth is already recorded).
  253. // Clear the pending flag so the row is stored as a normal buddy.
  254. item.Remove(wire.FeedbagAttributesPending)
  255. }
  256. case instance.UIN() == 0 && sn.UIN() != 0:
  257. // sender:aim, recipient: icq
  258. // Automatically request authorization on behalf of AIM user since
  259. // AIM does not support the ICQ authorization flow. The ICQ user
  260. // will appear offline until authorization is granted.
  261. blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, sn, instance.IdentScreenName())
  262. if err != nil {
  263. return nil, fmt.Errorf("contactPreAuthorizer.RequiresAuthorization: %w", err)
  264. }
  265. if blocked {
  266. item.Append(wire.NewTLVBE(wire.FeedbagAttributesPending, []byte{}))
  267. if err := s.sendLegacyAuthReq(ctx, instance, sn, "", state.ICQBasicInfo{}, 1); err != nil {
  268. return nil, fmt.Errorf("sendLegacyAuthReq: %w", err)
  269. }
  270. }
  271. }
  272. }
  273. toUpsert = append(toUpsert, item)
  274. }
  275. if len(toUpsert) > 0 {
  276. if err := s.feedbagManager.FeedbagUpsert(ctx, instance.IdentScreenName(), toUpsert); err != nil {
  277. return nil, err
  278. }
  279. }
  280. setSessionBuddyPrefs(items, instance)
  281. snacPayloadOut := wire.SNAC_0x13_0x0E_FeedbagStatus{}
  282. for _, item := range items {
  283. if authRequired[item.Name] {
  284. snacPayloadOut.Results = append(snacPayloadOut.Results, 0x000E)
  285. } else {
  286. snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000)
  287. }
  288. }
  289. s.messageRelayer.RelayToSelf(ctx, instance, wire.SNACMessage{
  290. Frame: wire.SNACFrame{
  291. FoodGroup: wire.Feedbag,
  292. SubGroup: wire.FeedbagStatus,
  293. RequestID: inFrame.RequestID,
  294. },
  295. Body: snacPayloadOut,
  296. })
  297. s.messageRelayer.RelayToOtherInstances(ctx, instance, wire.SNACMessage{
  298. Frame: wire.SNACFrame{
  299. FoodGroup: inFrame.FoodGroup,
  300. SubGroup: inFrame.SubGroup,
  301. RequestID: wire.ReqIDFromServer,
  302. },
  303. Body: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  304. Items: toUpsert,
  305. },
  306. })
  307. var filter []state.IdentScreenName
  308. var alertAll bool
  309. for _, item := range toUpsert {
  310. switch item.ClassID {
  311. case wire.FeedbagClassIdBuddy, wire.FeedbagClassIDPermit, wire.FeedbagClassIDDeny:
  312. filter = append(filter, state.NewIdentScreenName(item.Name))
  313. case wire.FeedbagClassIdBart:
  314. if err := s.setBARTItem(ctx, instance, item); err != nil {
  315. return nil, err
  316. }
  317. case wire.FeedbagClassIdPdinfo:
  318. alertAll = true
  319. }
  320. }
  321. // send ICQ "you were added" messages
  322. for _, item := range toUpsert {
  323. if item.ClassID == wire.FeedbagClassIdBuddy && !item.HasTag(wire.FeedbagAttributesPending) {
  324. buddyScreenName := state.NewIdentScreenName(item.Name)
  325. if buddyScreenName.UIN() == 0 {
  326. continue // icq users only
  327. }
  328. // make sure this user hasn't already been notified
  329. alreadySent, err := s.buddyAddedNotifierDeduper.HasBuddyAddedNotification(ctx, instance.IdentScreenName(), buddyScreenName)
  330. if err != nil {
  331. return nil, fmt.Errorf("buddyAddedNotifierDeduper.HasBuddyAddedNotification: %w", err)
  332. }
  333. if alreadySent {
  334. continue
  335. }
  336. // they haven't been notified yet, record it for next time
  337. if err := s.buddyAddedNotifierDeduper.RecordBuddyAddedNotification(ctx, instance.IdentScreenName(), buddyScreenName); err != nil {
  338. return nil, fmt.Errorf("buddyAddedNotifierDeduper.RecordBuddyAddedNotification: %w", err)
  339. }
  340. buddySess := s.sessionRetriever.RetrieveSession(buddyScreenName)
  341. if buddySess != nil && buddySess.UsesFeedbag() {
  342. s.messageRelayer.RelayToScreenName(ctx, buddyScreenName, wire.SNACMessage{
  343. Frame: wire.SNACFrame{
  344. FoodGroup: wire.Feedbag,
  345. SubGroup: wire.FeedbagBuddyAdded,
  346. Flags: wire.SNACFlagsExtendedInfo,
  347. },
  348. Body: wire.SNAC_0x13_0x1C_FeedbagBuddyAdded{
  349. TLVLBlock: wire.TLVLBlock{
  350. TLVList: wire.TLVList{
  351. wire.NewTLVBE(wire.FeedbagTLVVersion, uint16(4)),
  352. },
  353. },
  354. ScreenName: instance.DisplayScreenName().String(),
  355. },
  356. })
  357. } else {
  358. if err := s.sendLegacyBuddyAddedMsg(ctx, instance, buddyScreenName); err != nil {
  359. return nil, fmt.Errorf("sendLegacyBuddyAddedMsg: %w", err)
  360. }
  361. }
  362. }
  363. }
  364. if alertAll || len(filter) > 0 {
  365. if instance.InNotifyTxn() {
  366. if err := instance.NotifyTxn(filter...); err != nil {
  367. return nil, fmt.Errorf("NotifyTxn: %w", err)
  368. }
  369. } else {
  370. if err := s.buddyBroadcaster.BroadcastVisibility(ctx, instance, filter, true); err != nil {
  371. return nil, err
  372. }
  373. }
  374. }
  375. return nil, nil
  376. }
  377. // setBARTItem informs clients about buddy icon update. If the BART
  378. // store doesn't have the icon, then tell the client to upload the buddy icon.
  379. // If the icon already exists, tell the user's buddies about the icon change.
  380. func (s *FeedbagService) setBARTItem(ctx context.Context, instance *state.SessionInstance, item wire.FeedbagItem) error {
  381. b, hasBuf := item.Bytes(wire.FeedbagAttributesBartInfo)
  382. if !hasBuf {
  383. return errors.New("unable to extract icon payload")
  384. }
  385. itemType, err := strconv.ParseUint(item.Name, 0, 16)
  386. if err != nil {
  387. return fmt.Errorf("invalid BART item type %q: %w", item.Name, err)
  388. }
  389. bartID := wire.BARTID{
  390. Type: uint16(itemType),
  391. }
  392. if err := wire.UnmarshalBE(&bartID.BARTInfo, bytes.NewBuffer(b)); err != nil {
  393. return err
  394. }
  395. itemExists := false
  396. if bytes.Equal(bartID.Hash, wire.GetClearIconHash()) {
  397. s.logger.DebugContext(ctx, "user is clearing icon",
  398. "hash", fmt.Sprintf("%x", bartID.Hash))
  399. itemExists = true
  400. } else {
  401. existingItem, err := s.bartItemManager.BARTItem(ctx, bartID.Hash)
  402. if err != nil {
  403. return err
  404. }
  405. itemExists = len(existingItem) > 0
  406. }
  407. if itemExists {
  408. if bartID.Type == wire.BARTTypesBuddyIconSmall || bartID.Type == wire.BARTTypesBuddyIcon {
  409. instance.Session().SetBuddyIcon(bartID)
  410. // tell buddies about the icon update
  411. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
  412. return err
  413. }
  414. }
  415. s.logger.DebugContext(ctx, "icon already exists in BART store, don't upload the icon file",
  416. "hash", fmt.Sprintf("%x", bartID.Hash))
  417. } else {
  418. // icon doesn't exist, tell the client to upload buddy icon
  419. bartID.Flags |= wire.BARTFlagsUnknown
  420. if bartID.Type == wire.BARTTypesBuddyIconSmall || bartID.Type == wire.BARTTypesBuddyIcon {
  421. instance.Session().SetBuddyIcon(bartID)
  422. }
  423. s.logger.DebugContext(ctx, "icon doesn't exist in BART store, client must upload the icon file",
  424. "hash", fmt.Sprintf("%x", bartID.Hash))
  425. }
  426. s.messageRelayer.RelayToSelf(ctx, instance, wire.SNACMessage{
  427. Frame: wire.SNACFrame{
  428. FoodGroup: wire.OService,
  429. SubGroup: wire.OServiceBartReply,
  430. },
  431. Body: wire.SNAC_0x01_0x21_OServiceBARTReply{
  432. BARTID: bartID,
  433. },
  434. })
  435. if bartID.Type == wire.BARTTypesBuddyIconSmall || bartID.Type == wire.BARTTypesBuddyIcon {
  436. s.messageRelayer.RelayToScreenName(ctx, instance.IdentScreenName(), wire.SNACMessage{
  437. Frame: wire.SNACFrame{
  438. FoodGroup: wire.OService,
  439. SubGroup: wire.OServiceUserInfoUpdate,
  440. },
  441. Body: newOServiceUserInfoUpdate(instance),
  442. })
  443. }
  444. return nil
  445. }
  446. // DeleteItem removes items from feedbag (aka buddy list). Sends user buddy
  447. // arrival notifications for each online & visible buddy added to the feedbag.
  448. // Sends buddy arrival notifications to each unblocked buddy if current user is
  449. // visible. It returns wire.FeedbagStatus, which contains update confirmation.
  450. func (s *FeedbagService) DeleteItem(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x0A_FeedbagDeleteItem) (*wire.SNACMessage, error) {
  451. if err := s.feedbagManager.FeedbagDelete(ctx, instance.IdentScreenName(), inBody.Items); err != nil {
  452. return nil, err
  453. }
  454. snacPayloadOut := wire.SNAC_0x13_0x0E_FeedbagStatus{}
  455. for range inBody.Items {
  456. snacPayloadOut.Results = append(snacPayloadOut.Results, 0x0000) // success by default
  457. }
  458. s.messageRelayer.RelayToSelf(ctx, instance, wire.SNACMessage{
  459. Frame: wire.SNACFrame{
  460. FoodGroup: wire.Feedbag,
  461. SubGroup: wire.FeedbagStatus,
  462. RequestID: inFrame.RequestID,
  463. },
  464. Body: snacPayloadOut,
  465. })
  466. s.messageRelayer.RelayToOtherInstances(ctx, instance, wire.SNACMessage{
  467. Frame: wire.SNACFrame{
  468. FoodGroup: inFrame.FoodGroup,
  469. SubGroup: inFrame.SubGroup,
  470. RequestID: wire.ReqIDFromServer,
  471. },
  472. Body: inBody,
  473. })
  474. var filter []state.IdentScreenName
  475. for _, item := range inBody.Items {
  476. switch item.ClassID {
  477. case wire.FeedbagClassIdBuddy, wire.FeedbagClassIDDeny, wire.FeedbagClassIDPermit:
  478. filter = append(filter, state.NewIdentScreenName(item.Name))
  479. }
  480. }
  481. if instance.InNotifyTxn() {
  482. if err := instance.NotifyTxn(filter...); err != nil {
  483. return nil, fmt.Errorf("NotifyTxn: %w", err)
  484. }
  485. } else {
  486. if err := s.buddyBroadcaster.BroadcastVisibility(ctx, instance, filter, true); err != nil {
  487. return nil, err
  488. }
  489. }
  490. return nil, nil
  491. }
  492. // StartCluster signals the beginning of a batch of feedbag operations that clients should
  493. // process together to prevent UI flicker during rapid updates. It transmits the start message
  494. // to other session instances. It starts a new notification transaction, which records users
  495. // that should receive presence notifications when the transaction ends.
  496. func (s *FeedbagService) StartCluster(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x11_FeedbagStartCluster) {
  497. instance.BeginNotifyTxn()
  498. s.messageRelayer.RelayToOtherInstances(ctx, instance, wire.SNACMessage{
  499. Frame: inFrame,
  500. Body: inBody,
  501. })
  502. }
  503. // EndCluster signals the completion of a batched feedbag operation group. It transmits the end
  504. // message to other session instances. It broadcast presence notifications if any relevant
  505. // changes accumulated during the transaction.
  506. func (s *FeedbagService) EndCluster(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame) error {
  507. s.messageRelayer.RelayToOtherInstances(ctx, instance, wire.SNACMessage{
  508. Frame: inFrame,
  509. Body: wire.SNAC_0x13_0x12_FeedbagEndCluster{},
  510. })
  511. shouldNotify, filter := instance.EndNotifyTxn()
  512. if !shouldNotify {
  513. return nil
  514. }
  515. if err := s.buddyBroadcaster.BroadcastVisibility(ctx, instance, filter, true); err != nil {
  516. return err
  517. }
  518. return nil
  519. }
  520. // Use activates server-side buddy list state for feedbag clients at sign-on.
  521. // FeedbagUse and ClientOnline can arrive in either order; whichever handler runs
  522. // second performs the initial buddy presence broadcast when both are satisfied.
  523. func (s *FeedbagService) Use(ctx context.Context, instance *state.SessionInstance) error {
  524. if err := s.feedbagManager.UseFeedbag(ctx, instance.IdentScreenName()); err != nil {
  525. return fmt.Errorf("could not use feedbag: %w", err)
  526. }
  527. items, err := s.feedbagManager.Feedbag(ctx, instance.IdentScreenName())
  528. if err != nil {
  529. return fmt.Errorf("feedbagManager.Feedbag: %w", err)
  530. }
  531. setSessionBuddyPrefs(items, instance)
  532. instance.Session().SetUsesFeedbag()
  533. instance.SetContactsInit()
  534. // ICQ Lite order: ClientOnline before FeedbagUse — broadcast here.
  535. if instance.SignonComplete() {
  536. if err := s.buddyBroadcaster.BroadcastVisibility(ctx, instance, nil, false); err != nil {
  537. return fmt.Errorf("buddyBroadcaster.BroadcastVisibility: %w", err)
  538. }
  539. }
  540. return nil
  541. }
  542. // RequestAuthorizeToHost forwards an authorization request from the user who
  543. // wants to add a contact to the contact being added. If the recipient session
  544. // uses feedbag this sign-on, they receive SNAC(0x13,0x19); otherwise a
  545. // synthetic SNAC(0x04,0x06) ICBM channel message to host (ICQ ch4 auth req)
  546. // is relayed to the recipient via messageRelayer.
  547. func (s *FeedbagService) RequestAuthorizeToHost(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x18_FeedbagRequestAuthorizationToHost) error {
  548. recipient := state.NewIdentScreenName(inBody.ScreenName)
  549. recipSess := s.sessionRetriever.RetrieveSession(recipient)
  550. useFeedbag := recipSess != nil && recipSess.UsesFeedbag()
  551. if useFeedbag {
  552. firstDot := strings.IndexByte(inBody.Reason, '.')
  553. if firstDot >= 0 {
  554. // ICQ 5 ignores authorization requests with multiple periods. Strip
  555. // all periods following the first so that the message arrives.
  556. inBody.Reason = inBody.Reason[:firstDot+1] + strings.ReplaceAll(inBody.Reason[firstDot+1:], ".", "")
  557. }
  558. s.messageRelayer.RelayToScreenName(ctx, recipient, wire.SNACMessage{
  559. Frame: wire.SNACFrame{
  560. FoodGroup: wire.Feedbag,
  561. SubGroup: wire.FeedbagRequestAuthorizeToClient,
  562. Flags: wire.SNACFlagsExtendedInfo,
  563. },
  564. Body: wire.SNAC_0x13_0x19_FeedbagRequestAuthorizeToClient{
  565. TLVLBlock: wire.TLVLBlock{
  566. TLVList: wire.TLVList{
  567. wire.NewTLVBE(wire.FeedbagTLVVersion, uint16(2)),
  568. },
  569. },
  570. ScreenName: instance.IdentScreenName().String(),
  571. Reason: inBody.Reason,
  572. },
  573. })
  574. return nil
  575. }
  576. // if authorized, the recipient can reciprocate without requesting authorization
  577. authorized := 1
  578. blocked, err := s.contactPreAuthorizer.RequiresAuthorization(ctx, instance.IdentScreenName(), recipient)
  579. if err != nil {
  580. return fmt.Errorf("contactPreAuthorizer.RequiresAuthorization: %w", err)
  581. }
  582. if blocked {
  583. authorized = 0
  584. }
  585. var userInfo state.User
  586. if user, err := s.userManager.User(ctx, instance.IdentScreenName()); err != nil {
  587. return fmt.Errorf("userManager.User: %w", err)
  588. } else if user != nil {
  589. userInfo = *user
  590. } else {
  591. s.logger.ErrorContext(ctx, "user not found", "screen_name", instance.IdentScreenName())
  592. }
  593. return s.sendLegacyAuthReq(ctx, instance, recipient, inBody.Reason, userInfo.ICQInfo.Basic, authorized)
  594. }
  595. // sendLegacyAuthReq sends an offline authorization request to ICQ client
  596. func (s *FeedbagService) sendLegacyAuthReq(ctx context.Context, from *state.SessionInstance, to state.IdentScreenName, reason string, userInfo state.ICQBasicInfo, authorized int) error {
  597. frame := wire.SNACFrame{
  598. FoodGroup: wire.ICBM,
  599. SubGroup: wire.ICBMChannelMsgToHost,
  600. }
  601. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  602. ChannelID: wire.ICBMChannelICQ,
  603. ScreenName: to.String(),
  604. TLVRestBlock: wire.TLVRestBlock{
  605. TLVList: wire.TLVList{
  606. wire.NewTLVLE(wire.ICBMTLVData, wire.ICBMCh4Message{
  607. UIN: from.UIN(),
  608. MessageType: wire.ICBMMsgTypeAuthReq,
  609. Message: fmt.Sprintf("%s\xFE%s\xFE%s\xFE%s\xFE%d\xFE%s",
  610. userInfo.Nickname,
  611. userInfo.FirstName,
  612. userInfo.LastName,
  613. userInfo.EmailAddress,
  614. authorized,
  615. utf8ToLatin1(reason)),
  616. }),
  617. wire.NewTLVBE(wire.ICBMTLVStore, []byte{}),
  618. },
  619. },
  620. }
  621. if _, err := s.icbmSender(ctx, from, frame, snac); err != nil {
  622. return fmt.Errorf("icbmSender: %w", err)
  623. }
  624. return nil
  625. }
  626. // PreAuthorizeBuddy handles SNAC(0x13,0x14) FEEDBAG__PRE_AUTHORIZE_BUDDY. The client
  627. // pre-authorizes a buddy they just added so that user may add them later without
  628. // an authorization prompt. Persists a contactPreauth row and may notify the buddy:
  629. // SNAC(0x13,0x15) for online feedbag clients who do not block the sender; otherwise
  630. // an ICQ channel-4 ICBM (auth granted) for offline or non-feedbag clients.
  631. func (s *FeedbagService) PreAuthorizeBuddy(ctx context.Context, instance *state.SessionInstance, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x14_FeedbagPreAuthorizeBuddy) (*wire.SNACMessage, error) {
  632. buddy := state.NewIdentScreenName(inBody.ScreenName)
  633. if buddy == instance.IdentScreenName() {
  634. return &wire.SNACMessage{
  635. Frame: wire.SNACFrame{
  636. FoodGroup: wire.Feedbag,
  637. SubGroup: wire.FeedbagErr,
  638. RequestID: inFrame.RequestID,
  639. },
  640. Body: wire.SNACError{
  641. Code: wire.ErrorCodeNotSupportedByHost,
  642. },
  643. }, nil
  644. }
  645. if err := s.authorizeContact(ctx, instance.IdentScreenName(), state.NewIdentScreenName(inBody.ScreenName), inBody.Message); err != nil {
  646. return nil, fmt.Errorf("s.authorizeContact: %w", err)
  647. }
  648. return nil, nil
  649. }
  650. func utf8ToLatin1(s string) string {
  651. if isASCII(s) {
  652. return s
  653. }
  654. var result []byte
  655. for _, r := range s {
  656. if r <= 0xFF {
  657. result = append(result, byte(r))
  658. } else {
  659. result = append(result, '?')
  660. }
  661. }
  662. return string(result)
  663. }
  664. func isASCII(s string) bool {
  665. for i := 0; i < len(s); i++ {
  666. if s[i] > 0x7F {
  667. return false
  668. }
  669. }
  670. return true
  671. }
  672. func (s *FeedbagService) authorizeContact(ctx context.Context, granter state.IdentScreenName, requester state.IdentScreenName, message string) error {
  673. requesterSess := s.sessionRetriever.RetrieveSession(requester)
  674. if err := s.contactPreAuthorizer.RecordPreAuth(ctx, granter, requester); err != nil {
  675. if errors.Is(err, state.ErrNoUser) {
  676. s.logger.DebugContext(ctx, "user not found", "name", requester.String())
  677. return nil
  678. }
  679. return fmt.Errorf("RecordPreAuth: %w", err)
  680. }
  681. if requesterSess == nil {
  682. if err := s.sendLegacyAuthMsg(ctx, granter, requester); err != nil {
  683. return fmt.Errorf("sendLegacyAuthMsg: %w", err)
  684. }
  685. return nil
  686. }
  687. if cleared, err := s.clearPendingAuth(ctx, granter, requester); err != nil {
  688. return fmt.Errorf("clearPendingAuth: %w", err)
  689. } else if cleared {
  690. return nil
  691. }
  692. if requesterSess.UsesFeedbag() {
  693. rel, err := s.relationshipFetcher.Relationship(ctx, granter, requester)
  694. if err != nil {
  695. return fmt.Errorf("relationshipFetcher.Relationship: %w", err)
  696. }
  697. if rel.BlocksYou {
  698. return nil
  699. }
  700. s.messageRelayer.RelayToScreenName(ctx, requester, wire.SNACMessage{
  701. Frame: wire.SNACFrame{
  702. FoodGroup: wire.Feedbag,
  703. SubGroup: wire.FeedbagPreAuthorizedBuddy,
  704. },
  705. Body: wire.SNAC_0x13_0x15_FeedbagPreAuthorizedBuddy{
  706. ScreenName: granter.String(),
  707. Message: message,
  708. Flags: 0,
  709. },
  710. })
  711. return nil
  712. }
  713. // send legacy ICQ authorization message
  714. if err := s.sendLegacyAuthMsg(ctx, granter, requester); err != nil {
  715. return fmt.Errorf("sendLegacyAuthMsg: %w", err)
  716. }
  717. return nil
  718. }
  719. func (s *FeedbagService) sendLegacyAuthMsg(ctx context.Context, from state.IdentScreenName, to state.IdentScreenName) error {
  720. // send offline "auth granted" message
  721. frame := wire.SNACFrame{FoodGroup: wire.ICBM, SubGroup: wire.ICBMChannelMsgToHost}
  722. body := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  723. Cookie: uint64(time.Now().UnixNano()),
  724. ChannelID: wire.ICBMChannelICQ,
  725. ScreenName: to.String(),
  726. TLVRestBlock: wire.TLVRestBlock{
  727. TLVList: wire.TLVList{
  728. wire.NewTLVLE(wire.ICBMTLVData, wire.ICBMCh4Message{
  729. UIN: from.UIN(),
  730. MessageType: wire.ICBMMsgTypeAuthOK,
  731. }),
  732. wire.NewTLVBE(wire.ICBMTLVStore, []byte{}),
  733. },
  734. },
  735. }
  736. fromSess := state.NewSession()
  737. fromSess.SetIdentScreenName(from)
  738. fromSess.SetDisplayScreenName(state.DisplayScreenName(from.String()))
  739. if _, err := s.icbmSender(ctx, fromSess.AddInstance(), frame, body); err != nil {
  740. return err
  741. }
  742. return nil
  743. }
  744. // sendLegacyBuddyAddedMsg notifies from via a legacy ICQ channel-4 message
  745. // that to has added them to their contact list.
  746. func (s *FeedbagService) sendLegacyBuddyAddedMsg(ctx context.Context, from *state.SessionInstance, to state.IdentScreenName) error {
  747. frame := wire.SNACFrame{FoodGroup: wire.ICBM, SubGroup: wire.ICBMChannelMsgToHost}
  748. body := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  749. Cookie: uint64(time.Now().UnixNano()),
  750. ChannelID: wire.ICBMChannelICQ,
  751. ScreenName: to.String(),
  752. TLVRestBlock: wire.TLVRestBlock{
  753. TLVList: wire.TLVList{
  754. wire.NewTLVLE(wire.ICBMTLVData, wire.ICBMCh4Message{
  755. UIN: from.UIN(),
  756. MessageType: wire.ICBMMsgTypeAdded,
  757. }),
  758. wire.NewTLVBE(wire.ICBMTLVStore, []byte{}),
  759. },
  760. },
  761. }
  762. if _, err := s.icbmSender(ctx, from, frame, body); err != nil {
  763. return err
  764. }
  765. return nil
  766. }
  767. func (s *FeedbagService) clearPendingAuth(ctx context.Context, granter state.IdentScreenName, requester state.IdentScreenName) (bool, error) {
  768. items, err := s.feedbagManager.Feedbag(ctx, requester)
  769. if err != nil {
  770. return false, fmt.Errorf("failed to fetch feedbag items: %w", err)
  771. }
  772. // look for the pending buddy authorization
  773. var buddyItem *wire.FeedbagItem
  774. for _, item := range items {
  775. if item.ClassID == wire.FeedbagClassIdBuddy && item.Name == granter.String() {
  776. buddyItem = &item
  777. break
  778. }
  779. }
  780. // pending buddy authorization is not found, nothing to do
  781. if buddyItem == nil || !buddyItem.HasTag(wire.FeedbagAttributesPending) {
  782. return false, nil
  783. }
  784. // remove the pending buddy authorization tag
  785. buddyItem.Remove(wire.FeedbagAttributesPending)
  786. updates := []wire.FeedbagItem{*buddyItem}
  787. if err = s.feedbagManager.FeedbagUpsert(ctx, requester, updates); err != nil {
  788. return false, fmt.Errorf("failed to update feedbag: %w", err)
  789. }
  790. // send a "you were added" message to the granter
  791. granterSess := s.sessionRetriever.RetrieveSession(granter)
  792. if granterSess != nil && granterSess.UsesFeedbag() {
  793. s.messageRelayer.RelayToScreenName(ctx, granter, wire.SNACMessage{
  794. Frame: wire.SNACFrame{
  795. FoodGroup: wire.Feedbag,
  796. SubGroup: wire.FeedbagBuddyAdded,
  797. Flags: wire.SNACFlagsExtendedInfo,
  798. },
  799. Body: wire.SNAC_0x13_0x1C_FeedbagBuddyAdded{
  800. TLVLBlock: wire.TLVLBlock{
  801. TLVList: wire.TLVList{
  802. wire.NewTLVBE(wire.FeedbagTLVVersion, uint16(4)),
  803. },
  804. },
  805. ScreenName: requester.String(),
  806. },
  807. })
  808. } else {
  809. requesterSess := state.NewSession()
  810. requesterSess.SetIdentScreenName(requester)
  811. requesterSess.SetDisplayScreenName(state.DisplayScreenName(requester.String()))
  812. requesterSess.SetUIN(requester.UIN())
  813. if err := s.sendLegacyBuddyAddedMsg(ctx, requesterSess.AddInstance(), granter); err != nil {
  814. return false, fmt.Errorf("sendLegacyBuddyAddedMsg: %w", err)
  815. }
  816. }
  817. // clear the pending flag on the requester's buddy entry
  818. s.messageRelayer.RelayToScreenName(ctx, requester, wire.SNACMessage{
  819. Frame: wire.SNACFrame{
  820. FoodGroup: wire.Feedbag,
  821. SubGroup: wire.FeedbagUpdateItem,
  822. },
  823. Body: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  824. Items: updates,
  825. },
  826. })
  827. // tell the requester that we're friends
  828. s.messageRelayer.RelayToScreenName(ctx, requester, wire.SNACMessage{
  829. Frame: wire.SNACFrame{
  830. FoodGroup: wire.Feedbag,
  831. SubGroup: wire.FeedbagRespondAuthorizeToClient,
  832. Flags: wire.SNACFlagsExtendedInfo,
  833. },
  834. Body: wire.SNAC_0x13_0x1B_FeedbagRespondAuthorizeToClient{
  835. TLVLBlock: wire.TLVLBlock{
  836. TLVList: wire.TLVList{
  837. wire.NewTLVBE(wire.FeedbagTLVVersion, uint16(2)),
  838. },
  839. },
  840. ScreenName: granter.String(),
  841. Accepted: 1,
  842. },
  843. })
  844. if granterSess != nil {
  845. instances := granterSess.Instances()
  846. if len(instances) > 0 {
  847. // tell the granter that we're online
  848. if err := s.buddyBroadcaster.BroadcastVisibility(ctx, instances[0], []state.IdentScreenName{requester}, false); err != nil {
  849. s.logger.ErrorContext(ctx, "broadcastBuddyArrived failed", "err", err)
  850. }
  851. }
  852. }
  853. return true, nil
  854. }
  855. // RespondAuthorizeToHost forwards an authorization response from the user
  856. // whose authorization was requested (granter) to the user who made the
  857. // authorization request (requester).
  858. func (s *FeedbagService) RespondAuthorizeToHost(ctx context.Context, instance state.IdentScreenName, inFrame wire.SNACFrame, inBody wire.SNAC_0x13_0x1A_FeedbagRespondAuthorizeToHost) error {
  859. switch inBody.Accepted {
  860. case 0:
  861. err := s.rejectContact(ctx, instance, state.NewIdentScreenName(inBody.ScreenName), inBody.Reason)
  862. if err != nil {
  863. return err
  864. }
  865. case 1:
  866. if err := s.authorizeContact(ctx, instance, state.NewIdentScreenName(inBody.ScreenName), inBody.Reason); err != nil {
  867. return fmt.Errorf("s.authorizeContact: %w", err)
  868. }
  869. default:
  870. return fmt.Errorf("invalid accepted flag %d", inBody.Accepted)
  871. }
  872. return nil
  873. }
  874. func (s *FeedbagService) rejectContact(ctx context.Context, rejecter state.IdentScreenName, requester state.IdentScreenName, reason string) error {
  875. if toSess := s.sessionRetriever.RetrieveSession(requester); toSess != nil && toSess.UsesFeedbag() {
  876. s.messageRelayer.RelayToScreenName(ctx, requester, wire.SNACMessage{
  877. Frame: wire.SNACFrame{
  878. FoodGroup: wire.Feedbag,
  879. SubGroup: wire.FeedbagRespondAuthorizeToClient,
  880. Flags: wire.SNACFlagsExtendedInfo,
  881. },
  882. Body: wire.SNAC_0x13_0x1B_FeedbagRespondAuthorizeToClient{
  883. TLVLBlock: wire.TLVLBlock{
  884. TLVList: wire.TLVList{
  885. wire.NewTLVBE(wire.FeedbagTLVVersion, uint16(2)),
  886. },
  887. },
  888. ScreenName: rejecter.String(),
  889. Accepted: 0,
  890. Reason: reason,
  891. },
  892. })
  893. } else {
  894. frame := wire.SNACFrame{
  895. FoodGroup: wire.ICBM,
  896. SubGroup: wire.ICBMChannelMsgToHost,
  897. }
  898. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  899. ChannelID: wire.ICBMChannelICQ,
  900. ScreenName: requester.String(),
  901. TLVRestBlock: wire.TLVRestBlock{
  902. TLVList: wire.TLVList{
  903. wire.NewTLVLE(wire.ICBMTLVData, wire.ICBMCh4Message{
  904. UIN: rejecter.UIN(),
  905. MessageType: wire.ICBMMsgTypeAuthDeny,
  906. Message: reason,
  907. }),
  908. wire.NewTLVBE(wire.ICBMTLVStore, []byte{}),
  909. },
  910. },
  911. }
  912. fromSess := state.NewSession()
  913. fromSess.SetIdentScreenName(rejecter)
  914. fromSess.SetDisplayScreenName(state.DisplayScreenName(rejecter.String()))
  915. if _, err := s.icbmSender(ctx, fromSess.AddInstance(), frame, snac); err != nil {
  916. return fmt.Errorf("could not send ICBM message: %w", err)
  917. }
  918. }
  919. return nil
  920. }
  921. // setSessionBuddyPrefs sets session preferences based on the feedbag buddy prefs item, if present.
  922. func setSessionBuddyPrefs(items []wire.FeedbagItem, instance *state.SessionInstance) {
  923. for _, item := range items {
  924. if item.ClassID == wire.FeedbagClassIdBuddyPrefs {
  925. wantsTyping := wire.BuddyPref(item.TLVList, wire.FeedbagBuddyPrefsDiscloseTyping)
  926. instance.Session().SetTypingEventsEnabled(wantsTyping)
  927. break
  928. }
  929. }
  930. }
  931. // ForwardICQAuthEvents converts ICQ channel-4 payloads to feedbag SNACs and
  932. // sends them to feedbag-enabled recipient.
  933. func (s *FeedbagService) ForwardICQAuthEvents(ctx context.Context, sender state.IdentScreenName, recipient state.IdentScreenName, authMsg wire.ICBMCh4Message) error {
  934. switch authMsg.MessageType {
  935. case wire.ICBMMsgTypeAuthOK:
  936. msg := wire.SNAC_0x13_0x1A_FeedbagRespondAuthorizeToHost{
  937. ScreenName: recipient.String(),
  938. Accepted: 1,
  939. Reason: authMsg.Message,
  940. }
  941. return s.RespondAuthorizeToHost(ctx, sender, wire.SNACFrame{}, msg)
  942. case wire.ICBMMsgTypeAuthDeny:
  943. msg := wire.SNAC_0x13_0x1A_FeedbagRespondAuthorizeToHost{
  944. ScreenName: recipient.String(),
  945. Accepted: 0,
  946. Reason: authMsg.Message,
  947. }
  948. return s.RespondAuthorizeToHost(ctx, sender, wire.SNACFrame{}, msg)
  949. case wire.ICBMMsgTypeAdded:
  950. s.messageRelayer.RelayToScreenName(ctx, recipient, wire.SNACMessage{
  951. Frame: wire.SNACFrame{
  952. FoodGroup: wire.Feedbag,
  953. SubGroup: wire.FeedbagBuddyAdded,
  954. Flags: wire.SNACFlagsExtendedInfo,
  955. },
  956. Body: wire.SNAC_0x13_0x1C_FeedbagBuddyAdded{
  957. TLVLBlock: wire.TLVLBlock{
  958. TLVList: wire.TLVList{
  959. wire.NewTLVBE(wire.FeedbagTLVVersion, uint16(4)),
  960. },
  961. },
  962. ScreenName: sender.String(),
  963. },
  964. })
  965. case wire.ICBMMsgTypeAuthReq:
  966. reasonText := ""
  967. parts := strings.Split(authMsg.Message, "\xFE")
  968. if len(parts) >= 6 {
  969. reasonText = parts[5]
  970. }
  971. s.messageRelayer.RelayToScreenName(ctx, recipient, wire.SNACMessage{
  972. Frame: wire.SNACFrame{
  973. FoodGroup: wire.Feedbag,
  974. SubGroup: wire.FeedbagRequestAuthorizeToClient,
  975. Flags: wire.SNACFlagsExtendedInfo,
  976. },
  977. Body: wire.SNAC_0x13_0x19_FeedbagRequestAuthorizeToClient{
  978. TLVLBlock: wire.TLVLBlock{
  979. TLVList: wire.TLVList{
  980. wire.NewTLVBE(wire.FeedbagTLVVersion, uint16(2)),
  981. },
  982. },
  983. ScreenName: sender.String(),
  984. Reason: reasonText,
  985. },
  986. })
  987. default:
  988. s.logger.WarnContext(ctx, "unknown authMsg ICBM message type", "type", authMsg.MessageType)
  989. }
  990. return nil
  991. }