collection.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. package pub
  2. import (
  3. "errors"
  4. "fmt"
  5. "golang.org/x/exp/slices"
  6. "servitor/client"
  7. "servitor/object"
  8. "net/url"
  9. "sync"
  10. )
  11. /*
  12. Methods are:
  13. Category
  14. Kind
  15. Identifier
  16. Next
  17. Size
  18. Items (returns list)
  19. String // maybe just show this page, and Next can be a button
  20. // the infiniscroll will be provided by the View package
  21. */
  22. // Should probably take in a constructor, actor gives NewActivity
  23. // and Post gives NewPost, but not exactly, they can wrap them
  24. // in a function which also checks whether the element is
  25. // valid in the given context
  26. type Collection struct {
  27. kind string
  28. id *url.URL
  29. elements []any
  30. elementsErr error
  31. next any
  32. nextErr error
  33. size uint64
  34. sizeErr error
  35. construct func(any, *url.URL,) Tangible
  36. }
  37. func NewCollection(input any, source *url.URL, construct func(any, *url.URL) Tangible) (*Collection, error) {
  38. o, id, err := client.FetchUnknown(input, source)
  39. if err != nil {
  40. return nil, err
  41. }
  42. return NewCollectionFromObject(o, id, construct)
  43. }
  44. func NewCollectionFromObject(o object.Object, id *url.URL, construct func(any, *url.URL) Tangible) (*Collection, error) {
  45. c := &Collection{}
  46. c.id = id
  47. var err error
  48. if c.kind, err = o.GetString("type"); err != nil {
  49. return nil, err
  50. }
  51. if !slices.Contains([]string{
  52. "Collection", "OrderedCollection", "CollectionPage", "OrderedCollectionPage",
  53. }, c.kind) {
  54. return nil, fmt.Errorf("%w: %s is not a Collection", ErrWrongType, c.kind)
  55. }
  56. c.construct = construct
  57. if c.kind == "Collection" || c.kind == "CollectionPage" {
  58. c.elements, c.elementsErr = o.GetList("items")
  59. } else {
  60. c.elements, c.elementsErr = o.GetList("orderedItems")
  61. }
  62. if c.kind == "Collection" || c.kind == "OrderedCollection" {
  63. c.next, c.nextErr = o.GetAny("first")
  64. } else {
  65. c.next, c.nextErr = o.GetAny("next")
  66. }
  67. c.size, c.sizeErr = o.GetNumber("totalItems")
  68. return c, nil
  69. }
  70. func (c *Collection) Size() (uint64, error) {
  71. return c.size, c.sizeErr
  72. }
  73. func (c *Collection) Harvest(amount uint, startingPoint uint) ([]Tangible, Container, uint) {
  74. return c.harvestWithEmptyCount(amount, startingPoint, 0)
  75. }
  76. func (c *Collection) harvestWithEmptyCount(amount uint, startingPoint uint, emptyCount int) ([]Tangible, Container, uint) {
  77. if c == nil {
  78. panic("can't harvest nil collection")
  79. }
  80. if c.elementsErr != nil && !errors.Is(c.elementsErr, object.ErrKeyNotPresent) {
  81. return []Tangible{NewFailure(c.elementsErr)}, nil, 0
  82. }
  83. var length uint
  84. if errors.Is(c.elementsErr, object.ErrKeyNotPresent) {
  85. length = 0
  86. } else {
  87. length = uint(len(c.elements))
  88. }
  89. if length == 0 {
  90. emptyCount += 1
  91. }
  92. /*
  93. This is set at 3 because 3 seems to be the maximum amount that servers send, besides cases of infinite loops.
  94. Mastodon sends 3 in the following case:
  95. - the first is the Collection itself, which has no items because the items are in CollectionPages
  96. - the next page (the first CollectionPage) is empty because it only holds self-replies and there are none
  97. - the next page (the second CollectionPage) is empty because it holds replies from others and there are none
  98. */
  99. if emptyCount > 3 {
  100. return []Tangible{NewFailure(errors.New("refusing to read the next collection because >3 consecutive empty collections have been encountered"))}, nil, 0
  101. }
  102. var amountFromThisPage uint
  103. if startingPoint >= length {
  104. amountFromThisPage = 0
  105. } else if length > amount+startingPoint {
  106. amountFromThisPage = amount
  107. } else {
  108. amountFromThisPage = length - startingPoint
  109. }
  110. fromThisPage := make([]Tangible, amountFromThisPage)
  111. var fromLaterPages []Tangible
  112. var nextCollection Container
  113. var nextStartingPoint uint
  114. var wg sync.WaitGroup
  115. for i := uint(0); i < amountFromThisPage; i++ {
  116. i := i
  117. wg.Add(1)
  118. go func() {
  119. fromThisPage[i] = c.construct(c.elements[i+startingPoint], c.id)
  120. wg.Done()
  121. }()
  122. }
  123. wg.Add(1)
  124. go func() {
  125. if length > amount+startingPoint {
  126. fromLaterPages, nextCollection, nextStartingPoint = []Tangible{}, c, amount+startingPoint
  127. } else if errors.Is(c.nextErr, object.ErrKeyNotPresent) {
  128. fromLaterPages, nextCollection, nextStartingPoint = []Tangible{}, nil, 0
  129. } else if c.nextErr != nil {
  130. fromLaterPages, nextCollection, nextStartingPoint = []Tangible{NewFailure(c.nextErr)}, nil, 0
  131. } else if next, err := NewCollection(c.next, c.id, c.construct); err != nil {
  132. fromLaterPages, nextCollection, nextStartingPoint = []Tangible{NewFailure(err)}, nil, 0
  133. } else {
  134. fromLaterPages, nextCollection, nextStartingPoint = next.harvestWithEmptyCount(amount-amountFromThisPage, 0, emptyCount)
  135. }
  136. wg.Done()
  137. }()
  138. wg.Wait()
  139. return append(fromThisPage, fromLaterPages...), nextCollection, nextStartingPoint
  140. }