post.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. package pub
  2. import (
  3. "errors"
  4. "fmt"
  5. "golang.org/x/exp/slices"
  6. "servitor/ansi"
  7. "servitor/client"
  8. "servitor/mime"
  9. "servitor/object"
  10. "servitor/style"
  11. "net/url"
  12. "strings"
  13. "sync"
  14. "time"
  15. )
  16. type Post struct {
  17. kind string
  18. id *url.URL
  19. title string
  20. titleErr error
  21. body object.Markup
  22. bodyLinks []string
  23. bodyErr error
  24. media *Link
  25. mediaErr error
  26. created time.Time
  27. createdErr error
  28. edited time.Time
  29. editedErr error
  30. parent any
  31. parentErr error
  32. // just as body dies completely if members die,
  33. // attachments dies completely if any member dies
  34. attachments []*Link
  35. attachmentsErr error
  36. creators []Tangible
  37. recipients []Tangible
  38. comments *Collection
  39. commentsErr error
  40. }
  41. func NewPost(input any, source *url.URL) (*Post, error) {
  42. o, id, err := client.FetchUnknown(input, source)
  43. if err != nil {
  44. return nil, err
  45. }
  46. return NewPostFromObject(o, id)
  47. }
  48. func NewPostFromObject(o object.Object, id *url.URL) (*Post, error) {
  49. p := &Post{}
  50. p.id = id
  51. var err error
  52. if p.kind, err = o.GetString("type"); err != nil {
  53. return nil, err
  54. }
  55. if !slices.Contains([]string{
  56. "Article", "Audio", "Document", "Image", "Note", "Page", "Video",
  57. }, p.kind) {
  58. return nil, fmt.Errorf("%w: %s is not a Post", ErrWrongType, p.kind)
  59. }
  60. p.title, p.titleErr = o.GetString("name")
  61. p.body, p.bodyLinks, p.bodyErr = o.GetMarkup("content", "mediaType")
  62. p.created, p.createdErr = o.GetTime("published")
  63. p.edited, p.editedErr = o.GetTime("updated")
  64. p.parent, p.parentErr = o.GetAny("inReplyTo")
  65. if p.kind == "Image" || p.kind == "Audio" || p.kind == "Video" {
  66. p.media, p.mediaErr = getBestLinkShorthand(o, "url", strings.ToLower(p.kind))
  67. } else {
  68. p.media, p.mediaErr = getFirstLinkShorthand(o, "url")
  69. }
  70. var wg sync.WaitGroup
  71. wg.Add(4)
  72. go func() { p.creators = getActors(o, "attributedTo", p.id); wg.Done() }()
  73. go func() { p.recipients = getActors(o, "audience", p.id); wg.Done() }()
  74. go func() { p.attachments, p.attachmentsErr = getLinks(o, "attachment"); wg.Done() }()
  75. go func() {
  76. p.comments, p.commentsErr = getCollection(o, "replies", p.id)
  77. if errors.Is(p.commentsErr, object.ErrKeyNotPresent) {
  78. p.comments, p.commentsErr = getCollection(o, "comments", p.id)
  79. }
  80. wg.Done()
  81. }()
  82. wg.Wait()
  83. return p, nil
  84. }
  85. func (p *Post) Kind() string {
  86. return p.kind
  87. }
  88. func (p *Post) Children() Container {
  89. /* the if is necessary because my understanding is
  90. the first nil is a (*Collection)(nil) whereas
  91. the second is (Container)(nil) */
  92. if p.comments == nil {
  93. return nil
  94. } else {
  95. return p.comments
  96. }
  97. }
  98. func (p *Post) Parents(quantity uint) ([]Tangible, Tangible) {
  99. if quantity == 0 {
  100. panic("can't fetch zero parents")
  101. }
  102. if errors.Is(p.parentErr, object.ErrKeyNotPresent) {
  103. return []Tangible{}, nil
  104. }
  105. if p.parentErr != nil {
  106. return []Tangible{NewFailure(p.parentErr)}, nil
  107. }
  108. fetchedParent, fetchedParentErr := NewPost(p.parent, p.id)
  109. if fetchedParentErr != nil {
  110. return []Tangible{NewFailure(fetchedParentErr)}, nil
  111. }
  112. if quantity == 1 {
  113. return []Tangible{fetchedParent}, fetchedParent
  114. }
  115. fetchedParentParents, fetchedParentFrontier := fetchedParent.Parents(quantity - 1)
  116. return append([]Tangible{fetchedParent}, fetchedParentParents...), fetchedParentFrontier
  117. }
  118. func (p *Post) header(width int) string {
  119. output := ""
  120. if p.titleErr == nil {
  121. output += style.Bold(p.title) + "\n"
  122. } else if !errors.Is(p.titleErr, object.ErrKeyNotPresent) {
  123. output += style.Problem(fmt.Errorf("failed to get title: %w", p.titleErr)) + "\n"
  124. }
  125. if errors.Is(p.parentErr, object.ErrKeyNotPresent) {
  126. output += style.Color(strings.ToLower(p.kind))
  127. } else {
  128. output += style.Color("comment")
  129. }
  130. /* TODO: forgery checking is needed here; verify that the id of the post
  131. and id of the creators match */
  132. if len(p.creators) > 0 {
  133. output += " by "
  134. for i, creator := range p.creators {
  135. output += style.Color(creator.Name())
  136. if i != len(p.creators)-1 {
  137. output += ", "
  138. }
  139. }
  140. }
  141. if len(p.recipients) > 0 {
  142. output += " to "
  143. for i, recipient := range p.recipients {
  144. output += style.Color(recipient.Name())
  145. if i != len(p.recipients)-1 {
  146. output += ", "
  147. }
  148. }
  149. }
  150. if p.createdErr != nil && !errors.Is(p.createdErr, object.ErrKeyNotPresent) {
  151. output += " at " + style.Problem(p.createdErr)
  152. } else {
  153. output += " • " + style.Color(ago(p.created))
  154. }
  155. return ansi.Wrap(output, width)
  156. }
  157. func (p *Post) center(width int) (string, bool) {
  158. if errors.Is(p.bodyErr, object.ErrKeyNotPresent) {
  159. return "", false
  160. }
  161. if p.bodyErr != nil {
  162. return ansi.Wrap(style.Problem(p.bodyErr), width), true
  163. }
  164. rendered := p.body.Render(width)
  165. return rendered, true
  166. }
  167. func (p *Post) supplement(width int) (string, bool) {
  168. if errors.Is(p.attachmentsErr, object.ErrKeyNotPresent) {
  169. return "", false
  170. }
  171. if p.attachmentsErr != nil {
  172. return ansi.Wrap(style.Problem(fmt.Errorf("failed to load attachments: %w", p.attachmentsErr)), width), true
  173. }
  174. if len(p.attachments) == 0 {
  175. return "", false
  176. }
  177. // TODO: don't think this is good, rework it
  178. output := ""
  179. for i, attachment := range p.attachments {
  180. if output != "" {
  181. output += "\n"
  182. }
  183. alt, err := attachment.Alt()
  184. if err != nil {
  185. output += style.Problem(err)
  186. continue
  187. }
  188. output += style.LinkBlock(ansi.Wrap(alt, width-2), len(p.bodyLinks)+i+1)
  189. }
  190. return output, true
  191. }
  192. func (p *Post) footer(width int) string {
  193. if errors.Is(p.commentsErr, object.ErrKeyNotPresent) {
  194. return style.Color("comments disabled")
  195. } else if p.commentsErr != nil {
  196. return style.Color("comments enabled")
  197. } else if quantity, err := p.comments.Size(); errors.Is(err, object.ErrKeyNotPresent) {
  198. return style.Color("comments enabled")
  199. } else if err != nil {
  200. return style.Problem(err)
  201. } else if quantity == 1 {
  202. return style.Color(fmt.Sprintf("%d comment", quantity))
  203. } else {
  204. return style.Color(fmt.Sprintf("%d comments", quantity))
  205. }
  206. }
  207. func (p Post) String(width int) string {
  208. output := p.header(width)
  209. if body, present := p.center(width - 4); present {
  210. output += "\n\n" + ansi.Indent(body, " ", true)
  211. }
  212. if attachments, present := p.supplement(width - 4); present {
  213. output += "\n\n" + ansi.Indent(attachments, " ", true)
  214. }
  215. output += "\n\n" + p.footer(width)
  216. return output
  217. }
  218. func (p *Post) Preview(width int) string {
  219. output := p.header(width)
  220. body, bodyPresent := p.center(width)
  221. if bodyPresent {
  222. output += "\n" + body
  223. }
  224. if attachments, present := p.supplement(width); present {
  225. if bodyPresent {
  226. output += "\n"
  227. }
  228. output += "\n" + attachments
  229. }
  230. return ansi.Snip(output, width, 4, style.Color("\u2026"))
  231. }
  232. func (p *Post) Timestamp() time.Time {
  233. if p.createdErr != nil {
  234. return time.Time{}
  235. } else {
  236. return p.created
  237. }
  238. }
  239. func (p *Post) Name() string {
  240. if p.titleErr != nil {
  241. return style.Problem(p.titleErr)
  242. }
  243. return p.title
  244. }
  245. func (p *Post) Creators() []Tangible {
  246. return p.creators
  247. }
  248. func (p *Post) Recipients() []Tangible {
  249. return p.recipients
  250. }
  251. func (p *Post) Media() (string, *mime.MediaType, bool) {
  252. if p.mediaErr != nil {
  253. return "", nil, false
  254. }
  255. if p.kind == "Audio" || p.kind == "Video" || p.kind == "Image" {
  256. return p.media.SelectWithDefaultMediaType(mime.UnknownSubtype(strings.ToLower(p.kind)))
  257. }
  258. return p.media.Select()
  259. }
  260. func (p *Post) SelectLink(input int) (string, *mime.MediaType, bool) {
  261. input -= 1
  262. if len(p.bodyLinks) > input {
  263. return p.bodyLinks[input], mime.Unknown(), true
  264. }
  265. nextIndex := input - len(p.bodyLinks)
  266. if len(p.attachments) > nextIndex {
  267. return p.attachments[nextIndex].Select()
  268. }
  269. return "", nil, false
  270. }