feedbag_list.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. package state
  2. import (
  3. "errors"
  4. "fmt"
  5. "math"
  6. "slices"
  7. "strconv"
  8. "github.com/mk6i/open-oscar-server/wire"
  9. )
  10. // MaxLinkedScreenNames is the most linked screen names a feedbag may hold, as
  11. // advertised to clients in the feedbag rights reply.
  12. const MaxLinkedScreenNames = 4
  13. // ErrGroupNotFound is returned when a feedbag group cannot be found.
  14. var ErrGroupNotFound = errors.New("group not found")
  15. // ErrGroupExists is returned when a feedbag group cannot be created or renamed
  16. // because another group already has the target name.
  17. var ErrGroupExists = errors.New("group already exists")
  18. // ErrBuddyNotFound is returned when a feedbag buddy cannot be found.
  19. var ErrBuddyNotFound = errors.New("buddy not found")
  20. // FeedbagList provides operations for manipulating a collection of feedbag
  21. // items. It supports lookups by class/name/group, item insertion with
  22. // automatic ID generation, and transparent root group management.
  23. type FeedbagList struct {
  24. items []*wire.FeedbagItem
  25. randInt func(int) int
  26. pendingUpdates []*wire.FeedbagItem
  27. pendingDeletes []*wire.FeedbagItem
  28. }
  29. // NewFeedbagList creates a FeedbagList from the given items. The randInt
  30. // function is used for generating unique item/group IDs; inject a
  31. // deterministic function in tests to assert exact feedbag item slices.
  32. func NewFeedbagList(items []wire.FeedbagItem, randInt func(int) int) *FeedbagList {
  33. ptrs := make([]*wire.FeedbagItem, len(items))
  34. for i := range items {
  35. ptrs[i] = &items[i]
  36. }
  37. return &FeedbagList{
  38. items: ptrs,
  39. randInt: randInt,
  40. }
  41. }
  42. // SetMode upserts the permit/deny mode item.
  43. func (f *FeedbagList) SetMode(mode uint8) {
  44. f.upsertItem(wire.FeedbagItem{
  45. ClassID: wire.FeedbagClassIdPdinfo,
  46. TLVLBlock: wire.TLVLBlock{
  47. TLVList: wire.TLVList{
  48. wire.NewTLVBE(wire.FeedbagAttributesPdMode, mode),
  49. },
  50. },
  51. })
  52. }
  53. // SetIcon upserts the BART reference item that points at the user's buddy
  54. // icon for the given BART type. Returns the stored item and true if a new item
  55. // was inserted, or the existing item and false if it was updated in place.
  56. func (f *FeedbagList) SetIcon(bartType uint16, hash []byte) (wire.FeedbagItem, bool) {
  57. return f.upsertItem(wire.FeedbagItem{
  58. ClassID: wire.FeedbagClassIdBart,
  59. // The item is keyed by BART type, held as its decimal string — the
  60. // shape BuddyIconMetadata queries for.
  61. Name: strconv.FormatUint(uint64(bartType), 10),
  62. TLVLBlock: wire.TLVLBlock{
  63. TLVList: wire.TLVList{
  64. wire.NewTLVBE(wire.FeedbagAttributesBartInfo, wire.BARTInfo{
  65. Flags: wire.BARTFlagsCustom,
  66. Hash: hash,
  67. }),
  68. },
  69. },
  70. })
  71. }
  72. // AddGroup returns the existing group with the given name or creates a new
  73. // one with an auto-generated GroupID. When a new group is created, the root
  74. // group's order TLV is updated to include it. Call PendingUpdates to retrieve
  75. // new or modified items for persistence. Returns the group item.
  76. func (f *FeedbagList) AddGroup(name string) wire.FeedbagItem {
  77. if g := f.groupByName(name); g != nil {
  78. return *g
  79. }
  80. group := &wire.FeedbagItem{
  81. ClassID: wire.FeedbagClassIdGroup,
  82. Name: name,
  83. GroupID: f.genID(),
  84. }
  85. f.items = append(f.items, group)
  86. root := f.rootGroup()
  87. root.AppendOrderMembers(group.GroupID)
  88. f.trackUpdate(root)
  89. f.trackUpdate(group)
  90. return *group
  91. }
  92. // DeleteGroup marks a group item for deletion by name. If the group exists
  93. // and is not the root group, the root group's order TLV is updated.
  94. func (f *FeedbagList) DeleteGroup(groupName string) {
  95. groupItem := f.groupByName(groupName)
  96. if groupItem == nil {
  97. return
  98. }
  99. var toDelete []*wire.FeedbagItem
  100. for _, item := range f.items {
  101. if item.GroupID == groupItem.GroupID {
  102. toDelete = append(toDelete, item)
  103. }
  104. }
  105. for _, item := range toDelete {
  106. f.deleteItem(*item)
  107. }
  108. if len(toDelete) > 0 && groupItem.GroupID != 0 {
  109. for _, item := range f.items {
  110. if item.ClassID == wire.FeedbagClassIdGroup && item.GroupID == 0 {
  111. item.RemoveOrderMembers(groupItem.GroupID)
  112. f.trackUpdate(item)
  113. }
  114. }
  115. }
  116. }
  117. // AddBuddy upserts a buddy item in the given group (by name), optionally
  118. // attaching alias and note attributes. Returns true if a new buddy was inserted.
  119. func (f *FeedbagList) AddBuddy(groupName, screenName, alias, note string) (bool, error) {
  120. var attrs wire.TLVList
  121. if alias != "" {
  122. attrs.Append(wire.NewTLVBE(wire.FeedbagAttributesAlias, alias))
  123. }
  124. if note != "" {
  125. attrs.Append(wire.NewTLVBE(wire.FeedbagAttributesNote, note))
  126. }
  127. return f.addBuddyItem(groupName, screenName, attrs)
  128. }
  129. // addBuddyItem upserts a buddy item carrying the given attribute TLVs into the
  130. // named group. Returns true if a new buddy was inserted.
  131. func (f *FeedbagList) addBuddyItem(groupName, screenName string, attrs wire.TLVList) (bool, error) {
  132. group := f.groupByName(groupName)
  133. if group == nil {
  134. return false, fmt.Errorf("group %q not found", groupName)
  135. }
  136. item := wire.FeedbagItem{
  137. ClassID: wire.FeedbagClassIdBuddy,
  138. GroupID: group.GroupID,
  139. Name: screenName,
  140. }
  141. item.TLVList = attrs
  142. result, inserted := f.upsertItem(item)
  143. if inserted {
  144. group.AppendOrderMembers(result.ItemID)
  145. f.trackUpdate(group)
  146. }
  147. return inserted, nil
  148. }
  149. // DeleteBuddy marks a buddy item for deletion in the given group (by name).
  150. // The parent group's order TLV is updated to remove the buddy.
  151. // Pass "*" as groupName to remove the buddy from all groups.
  152. func (f *FeedbagList) DeleteBuddy(groupName, buddyName string) error {
  153. var groups []*wire.FeedbagItem
  154. if groupName == "*" {
  155. // delete from all groups
  156. for _, item := range f.items {
  157. if item.ClassID == wire.FeedbagClassIdGroup && item.GroupID != 0 {
  158. groups = append(groups, item)
  159. }
  160. }
  161. } else {
  162. group := f.groupByName(groupName)
  163. if group == nil {
  164. return fmt.Errorf("%w: %q", ErrGroupNotFound, groupName)
  165. }
  166. groups = []*wire.FeedbagItem{group}
  167. }
  168. for _, group := range groups {
  169. deleted, found := f.deleteItem(wire.FeedbagItem{
  170. ClassID: wire.FeedbagClassIdBuddy,
  171. GroupID: group.GroupID,
  172. Name: buddyName,
  173. })
  174. if found {
  175. group.RemoveOrderMembers(deleted.ItemID)
  176. f.trackUpdate(group)
  177. }
  178. }
  179. return nil
  180. }
  181. // RenameGroup changes a group's name in place, preserving its GroupID and
  182. // ItemID. Returns ErrGroupNotFound if oldName does not exist, or ErrGroupExists
  183. // if a different group already uses newName. The group is renamed in place
  184. // rather than re-inserted because upsertItem keys groups on name and would
  185. // otherwise create a duplicate.
  186. func (f *FeedbagList) RenameGroup(oldName, newName string) error {
  187. group := f.groupByName(oldName)
  188. if group == nil {
  189. return fmt.Errorf("%w: %q", ErrGroupNotFound, oldName)
  190. }
  191. if newName != oldName && f.groupByName(newName) != nil {
  192. return fmt.Errorf("%w: %q", ErrGroupExists, newName)
  193. }
  194. if group.Name == newName {
  195. return nil
  196. }
  197. group.Name = newName
  198. f.trackUpdate(group)
  199. return nil
  200. }
  201. // MoveBuddy moves a buddy between groups and/or repositions it within a group's
  202. // order. When toGroup names a different group than fromGroup, the buddy's
  203. // feedbag item is deleted from the source group and re-inserted into the
  204. // destination (a buddy's identity includes its GroupID at the protocol level),
  205. // carrying over all of its attribute TLVs. When beforeBuddy is non-empty,
  206. // the buddy is positioned immediately before that buddy in the destination
  207. // group's order; otherwise it is appended. Returns ErrGroupNotFound or
  208. // ErrBuddyNotFound if the source group/buddy or destination group is missing.
  209. func (f *FeedbagList) MoveBuddy(fromGroup, toGroup, buddyName, beforeBuddy string) error {
  210. src := f.groupByName(fromGroup)
  211. if src == nil {
  212. return fmt.Errorf("%w: %q", ErrGroupNotFound, fromGroup)
  213. }
  214. srcBuddy := f.buddyItem(src, buddyName)
  215. if srcBuddy == nil {
  216. return fmt.Errorf("%w: %q", ErrBuddyNotFound, buddyName)
  217. }
  218. dst := src
  219. if toGroup != "" && toGroup != fromGroup {
  220. dst = f.groupByName(toGroup)
  221. if dst == nil {
  222. return fmt.Errorf("%w: %q", ErrGroupNotFound, toGroup)
  223. }
  224. // preserve every attribute TLV (alias, note, auth state, etc.), not
  225. // just alias/note. Clone so the re-inserted item does not share a
  226. // backing array with the snapshot queued for deletion.
  227. attrs := slices.Clone(srcBuddy.TLVList)
  228. if err := f.DeleteBuddy(fromGroup, buddyName); err != nil {
  229. return err
  230. }
  231. if _, err := f.addBuddyItem(toGroup, buddyName, attrs); err != nil {
  232. return err
  233. }
  234. }
  235. if beforeBuddy != "" {
  236. moved := f.buddyItem(dst, buddyName)
  237. before := f.buddyItem(dst, beforeBuddy)
  238. if moved != nil && before != nil {
  239. f.reorderInGroupOrder(dst, moved.ItemID, before.ItemID)
  240. }
  241. }
  242. return nil
  243. }
  244. // SetBuddyAlias sets (or, when alias is empty, clears) the alias attribute on
  245. // every buddy item matching buddyName across all groups. Returns true if at
  246. // least one buddy item was found.
  247. func (f *FeedbagList) SetBuddyAlias(buddyName, alias string) (bool, error) {
  248. buddies := f.buddyItemsByName(buddyName)
  249. if len(buddies) == 0 {
  250. return false, nil
  251. }
  252. for _, buddy := range buddies {
  253. if alias != "" {
  254. buddy.Set(wire.NewTLVBE(wire.FeedbagAttributesAlias, alias))
  255. } else {
  256. buddy.Remove(wire.FeedbagAttributesAlias)
  257. }
  258. f.trackUpdate(buddy)
  259. }
  260. return true, nil
  261. }
  262. // SetGroupCollapsed sets (or, when collapsed is false, clears) the collapsed
  263. // attribute on a group. An empty groupName targets the unnamed default group.
  264. // Returns ErrGroupNotFound if the group does not exist.
  265. func (f *FeedbagList) SetGroupCollapsed(groupName string, collapsed bool) error {
  266. group := f.groupByName(groupName)
  267. if group == nil {
  268. return fmt.Errorf("%w: %q", ErrGroupNotFound, groupName)
  269. }
  270. if collapsed {
  271. group.Set(wire.NewTLVBE(wire.FeedbagAttributesCollapsed, []byte{}))
  272. } else {
  273. group.Remove(wire.FeedbagAttributesCollapsed)
  274. }
  275. f.trackUpdate(group)
  276. return nil
  277. }
  278. // PermitUser upserts a permit-list entry for the given screen name.
  279. func (f *FeedbagList) PermitUser(screenName string) {
  280. f.upsertItem(wire.FeedbagItem{
  281. ClassID: wire.FeedbagClassIDPermit,
  282. Name: screenName,
  283. })
  284. }
  285. // DenyUser upserts a deny-list entry for the given screen name.
  286. func (f *FeedbagList) DenyUser(screenName string) {
  287. f.upsertItem(wire.FeedbagItem{
  288. ClassID: wire.FeedbagClassIDDeny,
  289. Name: screenName,
  290. })
  291. }
  292. // DeletePermit marks a permit-list entry for deletion.
  293. func (f *FeedbagList) DeletePermit(screenName string) {
  294. f.deleteItem(wire.FeedbagItem{
  295. ClassID: wire.FeedbagClassIDPermit,
  296. Name: screenName,
  297. })
  298. }
  299. // DeleteDeny marks a deny-list entry for deletion.
  300. func (f *FeedbagList) DeleteDeny(screenName string) {
  301. f.deleteItem(wire.FeedbagItem{
  302. ClassID: wire.FeedbagClassIDDeny,
  303. Name: screenName,
  304. })
  305. }
  306. // AddLinkedScreenName adds a linked screen name.
  307. func (f *FeedbagList) AddLinkedScreenName(screenName string) {
  308. f.upsertItem(wire.FeedbagItem{
  309. ClassID: wire.FeedbagClassIdAlInfo,
  310. Name: screenName,
  311. })
  312. }
  313. // DeleteLinkedScreenName deletes a linked screen name.
  314. func (f *FeedbagList) DeleteLinkedScreenName(screenName string) {
  315. _, deleted := f.deleteItem(wire.FeedbagItem{
  316. ClassID: wire.FeedbagClassIdAlInfo,
  317. Name: screenName,
  318. })
  319. if deleted {
  320. // touch the root group so that the client purges its local
  321. // buddy list cache
  322. f.trackUpdate(f.rootGroup())
  323. }
  324. }
  325. // LinkedScreenNames returns all linked screen names in the feedbag.
  326. func (f *FeedbagList) LinkedScreenNames() []IdentScreenName {
  327. var names []IdentScreenName
  328. for _, item := range f.items {
  329. if item.ClassID == wire.FeedbagClassIdAlInfo {
  330. names = append(names, NewIdentScreenName(item.Name))
  331. }
  332. }
  333. return names
  334. }
  335. // HasLinkedScreenName returns whether the feedbag has a linked screen name.
  336. func (f *FeedbagList) HasLinkedScreenName(screenName string) bool {
  337. return slices.ContainsFunc(f.items, func(item *wire.FeedbagItem) bool {
  338. return item.ClassID == wire.FeedbagClassIdAlInfo && item.Name == NewIdentScreenName(screenName).String()
  339. })
  340. }
  341. // PendingUpdates returns items that were explicitly upserted via upsertItem
  342. // and items that were implicitly created or modified as side effects of other
  343. // operations (e.g., group order updates from upsertItem, root group updates
  344. // from AddGroup). The pending list is cleared after each call.
  345. func (f *FeedbagList) PendingUpdates() []wire.FeedbagItem {
  346. var result []wire.FeedbagItem
  347. for _, p := range f.pendingUpdates {
  348. result = append(result, *p)
  349. }
  350. f.pendingUpdates = nil
  351. if len(result) == 0 {
  352. return nil
  353. }
  354. return result
  355. }
  356. // PendingDeletes returns items marked for deletion via deleteItem.
  357. // The pending list is cleared after each call.
  358. func (f *FeedbagList) PendingDeletes() []wire.FeedbagItem {
  359. var result []wire.FeedbagItem
  360. for _, p := range f.pendingDeletes {
  361. result = append(result, *p)
  362. }
  363. f.pendingDeletes = nil
  364. return result
  365. }
  366. func (f *FeedbagList) Items() []wire.FeedbagItem {
  367. var result []wire.FeedbagItem
  368. for _, p := range f.items {
  369. result = append(result, *p)
  370. }
  371. return result
  372. }
  373. // Groups returns the feedbag's groups, excluding the root group. When the root
  374. // group has an order attribute, groups follow that order and groups missing
  375. // from it are appended in feedbag order; otherwise all groups are in feedbag
  376. // order.
  377. func (f *FeedbagList) Groups() []wire.FeedbagItem {
  378. groupOrder := func(items []*wire.FeedbagItem, groupID uint16) ([]uint16, bool) {
  379. grpIdx := slices.IndexFunc(items, func(item *wire.FeedbagItem) bool {
  380. return item.ClassID == wire.FeedbagClassIdGroup && item.GroupID == groupID
  381. })
  382. if grpIdx == -1 {
  383. return []uint16{}, false
  384. }
  385. ordering, hasOrder := items[grpIdx].Uint16SliceBE(wire.FeedbagAttributesOrder)
  386. return ordering, hasOrder
  387. }
  388. if ordering, hasOrder := groupOrder(f.items, 0); hasOrder {
  389. orderMap := make(map[uint16]int)
  390. for i, order := range ordering {
  391. orderMap[order] = i
  392. }
  393. var ordered []*wire.FeedbagItem
  394. var unordered []*wire.FeedbagItem
  395. for _, group := range f.items {
  396. if group.ClassID == wire.FeedbagClassIdGroup && group.GroupID > 0 {
  397. if _, ok := orderMap[group.GroupID]; ok {
  398. ordered = append(ordered, group)
  399. } else {
  400. unordered = append(unordered, group)
  401. }
  402. }
  403. }
  404. // order groups present in ordering TLV by their position in the ordering TLV
  405. slices.SortFunc(ordered, func(a, b *wire.FeedbagItem) int {
  406. return orderMap[a.GroupID] - orderMap[b.GroupID]
  407. })
  408. ret := make([]wire.FeedbagItem, 0, len(ordered)+len(unordered))
  409. for _, group := range ordered {
  410. ret = append(ret, *group)
  411. }
  412. for _, group := range unordered {
  413. ret = append(ret, *group)
  414. }
  415. return ret
  416. }
  417. var ret []wire.FeedbagItem
  418. for _, group := range f.items {
  419. if group.ClassID == wire.FeedbagClassIdGroup && group.GroupID != 0 {
  420. ret = append(ret, *group)
  421. }
  422. }
  423. return ret
  424. }
  425. // Buddies returns the buddies in groupItem. When groupItem has an order
  426. // attribute, buddies follow that order and buddies missing from it are
  427. // appended in feedbag order; otherwise all buddies are in feedbag order.
  428. func (f *FeedbagList) Buddies(groupItem wire.FeedbagItem) []wire.FeedbagItem {
  429. if groupItem.ClassID != wire.FeedbagClassIdGroup {
  430. panic("Buddies called on non-group item")
  431. }
  432. if ordering, hasOrder := groupItem.Uint16SliceBE(wire.FeedbagAttributesOrder); hasOrder {
  433. orderMap := make(map[uint16]int)
  434. for i, order := range ordering {
  435. orderMap[order] = i
  436. }
  437. var ordered []*wire.FeedbagItem
  438. var unordered []*wire.FeedbagItem
  439. for _, buddy := range f.items {
  440. if buddy.ClassID == wire.FeedbagClassIdBuddy && buddy.GroupID == groupItem.GroupID {
  441. if _, ok := orderMap[buddy.ItemID]; ok {
  442. ordered = append(ordered, buddy)
  443. } else {
  444. unordered = append(unordered, buddy)
  445. }
  446. }
  447. }
  448. // order buddies present in ordering TLV by their position in the ordering TLV
  449. slices.SortFunc(ordered, func(a, b *wire.FeedbagItem) int {
  450. return orderMap[a.ItemID] - orderMap[b.ItemID]
  451. })
  452. ret := make([]wire.FeedbagItem, 0, len(ordered)+len(unordered))
  453. for _, buddy := range ordered {
  454. ret = append(ret, *buddy)
  455. }
  456. for _, buddy := range unordered {
  457. ret = append(ret, *buddy)
  458. }
  459. return ret
  460. }
  461. var ret []wire.FeedbagItem
  462. for _, buddy := range f.items {
  463. if buddy.ClassID == wire.FeedbagClassIdBuddy && buddy.GroupID == groupItem.GroupID {
  464. ret = append(ret, *buddy)
  465. }
  466. }
  467. return ret
  468. }
  469. // rootGroup retrieves the root group, creating one if non-existent.
  470. func (f *FeedbagList) rootGroup() *wire.FeedbagItem {
  471. var root *wire.FeedbagItem
  472. for _, item := range f.items {
  473. if item.ClassID == wire.FeedbagClassIdGroup && item.GroupID == 0 {
  474. root = item
  475. break
  476. }
  477. }
  478. if root == nil {
  479. root = &wire.FeedbagItem{ClassID: wire.FeedbagClassIdGroup, GroupID: 0}
  480. f.items = append(f.items, root)
  481. f.trackUpdate(root)
  482. }
  483. return root
  484. }
  485. // groupByName returns the group item with the given name, or nil if not found.
  486. // The root group (GroupID 0) is never returned; it holds the master group order
  487. // rather than buddies, and an empty name matches the unnamed default buddy group.
  488. func (f *FeedbagList) groupByName(name string) *wire.FeedbagItem {
  489. for _, item := range f.items {
  490. if item.ClassID == wire.FeedbagClassIdGroup && item.GroupID != 0 && item.Name == name {
  491. return item
  492. }
  493. }
  494. return nil
  495. }
  496. // buddyItem returns the buddy item with the given name in the given group, or
  497. // nil if not found. Names are normalized before comparison.
  498. func (f *FeedbagList) buddyItem(group *wire.FeedbagItem, buddyName string) *wire.FeedbagItem {
  499. want := NewIdentScreenName(buddyName).String()
  500. for _, item := range f.items {
  501. if item.ClassID != wire.FeedbagClassIdBuddy || item.GroupID != group.GroupID {
  502. continue
  503. }
  504. if NewIdentScreenName(item.Name).String() == want {
  505. return item
  506. }
  507. }
  508. return nil
  509. }
  510. // buddyItemsByName returns every buddy item matching buddyName across all
  511. // groups. Names are normalized before comparison.
  512. func (f *FeedbagList) buddyItemsByName(buddyName string) []*wire.FeedbagItem {
  513. want := NewIdentScreenName(buddyName).String()
  514. var out []*wire.FeedbagItem
  515. for _, item := range f.items {
  516. if item.ClassID != wire.FeedbagClassIdBuddy {
  517. continue
  518. }
  519. if NewIdentScreenName(item.Name).String() == want {
  520. out = append(out, item)
  521. }
  522. }
  523. return out
  524. }
  525. // reorderInGroupOrder moves itemID so that it sits immediately before
  526. // beforeItemID in the group's order TLV. If beforeItemID is not present, itemID
  527. // is appended. The group is tracked as updated.
  528. func (f *FeedbagList) reorderInGroupOrder(group *wire.FeedbagItem, itemID, beforeItemID uint16) {
  529. order, ok := group.Uint16SliceBE(wire.FeedbagAttributesOrder)
  530. if !ok {
  531. return
  532. }
  533. filtered := make([]uint16, 0, len(order))
  534. for _, id := range order {
  535. if id != itemID {
  536. filtered = append(filtered, id)
  537. }
  538. }
  539. insertAt := len(filtered)
  540. for i, id := range filtered {
  541. if id == beforeItemID {
  542. insertAt = i
  543. break
  544. }
  545. }
  546. reordered := make([]uint16, 0, len(filtered)+1)
  547. reordered = append(reordered, filtered[:insertAt]...)
  548. reordered = append(reordered, itemID)
  549. reordered = append(reordered, filtered[insertAt:]...)
  550. group.Set(wire.NewTLVBE(wire.FeedbagAttributesOrder, reordered))
  551. f.trackUpdate(group)
  552. }
  553. // trackUpdate adds item to the pending-updates list if not already present.
  554. func (f *FeedbagList) trackUpdate(item *wire.FeedbagItem) {
  555. if slices.Contains(f.pendingUpdates, item) {
  556. return // already tracked
  557. }
  558. f.pendingUpdates = append(f.pendingUpdates, item)
  559. }
  560. // itemsMatch reports whether two feedbag items are considered the same for
  561. // upsert/delete (grouped classes: ClassID, Name, GroupID; others: ClassID and
  562. // Name). Stored items are assumed to have normalized names; the input (b) name
  563. // is normalized for comparison when the class is buddy, permit, or deny.
  564. func (f *FeedbagList) itemsMatch(a, b *wire.FeedbagItem) bool {
  565. if a.ClassID != b.ClassID {
  566. return false
  567. }
  568. var nameMatch bool
  569. if hasScreenName(a.ClassID) {
  570. nameMatch = NewIdentScreenName(a.Name).String() == NewIdentScreenName(b.Name).String()
  571. } else {
  572. nameMatch = a.Name == b.Name
  573. }
  574. if !nameMatch {
  575. return false
  576. }
  577. // these two classes are allowed within groups
  578. if a.ClassID == wire.FeedbagClassIdBuddy || a.ClassID == wire.FeedbagClassIdMin {
  579. return a.GroupID == b.GroupID
  580. }
  581. return true
  582. }
  583. // deleteItem removes the first item matching the same criteria as upsertItem:
  584. // grouped items by ClassID, Name, and GroupID; other items by ClassID and Name.
  585. // Returns the deleted item and true if found, or a zero item and false otherwise.
  586. func (f *FeedbagList) deleteItem(item wire.FeedbagItem) (wire.FeedbagItem, bool) {
  587. for i, existing := range f.items {
  588. if f.itemsMatch(existing, &item) {
  589. f.pendingDeletes = append(f.pendingDeletes, existing)
  590. f.items = append(f.items[:i], f.items[i+1:]...)
  591. return *existing, true
  592. }
  593. }
  594. return wire.FeedbagItem{}, false
  595. }
  596. // upsertItem updates an existing feedbag item or inserts a new one. Items of a
  597. // grouped class are matched by GroupID, ClassID, and Name; all other items are
  598. // matched by ClassID and Name. When matched, the existing item is replaced in
  599. // place (preserving its ItemID). When no match is found, a new item is inserted
  600. // with an auto-generated ItemID. Names for buddy, permit, and deny items are
  601. // normalized before storage. Returns the stored item and true if a new item
  602. // was inserted, or the existing item and false if it was updated/unchanged.
  603. func (f *FeedbagList) upsertItem(item wire.FeedbagItem) (wire.FeedbagItem, bool) {
  604. if hasScreenName(item.ClassID) {
  605. item.Name = NewIdentScreenName(item.Name).String() // normalize name
  606. }
  607. for _, existing := range f.items {
  608. if f.itemsMatch(existing, &item) {
  609. if !existing.IsEqual(item) {
  610. item.ItemID = existing.ItemID
  611. item.GroupID = existing.GroupID
  612. *existing = item
  613. f.trackUpdate(existing)
  614. }
  615. return *existing, false
  616. }
  617. }
  618. item.ItemID = f.genID()
  619. f.items = append(f.items, &item)
  620. f.pendingUpdates = append(f.pendingUpdates, &item)
  621. return item, true
  622. }
  623. // genID generates a unique ID that does not conflict with any existing ItemID
  624. // or GroupID in the list.
  625. func (f *FeedbagList) genID() uint16 {
  626. num := uint16(f.randInt(math.MaxUint16))
  627. for itemID := num; itemID != num-1; itemID++ {
  628. if itemID == 0 {
  629. continue
  630. }
  631. exists := false
  632. for _, item := range f.items {
  633. if item.GroupID == itemID || item.ItemID == itemID {
  634. exists = true
  635. break
  636. }
  637. }
  638. if !exists {
  639. return itemID
  640. }
  641. }
  642. return 0
  643. }
  644. // hasScreenName reports whether the feedbag class stores a screen name that
  645. // should be normalized (buddy, permit, deny).
  646. func hasScreenName(classID uint16) bool {
  647. return classID == wire.FeedbagClassIdBuddy ||
  648. classID == wire.FeedbagClassIDPermit ||
  649. classID == wire.FeedbagClassIdAlInfo ||
  650. classID == wire.FeedbagClassIDDeny
  651. }