feedbag_list.go 19 KB

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