123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- package apmodels
- import (
- "errors"
- "fmt"
- "net/url"
- "time"
- "github.com/go-fed/activity/streams"
- "github.com/go-fed/activity/streams/vocab"
- "github.com/owncast/owncast/activitypub/crypto"
- "github.com/owncast/owncast/core/data"
- "github.com/owncast/owncast/models"
- log "github.com/sirupsen/logrus"
- )
- // ActivityPubActor represents a single actor in handling ActivityPub activity.
- type ActivityPubActor struct {
- // RequestObject is the actual follow request object.
- RequestObject vocab.ActivityStreamsFollow
- // W3IDSecurityV1PublicKey is the public key of the actor.
- W3IDSecurityV1PublicKey vocab.W3IDSecurityV1PublicKeyProperty
- // ActorIRI is the IRI of the remote actor.
- ActorIri *url.URL
- // FollowRequestIRI is the unique identifier of the follow request.
- FollowRequestIri *url.URL
- // Inbox is the inbox URL of the remote follower
- Inbox *url.URL
- // Image is the avatar image of the Actor.
- Image *url.URL
- // DisabledAt is the time, if any, this follower was blocked/removed.
- DisabledAt *time.Time
- // Name is the display name of the follower.
- Name string
- // Username is the account username of the remote actor.
- Username string
- // FullUsername is the username@account.tld representation of the user.
- FullUsername string
- }
- // DeleteRequest represents a request for delete.
- type DeleteRequest struct {
- ActorIri string
- }
- // ExternalEntity represents an ActivityPub Person, Service or Application.
- type ExternalEntity interface {
- GetJSONLDId() vocab.JSONLDIdProperty
- GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty
- GetActivityStreamsName() vocab.ActivityStreamsNameProperty
- GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty
- GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty
- GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
- }
- // MakeActorFromExernalAPEntity takes a full ActivityPub entity and returns our
- // internal representation of an actor.
- func MakeActorFromExernalAPEntity(entity ExternalEntity) (*ActivityPubActor, error) {
- // Username is required (but not a part of the official ActivityPub spec)
- if entity.GetActivityStreamsPreferredUsername() == nil || entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString() == "" {
- return nil, errors.New("remote activitypub entity does not have a preferred username set, rejecting")
- }
- username := GetFullUsernameFromExternalEntity(entity)
- // Key is required
- if entity.GetW3IDSecurityV1PublicKey() == nil {
- return nil, errors.New("remote activitypub entity does not have a public key set, rejecting")
- }
- // Name is optional
- var name string
- if entity.GetActivityStreamsName() != nil && !entity.GetActivityStreamsName().Empty() {
- name = entity.GetActivityStreamsName().At(0).GetXMLSchemaString()
- }
- // Image is optional
- var image *url.URL
- if entity.GetActivityStreamsIcon() != nil && !entity.GetActivityStreamsIcon().Empty() && entity.GetActivityStreamsIcon().At(0).GetActivityStreamsImage() != nil {
- image = entity.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().Begin().GetIRI()
- }
- apActor := ActivityPubActor{
- ActorIri: entity.GetJSONLDId().Get(),
- Inbox: entity.GetActivityStreamsInbox().GetIRI(),
- Name: name,
- Username: entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString(),
- FullUsername: username,
- W3IDSecurityV1PublicKey: entity.GetW3IDSecurityV1PublicKey(),
- Image: image,
- }
- return &apActor, nil
- }
- // MakeActorPropertyWithID will return an actor property filled with the provided IRI.
- func MakeActorPropertyWithID(idIRI *url.URL) vocab.ActivityStreamsActorProperty {
- actor := streams.NewActivityStreamsActorProperty()
- actor.AppendIRI(idIRI)
- return actor
- }
- // MakeServiceForAccount will create a new local actor service with the the provided username.
- func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
- actorIRI := MakeLocalIRIForAccount(accountName)
- person := streams.NewActivityStreamsService()
- nameProperty := streams.NewActivityStreamsNameProperty()
- nameProperty.AppendXMLSchemaString(data.GetServerName())
- person.SetActivityStreamsName(nameProperty)
- preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty()
- preferredUsernameProperty.SetXMLSchemaString(accountName)
- person.SetActivityStreamsPreferredUsername(preferredUsernameProperty)
- inboxIRI := MakeLocalIRIForResource("/user/" + accountName + "/inbox")
- inboxProp := streams.NewActivityStreamsInboxProperty()
- inboxProp.SetIRI(inboxIRI)
- person.SetActivityStreamsInbox(inboxProp)
- needsFollowApprovalProperty := streams.NewActivityStreamsManuallyApprovesFollowersProperty()
- needsFollowApprovalProperty.Set(data.GetFederationIsPrivate())
- person.SetActivityStreamsManuallyApprovesFollowers(needsFollowApprovalProperty)
- outboxIRI := MakeLocalIRIForResource("/user/" + accountName + "/outbox")
- outboxProp := streams.NewActivityStreamsOutboxProperty()
- outboxProp.SetIRI(outboxIRI)
- person.SetActivityStreamsOutbox(outboxProp)
- id := streams.NewJSONLDIdProperty()
- id.Set(actorIRI)
- person.SetJSONLDId(id)
- publicKey := crypto.GetPublicKey(actorIRI)
- publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty()
- publicKeyType := streams.NewW3IDSecurityV1PublicKey()
- pubKeyIDProp := streams.NewJSONLDIdProperty()
- pubKeyIDProp.Set(publicKey.ID)
- publicKeyType.SetJSONLDId(pubKeyIDProp)
- ownerProp := streams.NewW3IDSecurityV1OwnerProperty()
- ownerProp.SetIRI(publicKey.Owner)
- publicKeyType.SetW3IDSecurityV1Owner(ownerProp)
- publicKeyPemProp := streams.NewW3IDSecurityV1PublicKeyPemProperty()
- publicKeyPemProp.Set(publicKey.PublicKeyPem)
- publicKeyType.SetW3IDSecurityV1PublicKeyPem(publicKeyPemProp)
- publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKeyType)
- person.SetW3IDSecurityV1PublicKey(publicKeyProp)
- if t, err := data.GetServerInitTime(); t != nil {
- publishedDateProp := streams.NewActivityStreamsPublishedProperty()
- publishedDateProp.Set(t.Time)
- person.SetActivityStreamsPublished(publishedDateProp)
- } else {
- log.Errorln("unable to fetch server init time", err)
- }
- // Profile properties
- // Avatar
- uniquenessString := data.GetLogoUniquenessString()
- userAvatarURLString := data.GetServerURL() + "/logo/external"
- userAvatarURL, err := url.Parse(userAvatarURLString)
- userAvatarURL.RawQuery = "uc=" + uniquenessString
- if err != nil {
- log.Errorln("unable to parse user avatar url", userAvatarURLString, err)
- }
- image := streams.NewActivityStreamsImage()
- imgProp := streams.NewActivityStreamsUrlProperty()
- imgProp.AppendIRI(userAvatarURL)
- image.SetActivityStreamsUrl(imgProp)
- icon := streams.NewActivityStreamsIconProperty()
- icon.AppendActivityStreamsImage(image)
- person.SetActivityStreamsIcon(icon)
- // Actor URL
- urlProperty := streams.NewActivityStreamsUrlProperty()
- urlProperty.AppendIRI(actorIRI)
- person.SetActivityStreamsUrl(urlProperty)
- // Profile header
- headerImage := streams.NewActivityStreamsImage()
- headerImgPropURL := streams.NewActivityStreamsUrlProperty()
- headerImgPropURL.AppendIRI(userAvatarURL)
- headerImage.SetActivityStreamsUrl(headerImgPropURL)
- headerImageProp := streams.NewActivityStreamsImageProperty()
- headerImageProp.AppendActivityStreamsImage(headerImage)
- person.SetActivityStreamsImage(headerImageProp)
- // Profile bio
- summaryProperty := streams.NewActivityStreamsSummaryProperty()
- summaryProperty.AppendXMLSchemaString(data.GetServerSummary())
- person.SetActivityStreamsSummary(summaryProperty)
- // Links
- if serverURL := data.GetServerURL(); serverURL != "" {
- addMetadataLinkToProfile(person, "Stream", serverURL)
- }
- for _, link := range data.GetSocialHandles() {
- addMetadataLinkToProfile(person, link.Platform, link.URL)
- }
- // Discoverable
- discoverableProperty := streams.NewTootDiscoverableProperty()
- discoverableProperty.Set(true)
- person.SetTootDiscoverable(discoverableProperty)
- // Followers
- followersProperty := streams.NewActivityStreamsFollowersProperty()
- followersURL := *actorIRI
- followersURL.Path = actorIRI.Path + "/followers"
- followersProperty.SetIRI(&followersURL)
- person.SetActivityStreamsFollowers(followersProperty)
- // Tags
- tagProp := streams.NewActivityStreamsTagProperty()
- for _, tagString := range data.GetServerMetadataTags() {
- hashtag := MakeHashtag(tagString)
- tagProp.AppendTootHashtag(hashtag)
- }
- person.SetActivityStreamsTag(tagProp)
- // Work around an issue where a single attachment will not serialize
- // as an array, so add another item to the mix.
- if len(data.GetSocialHandles()) == 1 {
- addMetadataLinkToProfile(person, "Owncast", "https://owncast.online")
- }
- return person
- }
- // GetFullUsernameFromExternalEntity will return the full username from an
- // internal representation of an ExternalEntity. Returns user@host.tld.
- func GetFullUsernameFromExternalEntity(entity ExternalEntity) string {
- hostname := entity.GetJSONLDId().GetIRI().Hostname()
- username := entity.GetActivityStreamsPreferredUsername().GetXMLSchemaString()
- fullUsername := fmt.Sprintf("%s@%s", username, hostname)
- return fullUsername
- }
- func addMetadataLinkToProfile(profile vocab.ActivityStreamsService, name string, url string) {
- attachments := profile.GetActivityStreamsAttachment()
- if attachments == nil {
- attachments = streams.NewActivityStreamsAttachmentProperty()
- }
- displayName := name
- socialHandle := models.GetSocialHandle(name)
- if socialHandle != nil {
- displayName = socialHandle.Platform
- }
- linkValue := fmt.Sprintf("<a href=\"%s\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\">%s</a>", url, url)
- attachment := streams.NewActivityStreamsObject()
- attachmentProp := streams.NewJSONLDTypeProperty()
- attachmentProp.AppendXMLSchemaString("PropertyValue")
- attachment.SetJSONLDType(attachmentProp)
- attachmentName := streams.NewActivityStreamsNameProperty()
- attachmentName.AppendXMLSchemaString(displayName)
- attachment.SetActivityStreamsName(attachmentName)
- attachment.GetUnknownProperties()["value"] = linkValue
- attachments.AppendActivityStreamsObject(attachment)
- profile.SetActivityStreamsAttachment(attachments)
- }
|