mirror of
				https://github.com/owncast/owncast.git
				synced 2025-11-04 05:17:27 +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><a href=\"%s\">%s</a>", 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)
 | 
						|
}
 |