post.go 6.8 KB

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