gotosocial/internal/typeutils/internaltoas.go
kim 7cc40302a5
[chore] consolidate caching libraries (#704)
* add miekg/dns dependency

* set/validate accountDomain

* move finger to dereferencer

* totally break GetRemoteAccount

* start reworking finger func a bit

* start reworking getRemoteAccount a bit

* move mention parts to namestring

* rework webfingerget

* use util function to extract webfinger parts

* use accountDomain

* rework finger again, final form

* just a real nasty commit, the worst

* remove refresh from account

* use new ASRepToAccount signature

* fix incorrect debug call

* fix for new getRemoteAccount

* rework GetRemoteAccount

* start updating tests to remove repetition

* break a lot of tests
Move shared test logic into the testrig,
rather than having it scattered all over
the place. This allows us to just mock
the transport controller once, and have
all tests use it (unless they need not to
for some other reason).

* fix up tests to use main mock httpclient

* webfinger only if necessary

* cheeky linting with the lads

* update mentionName regex
recognize instance accounts

* don't finger instance accounts

* test webfinger part extraction

* increase default worker count to 4 per cpu

* don't repeat regex parsing

* final search for discovered accountDomain

* be more permissive in namestring lookup

* add more extraction tests

* simplify GetParseMentionFunc

* skip long search if local account

* fix broken test

* consolidate to all use same caching libraries

Signed-off-by: kim <grufwub@gmail.com>

* perform more caching in the database layer

Signed-off-by: kim <grufwub@gmail.com>

* remove ASNote cache

Signed-off-by: kim <grufwub@gmail.com>

* update cache library, improve db tracing hooks

Signed-off-by: kim <grufwub@gmail.com>

* return ErrNoEntries if no account status IDs found, small formatting changes

Signed-off-by: kim <grufwub@gmail.com>

* fix tests, thanks tobi!

Signed-off-by: kim <grufwub@gmail.com>

Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
2022-07-10 17:18:21 +02:00

1167 lines
37 KiB
Go

/*
GoToSocial
Copyright (C) 2021-2022 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 <http://www.gnu.org/licenses/>.
*/
package typeutils
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"net/url"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// const (
// // highestID is the highest possible ULID
// highestID = "ZZZZZZZZZZZZZZZZZZZZZZZZZZ"
// // lowestID is the lowest possible ULID
// lowestID = "00000000000000000000000000"
// )
// Converts a gts model account into an Activity Streams person type.
func (c *converter) AccountToAS(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)
// 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, err := c.db.GetAttachmentByID(ctx, a.AvatarMediaAttachmentID)
if err == nil {
a.AvatarMediaAttachment = avatar
} else {
logrus.Errorf("AccountToAS: error getting Avatar with id %s: %s", a.AvatarMediaAttachmentID, err)
}
}
if a.AvatarMediaAttachment != nil {
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, err := c.db.GetAttachmentByID(ctx, a.HeaderMediaAttachmentID)
if err == nil {
a.HeaderMediaAttachment = header
} else {
logrus.Errorf("AccountToAS: error getting Header with id %s: %s", a.HeaderMediaAttachmentID, err)
}
}
if a.HeaderMediaAttachment != nil {
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)
}
}
return person, nil
}
// Converts a gts model account into a VERY MINIMAL Activity Streams person type.
//
// 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) {
// 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, err := c.db.GetStatusByID(ctx, s.InReplyToID)
if err != nil {
return nil, fmt.Errorf("StatusToAS: error getting replied to status %s: %s", s.InReplyToID, 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(pub.PublicActivityPubIRI)
if err != nil {
return nil, fmt.Errorf("StatusToAS: error parsing url %s: %s", pub.PublicActivityPubIRI, 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.TargetAccount.URI)
if err != nil {
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.TargetAccount.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.TargetAccount.URI)
if err != nil {
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.TargetAccount.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.TargetAccount.URI)
if err != nil {
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.TargetAccount.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.TargetAccount.URI)
if err != nil {
return nil, fmt.Errorf("StatusToAS: error parsing uri %s: %s", m.TargetAccount.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)
// sensitive
sensitiveProp := streams.NewActivityStreamsSensitiveProperty()
sensitiveProp.AppendXMLSchemaBoolean(s.Sensitive)
status.SetActivityStreamsSensitive(sensitiveProp)
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 == "" {
accountDomain := config.GetAccountDomain()
if accountDomain == "" {
accountDomain = config.GetHost()
}
domain = 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
ccProp := streams.NewActivityStreamsCcProperty()
boostedAccountURI, err := url.Parse(boostedAccount.URI)
if err != nil {
return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", boostedAccount.URI, err)
}
ccProp.AppendIRI(boostedAccountURI)
// maybe CC it to public depending on the boosted status visibility
switch boostWrapperStatus.BoostOf.Visibility {
case gtsmodel.VisibilityPublic, gtsmodel.VisibilityUnlocked:
publicURI, err := url.Parse(pub.PublicActivityPubIRI)
if err != nil {
return nil, fmt.Errorf("BoostToAS: error parsing uri %s: %s", pub.PublicActivityPubIRI, err)
}
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
}
/*
the goal is to end up with something like this:
{
"id": "https://example.org/users/whatever/outbox?page=true",
"type": "OrderedCollectionPage",
"next": "https://example.org/users/whatever/outbox?max_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
"prev": "https://example.org/users/whatever/outbox?min_id=01FJC1Q0E3SSQR59TD2M1KP4V8&page=true",
"partOf": "https://example.org/users/whatever/outbox",
"orderedItems": [
"id": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7/activity",
"type": "Create",
"actor": "https://example.org/users/whatever",
"published": "2021-10-18T20:06:18Z",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://example.org/users/whatever/followers"
],
"object": "https://example.org/users/whatever/statuses/01FJC1MKPVX2VMWP2ST93Q90K7"
]
}
*/
func (c *converter) StatusesToASOutboxPage(ctx context.Context, outboxID string, maxID string, minID string, statuses []*gtsmodel.Status) (vocab.ActivityStreamsOrderedCollectionPage, error) {
page := streams.NewActivityStreamsOrderedCollectionPage()
// .id
pageIDProp := streams.NewJSONLDIdProperty()
pageID := fmt.Sprintf("%s?page=true", outboxID)
if minID != "" {
pageID = fmt.Sprintf("%s&minID=%s", pageID, minID)
}
if maxID != "" {
pageID = fmt.Sprintf("%s&maxID=%s", pageID, maxID)
}
pageIDURI, err := url.Parse(pageID)
if err != nil {
return nil, err
}
pageIDProp.SetIRI(pageIDURI)
page.SetJSONLDId(pageIDProp)
// .partOf
collectionIDURI, err := url.Parse(outboxID)
if err != nil {
return nil, err
}
partOfProp := streams.NewActivityStreamsPartOfProperty()
partOfProp.SetIRI(collectionIDURI)
page.SetActivityStreamsPartOf(partOfProp)
// .orderedItems
itemsProp := streams.NewActivityStreamsOrderedItemsProperty()
var highest string
var lowest string
for _, s := range statuses {
note, err := c.StatusToAS(ctx, s)
if err != nil {
return nil, err
}
create, err := c.WrapNoteInCreate(note, true)
if err != nil {
return nil, err
}
itemsProp.AppendActivityStreamsCreate(create)
if highest == "" || s.ID > highest {
highest = s.ID
}
if lowest == "" || s.ID < lowest {
lowest = s.ID
}
}
page.SetActivityStreamsOrderedItems(itemsProp)
// .next
if lowest != "" {
nextProp := streams.NewActivityStreamsNextProperty()
nextPropIDString := fmt.Sprintf("%s?page=true&max_id=%s", outboxID, lowest)
nextPropIDURI, err := url.Parse(nextPropIDString)
if err != nil {
return nil, err
}
nextProp.SetIRI(nextPropIDURI)
page.SetActivityStreamsNext(nextProp)
}
// .prev
if highest != "" {
prevProp := streams.NewActivityStreamsPrevProperty()
prevPropIDString := fmt.Sprintf("%s?page=true&min_id=%s", outboxID, highest)
prevPropIDURI, err := url.Parse(prevPropIDString)
if err != nil {
return nil, err
}
prevProp.SetIRI(prevPropIDURI)
page.SetActivityStreamsPrev(prevProp)
}
return page, nil
}
/*
we want something that looks like this:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.org/users/whatever/outbox",
"type": "OrderedCollection",
"first": "https://example.org/users/whatever/outbox?page=true"
}
*/
func (c *converter) OutboxToASCollection(ctx context.Context, outboxID string) (vocab.ActivityStreamsOrderedCollection, error) {
collection := streams.NewActivityStreamsOrderedCollection()
collectionIDProp := streams.NewJSONLDIdProperty()
outboxIDURI, err := url.Parse(outboxID)
if err != nil {
return nil, fmt.Errorf("error parsing url %s", outboxID)
}
collectionIDProp.SetIRI(outboxIDURI)
collection.SetJSONLDId(collectionIDProp)
collectionFirstProp := streams.NewActivityStreamsFirstProperty()
collectionFirstPropID := fmt.Sprintf("%s?page=true", outboxID)
collectionFirstPropIDURI, err := url.Parse(collectionFirstPropID)
if err != nil {
return nil, fmt.Errorf("error parsing url %s", collectionFirstPropID)
}
collectionFirstProp.SetIRI(collectionFirstPropIDURI)
collection.SetActivityStreamsFirst(collectionFirstProp)
return collection, nil
}