post.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. package pub
  2. import (
  3. "net/url"
  4. "strings"
  5. "time"
  6. "mimicry/style"
  7. "mimicry/ansi"
  8. "mimicry/object"
  9. "errors"
  10. "mimicry/client"
  11. "fmt"
  12. "golang.org/x/exp/slices"
  13. "mimicry/mime"
  14. "mimicry/render"
  15. "sync"
  16. )
  17. type Post struct {
  18. kind string
  19. id *url.URL
  20. title string
  21. titleErr error
  22. body string
  23. bodyErr error
  24. mediaType *mime.MediaType
  25. mediaTypeErr error
  26. link *Link
  27. linkErr error
  28. created time.Time
  29. createdErr error
  30. edited time.Time
  31. editedErr error
  32. parent any
  33. parentErr error
  34. // just as body dies completely if members die,
  35. // attachments dies completely if any member dies
  36. attachments []*Link
  37. attachmentsErr error
  38. creators []TangibleWithName
  39. recipients []TangibleWithName
  40. comments *Collection
  41. commentsErr error
  42. }
  43. func NewPost(input any, source *url.URL) (*Post, error) {
  44. o, id, err := client.FetchUnknown(input, source)
  45. if err != nil { return nil, err }
  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. // TODO: for Lemmy, may have to auto-unwrap Create into a Post
  56. if !slices.Contains([]string{
  57. "Article", "Audio", "Document", "Image", "Note", "Page", "Video",
  58. }, p.kind) {
  59. return nil, fmt.Errorf("%w: %s is not a Post", ErrWrongType, p.kind)
  60. }
  61. p.title, p.titleErr = o.GetNatural("name", "en")
  62. p.body, p.bodyErr = o.GetNatural("content", "en")
  63. p.mediaType, p.mediaTypeErr = o.GetMediaType("mediaType")
  64. p.created, p.createdErr = o.GetTime("published")
  65. p.edited, p.editedErr = o.GetTime("updated")
  66. p.parent, p.parentErr = o.GetAny("inReplyTo")
  67. if p.kind == "Image" || p.kind == "Audio" || p.kind == "Video" {
  68. p.link, p.linkErr = getBestLinkShorthand(o, "url", strings.ToLower(p.kind))
  69. } else {
  70. p.link, p.linkErr = getFirstLinkShorthand(o, "url")
  71. }
  72. var wg sync.WaitGroup
  73. wg.Add(4)
  74. go func() {p.creators = getActors(o, "attributedTo", p.id); wg.Done()}()
  75. go func() {p.recipients = getActors(o, "audience", p.id); wg.Done()}()
  76. go func() {p.attachments, p.attachmentsErr = getLinks(o, "attachment"); wg.Done()}()
  77. go func() {
  78. p.comments, p.commentsErr = getCollection(o, "replies", p.id)
  79. if errors.Is(p.commentsErr, object.ErrKeyNotPresent) {
  80. p.comments, p.commentsErr = getCollection(o, "comments", p.id)
  81. }
  82. wg.Done()
  83. }()
  84. wg.Wait()
  85. return p, nil
  86. }
  87. func (p *Post) Kind() (string) {
  88. return p.kind
  89. }
  90. func (p *Post) Children(quantity uint) ([]Tangible, Container, uint) {
  91. if errors.Is(p.commentsErr, object.ErrKeyNotPresent) {
  92. return []Tangible{}, nil, 0
  93. }
  94. if p.commentsErr != nil {
  95. return []Tangible{
  96. NewFailure(p.commentsErr),
  97. }, nil, 0
  98. }
  99. return p.comments.Harvest(quantity, 0)
  100. }
  101. func (p *Post) Parents(quantity uint) []Tangible {
  102. if quantity == 0 {
  103. return []Tangible{}
  104. }
  105. if errors.Is(p.parentErr, object.ErrKeyNotPresent) {
  106. return []Tangible{}
  107. }
  108. if p.parentErr != nil {
  109. return []Tangible{NewFailure(p.parentErr)}
  110. }
  111. fetchedParent, fetchedParentErr := NewPost(p.parent, p.id)
  112. if fetchedParentErr != nil {
  113. return []Tangible{NewFailure(fetchedParentErr)}
  114. }
  115. return append([]Tangible{fetchedParent}, fetchedParent.Parents(quantity - 1)...)
  116. }
  117. func (p *Post) header(width int) string {
  118. output := ""
  119. if p.titleErr == nil {
  120. output += style.Bold(p.title) + "\n"
  121. } else if !errors.Is(p.titleErr, object.ErrKeyNotPresent) {
  122. output += style.Problem(fmt.Errorf("failed to get title: %w", p.titleErr)) + "\n"
  123. }
  124. if errors.Is(p.parentErr, object.ErrKeyNotPresent) {
  125. output += style.Color(strings.ToLower(p.kind))
  126. } else {
  127. output += style.Color("comment")
  128. }
  129. if len(p.creators) > 0 {
  130. output += " by "
  131. for i, creator := range p.creators {
  132. output += style.Color(creator.Name())
  133. if i != len(p.creators) - 1 {
  134. output += ", "
  135. }
  136. }
  137. }
  138. if len(p.recipients) > 0 {
  139. output += " to "
  140. for i, recipient := range p.recipients {
  141. output += style.Color(recipient.Name())
  142. if i != len(p.recipients) - 1 {
  143. output += ", "
  144. }
  145. }
  146. }
  147. if p.createdErr != nil && !errors.Is(p.createdErr, object.ErrKeyNotPresent) {
  148. output += " at " + style.Problem(p.createdErr)
  149. } else {
  150. output += " at " + style.Color(p.created.Format(timeFormat))
  151. }
  152. return ansi.Wrap(output, width)
  153. }
  154. func (p *Post) center(width int) (string, bool) {
  155. if errors.Is(p.bodyErr, object.ErrKeyNotPresent) {
  156. return "", false
  157. }
  158. if p.bodyErr != nil {
  159. return ansi.Wrap(style.Problem(p.bodyErr), width), true
  160. }
  161. mediaType := p.mediaType
  162. if errors.Is(p.mediaTypeErr, object.ErrKeyNotPresent) {
  163. mediaType = mime.Default()
  164. } else if p.mediaTypeErr != nil {
  165. return ansi.Wrap(style.Problem(p.mediaTypeErr), width), true
  166. }
  167. rendered, err := render.Render(p.body, mediaType.Essence, width)
  168. if err != nil {
  169. return style.Problem(err), true
  170. }
  171. return rendered, true
  172. }
  173. func (p *Post) supplement(width int) (string, bool) {
  174. if errors.Is(p.attachmentsErr, object.ErrKeyNotPresent) {
  175. return "", false
  176. }
  177. if p.attachmentsErr != nil {
  178. return ansi.Wrap(style.Problem(fmt.Errorf("failed to load attachments: %w", p.attachmentsErr)), width), true
  179. }
  180. if len(p.attachments) == 0 {
  181. return "", false
  182. }
  183. output := ""
  184. for _, attachment := range p.attachments {
  185. if output != "" { output += "\n" }
  186. alt, err := attachment.Alt()
  187. if err != nil {
  188. output += style.Problem(err)
  189. continue
  190. }
  191. output += style.LinkBlock(alt)
  192. }
  193. return ansi.Wrap(output, width), true
  194. }
  195. func (p *Post) footer(width int) string {
  196. if errors.Is(p.commentsErr, object.ErrKeyNotPresent) {
  197. return style.Color("comments disabled")
  198. } else if p.commentsErr != nil {
  199. return style.Color("comments enabled")
  200. } else if quantity, err := p.comments.Size(); errors.Is(err, object.ErrKeyNotPresent) {
  201. return style.Color("comments enabled")
  202. } else if err != nil {
  203. return style.Problem(err)
  204. } else if quantity == 1 {
  205. return style.Color(fmt.Sprintf("%d comment", quantity))
  206. } else {
  207. return style.Color(fmt.Sprintf("%d comments", quantity))
  208. }
  209. }
  210. func (p Post) String(width int) string {
  211. output := p.header(width)
  212. if body, present := p.center(width - 4); present {
  213. output += "\n\n" + ansi.Indent(body, " ", true)
  214. }
  215. if attachments, present := p.supplement(width - 4); present {
  216. output += "\n\n" + ansi.Indent(attachments, " ", true)
  217. }
  218. output += "\n\n" + p.footer(width)
  219. return output
  220. }
  221. func (p *Post) Preview(width int) string {
  222. output := p.header(width)
  223. if body, present := p.center(width); present {
  224. if attachments, present := p.supplement(width); present {
  225. output += "\n" + ansi.Snip(body + "\n" + attachments, width, 4, style.Color("\u2026"))
  226. } else {
  227. output += "\n" + ansi.Snip(body, width, 4, style.Color("\u2026"))
  228. }
  229. }
  230. return output
  231. }