mirror of
				https://github.com/owncast/owncast.git
				synced 2025-11-01 02:44:31 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			307 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			307 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package outbox
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"net/url"
 | |
| 	"path/filepath"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/go-fed/activity/streams"
 | |
| 	"github.com/go-fed/activity/streams/vocab"
 | |
| 	"github.com/owncast/owncast/activitypub/apmodels"
 | |
| 	"github.com/owncast/owncast/activitypub/crypto"
 | |
| 	"github.com/owncast/owncast/activitypub/persistence"
 | |
| 	"github.com/owncast/owncast/activitypub/requests"
 | |
| 	"github.com/owncast/owncast/activitypub/resolvers"
 | |
| 	"github.com/owncast/owncast/activitypub/webfinger"
 | |
| 	"github.com/owncast/owncast/activitypub/workerpool"
 | |
| 	"github.com/pkg/errors"
 | |
| 
 | |
| 	"github.com/owncast/owncast/config"
 | |
| 	"github.com/owncast/owncast/core/data"
 | |
| 	"github.com/owncast/owncast/utils"
 | |
| 	log "github.com/sirupsen/logrus"
 | |
| 	"github.com/teris-io/shortid"
 | |
| )
 | |
| 
 | |
| // SendLive will send all followers the message saying you started a live stream.
 | |
| func SendLive() error {
 | |
| 	textContent := data.GetFederationGoLiveMessage()
 | |
| 
 | |
| 	// If the message is empty then do not send it.
 | |
| 	if textContent == "" {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	tagStrings := []string{}
 | |
| 	reg := regexp.MustCompile("[^a-zA-Z0-9]+")
 | |
| 
 | |
| 	tagProp := streams.NewActivityStreamsTagProperty()
 | |
| 	for _, tagString := range data.GetServerMetadataTags() {
 | |
| 		tagWithoutSpecialCharacters := reg.ReplaceAllString(tagString, "")
 | |
| 		hashtag := apmodels.MakeHashtag(tagWithoutSpecialCharacters)
 | |
| 		tagProp.AppendTootHashtag(hashtag)
 | |
| 		tagString := getHashtagLinkHTMLFromTagString(tagWithoutSpecialCharacters)
 | |
| 		tagStrings = append(tagStrings, tagString)
 | |
| 	}
 | |
| 
 | |
| 	// Manually add Owncast hashtag if it doesn't already exist so it shows up
 | |
| 	// in Owncast search results.
 | |
| 	// We can remove this down the road, but it'll be nice for now.
 | |
| 	if _, exists := utils.FindInSlice(tagStrings, "owncast"); !exists {
 | |
| 		hashtag := apmodels.MakeHashtag("owncast")
 | |
| 		tagProp.AppendTootHashtag(hashtag)
 | |
| 	}
 | |
| 
 | |
| 	tagsString := strings.Join(tagStrings, " ")
 | |
| 
 | |
| 	var streamTitle string
 | |
| 	if title := data.GetStreamTitle(); title != "" {
 | |
| 		streamTitle = fmt.Sprintf("<p>%s</p>", title)
 | |
| 	}
 | |
| 	textContent = fmt.Sprintf("<p>%s</p>%s<p>%s</p><p><a href=\"%s\">%s</a></p>", textContent, streamTitle, tagsString, data.GetServerURL(), data.GetServerURL())
 | |
| 
 | |
| 	activity, _, note, noteID := createBaseOutboundMessage(textContent)
 | |
| 
 | |
| 	// To the public if we're not treating ActivityPub as "private".
 | |
| 	if !data.GetFederationIsPrivate() {
 | |
| 		note = apmodels.MakeNotePublic(note)
 | |
| 		activity = apmodels.MakeActivityPublic(activity)
 | |
| 	}
 | |
| 
 | |
| 	note.SetActivityStreamsTag(tagProp)
 | |
| 
 | |
| 	// Attach an image along with the Federated message.
 | |
| 	previewURL, err := url.Parse(data.GetServerURL())
 | |
| 	if err == nil {
 | |
| 		var imageToAttach string
 | |
| 		var mediaType string
 | |
| 		previewGif := filepath.Join(config.TempDir, "preview.gif")
 | |
| 		thumbnailJpg := filepath.Join(config.TempDir, "thumbnail.jpg")
 | |
| 		uniquenessString := shortid.MustGenerate()
 | |
| 		if utils.DoesFileExists(previewGif) {
 | |
| 			imageToAttach = "preview.gif"
 | |
| 			mediaType = "image/gif"
 | |
| 		} else if utils.DoesFileExists(thumbnailJpg) {
 | |
| 			imageToAttach = "thumbnail.jpg"
 | |
| 			mediaType = "image/jpeg"
 | |
| 		}
 | |
| 		if imageToAttach != "" {
 | |
| 			previewURL.Path = imageToAttach
 | |
| 			previewURL.RawQuery = "us=" + uniquenessString
 | |
| 			apmodels.AddImageAttachmentToNote(note, previewURL.String(), mediaType)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if data.GetNSFW() {
 | |
| 		// Mark content as sensitive.
 | |
| 		sensitive := streams.NewActivityStreamsSensitiveProperty()
 | |
| 		sensitive.AppendXMLSchemaBoolean(true)
 | |
| 		note.SetActivityStreamsSensitive(sensitive)
 | |
| 	}
 | |
| 
 | |
| 	b, err := apmodels.Serialize(activity)
 | |
| 	if err != nil {
 | |
| 		log.Errorln("unable to serialize go live message activity", err)
 | |
| 		return errors.New("unable to serialize go live message activity " + err.Error())
 | |
| 	}
 | |
| 
 | |
| 	if err := SendToFollowers(b); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if err := Add(note, noteID, true); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // SendDirectMessageToAccount will send a direct message to a single account.
 | |
| func SendDirectMessageToAccount(textContent, account string) error {
 | |
| 	links, err := webfinger.GetWebfingerLinks(account)
 | |
| 	if err != nil {
 | |
| 		return errors.Wrap(err, "unable to get webfinger links when sending private message")
 | |
| 	}
 | |
| 	user := apmodels.MakeWebFingerRequestResponseFromData(links)
 | |
| 
 | |
| 	iri := user.Self
 | |
| 	actor, err := resolvers.GetResolvedActorFromIRI(iri)
 | |
| 	if err != nil {
 | |
| 		return errors.Wrap(err, "unable to resolve actor to send message to")
 | |
| 	}
 | |
| 
 | |
| 	activity, _, note, _ := createBaseOutboundMessage(textContent)
 | |
| 
 | |
| 	// Set direct message visibility
 | |
| 	activity = apmodels.MakeActivityDirect(activity, actor.ActorIri)
 | |
| 	note = apmodels.MakeNoteDirect(note, actor.ActorIri)
 | |
| 	object := activity.GetActivityStreamsObject()
 | |
| 	object.SetActivityStreamsNote(0, note)
 | |
| 
 | |
| 	b, err := apmodels.Serialize(activity)
 | |
| 	if err != nil {
 | |
| 		log.Errorln("unable to serialize custom fediverse message activity", err)
 | |
| 		return errors.Wrap(err, "unable to serialize custom fediverse message activity")
 | |
| 	}
 | |
| 
 | |
| 	return SendToUser(actor.Inbox, b)
 | |
| }
 | |
| 
 | |
| // SendPublicMessage will send a public message to all followers.
 | |
| func SendPublicMessage(textContent string) error {
 | |
| 	originalContent := textContent
 | |
| 	textContent = utils.RenderSimpleMarkdown(textContent)
 | |
| 
 | |
| 	tagProp := streams.NewActivityStreamsTagProperty()
 | |
| 
 | |
| 	hashtagStrings := utils.GetHashtagsFromText(originalContent)
 | |
| 
 | |
| 	for _, hashtag := range hashtagStrings {
 | |
| 		tagWithoutHashtag := strings.TrimPrefix(hashtag, "#")
 | |
| 
 | |
| 		// Replace the instances of the tag with a link to the tag page.
 | |
| 		tagHTML := getHashtagLinkHTMLFromTagString(tagWithoutHashtag)
 | |
| 		textContent = strings.ReplaceAll(textContent, hashtag, tagHTML)
 | |
| 
 | |
| 		// Create Hashtag object for the tag.
 | |
| 		hashtag := apmodels.MakeHashtag(tagWithoutHashtag)
 | |
| 		tagProp.AppendTootHashtag(hashtag)
 | |
| 	}
 | |
| 
 | |
| 	activity, _, note, noteID := createBaseOutboundMessage(textContent)
 | |
| 	note.SetActivityStreamsTag(tagProp)
 | |
| 
 | |
| 	if !data.GetFederationIsPrivate() {
 | |
| 		note = apmodels.MakeNotePublic(note)
 | |
| 		activity = apmodels.MakeActivityPublic(activity)
 | |
| 	}
 | |
| 
 | |
| 	b, err := apmodels.Serialize(activity)
 | |
| 	if err != nil {
 | |
| 		log.Errorln("unable to serialize custom fediverse message activity", err)
 | |
| 		return errors.New("unable to serialize custom fediverse message activity " + err.Error())
 | |
| 	}
 | |
| 
 | |
| 	if err := SendToFollowers(b); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if err := Add(note, noteID, false); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // nolint: unparam
 | |
| func createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, string, vocab.ActivityStreamsNote, string) {
 | |
| 	localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
 | |
| 	noteID := shortid.MustGenerate()
 | |
| 	noteIRI := apmodels.MakeLocalIRIForResource(noteID)
 | |
| 	id := shortid.MustGenerate()
 | |
| 	activity := apmodels.CreateCreateActivity(id, localActor)
 | |
| 	object := streams.NewActivityStreamsObjectProperty()
 | |
| 	activity.SetActivityStreamsObject(object)
 | |
| 
 | |
| 	note := apmodels.MakeNote(textContent, noteIRI, localActor)
 | |
| 	object.AppendActivityStreamsNote(note)
 | |
| 
 | |
| 	return activity, id, note, noteID
 | |
| }
 | |
| 
 | |
| // Get Hashtag HTML link for a given tag (without # prefix).
 | |
| func getHashtagLinkHTMLFromTagString(baseHashtag string) string {
 | |
| 	return fmt.Sprintf("<a class=\"hashtag\" href=\"https://directory.owncast.online/tags/%s\">#%s</a>", baseHashtag, baseHashtag)
 | |
| }
 | |
| 
 | |
| // SendToFollowers will send an arbitrary payload to all follower inboxes.
 | |
| func SendToFollowers(payload []byte) error {
 | |
| 	localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
 | |
| 
 | |
| 	followers, _, err := persistence.GetFederationFollowers(-1, 0)
 | |
| 	if err != nil {
 | |
| 		log.Errorln("unable to fetch followers to send to", err)
 | |
| 		return errors.New("unable to fetch followers to send payload to")
 | |
| 	}
 | |
| 
 | |
| 	for _, follower := range followers {
 | |
| 		inbox, _ := url.Parse(follower.Inbox)
 | |
| 		req, err := crypto.CreateSignedRequest(payload, inbox, localActor)
 | |
| 		if err != nil {
 | |
| 			log.Errorln("unable to create outbox request", follower.Inbox, err)
 | |
| 			return errors.New("unable to create outbox request: " + follower.Inbox)
 | |
| 		}
 | |
| 
 | |
| 		workerpool.AddToOutboundQueue(req)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // SendToUser will send a payload to a single specific inbox.
 | |
| func SendToUser(inbox *url.URL, payload []byte) error {
 | |
| 	localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
 | |
| 
 | |
| 	req, err := requests.CreateSignedRequest(payload, inbox, localActor)
 | |
| 	if err != nil {
 | |
| 		return errors.Wrap(err, "unable to create outbox request")
 | |
| 	}
 | |
| 
 | |
| 	workerpool.AddToOutboundQueue(req)
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // UpdateFollowersWithAccountUpdates will send an update to all followers alerting of a profile update.
 | |
| func UpdateFollowersWithAccountUpdates() error {
 | |
| 	// Don't do anything if federation is disabled.
 | |
| 	if !data.GetFederationEnabled() {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	id := shortid.MustGenerate()
 | |
| 	objectID := apmodels.MakeLocalIRIForResource(id)
 | |
| 	activity := apmodels.MakeUpdateActivity(objectID)
 | |
| 
 | |
| 	actor := streams.NewActivityStreamsPerson()
 | |
| 	actorID := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
 | |
| 	actorIDProperty := streams.NewJSONLDIdProperty()
 | |
| 	actorIDProperty.Set(actorID)
 | |
| 	actor.SetJSONLDId(actorIDProperty)
 | |
| 
 | |
| 	actorProperty := streams.NewActivityStreamsActorProperty()
 | |
| 	actorProperty.AppendActivityStreamsPerson(actor)
 | |
| 	activity.SetActivityStreamsActor(actorProperty)
 | |
| 
 | |
| 	obj := streams.NewActivityStreamsObjectProperty()
 | |
| 	obj.AppendIRI(actorID)
 | |
| 	activity.SetActivityStreamsObject(obj)
 | |
| 
 | |
| 	b, err := apmodels.Serialize(activity)
 | |
| 	if err != nil {
 | |
| 		log.Errorln("unable to serialize send update actor activity", err)
 | |
| 		return errors.New("unable to serialize send update actor activity")
 | |
| 	}
 | |
| 	return SendToFollowers(b)
 | |
| }
 | |
| 
 | |
| // Add will save an ActivityPub object to the datastore.
 | |
| func Add(item vocab.Type, id string, isLiveNotification bool) error {
 | |
| 	iri := item.GetJSONLDId().GetIRI().String()
 | |
| 	typeString := item.GetTypeName()
 | |
| 
 | |
| 	if iri == "" {
 | |
| 		log.Errorln("Unable to get iri from item")
 | |
| 		return errors.New("Unable to get iri from item " + id)
 | |
| 	}
 | |
| 
 | |
| 	b, err := apmodels.Serialize(item)
 | |
| 	if err != nil {
 | |
| 		log.Errorln("unable to serialize model when saving to outbox", err)
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return persistence.AddToOutbox(iri, b, typeString, isLiveNotification)
 | |
| }
 | 
