outbox.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. package outbox
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/url"
  6. "path/filepath"
  7. "regexp"
  8. "strings"
  9. "github.com/go-fed/activity/streams"
  10. "github.com/go-fed/activity/streams/vocab"
  11. "github.com/owncast/owncast/activitypub/apmodels"
  12. "github.com/owncast/owncast/activitypub/persistence"
  13. "github.com/owncast/owncast/activitypub/requests"
  14. "github.com/owncast/owncast/activitypub/workerpool"
  15. "github.com/owncast/owncast/config"
  16. "github.com/owncast/owncast/core/data"
  17. "github.com/owncast/owncast/utils"
  18. log "github.com/sirupsen/logrus"
  19. "github.com/teris-io/shortid"
  20. )
  21. // SendLive will send all followers the message saying you started a live stream.
  22. func SendLive() error {
  23. textContent := data.GetFederationGoLiveMessage()
  24. // If the message is empty then do not send it.
  25. if textContent == "" {
  26. return nil
  27. }
  28. tagStrings := []string{}
  29. reg := regexp.MustCompile("[^a-zA-Z0-9]+")
  30. tagProp := streams.NewActivityStreamsTagProperty()
  31. for _, tagString := range data.GetServerMetadataTags() {
  32. tagWithoutSpecialCharacters := reg.ReplaceAllString(tagString, "")
  33. hashtag := apmodels.MakeHashtag(tagWithoutSpecialCharacters)
  34. tagProp.AppendTootHashtag(hashtag)
  35. tagString := getHashtagLinkHTMLFromTagString(tagWithoutSpecialCharacters)
  36. tagStrings = append(tagStrings, tagString)
  37. }
  38. // Manually add Owncast hashtag if it doesn't already exist so it shows up
  39. // in Owncast search results.
  40. // We can remove this down the road, but it'll be nice for now.
  41. if _, exists := utils.FindInSlice(tagStrings, "owncast"); !exists {
  42. hashtag := apmodels.MakeHashtag("owncast")
  43. tagProp.AppendTootHashtag(hashtag)
  44. }
  45. tagsString := strings.Join(tagStrings, " ")
  46. var streamTitle string
  47. if title := data.GetStreamTitle(); title != "" {
  48. streamTitle = fmt.Sprintf("<p>%s</p>", title)
  49. }
  50. textContent = fmt.Sprintf("<p>%s</p><p>%s</p><p>%s</p><a href=\"%s\">%s</a>", textContent, streamTitle, tagsString, data.GetServerURL(), data.GetServerURL())
  51. activity, _, note, noteID := createBaseOutboundMessage(textContent)
  52. note.SetActivityStreamsTag(tagProp)
  53. // Attach an image along with the Federated message.
  54. previewURL, err := url.Parse(data.GetServerURL())
  55. if err == nil {
  56. var imageToAttach string
  57. previewGif := filepath.Join(config.WebRoot, "preview.gif")
  58. thumbnailJpg := filepath.Join(config.WebRoot, "thumbnail.jpg")
  59. uniquenessString := shortid.MustGenerate()
  60. if utils.DoesFileExists(previewGif) {
  61. imageToAttach = "preview.gif"
  62. } else if utils.DoesFileExists(thumbnailJpg) {
  63. imageToAttach = "thumbnail.jpg"
  64. }
  65. if imageToAttach != "" {
  66. previewURL.Path = imageToAttach
  67. previewURL.RawQuery = "us=" + uniquenessString
  68. apmodels.AddImageAttachmentToNote(note, previewURL.String())
  69. }
  70. }
  71. if data.GetNSFW() {
  72. // Mark content as sensitive.
  73. sensitive := streams.NewActivityStreamsSensitiveProperty()
  74. sensitive.AppendXMLSchemaBoolean(true)
  75. note.SetActivityStreamsSensitive(sensitive)
  76. }
  77. b, err := apmodels.Serialize(activity)
  78. if err != nil {
  79. log.Errorln("unable to serialize go live message activity", err)
  80. return errors.New("unable to serialize go live message activity " + err.Error())
  81. }
  82. if err := SendToFollowers(b); err != nil {
  83. return err
  84. }
  85. if err := Add(note, noteID, true); err != nil {
  86. return err
  87. }
  88. return nil
  89. }
  90. // SendPublicMessage will send a public message to all followers.
  91. func SendPublicMessage(textContent string) error {
  92. originalContent := textContent
  93. textContent = utils.RenderSimpleMarkdown(textContent)
  94. tagProp := streams.NewActivityStreamsTagProperty()
  95. hashtagStrings := utils.GetHashtagsFromText(originalContent)
  96. for _, hashtag := range hashtagStrings {
  97. tagWithoutHashtag := strings.TrimPrefix(hashtag, "#")
  98. // Replace the instances of the tag with a link to the tag page.
  99. tagHTML := getHashtagLinkHTMLFromTagString(tagWithoutHashtag)
  100. textContent = strings.ReplaceAll(textContent, hashtag, tagHTML)
  101. // Create Hashtag object for the tag.
  102. hashtag := apmodels.MakeHashtag(tagWithoutHashtag)
  103. tagProp.AppendTootHashtag(hashtag)
  104. }
  105. activity, _, note, noteID := createBaseOutboundMessage(textContent)
  106. note.SetActivityStreamsTag(tagProp)
  107. b, err := apmodels.Serialize(activity)
  108. if err != nil {
  109. log.Errorln("unable to serialize custom fediverse message activity", err)
  110. return errors.New("unable to serialize custom fediverse message activity " + err.Error())
  111. }
  112. if err := SendToFollowers(b); err != nil {
  113. return err
  114. }
  115. if err := Add(note, noteID, false); err != nil {
  116. return err
  117. }
  118. return nil
  119. }
  120. // nolint: unparam
  121. func createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, string, vocab.ActivityStreamsNote, string) {
  122. localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
  123. noteID := shortid.MustGenerate()
  124. noteIRI := apmodels.MakeLocalIRIForResource(noteID)
  125. id := shortid.MustGenerate()
  126. activity := apmodels.CreateCreateActivity(id, localActor)
  127. object := streams.NewActivityStreamsObjectProperty()
  128. activity.SetActivityStreamsObject(object)
  129. note := apmodels.MakeNote(textContent, noteIRI, localActor)
  130. object.AppendActivityStreamsNote(note)
  131. return activity, id, note, noteID
  132. }
  133. // Get Hashtag HTML link for a given tag (without # prefix).
  134. func getHashtagLinkHTMLFromTagString(baseHashtag string) string {
  135. return fmt.Sprintf("<a class=\"hashtag\" href=\"https://directory.owncast.online/tags/%s\">#%s</a>", baseHashtag, baseHashtag)
  136. }
  137. // SendToFollowers will send an arbitrary payload to all follower inboxes.
  138. func SendToFollowers(payload []byte) error {
  139. localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
  140. followers, _, err := persistence.GetFederationFollowers(-1, 0)
  141. if err != nil {
  142. log.Errorln("unable to fetch followers to send to", err)
  143. return errors.New("unable to fetch followers to send payload to")
  144. }
  145. for _, follower := range followers {
  146. inbox, _ := url.Parse(follower.Inbox)
  147. req, err := requests.CreateSignedRequest(payload, inbox, localActor)
  148. if err != nil {
  149. log.Errorln("unable to create outbox request", follower.Inbox, err)
  150. return errors.New("unable to create outbox request: " + follower.Inbox)
  151. }
  152. workerpool.AddToOutboundQueue(req)
  153. }
  154. return nil
  155. }
  156. // UpdateFollowersWithAccountUpdates will send an update to all followers alerting of a profile update.
  157. func UpdateFollowersWithAccountUpdates() error {
  158. // Don't do anything if federation is disabled.
  159. if !data.GetFederationEnabled() {
  160. return nil
  161. }
  162. id := shortid.MustGenerate()
  163. objectID := apmodels.MakeLocalIRIForResource(id)
  164. activity := apmodels.MakeUpdateActivity(objectID)
  165. actor := streams.NewActivityStreamsPerson()
  166. actorID := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
  167. actorIDProperty := streams.NewJSONLDIdProperty()
  168. actorIDProperty.Set(actorID)
  169. actor.SetJSONLDId(actorIDProperty)
  170. actorProperty := streams.NewActivityStreamsActorProperty()
  171. actorProperty.AppendActivityStreamsPerson(actor)
  172. activity.SetActivityStreamsActor(actorProperty)
  173. obj := streams.NewActivityStreamsObjectProperty()
  174. obj.AppendIRI(actorID)
  175. activity.SetActivityStreamsObject(obj)
  176. b, err := apmodels.Serialize(activity)
  177. if err != nil {
  178. log.Errorln("unable to serialize send update actor activity", err)
  179. return errors.New("unable to serialize send update actor activity")
  180. }
  181. return SendToFollowers(b)
  182. }
  183. // Add will save an ActivityPub object to the datastore.
  184. func Add(item vocab.Type, id string, isLiveNotification bool) error {
  185. iri := item.GetJSONLDId().GetIRI().String()
  186. typeString := item.GetTypeName()
  187. if iri == "" {
  188. log.Errorln("Unable to get iri from item")
  189. return errors.New("Unable to get iri from item " + id)
  190. }
  191. b, err := apmodels.Serialize(item)
  192. if err != nil {
  193. log.Errorln("unable to serialize model when saving to outbox", err)
  194. return err
  195. }
  196. return persistence.AddToOutbox(iri, b, typeString, isLiveNotification)
  197. }