collection.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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. }
  36. func NewCollection(input any, source *url.URL) (*Collection, error) {
  37. o, id, err := client.FetchUnknown(input, source)
  38. if err != nil {
  39. return nil, err
  40. }
  41. return NewCollectionFromObject(o, id)
  42. }
  43. func NewCollectionFromObject(o object.Object, id *url.URL) (*Collection, error) {
  44. c := &Collection{}
  45. c.id = id
  46. var err error
  47. if c.kind, err = o.GetString("type"); err != nil {
  48. return nil, err
  49. }
  50. if !slices.Contains([]string{
  51. "Collection", "OrderedCollection", "CollectionPage", "OrderedCollectionPage",
  52. }, c.kind) {
  53. return nil, fmt.Errorf("%w: %s is not a Collection", ErrWrongType, c.kind)
  54. }
  55. if c.kind == "Collection" || c.kind == "CollectionPage" {
  56. c.elements, c.elementsErr = o.GetList("items")
  57. } else {
  58. c.elements, c.elementsErr = o.GetList("orderedItems")
  59. }
  60. if c.kind == "Collection" || c.kind == "OrderedCollection" {
  61. c.next, c.nextErr = o.GetAny("first")
  62. } else {
  63. c.next, c.nextErr = o.GetAny("next")
  64. }
  65. c.size, c.sizeErr = o.GetNumber("totalItems")
  66. return c, nil
  67. }
  68. func (c *Collection) Size() (uint64, error) {
  69. return c.size, c.sizeErr
  70. }
  71. func (c *Collection) Harvest(amount uint, startingPoint uint) ([]Tangible, Container, uint) {
  72. return c.harvestWithEmptyCount(amount, startingPoint, 0)
  73. }
  74. func (c *Collection) harvestWithEmptyCount(amount uint, startingPoint uint, emptyCount int) ([]Tangible, Container, uint) {
  75. if c == nil {
  76. panic("can't harvest nil collection")
  77. }
  78. if c.elementsErr != nil && !errors.Is(c.elementsErr, object.ErrKeyNotPresent) {
  79. return []Tangible{NewFailure(c.elementsErr)}, nil, 0
  80. }
  81. var length uint
  82. if errors.Is(c.elementsErr, object.ErrKeyNotPresent) {
  83. length = 0
  84. } else {
  85. length = uint(len(c.elements))
  86. }
  87. if length == 0 {
  88. emptyCount += 1
  89. }
  90. /*
  91. This is set at 3 because 3 seems to be the maximum amount that servers send, besides cases of infinite loops.
  92. Mastodon sends 3 in the following case:
  93. - the first is the Collection itself, which has no items because the items are in CollectionPages
  94. - the next page (the first CollectionPage) is empty because it only holds self-replies and there are none
  95. - the next page (the second CollectionPage) is empty because it holds replies from others and there are none
  96. */
  97. if emptyCount > 3 {
  98. return []Tangible{NewFailure(errors.New("refusing to read the next collection because >3 consecutive empty collections have been encountered"))}, nil, 0
  99. }
  100. var amountFromThisPage uint
  101. if startingPoint >= length {
  102. amountFromThisPage = 0
  103. } else if length > amount+startingPoint {
  104. amountFromThisPage = amount
  105. } else {
  106. amountFromThisPage = length - startingPoint
  107. }
  108. fromThisPage := make([]Tangible, amountFromThisPage)
  109. var fromLaterPages []Tangible
  110. var nextCollection Container
  111. var nextStartingPoint uint
  112. var wg sync.WaitGroup
  113. for i := uint(0); i < amountFromThisPage; i++ {
  114. i := i
  115. wg.Add(1)
  116. go func() {
  117. fromThisPage[i] = NewTangible(c.elements[i+startingPoint], c.id)
  118. wg.Done()
  119. }()
  120. }
  121. wg.Add(1)
  122. go func() {
  123. if length > amount+startingPoint {
  124. fromLaterPages, nextCollection, nextStartingPoint = []Tangible{}, c, amount+startingPoint
  125. } else if errors.Is(c.nextErr, object.ErrKeyNotPresent) {
  126. fromLaterPages, nextCollection, nextStartingPoint = []Tangible{}, nil, 0
  127. } else if c.nextErr != nil {
  128. fromLaterPages, nextCollection, nextStartingPoint = []Tangible{NewFailure(c.nextErr)}, nil, 0
  129. } else if next, err := NewCollection(c.next, c.id); err != nil {
  130. fromLaterPages, nextCollection, nextStartingPoint = []Tangible{NewFailure(err)}, nil, 0
  131. } else {
  132. fromLaterPages, nextCollection, nextStartingPoint = next.harvestWithEmptyCount(amount-amountFromThisPage, 0, emptyCount)
  133. }
  134. wg.Done()
  135. }()
  136. wg.Wait()
  137. return append(fromThisPage, fromLaterPages...), nextCollection, nextStartingPoint
  138. }