actor.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. package pub
  2. import (
  3. "net/url"
  4. "mimicry/style"
  5. "errors"
  6. "mimicry/object"
  7. "time"
  8. "mimicry/client"
  9. "golang.org/x/exp/slices"
  10. "fmt"
  11. "strings"
  12. "mimicry/ansi"
  13. "mimicry/mime"
  14. "mimicry/render"
  15. )
  16. type Actor struct {
  17. kind string
  18. name string; nameErr error
  19. handle string; handleErr error
  20. id *url.URL
  21. bio string; bioErr error
  22. mediaType *mime.MediaType; mediaTypeErr error
  23. joined time.Time; joinedErr error
  24. pfp *Link; pfpErr error
  25. banner *Link; bannerErr error
  26. posts *Collection; postsErr error
  27. }
  28. func NewActor(input any, source *url.URL) (*Actor, error) {
  29. a := &Actor{}
  30. var o object.Object; var err error
  31. o, a.id, err = client.FetchUnknown(input, source)
  32. if err != nil { return nil, err }
  33. if a.kind, err = o.GetString("type"); err != nil {
  34. return nil, err
  35. }
  36. if !slices.Contains([]string{
  37. "Application", "Group", "Organization", "Person", "Service",
  38. }, a.kind) {
  39. return nil, fmt.Errorf("%w: %s is not an Actor", ErrWrongType, a.kind)
  40. }
  41. a.name, a.nameErr = o.GetNatural("name", "en")
  42. a.handle, a.handleErr = o.GetString("preferredUsername")
  43. a.bio, a.bioErr = o.GetNatural("summary", "en")
  44. a.mediaType, a.mediaTypeErr = o.GetMediaType("mediaType")
  45. a.joined, a.joinedErr = o.GetTime("published")
  46. // TODO: parallelize
  47. a.pfp, a.pfpErr = getBestLink(o, "icon", "image")
  48. a.banner, a.bannerErr = getBestLink(o, "image", "image")
  49. a.posts, a.postsErr = getCollection(o, "outbox", a.id)
  50. return a, nil
  51. }
  52. func (a *Actor) Kind() string {
  53. return a.kind
  54. }
  55. func (a *Actor) Parents(quantity uint) []Tangible {
  56. return []Tangible{}
  57. }
  58. func (a *Actor) Children(quantity uint) ([]Tangible, Container, uint) {
  59. if errors.Is(a.postsErr, object.ErrKeyNotPresent) {
  60. return []Tangible{}, nil, 0
  61. }
  62. if a.postsErr != nil {
  63. return []Tangible{
  64. NewFailure(a.postsErr),
  65. }, nil, 0
  66. }
  67. return a.posts.Harvest(quantity, 0)
  68. }
  69. // TODO: here is where I'd put forgery errors in
  70. func (a *Actor) Name() string {
  71. var output string
  72. if a.nameErr == nil {
  73. output = a.name
  74. } else if !errors.Is(a.nameErr, object.ErrKeyNotPresent) {
  75. output = style.Problem(a.nameErr)
  76. }
  77. if a.id != nil && !errors.Is(a.handleErr, object.ErrKeyNotPresent) {
  78. if output != "" { output += " " }
  79. if a.handleErr != nil {
  80. output += style.Problem(a.handleErr)
  81. } else {
  82. output += style.Italic("@" + a.handle + "@" + a.id.Host)
  83. }
  84. }
  85. if a.kind != "Person" {
  86. if output != "" { output += " " }
  87. output += "(" + strings.ToLower(a.kind) + ")"
  88. } else if output == "" {
  89. output = strings.ToLower(a.kind)
  90. }
  91. return style.Color(output)
  92. }
  93. func (a *Actor) header(width int) string {
  94. output := a.Name()
  95. if a.joinedErr != nil && !errors.Is(a.joinedErr, object.ErrKeyNotPresent) {
  96. output += "\njoined " + style.Problem(a.joinedErr)
  97. } else {
  98. output += "\njoined " + style.Color(a.joined.Format(timeFormat))
  99. }
  100. return ansi.Wrap(output, width)
  101. }
  102. func (a *Actor) center(width int) (string, bool) {
  103. if errors.Is(a.bioErr, object.ErrKeyNotPresent) {
  104. return "", false
  105. }
  106. if a.bioErr != nil {
  107. return ansi.Wrap(style.Problem(a.bioErr), width), true
  108. }
  109. mediaType := a.mediaType
  110. if errors.Is(a.mediaTypeErr, object.ErrKeyNotPresent) {
  111. mediaType = mime.Default()
  112. } else if a.mediaTypeErr != nil {
  113. return ansi.Wrap(style.Problem(a.mediaTypeErr), width), true
  114. }
  115. rendered, err := render.Render(a.bio, mediaType.Essence, width)
  116. if err != nil {
  117. return style.Problem(err), true
  118. }
  119. return rendered, true
  120. }
  121. func (a *Actor) String(width int) string {
  122. output := a.header(width)
  123. if body, present := a.center(width - 4); present {
  124. output += "\n\n" + ansi.Indent(body, " ", true)
  125. }
  126. return output
  127. }
  128. func (a Actor) Preview(width int) string {
  129. output := a.header(width)
  130. // TODO this needs to be truncated
  131. if body, present := a.center(width); present {
  132. output += "\n" + ansi.Snip(body, width, 4, style.Color("\u2026"))
  133. }
  134. return output
  135. }