/* GoToSocial Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package typeutils import ( "context" "crypto/x509" "encoding/pem" "fmt" "net/url" "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) // Converts a gts model account into an Activity Streams person type, following // the spec laid out for mastodon here: https://docs.joinmastodon.org/spec/activitypub/ func (c *converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) { // first check if we have this person in our asCache already if personI, err := c.asCache.Fetch(a.ID); err == nil { if person, ok := personI.(vocab.ActivityStreamsPerson); ok { // we have it, so just return it as-is return person, nil } } person := streams.NewActivityStreamsPerson() // id should be the activitypub URI of this user // something like https://example.org/users/example_user profileIDURI, err := url.Parse(a.URI) if err != nil { return nil, err } idProp := streams.NewJSONLDIdProperty() idProp.SetIRI(profileIDURI) person.SetJSONLDId(idProp) // following // The URI for retrieving a list of accounts this user is following followingURI, err := url.Parse(a.FollowingURI) if err != nil { return nil, err } followingProp := streams.NewActivityStreamsFollowingProperty() followingProp.SetIRI(followingURI) person.SetActivityStreamsFollowing(followingProp) // followers // The URI for retrieving a list of this user's followers followersURI, err := url.Parse(a.FollowersURI) if err != nil { return nil, err } followersProp := streams.NewActivityStreamsFollowersProperty() followersProp.SetIRI(followersURI) person.SetActivityStreamsFollowers(followersProp) // inbox // the activitypub inbox of this user for accepting messages inboxURI, err := url.Parse(a.InboxURI) if err != nil { return nil, err } inboxProp := streams.NewActivityStreamsInboxProperty() inboxProp.SetIRI(inboxURI) person.SetActivityStreamsInbox(inboxProp) // outbox // the activitypub outbox of this user for serving messages outboxURI, err := url.Parse(a.OutboxURI) if err != nil { return nil, err } outboxProp := streams.NewActivityStreamsOutboxProperty() outboxProp.SetIRI(outboxURI) person.SetActivityStreamsOutbox(outboxProp) // featured posts // Pinned posts. featuredURI, err := url.Parse(a.FeaturedCollectionURI) if err != nil { return nil, err } featuredProp := streams.NewTootFeaturedProperty() featuredProp.SetIRI(featuredURI) person.SetTootFeatured(featuredProp) // featuredTags // NOT IMPLEMENTED // preferredUsername // Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI. preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty() preferredUsernameProp.SetXMLSchemaString(a.Username) person.SetActivityStreamsPreferredUsername(preferredUsernameProp) // name // Used as profile display name. nameProp := streams.NewActivityStreamsNameProperty() if a.Username != "" { nameProp.AppendXMLSchemaString(a.DisplayName) } else { nameProp.AppendXMLSchemaString(a.Username) } person.SetActivityStreamsName(nameProp) // summary // Used as profile bio. if a.Note != "" { summaryProp := streams.NewActivityStreamsSummaryProperty() summaryProp.AppendXMLSchemaString(a.Note) person.SetActivityStreamsSummary(summaryProp) } // url // Used as profile link. profileURL, err := url.Parse(a.URL) if err != nil { return nil, err } urlProp := streams.NewActivityStreamsUrlProperty() urlProp.AppendIRI(profileURL) person.SetActivityStreamsUrl(urlProp) // manuallyApprovesFollowers // Will be shown as a locked account. manuallyApprovesFollowersProp := streams.NewActivityStreamsManuallyApprovesFollowersProperty() manuallyApprovesFollowersProp.Set(a.Locked) person.SetActivityStreamsManuallyApprovesFollowers(manuallyApprovesFollowersProp) // discoverable // Will be shown in the profile directory. discoverableProp := streams.NewTootDiscoverableProperty() discoverableProp.Set(a.Discoverable) person.SetTootDiscoverable(discoverableProp) // devices // NOT IMPLEMENTED, probably won't implement // alsoKnownAs // Required for Move activity. // TODO: NOT IMPLEMENTED **YET** -- this needs to be added as an activitypub extension to https://github.com/go-fed/activity, see https://github.com/go-fed/activity/tree/master/astool // publicKey // Required for signatures. publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty() // create the public key publicKey := streams.NewW3IDSecurityV1PublicKey() // set ID for the public key publicKeyIDProp := streams.NewJSONLDIdProperty() publicKeyURI, err := url.Parse(a.PublicKeyURI) if err != nil { return nil, err } publicKeyIDProp.SetIRI(publicKeyURI) publicKey.SetJSONLDId(publicKeyIDProp) // set owner for the public key publicKeyOwnerProp := streams.NewW3IDSecurityV1OwnerProperty() publicKeyOwnerProp.SetIRI(profileIDURI) publicKey.SetW3IDSecurityV1Owner(publicKeyOwnerProp) // set the pem key itself encodedPublicKey, err := x509.MarshalPKIXPublicKey(a.PublicKey) if err != nil { return nil, err } publicKeyBytes := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: encodedPublicKey, }) publicKeyPEMProp := streams.NewW3IDSecurityV1PublicKeyPemProperty() publicKeyPEMProp.Set(string(publicKeyBytes)) publicKey.SetW3IDSecurityV1PublicKeyPem(publicKeyPEMProp) // append the public key to the public key property publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey) // set the public key property on the Person person.SetW3IDSecurityV1PublicKey(publicKeyProp) // tag // TODO: Any tags used in the summary of this profile // attachment // Used for profile fields. // TODO: The PropertyValue type has to be added: https://schema.org/PropertyValue // endpoints // NOT IMPLEMENTED -- this is for shared inbox which we don't use // icon // Used as profile avatar. if a.AvatarMediaAttachmentID != "" { if a.AvatarMediaAttachment == nil { avatar := >smodel.MediaAttachment{} if err := c.db.GetByID(ctx, a.AvatarMediaAttachmentID, avatar); err != nil { return nil, err } a.AvatarMediaAttachment = avatar } iconProperty := streams.NewActivityStreamsIconProperty() iconImage := streams.NewActivityStreamsImage() mediaType := streams.NewActivityStreamsMediaTypeProperty() mediaType.Set(a.AvatarMediaAttachment.File.ContentType) iconImage.SetActivityStreamsMediaType(mediaType) avatarURLProperty := streams.NewActivityStreamsUrlProperty() avatarURL, err := url.Parse(a.AvatarMediaAttachment.URL) if err != nil { return nil, err } avatarURLProperty.AppendIRI(avatarURL) iconImage.SetActivityStreamsUrl(avatarURLProperty) iconProperty.AppendActivityStreamsImage(iconImage) person.SetActivityStreamsIcon(iconProperty) } // image // Used as profile header. if a.HeaderMediaAttachmentID != "" { if a.HeaderMediaAttachment == nil { header := >smodel.MediaAttachment{} if err := c.db.GetByID(ctx, a.HeaderMediaAttachmentID, header); err != nil { return nil, err } a.HeaderMediaAttachment = header } headerProperty := streams.NewActivityStreamsImageProperty() headerImage := streams.NewActivityStreamsImage() mediaType := streams.NewActivityStreamsMediaTypeProperty() mediaType.Set(a.HeaderMediaAttachment.File.ContentType) headerImage.SetActivityStreamsMediaType(mediaType) headerURLProperty := streams.NewActivityStreamsUrlProperty() headerURL, err := url.Parse(a.HeaderMediaAttachment.URL) if err != nil { return nil, err } headerURLProperty.AppendIRI(headerURL) headerImage.SetActivityStreamsUrl(headerURLProperty) headerProperty.AppendActivityStreamsImage(headerImage) person.SetActivityStreamsImage(headerProperty) } // put the person in our cache in case we need it again soon if err := c.asCache.Store(a.ID, person); err != nil { return nil, err } return person, nil } // Converts a gts model account into a VERY MINIMAL Activity Streams person type, following // the spec laid out for mastodon here: https://docs.joinmastodon.org/spec/activitypub/ // // The returned account will just have the Type, Username, PublicKey, and ID properties set. func (c *converter) AccountToASMinimal(ctx context.Context, a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) { person := streams.NewActivityStreamsPerson() // id should be the activitypub URI of this user // something like https://example.org/users/example_user profileIDURI, err := url.Parse(a.URI) if err != nil { return nil, err } idProp := streams.NewJSONLDIdProperty() idProp.SetIRI(profileIDURI) person.SetJSONLDId(idProp) // preferredUsername // Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI. preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty() preferredUsernameProp.SetXMLSchemaString(a.Username) person.SetActivityStreamsPreferredUsername(preferredUsernameProp) // publicKey // Required for signatures. publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty() // create the public key publicKey := streams.NewW3IDSecurityV1PublicKey() // set ID for the public key publicKeyIDProp := streams.NewJSONLDIdProperty() publicKeyURI, err := url.Parse(a.PublicKeyURI) if err != nil { return nil, err } publicKeyIDProp.SetIRI(publicKeyURI) publicKey.SetJSONLDId(publicKeyIDProp) // set owner for the public key publicKeyOwnerProp := streams.NewW3IDSecurityV1OwnerProperty() publicKeyOwnerProp.SetIRI(profileIDURI) publicKey.SetW3IDSecurityV1Owner(publicKeyOwnerProp) // set the pem key itself encodedPublicKey, err := x509.MarshalPKIXPublicKey(a.PublicKey) if err != nil { return nil, err } publicKeyBytes := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: encodedPublicKey, }) publicKeyPEMProp := streams.NewW3IDSecurityV1PublicKeyPemProperty() publicKeyPEMProp.Set(string(publicKeyBytes)) publicKey.SetW3IDSecurityV1PublicKeyPem(publicKeyPEMProp) // append the public key to the public key property publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey) // set the public key property on the Person person.SetW3IDSecurityV1PublicKey(publicKeyProp) return person, nil } func (c *converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) { // first check if we have this note in our asCache already if noteI, err := c.asCache.Fetch(s.ID); err == nil { if note, ok := noteI.(vocab.ActivityStreamsNote); ok { // we have it, so just return it as-is return note, nil } } // ensure prerequisites here before we get stuck in // check if author account is already attached to status and attach it if not // if we can't retrieve this, bail here already because we can't attribute the status to anyone if s.Account == nil { a, err := c.db.GetAccountByID(ctx, s.AccountID) if err != nil { return nil, fmt.Errorf("StatusToAS: error retrieving author account from db: %s", err) } s.Account = a } // create the Note! status := streams.NewActivityStreamsNote() // id statusURI, err := url.Parse(s.URI) if err != nil { return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URI, err) } statusIDProp := streams.NewJSONLDIdProperty() statusIDProp.SetIRI(statusURI) status.SetJSONLDId(statusIDProp) // type // will be set automatically by go-fed // summary aka cw statusSummaryProp := streams.NewActivityStreamsSummaryProperty() statusSummaryProp.AppendXMLSchemaString(s.ContentWarning) status.SetActivityStreamsSummary(statusSummaryProp) // inReplyTo if s.InReplyToID != "" { // fetch the replied status if we don't have it on hand already if s.InReplyTo == nil { rs := >smodel.Status{} if err := c.db.GetByID(ctx, s.InReplyToID, rs); err != nil { return nil, fmt.Errorf("StatusToAS: error retrieving replied-to status from db: %s", err) } s.InReplyTo = rs } rURI, err := url.Parse(s.InReplyTo.URI) if err != nil { return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.InReplyTo.URI, err) } inReplyToProp := streams.NewActivityStreamsInReplyToProperty() inReplyToProp.AppendIRI(rURI) status.SetActivityStreamsInReplyTo(inReplyToProp) } // published publishedProp := streams.NewActivityStreamsPublishedProperty() publishedProp.Set(s.CreatedAt) status.SetActivityStreamsPublished(publishedProp) // url if s.URL != "" { sURL, err := url.Parse(s.URL) if err != nil { return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.URL, err) } urlProp := streams.NewActivityStreamsUrlProperty() urlProp.AppendIRI(sURL) status.SetActivityStreamsUrl(urlProp) } // attributedTo authorAccountURI, err := url.Parse(s.Account.URI) if err != nil { return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.Account.URI, err) } attributedToProp := streams.NewActivityStreamsAttributedToProperty() attributedToProp.AppendIRI(authorAccountURI) status.SetActivityStreamsAttributedTo(attributedToProp) // tags tagProp := streams.NewActivityStreamsTagProperty() // tag -- mentions for _, m := range s.Mentions { asMention, err := c.MentionToAS(ctx, m) if err != nil { return nil, fmt.Errorf("StatusToAS: error converting mention to AS mention: %s", err) } tagProp.AppendActivityStreamsMention(asMention) } // tag -- emojis // TODO // tag -- hashtags // TODO status.SetActivityStreamsTag(tagProp) // parse out some URIs we need here authorFollowersURI, err := url.Parse(s.Account.FollowersURI) if err != nil { return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", s.Account.FollowersURI, err) } publicURI, err := url.Parse(asPublicURI) if err != nil { return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", asPublicURI, err) } // to and cc toProp := streams.NewActivityStreamsToProperty() ccProp := streams.NewActivityStreamsCcProperty() switch s.Visibility { case gtsmodel.VisibilityDirect: // if DIRECT, then only mentioned users should be added to TO, and nothing to CC for _, m := range s.Mentions { iri, err := url.Parse(m.OriginAccount.URI) if err != nil { return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err) } toProp.AppendIRI(iri) } case gtsmodel.VisibilityMutualsOnly: // TODO case gtsmodel.VisibilityFollowersOnly: // if FOLLOWERS ONLY then we want to add followers to TO, and mentions to CC toProp.AppendIRI(authorFollowersURI) for _, m := range s.Mentions { iri, err := url.Parse(m.OriginAccount.URI) if err != nil { return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err) } ccProp.AppendIRI(iri) } case gtsmodel.VisibilityUnlocked: // if UNLOCKED, we want to add followers to TO, and public and mentions to CC toProp.AppendIRI(authorFollowersURI) ccProp.AppendIRI(publicURI) for _, m := range s.Mentions { iri, err := url.Parse(m.OriginAccount.URI) if err != nil { return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err) } ccProp.AppendIRI(iri) } case gtsmodel.VisibilityPublic: // if PUBLIC, we want to add public to TO, and followers and mentions to CC toProp.AppendIRI(publicURI) ccProp.AppendIRI(authorFollowersURI) for _, m := range s.Mentions { iri, err := url.Parse(m.OriginAccount.URI) if err != nil { return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.OriginAccount.URI, err) } ccProp.AppendIRI(iri) } } status.SetActivityStreamsTo(toProp) status.SetActivityStreamsCc(ccProp) // conversation // TODO // content -- the actual post itself contentProp := streams.NewActivityStreamsContentProperty() contentProp.AppendXMLSchemaString(s.Content) status.SetActivityStreamsContent(contentProp) // attachment attachmentProp := streams.NewActivityStreamsAttachmentProperty() for _, a := range s.Attachments { doc, err := c.AttachmentToAS(ctx, a) if err != nil { return nil, fmt.Errorf("StatusToAS: error converting attachment: %s", err) } attachmentProp.AppendActivityStreamsDocument(doc) } status.SetActivityStreamsAttachment(attachmentProp) // replies repliesCollection, err := c.StatusToASRepliesCollection(ctx, s, false) if err != nil { return nil, fmt.Errorf("error creating repliesCollection: %s", err) } repliesProp := streams.NewActivityStreamsRepliesProperty() repliesProp.SetActivityStreamsCollection(repliesCollection) status.SetActivityStreamsReplies(repliesProp) // put the note in our cache in case we need it again soon if err := c.asCache.Store(s.ID, status); err != nil { return nil, err } return status, nil } func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) { // parse out the various URIs we need for this // origin account (who's doing the follow) originAccountURI, err := url.Parse(originAccount.URI) if err != nil { return nil, fmt.Errorf("followtoasfollow: error parsing origin account uri: %s", err) } originActor := streams.NewActivityStreamsActorProperty() originActor.AppendIRI(originAccountURI) // target account (who's being followed) targetAccountURI, err := url.Parse(targetAccount.URI) if err != nil { return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err) } // uri of the follow activity itself followURI, err := url.Parse(f.URI) if err != nil { return nil, fmt.Errorf("followtoasfollow: error parsing follow uri: %s", err) } // start preparing the follow activity follow := streams.NewActivityStreamsFollow() // set the actor follow.SetActivityStreamsActor(originActor) // set the id followIDProp := streams.NewJSONLDIdProperty() followIDProp.SetIRI(followURI) follow.SetJSONLDId(followIDProp) // set the object followObjectProp := streams.NewActivityStreamsObjectProperty() followObjectProp.AppendIRI(targetAccountURI) follow.SetActivityStreamsObject(followObjectProp) // set the To property followToProp := streams.NewActivityStreamsToProperty() followToProp.AppendIRI(targetAccountURI) follow.SetActivityStreamsTo(followToProp) return follow, nil } func (c *converter) MentionToAS(ctx context.Context, m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error) { if m.TargetAccount == nil { a, err := c.db.GetAccountByID(ctx, m.TargetAccountID) if err != nil { return nil, fmt.Errorf("MentionToAS: error getting target account from db: %s", err) } m.TargetAccount = a } // create the mention mention := streams.NewActivityStreamsMention() // href -- this should be the URI of the mentioned user hrefProp := streams.NewActivityStreamsHrefProperty() hrefURI, err := url.Parse(m.TargetAccount.URI) if err != nil { return nil, fmt.Errorf("MentionToAS: error parsing uri %s: %s", m.TargetAccount.URI, err) } hrefProp.SetIRI(hrefURI) mention.SetActivityStreamsHref(hrefProp) // name -- this should be the namestring of the mentioned user, something like @whatever@example.org var domain string if m.TargetAccount.Domain == "" { domain = c.config.AccountDomain } else { domain = m.TargetAccount.Domain } username := m.TargetAccount.Username nameString := fmt.Sprintf("@%s@%s", username, domain) nameProp := streams.NewActivityStreamsNameProperty() nameProp.AppendXMLSchemaString(nameString) mention.SetActivityStreamsName(nameProp) return mention, nil } func (c *converter) AttachmentToAS(ctx context.Context, a *gtsmodel.MediaAttachment) (vocab.ActivityStreamsDocument, error) { // type -- Document doc := streams.NewActivityStreamsDocument() // mediaType aka mime content type mediaTypeProp := streams.NewActivityStreamsMediaTypeProperty() mediaTypeProp.Set(a.File.ContentType) doc.SetActivityStreamsMediaType(mediaTypeProp) // url -- for the original image not the thumbnail urlProp := streams.NewActivityStreamsUrlProperty() imageURL, err := url.Parse(a.URL) if err != nil { return nil, fmt.Errorf("AttachmentToAS: error parsing uri %s: %s", a.URL, err) } urlProp.AppendIRI(imageURL) doc.SetActivityStreamsUrl(urlProp) // name -- aka image description nameProp := streams.NewActivityStreamsNameProperty() nameProp.AppendXMLSchemaString(a.Description) doc.SetActivityStreamsName(nameProp) // blurhash blurProp := streams.NewTootBlurhashProperty() blurProp.Set(a.Blurhash) doc.SetTootBlurhash(blurProp) // focalpoint // TODO return doc, nil } /* We want to end up with something like this: { "@context": "https://www.w3.org/ns/activitystreams", "actor": "https://ondergrond.org/users/dumpsterqueer", "id": "https://ondergrond.org/users/dumpsterqueer#likes/44584", "object": "https://testingtesting123.xyz/users/gotosocial_test_account/statuses/771aea80-a33d-4d6d-8dfd-57d4d2bfcbd4", "type": "Like" } */ func (c *converter) FaveToAS(ctx context.Context, f *gtsmodel.StatusFave) (vocab.ActivityStreamsLike, error) { // check if targetStatus is already pinned to this fave, and fetch it if not if f.Status == nil { s, err := c.db.GetStatusByID(ctx, f.StatusID) if err != nil { return nil, fmt.Errorf("FaveToAS: error fetching target status from database: %s", err) } f.Status = s } // check if the targetAccount is already pinned to this fave, and fetch it if not if f.TargetAccount == nil { a, err := c.db.GetAccountByID(ctx, f.TargetAccountID) if err != nil { return nil, fmt.Errorf("FaveToAS: error fetching target account from database: %s", err) } f.TargetAccount = a } // check if the faving account is already pinned to this fave, and fetch it if not if f.Account == nil { a, err := c.db.GetAccountByID(ctx, f.AccountID) if err != nil { return nil, fmt.Errorf("FaveToAS: error fetching faving account from database: %s", err) } f.Account = a } // create the like like := streams.NewActivityStreamsLike() // set the actor property to the fave-ing account's URI actorProp := streams.NewActivityStreamsActorProperty() actorIRI, err := url.Parse(f.Account.URI) if err != nil { return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Account.URI, err) } actorProp.AppendIRI(actorIRI) like.SetActivityStreamsActor(actorProp) // set the ID property to the fave's URI idProp := streams.NewJSONLDIdProperty() idIRI, err := url.Parse(f.URI) if err != nil { return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.URI, err) } idProp.Set(idIRI) like.SetJSONLDId(idProp) // set the object property to the target status's URI objectProp := streams.NewActivityStreamsObjectProperty() statusIRI, err := url.Parse(f.Status.URI) if err != nil { return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.Status.URI, err) } objectProp.AppendIRI(statusIRI) like.SetActivityStreamsObject(objectProp) // set the TO property to the target account's IRI toProp := streams.NewActivityStreamsToProperty() toIRI, err := url.Parse(f.TargetAccount.URI) if err != nil { return nil, fmt.Errorf("FaveToAS: error parsing uri %s: %s", f.TargetAccount.URI, err) } toProp.AppendIRI(toIRI) like.SetActivityStreamsTo(toProp) return like, nil } func (c *converter) BoostToAS(ctx context.Context, boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error) { // the boosted status is probably pinned to the boostWrapperStatus but double check to make sure if boostWrapperStatus.BoostOf == nil { b, err := c.db.GetStatusByID(ctx, boostWrapperStatus.BoostOfID) if err != nil { return nil, fmt.Errorf("BoostToAS: error getting status with ID %s from the db: %s", boostWrapperStatus.BoostOfID, err) } boostWrapperStatus.BoostOf = b } // create the announce announce := streams.NewActivityStreamsAnnounce() // set the actor boosterURI, err := url.Parse(boostingAccount.URI) if err != nil { return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostingAccount.URI, err) } actorProp := streams.NewActivityStreamsActorProperty() actorProp.AppendIRI(boosterURI) announce.SetActivityStreamsActor(actorProp) // set the ID boostIDURI, err := url.Parse(boostWrapperStatus.URI) if err != nil { return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.URI, err) } idProp := streams.NewJSONLDIdProperty() idProp.SetIRI(boostIDURI) announce.SetJSONLDId(idProp) // set the object boostedStatusURI, err := url.Parse(boostWrapperStatus.BoostOf.URI) if err != nil { return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostWrapperStatus.BoostOf.URI, err) } objectProp := streams.NewActivityStreamsObjectProperty() objectProp.AppendIRI(boostedStatusURI) announce.SetActivityStreamsObject(objectProp) // set the published time publishedProp := streams.NewActivityStreamsPublishedProperty() publishedProp.Set(boostWrapperStatus.CreatedAt) announce.SetActivityStreamsPublished(publishedProp) // set the to followersURI, err := url.Parse(boostingAccount.FollowersURI) if err != nil { return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostingAccount.FollowersURI, err) } toProp := streams.NewActivityStreamsToProperty() toProp.AppendIRI(followersURI) announce.SetActivityStreamsTo(toProp) // set the cc boostedURI, err := url.Parse(boostedAccount.URI) if err != nil { return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostedAccount.URI, err) } publicURI, err := url.Parse(asPublicURI) if err != nil { return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", asPublicURI, err) } ccProp := streams.NewActivityStreamsCcProperty() ccProp.AppendIRI(boostedURI) ccProp.AppendIRI(publicURI) announce.SetActivityStreamsCc(ccProp) return announce, nil } /* we want to end up with something like this: { "@context": "https://www.w3.org/ns/activitystreams", "actor": "https://example.org/users/some_user", "id":"https://example.org/users/some_user/blocks/SOME_ULID_OF_A_BLOCK", "object":"https://some_other.instance/users/some_other_user", "type":"Block" } */ func (c *converter) BlockToAS(ctx context.Context, b *gtsmodel.Block) (vocab.ActivityStreamsBlock, error) { if b.Account == nil { a, err := c.db.GetAccountByID(ctx, b.AccountID) if err != nil { return nil, fmt.Errorf("BlockToAS: error getting block owner account from database: %s", err) } b.Account = a } if b.TargetAccount == nil { a, err := c.db.GetAccountByID(ctx, b.TargetAccountID) if err != nil { return nil, fmt.Errorf("BlockToAS: error getting block target account from database: %s", err) } b.TargetAccount = a } // create the block block := streams.NewActivityStreamsBlock() // set the actor property to the block-ing account's URI actorProp := streams.NewActivityStreamsActorProperty() actorIRI, err := url.Parse(b.Account.URI) if err != nil { return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.Account.URI, err) } actorProp.AppendIRI(actorIRI) block.SetActivityStreamsActor(actorProp) // set the ID property to the blocks's URI idProp := streams.NewJSONLDIdProperty() idIRI, err := url.Parse(b.URI) if err != nil { return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.URI, err) } idProp.Set(idIRI) block.SetJSONLDId(idProp) // set the object property to the target account's URI objectProp := streams.NewActivityStreamsObjectProperty() targetIRI, err := url.Parse(b.TargetAccount.URI) if err != nil { return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.TargetAccount.URI, err) } objectProp.AppendIRI(targetIRI) block.SetActivityStreamsObject(objectProp) // set the TO property to the target account's IRI toProp := streams.NewActivityStreamsToProperty() toIRI, err := url.Parse(b.TargetAccount.URI) if err != nil { return nil, fmt.Errorf("BlockToAS: error parsing uri %s: %s", b.TargetAccount.URI, err) } toProp.AppendIRI(toIRI) block.SetActivityStreamsTo(toProp) return block, nil } /* the goal is to end up with something like this: { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies", "type": "Collection", "first": { "id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?page=true", "type": "CollectionPage", "next": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?only_other_accounts=true&page=true", "partOf": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies", "items": [] } } */ func (c *converter) StatusToASRepliesCollection(ctx context.Context, status *gtsmodel.Status, onlyOtherAccounts bool) (vocab.ActivityStreamsCollection, error) { collectionID := fmt.Sprintf("%s/replies", status.URI) collectionIDURI, err := url.Parse(collectionID) if err != nil { return nil, err } collection := streams.NewActivityStreamsCollection() // collection.id collectionIDProp := streams.NewJSONLDIdProperty() collectionIDProp.SetIRI(collectionIDURI) collection.SetJSONLDId(collectionIDProp) // first first := streams.NewActivityStreamsFirstProperty() firstPage := streams.NewActivityStreamsCollectionPage() // first.id firstPageIDProp := streams.NewJSONLDIdProperty() firstPageID, err := url.Parse(fmt.Sprintf("%s?page=true", collectionID)) if err != nil { return nil, gtserror.NewErrorInternalError(err) } firstPageIDProp.SetIRI(firstPageID) firstPage.SetJSONLDId(firstPageIDProp) // first.next nextProp := streams.NewActivityStreamsNextProperty() nextPropID, err := url.Parse(fmt.Sprintf("%s?only_other_accounts=%t&page=true", collectionID, onlyOtherAccounts)) if err != nil { return nil, gtserror.NewErrorInternalError(err) } nextProp.SetIRI(nextPropID) firstPage.SetActivityStreamsNext(nextProp) // first.partOf partOfProp := streams.NewActivityStreamsPartOfProperty() partOfProp.SetIRI(collectionIDURI) firstPage.SetActivityStreamsPartOf(partOfProp) first.SetActivityStreamsCollectionPage(firstPage) // collection.first collection.SetActivityStreamsFirst(first) return collection, nil } /* the goal is to end up with something like this: { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?only_other_accounts=true&page=true", "type": "CollectionPage", "next": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies?min_id=106720870266901180&only_other_accounts=true&page=true", "partOf": "https://example.org/users/whatever/statuses/01FCNEXAGAKPEX1J7VJRPJP490/replies", "items": [ "https://example.com/users/someone/statuses/106720752853216226", "https://somewhere.online/users/eeeeeeeeeep/statuses/106720870163727231" ] } */ func (c *converter) StatusURIsToASRepliesPage(ctx context.Context, status *gtsmodel.Status, onlyOtherAccounts bool, minID string, replies map[string]*url.URL) (vocab.ActivityStreamsCollectionPage, error) { collectionID := fmt.Sprintf("%s/replies", status.URI) page := streams.NewActivityStreamsCollectionPage() // .id pageIDProp := streams.NewJSONLDIdProperty() pageIDString := fmt.Sprintf("%s?page=true&only_other_accounts=%t", collectionID, onlyOtherAccounts) if minID != "" { pageIDString = fmt.Sprintf("%s&min_id=%s", pageIDString, minID) } pageID, err := url.Parse(pageIDString) if err != nil { return nil, gtserror.NewErrorInternalError(err) } pageIDProp.SetIRI(pageID) page.SetJSONLDId(pageIDProp) // .partOf collectionIDURI, err := url.Parse(collectionID) if err != nil { return nil, err } partOfProp := streams.NewActivityStreamsPartOfProperty() partOfProp.SetIRI(collectionIDURI) page.SetActivityStreamsPartOf(partOfProp) // .items items := streams.NewActivityStreamsItemsProperty() var highestID string for k, v := range replies { items.AppendIRI(v) if k > highestID { highestID = k } } page.SetActivityStreamsItems(items) // .next nextProp := streams.NewActivityStreamsNextProperty() nextPropIDString := fmt.Sprintf("%s?only_other_accounts=%t&page=true", collectionID, onlyOtherAccounts) if highestID != "" { nextPropIDString = fmt.Sprintf("%s&min_id=%s", nextPropIDString, highestID) } nextPropID, err := url.Parse(nextPropIDString) if err != nil { return nil, gtserror.NewErrorInternalError(err) } nextProp.SetIRI(nextPropID) page.SetActivityStreamsNext(nextProp) return page, nil }