summaryrefslogtreecommitdiff
path: root/internal/typeutils
diff options
context:
space:
mode:
Diffstat (limited to 'internal/typeutils')
-rw-r--r--internal/typeutils/asextractionutil.go570
-rw-r--r--internal/typeutils/asinterfaces.go265
-rw-r--r--internal/typeutils/astointernal.go83
-rw-r--r--internal/typeutils/astointernal_test.go9
-rw-r--r--internal/typeutils/converter.go22
-rw-r--r--internal/typeutils/converter_test.go3
-rw-r--r--internal/typeutils/internaltoas.go145
-rw-r--r--internal/typeutils/internaltoas_test.go2
8 files changed, 208 insertions, 891 deletions
diff --git a/internal/typeutils/asextractionutil.go b/internal/typeutils/asextractionutil.go
deleted file mode 100644
index b3e6eb2c4..000000000
--- a/internal/typeutils/asextractionutil.go
+++ /dev/null
@@ -1,570 +0,0 @@
-/*
- 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 <http://www.gnu.org/licenses/>.
-*/
-
-package typeutils
-
-import (
- "crypto/rsa"
- "crypto/x509"
- "encoding/pem"
- "errors"
- "fmt"
- "net/url"
- "strings"
- "time"
-
- "github.com/go-fed/activity/pub"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-func extractPreferredUsername(i withPreferredUsername) (string, error) {
- u := i.GetActivityStreamsPreferredUsername()
- if u == nil || !u.IsXMLSchemaString() {
- return "", errors.New("preferredUsername was not a string")
- }
- if u.GetXMLSchemaString() == "" {
- return "", errors.New("preferredUsername was empty")
- }
- return u.GetXMLSchemaString(), nil
-}
-
-func extractName(i withName) (string, error) {
- nameProp := i.GetActivityStreamsName()
- if nameProp == nil {
- return "", errors.New("activityStreamsName not found")
- }
-
- // take the first name string we can find
- for iter := nameProp.Begin(); iter != nameProp.End(); iter = iter.Next() {
- if iter.IsXMLSchemaString() && iter.GetXMLSchemaString() != "" {
- return iter.GetXMLSchemaString(), nil
- }
- }
-
- return "", errors.New("activityStreamsName not found")
-}
-
-func extractInReplyToURI(i withInReplyTo) (*url.URL, error) {
- inReplyToProp := i.GetActivityStreamsInReplyTo()
- if inReplyToProp == nil {
- return nil, errors.New("in reply to prop was nil")
- }
- for iter := inReplyToProp.Begin(); iter != inReplyToProp.End(); iter = iter.Next() {
- if iter.IsIRI() {
- if iter.GetIRI() != nil {
- return iter.GetIRI(), nil
- }
- }
- }
- return nil, errors.New("couldn't find iri for in reply to")
-}
-
-func extractTos(i withTo) ([]*url.URL, error) {
- to := []*url.URL{}
- toProp := i.GetActivityStreamsTo()
- if toProp == nil {
- return nil, errors.New("toProp was nil")
- }
- for iter := toProp.Begin(); iter != toProp.End(); iter = iter.Next() {
- if iter.IsIRI() {
- if iter.GetIRI() != nil {
- to = append(to, iter.GetIRI())
- }
- }
- }
- return to, nil
-}
-
-func extractCCs(i withCC) ([]*url.URL, error) {
- cc := []*url.URL{}
- ccProp := i.GetActivityStreamsCc()
- if ccProp == nil {
- return cc, nil
- }
- for iter := ccProp.Begin(); iter != ccProp.End(); iter = iter.Next() {
- if iter.IsIRI() {
- if iter.GetIRI() != nil {
- cc = append(cc, iter.GetIRI())
- }
- }
- }
- return cc, nil
-}
-
-func extractAttributedTo(i withAttributedTo) (*url.URL, error) {
- attributedToProp := i.GetActivityStreamsAttributedTo()
- if attributedToProp == nil {
- return nil, errors.New("attributedToProp was nil")
- }
- for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
- if iter.IsIRI() {
- if iter.GetIRI() != nil {
- return iter.GetIRI(), nil
- }
- }
- }
- return nil, errors.New("couldn't find iri for attributed to")
-}
-
-func extractPublished(i withPublished) (time.Time, error) {
- publishedProp := i.GetActivityStreamsPublished()
- if publishedProp == nil {
- return time.Time{}, errors.New("published prop was nil")
- }
-
- if !publishedProp.IsXMLSchemaDateTime() {
- return time.Time{}, errors.New("published prop was not date time")
- }
-
- t := publishedProp.Get()
- if t.IsZero() {
- return time.Time{}, errors.New("published time was zero")
- }
- return t, nil
-}
-
-// extractIconURL extracts a URL to a supported image file from something like:
-// "icon": {
-// "mediaType": "image/jpeg",
-// "type": "Image",
-// "url": "http://example.org/path/to/some/file.jpeg"
-// },
-func extractIconURL(i withIcon) (*url.URL, error) {
- iconProp := i.GetActivityStreamsIcon()
- if iconProp == nil {
- return nil, errors.New("icon property was nil")
- }
-
- // icon can potentially contain multiple entries, so we iterate through all of them
- // here in order to find the first one that meets these criteria:
- // 1. is an image
- // 2. has a URL so we can grab it
- for iter := iconProp.Begin(); iter != iconProp.End(); iter = iter.Next() {
- // 1. is an image
- if !iter.IsActivityStreamsImage() {
- continue
- }
- imageValue := iter.GetActivityStreamsImage()
- if imageValue == nil {
- continue
- }
-
- // 2. has a URL so we can grab it
- url, err := extractURL(imageValue)
- if err == nil && url != nil {
- return url, nil
- }
- }
- // if we get to this point we didn't find an icon meeting our criteria :'(
- return nil, errors.New("could not extract valid image from icon")
-}
-
-// extractImageURL extracts a URL to a supported image file from something like:
-// "image": {
-// "mediaType": "image/jpeg",
-// "type": "Image",
-// "url": "http://example.org/path/to/some/file.jpeg"
-// },
-func extractImageURL(i withImage) (*url.URL, error) {
- imageProp := i.GetActivityStreamsImage()
- if imageProp == nil {
- return nil, errors.New("icon property was nil")
- }
-
- // icon can potentially contain multiple entries, so we iterate through all of them
- // here in order to find the first one that meets these criteria:
- // 1. is an image
- // 2. has a URL so we can grab it
- for iter := imageProp.Begin(); iter != imageProp.End(); iter = iter.Next() {
- // 1. is an image
- if !iter.IsActivityStreamsImage() {
- continue
- }
- imageValue := iter.GetActivityStreamsImage()
- if imageValue == nil {
- continue
- }
-
- // 2. has a URL so we can grab it
- url, err := extractURL(imageValue)
- if err == nil && url != nil {
- return url, nil
- }
- }
- // if we get to this point we didn't find an image meeting our criteria :'(
- return nil, errors.New("could not extract valid image from image property")
-}
-
-func extractSummary(i withSummary) (string, error) {
- summaryProp := i.GetActivityStreamsSummary()
- if summaryProp == nil {
- return "", errors.New("summary property was nil")
- }
-
- for iter := summaryProp.Begin(); iter != summaryProp.End(); iter = iter.Next() {
- if iter.IsXMLSchemaString() && iter.GetXMLSchemaString() != "" {
- return iter.GetXMLSchemaString(), nil
- }
- }
-
- return "", errors.New("could not extract summary")
-}
-
-func extractDiscoverable(i withDiscoverable) (bool, error) {
- if i.GetTootDiscoverable() == nil {
- return false, errors.New("discoverable was nil")
- }
- return i.GetTootDiscoverable().Get(), nil
-}
-
-func extractURL(i withURL) (*url.URL, error) {
- urlProp := i.GetActivityStreamsUrl()
- if urlProp == nil {
- return nil, errors.New("url property was nil")
- }
-
- for iter := urlProp.Begin(); iter != urlProp.End(); iter = iter.Next() {
- if iter.IsIRI() && iter.GetIRI() != nil {
- return iter.GetIRI(), nil
- }
- }
-
- return nil, errors.New("could not extract url")
-}
-
-func extractPublicKeyForOwner(i withPublicKey, forOwner *url.URL) (*rsa.PublicKey, *url.URL, error) {
- publicKeyProp := i.GetW3IDSecurityV1PublicKey()
- if publicKeyProp == nil {
- return nil, nil, errors.New("public key property was nil")
- }
-
- for iter := publicKeyProp.Begin(); iter != publicKeyProp.End(); iter = iter.Next() {
- pkey := iter.Get()
- if pkey == nil {
- continue
- }
-
- pkeyID, err := pub.GetId(pkey)
- if err != nil || pkeyID == nil {
- continue
- }
-
- if pkey.GetW3IDSecurityV1Owner() == nil || pkey.GetW3IDSecurityV1Owner().Get() == nil || pkey.GetW3IDSecurityV1Owner().Get().String() != forOwner.String() {
- continue
- }
-
- if pkey.GetW3IDSecurityV1PublicKeyPem() == nil {
- continue
- }
-
- pkeyPem := pkey.GetW3IDSecurityV1PublicKeyPem().Get()
- if pkeyPem == "" {
- continue
- }
-
- block, _ := pem.Decode([]byte(pkeyPem))
- if block == nil || block.Type != "PUBLIC KEY" {
- return nil, nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
- }
-
- p, err := x509.ParsePKIXPublicKey(block.Bytes)
- if err != nil {
- return nil, nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
- }
- if p == nil {
- return nil, nil, errors.New("returned public key was empty")
- }
-
- if publicKey, ok := p.(*rsa.PublicKey); ok {
- return publicKey, pkeyID, nil
- }
- }
- return nil, nil, errors.New("couldn't find public key")
-}
-
-func extractContent(i withContent) (string, error) {
- contentProperty := i.GetActivityStreamsContent()
- if contentProperty == nil {
- return "", errors.New("content property was nil")
- }
- for iter := contentProperty.Begin(); iter != contentProperty.End(); iter = iter.Next() {
- if iter.IsXMLSchemaString() && iter.GetXMLSchemaString() != "" {
- return iter.GetXMLSchemaString(), nil
- }
- }
- return "", errors.New("no content found")
-}
-
-func extractAttachments(i withAttachment) ([]*gtsmodel.MediaAttachment, error) {
- attachments := []*gtsmodel.MediaAttachment{}
- attachmentProp := i.GetActivityStreamsAttachment()
- if attachmentProp == nil {
- return attachments, nil
- }
- for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() {
- t := iter.GetType()
- if t == nil {
- continue
- }
- attachmentable, ok := t.(Attachmentable)
- if !ok {
- continue
- }
- attachment, err := extractAttachment(attachmentable)
- if err != nil {
- continue
- }
- attachments = append(attachments, attachment)
- }
- return attachments, nil
-}
-
-func extractAttachment(i Attachmentable) (*gtsmodel.MediaAttachment, error) {
- attachment := &gtsmodel.MediaAttachment{
- File: gtsmodel.File{},
- }
-
- attachmentURL, err := extractURL(i)
- if err != nil {
- return nil, err
- }
- attachment.RemoteURL = attachmentURL.String()
-
- mediaType := i.GetActivityStreamsMediaType()
- if mediaType == nil {
- return nil, errors.New("no media type")
- }
- if mediaType.Get() == "" {
- return nil, errors.New("no media type")
- }
- attachment.File.ContentType = mediaType.Get()
- attachment.Type = gtsmodel.FileTypeImage
-
- name, err := extractName(i)
- if err == nil {
- attachment.Description = name
- }
-
- attachment.Processing = gtsmodel.ProcessingStatusReceived
-
- return attachment, nil
-}
-
-// func extractBlurhash(i withBlurhash) (string, error) {
-// if i.GetTootBlurhashProperty() == nil {
-// return "", errors.New("blurhash property was nil")
-// }
-// if i.GetTootBlurhashProperty().Get() == "" {
-// return "", errors.New("empty blurhash string")
-// }
-// return i.GetTootBlurhashProperty().Get(), nil
-// }
-
-func extractHashtags(i withTag) ([]*gtsmodel.Tag, error) {
- tags := []*gtsmodel.Tag{}
- tagsProp := i.GetActivityStreamsTag()
- if tagsProp == nil {
- return tags, nil
- }
- for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
- t := iter.GetType()
- if t == nil {
- continue
- }
-
- if t.GetTypeName() != "Hashtag" {
- continue
- }
-
- hashtaggable, ok := t.(Hashtaggable)
- if !ok {
- continue
- }
-
- tag, err := extractHashtag(hashtaggable)
- if err != nil {
- continue
- }
-
- tags = append(tags, tag)
- }
- return tags, nil
-}
-
-func extractHashtag(i Hashtaggable) (*gtsmodel.Tag, error) {
- tag := &gtsmodel.Tag{}
-
- hrefProp := i.GetActivityStreamsHref()
- if hrefProp == nil || !hrefProp.IsIRI() {
- return nil, errors.New("no href prop")
- }
- tag.URL = hrefProp.GetIRI().String()
-
- name, err := extractName(i)
- if err != nil {
- return nil, err
- }
- tag.Name = strings.TrimPrefix(name, "#")
-
- return tag, nil
-}
-
-func extractEmojis(i withTag) ([]*gtsmodel.Emoji, error) {
- emojis := []*gtsmodel.Emoji{}
- tagsProp := i.GetActivityStreamsTag()
- if tagsProp == nil {
- return emojis, nil
- }
- for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
- t := iter.GetType()
- if t == nil {
- continue
- }
-
- if t.GetTypeName() != "Emoji" {
- continue
- }
-
- emojiable, ok := t.(Emojiable)
- if !ok {
- continue
- }
-
- emoji, err := extractEmoji(emojiable)
- if err != nil {
- continue
- }
-
- emojis = append(emojis, emoji)
- }
- return emojis, nil
-}
-
-func extractEmoji(i Emojiable) (*gtsmodel.Emoji, error) {
- emoji := &gtsmodel.Emoji{}
-
- idProp := i.GetJSONLDId()
- if idProp == nil || !idProp.IsIRI() {
- return nil, errors.New("no id for emoji")
- }
- uri := idProp.GetIRI()
- emoji.URI = uri.String()
- emoji.Domain = uri.Host
-
- name, err := extractName(i)
- if err != nil {
- return nil, err
- }
- emoji.Shortcode = strings.Trim(name, ":")
-
- if i.GetActivityStreamsIcon() == nil {
- return nil, errors.New("no icon for emoji")
- }
- imageURL, err := extractIconURL(i)
- if err != nil {
- return nil, errors.New("no url for emoji image")
- }
- emoji.ImageRemoteURL = imageURL.String()
-
- return emoji, nil
-}
-
-func extractMentions(i withTag) ([]*gtsmodel.Mention, error) {
- mentions := []*gtsmodel.Mention{}
- tagsProp := i.GetActivityStreamsTag()
- if tagsProp == nil {
- return mentions, nil
- }
- for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
- t := iter.GetType()
- if t == nil {
- continue
- }
-
- if t.GetTypeName() != "Mention" {
- continue
- }
-
- mentionable, ok := t.(Mentionable)
- if !ok {
- continue
- }
-
- mention, err := extractMention(mentionable)
- if err != nil {
- continue
- }
-
- mentions = append(mentions, mention)
- }
- return mentions, nil
-}
-
-func extractMention(i Mentionable) (*gtsmodel.Mention, error) {
- mention := &gtsmodel.Mention{}
-
- mentionString, err := extractName(i)
- if err != nil {
- return nil, err
- }
-
- // just make sure the mention string is valid so we can handle it properly later on...
- username, domain, err := util.ExtractMentionParts(mentionString)
- if err != nil {
- return nil, err
- }
- if username == "" || domain == "" {
- return nil, errors.New("username or domain was empty")
- }
- mention.NameString = mentionString
-
- // the href prop should be the AP URI of a user we know, eg https://example.org/users/whatever_user
- hrefProp := i.GetActivityStreamsHref()
- if hrefProp == nil || !hrefProp.IsIRI() {
- return nil, errors.New("no href prop")
- }
- mention.MentionedAccountURI = hrefProp.GetIRI().String()
- return mention, nil
-}
-
-func extractActor(i withActor) (*url.URL, error) {
- actorProp := i.GetActivityStreamsActor()
- if actorProp == nil {
- return nil, errors.New("actor property was nil")
- }
- for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() {
- if iter.IsIRI() && iter.GetIRI() != nil {
- return iter.GetIRI(), nil
- }
- }
- return nil, errors.New("no iri found for actor prop")
-}
-
-func extractObject(i withObject) (*url.URL, error) {
- objectProp := i.GetActivityStreamsObject()
- if objectProp == nil {
- return nil, errors.New("object property was nil")
- }
- for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
- if iter.IsIRI() && iter.GetIRI() != nil {
- return iter.GetIRI(), nil
- }
- }
- return nil, errors.New("no iri found for object prop")
-}
diff --git a/internal/typeutils/asinterfaces.go b/internal/typeutils/asinterfaces.go
deleted file mode 100644
index d0b1cf617..000000000
--- a/internal/typeutils/asinterfaces.go
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- 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 <http://www.gnu.org/licenses/>.
-*/
-
-package typeutils
-
-import "github.com/go-fed/activity/streams/vocab"
-
-// Accountable represents the minimum activitypub interface for representing an 'account'.
-// This interface is fulfilled by: Person, Application, Organization, Service, and Group
-type Accountable interface {
- withJSONLDId
- withTypeName
-
- withPreferredUsername
- withIcon
- withName
- withImage
- withSummary
- withDiscoverable
- withURL
- withPublicKey
- withInbox
- withOutbox
- withFollowing
- withFollowers
- withFeatured
-}
-
-// Statusable represents the minimum activitypub interface for representing a 'status'.
-// This interface is fulfilled by: Article, Document, Image, Video, Note, Page, Event, Place, Mention, Profile
-type Statusable interface {
- withJSONLDId
- withTypeName
-
- withSummary
- withInReplyTo
- withPublished
- withURL
- withAttributedTo
- withTo
- withCC
- withSensitive
- withConversation
- withContent
- withAttachment
- withTag
- withReplies
-}
-
-// Attachmentable represents the minimum activitypub interface for representing a 'mediaAttachment'.
-// This interface is fulfilled by: Audio, Document, Image, Video
-type Attachmentable interface {
- withTypeName
- withMediaType
- withURL
- withName
-}
-
-// Hashtaggable represents the minimum activitypub interface for representing a 'hashtag' tag.
-type Hashtaggable interface {
- withTypeName
- withHref
- withName
-}
-
-// Emojiable represents the minimum interface for an 'emoji' tag.
-type Emojiable interface {
- withJSONLDId
- withTypeName
- withName
- withUpdated
- withIcon
-}
-
-// Mentionable represents the minimum interface for a 'mention' tag.
-type Mentionable interface {
- withName
- withHref
-}
-
-// Followable represents the minimum interface for an activitystreams 'follow' activity.
-type Followable interface {
- withJSONLDId
- withTypeName
-
- withActor
- withObject
-}
-
-// Likeable represents the minimum interface for an activitystreams 'like' activity.
-type Likeable interface {
- withJSONLDId
- withTypeName
-
- withActor
- withObject
-}
-
-// Blockable represents the minimum interface for an activitystreams 'block' activity.
-type Blockable interface {
- withJSONLDId
- withTypeName
-
- withActor
- withObject
-}
-
-// Announceable represents the minimum interface for an activitystreams 'announce' activity.
-type Announceable interface {
- withJSONLDId
- withTypeName
-
- withActor
- withObject
- withPublished
- withTo
- withCC
-}
-
-type withJSONLDId interface {
- GetJSONLDId() vocab.JSONLDIdProperty
-}
-
-type withTypeName interface {
- GetTypeName() string
-}
-
-type withPreferredUsername interface {
- GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty
-}
-
-type withIcon interface {
- GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty
-}
-
-type withName interface {
- GetActivityStreamsName() vocab.ActivityStreamsNameProperty
-}
-
-type withImage interface {
- GetActivityStreamsImage() vocab.ActivityStreamsImageProperty
-}
-
-type withSummary interface {
- GetActivityStreamsSummary() vocab.ActivityStreamsSummaryProperty
-}
-
-type withDiscoverable interface {
- GetTootDiscoverable() vocab.TootDiscoverableProperty
-}
-
-type withURL interface {
- GetActivityStreamsUrl() vocab.ActivityStreamsUrlProperty
-}
-
-type withPublicKey interface {
- GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
-}
-
-type withInbox interface {
- GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty
-}
-
-type withOutbox interface {
- GetActivityStreamsOutbox() vocab.ActivityStreamsOutboxProperty
-}
-
-type withFollowing interface {
- GetActivityStreamsFollowing() vocab.ActivityStreamsFollowingProperty
-}
-
-type withFollowers interface {
- GetActivityStreamsFollowers() vocab.ActivityStreamsFollowersProperty
-}
-
-type withFeatured interface {
- GetTootFeatured() vocab.TootFeaturedProperty
-}
-
-type withAttributedTo interface {
- GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty
-}
-
-type withAttachment interface {
- GetActivityStreamsAttachment() vocab.ActivityStreamsAttachmentProperty
-}
-
-type withTo interface {
- GetActivityStreamsTo() vocab.ActivityStreamsToProperty
-}
-
-type withInReplyTo interface {
- GetActivityStreamsInReplyTo() vocab.ActivityStreamsInReplyToProperty
-}
-
-type withCC interface {
- GetActivityStreamsCc() vocab.ActivityStreamsCcProperty
-}
-
-type withSensitive interface {
- // TODO
-}
-
-type withConversation interface {
- // TODO
-}
-
-type withContent interface {
- GetActivityStreamsContent() vocab.ActivityStreamsContentProperty
-}
-
-type withPublished interface {
- GetActivityStreamsPublished() vocab.ActivityStreamsPublishedProperty
-}
-
-type withTag interface {
- GetActivityStreamsTag() vocab.ActivityStreamsTagProperty
-}
-
-type withReplies interface {
- GetActivityStreamsReplies() vocab.ActivityStreamsRepliesProperty
-}
-
-type withMediaType interface {
- GetActivityStreamsMediaType() vocab.ActivityStreamsMediaTypeProperty
-}
-
-// type withBlurhash interface {
-// GetTootBlurhashProperty() vocab.TootBlurhashProperty
-// }
-
-// type withFocalPoint interface {
-// // TODO
-// }
-
-type withHref interface {
- GetActivityStreamsHref() vocab.ActivityStreamsHrefProperty
-}
-
-type withUpdated interface {
- GetActivityStreamsUpdated() vocab.ActivityStreamsUpdatedProperty
-}
-
-type withActor interface {
- GetActivityStreamsActor() vocab.ActivityStreamsActorProperty
-}
-
-type withObject interface {
- GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty
-}
diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go
index 394de6e82..f754d282a 100644
--- a/internal/typeutils/astointernal.go
+++ b/internal/typeutils/astointernal.go
@@ -24,11 +24,12 @@ import (
"net/url"
"strings"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
-func (c *converter) ASRepresentationToAccount(accountable Accountable, update bool) (*gtsmodel.Account, error) {
+func (c *converter) ASRepresentationToAccount(accountable ap.Accountable, update bool) (*gtsmodel.Account, error) {
// first check if we actually already know this account
uriProp := accountable.GetJSONLDId()
if uriProp == nil || !uriProp.IsIRI() {
@@ -55,7 +56,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable, update bo
// Username aka preferredUsername
// We need this one so bail if it's not set.
- username, err := extractPreferredUsername(accountable)
+ username, err := ap.ExtractPreferredUsername(accountable)
if err != nil {
return nil, fmt.Errorf("couldn't extract username: %s", err)
}
@@ -66,27 +67,27 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable, update bo
// avatar aka icon
// if this one isn't extractable in a format we recognise we'll just skip it
- if avatarURL, err := extractIconURL(accountable); err == nil {
+ if avatarURL, err := ap.ExtractIconURL(accountable); err == nil {
acct.AvatarRemoteURL = avatarURL.String()
}
// header aka image
// if this one isn't extractable in a format we recognise we'll just skip it
- if headerURL, err := extractImageURL(accountable); err == nil {
+ if headerURL, err := ap.ExtractImageURL(accountable); err == nil {
acct.HeaderRemoteURL = headerURL.String()
}
// display name aka name
// we default to the username, but take the more nuanced name property if it exists
acct.DisplayName = username
- if displayName, err := extractName(accountable); err == nil {
+ if displayName, err := ap.ExtractName(accountable); err == nil {
acct.DisplayName = displayName
}
// TODO: fields aka attachment array
// note aka summary
- note, err := extractSummary(accountable)
+ note, err := ap.ExtractSummary(accountable)
if err == nil && note != "" {
acct.Note = note
}
@@ -110,13 +111,13 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable, update bo
// discoverable
// default to false -- take custom value if it's set though
acct.Discoverable = false
- discoverable, err := extractDiscoverable(accountable)
+ discoverable, err := ap.ExtractDiscoverable(accountable)
if err == nil {
acct.Discoverable = discoverable
}
// url property
- url, err := extractURL(accountable)
+ url, err := ap.ExtractURL(accountable)
if err == nil {
// take the URL if we can find it
acct.URL = url.String()
@@ -155,7 +156,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable, update bo
// TODO: alsoKnownAs
// publicKey
- pkey, pkeyURL, err := extractPublicKeyForOwner(accountable, uri)
+ pkey, pkeyURL, err := ap.ExtractPublicKeyForOwner(accountable, uri)
if err != nil {
return nil, fmt.Errorf("couldn't get public key for person %s: %s", uri.String(), err)
}
@@ -165,7 +166,7 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable, update bo
return acct, nil
}
-func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error) {
+func (c *converter) ASStatusToStatus(statusable ap.Statusable) (*gtsmodel.Status, error) {
status := &gtsmodel.Status{}
// uri at which this status is reachable
@@ -176,49 +177,49 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
status.URI = uriProp.GetIRI().String()
// web url for viewing this status
- if statusURL, err := extractURL(statusable); err == nil {
+ if statusURL, err := ap.ExtractURL(statusable); err == nil {
status.URL = statusURL.String()
}
// the html-formatted content of this status
- if content, err := extractContent(statusable); err == nil {
+ if content, err := ap.ExtractContent(statusable); err == nil {
status.Content = content
}
// attachments to dereference and fetch later on (we don't do that here)
- if attachments, err := extractAttachments(statusable); err == nil {
+ if attachments, err := ap.ExtractAttachments(statusable); err == nil {
status.GTSMediaAttachments = attachments
}
// hashtags to dereference later on
- if hashtags, err := extractHashtags(statusable); err == nil {
+ if hashtags, err := ap.ExtractHashtags(statusable); err == nil {
status.GTSTags = hashtags
}
// emojis to dereference and fetch later on
- if emojis, err := extractEmojis(statusable); err == nil {
+ if emojis, err := ap.ExtractEmojis(statusable); err == nil {
status.GTSEmojis = emojis
}
// mentions to dereference later on
- if mentions, err := extractMentions(statusable); err == nil {
+ if mentions, err := ap.ExtractMentions(statusable); err == nil {
status.GTSMentions = mentions
}
// cw string for this status
- if cw, err := extractSummary(statusable); err == nil {
+ if cw, err := ap.ExtractSummary(statusable); err == nil {
status.ContentWarning = cw
}
// when was this status created?
- published, err := extractPublished(statusable)
+ published, err := ap.ExtractPublished(statusable)
if err == nil {
status.CreatedAt = published
}
// which account posted this status?
// if we don't know the account yet we can dereference it later
- attributedTo, err := extractAttributedTo(statusable)
+ attributedTo, err := ap.ExtractAttributedTo(statusable)
if err != nil {
return nil, errors.New("attributedTo was empty")
}
@@ -233,8 +234,8 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
status.GTSAuthorAccount = statusOwner
// check if there's a post that this is a reply to
- inReplyToURI, err := extractInReplyToURI(statusable)
- if err == nil {
+ inReplyToURI := ap.ExtractInReplyToURI(statusable)
+ if inReplyToURI != nil {
// something is set so we can at least set this field on the
// status and dereference using this later if we need to
status.InReplyToURI = inReplyToURI.String()
@@ -259,12 +260,12 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
// visibility entry for this status
var visibility gtsmodel.Visibility
- to, err := extractTos(statusable)
+ to, err := ap.ExtractTos(statusable)
if err != nil {
return nil, fmt.Errorf("error extracting TO values: %s", err)
}
- cc, err := extractCCs(statusable)
+ cc, err := ap.ExtractCCs(statusable)
if err != nil {
return nil, fmt.Errorf("error extracting CC values: %s", err)
}
@@ -315,7 +316,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
return status, nil
}
-func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error) {
+func (c *converter) ASFollowToFollowRequest(followable ap.Followable) (*gtsmodel.FollowRequest, error) {
idProp := followable.GetJSONLDId()
if idProp == nil || !idProp.IsIRI() {
@@ -323,7 +324,7 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo
}
uri := idProp.GetIRI().String()
- origin, err := extractActor(followable)
+ origin, err := ap.ExtractActor(followable)
if err != nil {
return nil, errors.New("error extracting actor property from follow")
}
@@ -332,7 +333,7 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
}
- target, err := extractObject(followable)
+ target, err := ap.ExtractObject(followable)
if err != nil {
return nil, errors.New("error extracting object property from follow")
}
@@ -350,14 +351,14 @@ func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.Fo
return followRequest, nil
}
-func (c *converter) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error) {
+func (c *converter) ASFollowToFollow(followable ap.Followable) (*gtsmodel.Follow, error) {
idProp := followable.GetJSONLDId()
if idProp == nil || !idProp.IsIRI() {
return nil, errors.New("no id property set on follow, or was not an iri")
}
uri := idProp.GetIRI().String()
- origin, err := extractActor(followable)
+ origin, err := ap.ExtractActor(followable)
if err != nil {
return nil, errors.New("error extracting actor property from follow")
}
@@ -366,7 +367,7 @@ func (c *converter) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, e
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
}
- target, err := extractObject(followable)
+ target, err := ap.ExtractObject(followable)
if err != nil {
return nil, errors.New("error extracting object property from follow")
}
@@ -384,14 +385,14 @@ func (c *converter) ASFollowToFollow(followable Followable) (*gtsmodel.Follow, e
return follow, nil
}
-func (c *converter) ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error) {
+func (c *converter) ASLikeToFave(likeable ap.Likeable) (*gtsmodel.StatusFave, error) {
idProp := likeable.GetJSONLDId()
if idProp == nil || !idProp.IsIRI() {
return nil, errors.New("no id property set on like, or was not an iri")
}
uri := idProp.GetIRI().String()
- origin, err := extractActor(likeable)
+ origin, err := ap.ExtractActor(likeable)
if err != nil {
return nil, errors.New("error extracting actor property from like")
}
@@ -400,7 +401,7 @@ func (c *converter) ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error
return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err)
}
- target, err := extractObject(likeable)
+ target, err := ap.ExtractObject(likeable)
if err != nil {
return nil, errors.New("error extracting object property from like")
}
@@ -426,14 +427,14 @@ func (c *converter) ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error
}, nil
}
-func (c *converter) ASBlockToBlock(blockable Blockable) (*gtsmodel.Block, error) {
+func (c *converter) ASBlockToBlock(blockable ap.Blockable) (*gtsmodel.Block, error) {
idProp := blockable.GetJSONLDId()
if idProp == nil || !idProp.IsIRI() {
return nil, errors.New("ASBlockToBlock: no id property set on block, or was not an iri")
}
uri := idProp.GetIRI().String()
- origin, err := extractActor(blockable)
+ origin, err := ap.ExtractActor(blockable)
if err != nil {
return nil, errors.New("ASBlockToBlock: error extracting actor property from block")
}
@@ -442,7 +443,7 @@ func (c *converter) ASBlockToBlock(blockable Blockable) (*gtsmodel.Block, error)
return nil, fmt.Errorf("ASBlockToBlock: error extracting account with uri %s from the database: %s", origin.String(), err)
}
- target, err := extractObject(blockable)
+ target, err := ap.ExtractObject(blockable)
if err != nil {
return nil, errors.New("ASBlockToBlock: error extracting object property from block")
}
@@ -461,7 +462,7 @@ func (c *converter) ASBlockToBlock(blockable Blockable) (*gtsmodel.Block, error)
}, nil
}
-func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Status, bool, error) {
+func (c *converter) ASAnnounceToStatus(announceable ap.Announceable) (*gtsmodel.Status, bool, error) {
status := &gtsmodel.Status{}
isNew := true
@@ -480,7 +481,7 @@ func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Sta
status.URI = uri
// get the URI of the announced/boosted status
- boostedStatusURI, err := extractObject(announceable)
+ boostedStatusURI, err := ap.ExtractObject(announceable)
if err != nil {
return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error getting object from announce: %s", err)
}
@@ -491,7 +492,7 @@ func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Sta
}
// get the published time for the announce
- published, err := extractPublished(announceable)
+ published, err := ap.ExtractPublished(announceable)
if err != nil {
return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error extracting published time: %s", err)
}
@@ -499,7 +500,7 @@ func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Sta
status.UpdatedAt = published
// get the actor's IRI (ie., the person who boosted the status)
- actor, err := extractActor(announceable)
+ actor, err := ap.ExtractActor(announceable)
if err != nil {
return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error extracting actor: %s", err)
}
@@ -522,12 +523,12 @@ func (c *converter) ASAnnounceToStatus(announceable Announceable) (*gtsmodel.Sta
// parse the visibility from the To and CC entries
var visibility gtsmodel.Visibility
- to, err := extractTos(announceable)
+ to, err := ap.ExtractTos(announceable)
if err != nil {
return nil, isNew, fmt.Errorf("error extracting TO values: %s", err)
}
- cc, err := extractCCs(announceable)
+ cc, err := ap.ExtractCCs(announceable)
if err != nil {
return nil, isNew, fmt.Errorf("error extracting CC values: %s", err)
}
diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go
index 9d6ce4e0a..2e33271c5 100644
--- a/internal/typeutils/astointernal_test.go
+++ b/internal/typeutils/astointernal_test.go
@@ -28,6 +28,7 @@ import (
"github.com/go-fed/activity/streams/vocab"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -342,7 +343,7 @@ func (suite *ASToInternalTestSuite) SetupSuite() {
}
func (suite *ASToInternalTestSuite) SetupTest() {
- testrig.StandardDBSetup(suite.db)
+ testrig.StandardDBSetup(suite.db, nil)
}
func (suite *ASToInternalTestSuite) TestParsePerson() {
@@ -364,7 +365,7 @@ func (suite *ASToInternalTestSuite) TestParseGargron() {
t, err := streams.ToType(context.Background(), m)
assert.NoError(suite.T(), err)
- rep, ok := t.(typeutils.Accountable)
+ rep, ok := t.(ap.Accountable)
assert.True(suite.T(), ok)
acct, err := suite.typeconverter.ASRepresentationToAccount(rep, false)
@@ -391,7 +392,7 @@ func (suite *ASToInternalTestSuite) TestParseStatus() {
first := obj.Begin()
assert.NotNil(suite.T(), first)
- rep, ok := first.GetType().(typeutils.Statusable)
+ rep, ok := first.GetType().(ap.Statusable)
assert.True(suite.T(), ok)
status, err := suite.typeconverter.ASStatusToStatus(rep)
@@ -418,7 +419,7 @@ func (suite *ASToInternalTestSuite) TestParseStatusWithMention() {
first := obj.Begin()
assert.NotNil(suite.T(), first)
- rep, ok := first.GetType().(typeutils.Statusable)
+ rep, ok := first.GetType().(ap.Statusable)
assert.True(suite.T(), ok)
status, err := suite.typeconverter.ASStatusToStatus(rep)
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index 57c2a1f6d..10d9a0f18 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -19,7 +19,10 @@
package typeutils
import (
+ "net/url"
+
"github.com/go-fed/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
@@ -99,17 +102,17 @@ type TypeConverter interface {
// If update is false, and the account is already known in the database, then the existing account entry will be returned.
// If update is true, then even if the account is already known, all fields in the accountable will be parsed and a new *gtsmodel.Account
// will be generated. This is useful when one needs to force refresh of an account, eg., during an Update of a Profile.
- ASRepresentationToAccount(accountable Accountable, update bool) (*gtsmodel.Account, error)
+ ASRepresentationToAccount(accountable ap.Accountable, update bool) (*gtsmodel.Account, error)
// ASStatus converts a remote activitystreams 'status' representation into a gts model status.
- ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error)
+ ASStatusToStatus(statusable ap.Statusable) (*gtsmodel.Status, error)
// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request.
- ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error)
+ ASFollowToFollowRequest(followable ap.Followable) (*gtsmodel.FollowRequest, error)
// ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow.
- ASFollowToFollow(followable Followable) (*gtsmodel.Follow, error)
+ ASFollowToFollow(followable ap.Followable) (*gtsmodel.Follow, error)
// ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave.
- ASLikeToFave(likeable Likeable) (*gtsmodel.StatusFave, error)
+ ASLikeToFave(likeable ap.Likeable) (*gtsmodel.StatusFave, error)
// ASBlockToBlock converts a remote activity streams 'block' representation into a gts model block.
- ASBlockToBlock(blockable Blockable) (*gtsmodel.Block, error)
+ ASBlockToBlock(blockable ap.Blockable) (*gtsmodel.Block, error)
// ASAnnounceToStatus converts an activitystreams 'announce' into a status.
//
// The returned bool indicates whether this status is new (true) or not new (false).
@@ -122,7 +125,7 @@ type TypeConverter interface {
// This is useful when multiple users on an instance might receive the same boost, and we only want to process the boost once.
//
// NOTE -- this is different from one status being boosted multiple times! In this case, new boosts should indeed be created.
- ASAnnounceToStatus(announceable Announceable) (status *gtsmodel.Status, new bool, err error)
+ ASAnnounceToStatus(announceable ap.Announceable) (status *gtsmodel.Status, new bool, err error)
/*
INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL
@@ -150,7 +153,10 @@ type TypeConverter interface {
BoostToAS(boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) (vocab.ActivityStreamsAnnounce, error)
// BlockToAS converts a gts model block into an activityStreams BLOCK, suitable for federation.
BlockToAS(block *gtsmodel.Block) (vocab.ActivityStreamsBlock, error)
-
+ // StatusToASRepliesCollection converts a gts model status into an activityStreams REPLIES collection.
+ StatusToASRepliesCollection(status *gtsmodel.Status, onlyOtherAccounts bool) (vocab.ActivityStreamsCollection, error)
+ // StatusURIsToASRepliesPage returns a collection page with appropriate next/part of pagination.
+ StatusURIsToASRepliesPage(status *gtsmodel.Status, onlyOtherAccounts bool, minID string, replies map[string]*url.URL) (vocab.ActivityStreamsCollectionPage, error)
/*
INTERNAL (gts) MODEL TO INTERNAL MODEL
*/
diff --git a/internal/typeutils/converter_test.go b/internal/typeutils/converter_test.go
index b2272f50c..c104ab06c 100644
--- a/internal/typeutils/converter_test.go
+++ b/internal/typeutils/converter_test.go
@@ -21,6 +21,7 @@ package typeutils_test
import (
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
@@ -34,7 +35,7 @@ type ConverterStandardTestSuite struct {
db db.DB
log *logrus.Logger
accounts map[string]*gtsmodel.Account
- people map[string]typeutils.Accountable
+ people map[string]ap.Accountable
typeconverter typeutils.TypeConverter
}
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index b24b07e13..333f131d4 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -27,6 +27,7 @@ import (
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
@@ -505,7 +506,14 @@ func (c *converter) StatusToAS(s *gtsmodel.Status) (vocab.ActivityStreamsNote, e
status.SetActivityStreamsAttachment(attachmentProp)
// replies
- // TODO
+ repliesCollection, err := c.StatusToASRepliesCollection(s, false)
+ if err != nil {
+ return nil, fmt.Errorf("error creating repliesCollection: %s", err)
+ }
+
+ repliesProp := streams.NewActivityStreamsRepliesProperty()
+ repliesProp.SetActivityStreamsCollection(repliesCollection)
+ status.SetActivityStreamsReplies(repliesProp)
return status, nil
}
@@ -850,3 +858,138 @@ func (c *converter) BlockToAS(b *gtsmodel.Block) (vocab.ActivityStreamsBlock, er
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(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(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
+}
diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go
index 8eb827e35..caa56ce0d 100644
--- a/internal/typeutils/internaltoas_test.go
+++ b/internal/typeutils/internaltoas_test.go
@@ -47,7 +47,7 @@ func (suite *InternalToASTestSuite) SetupSuite() {
}
func (suite *InternalToASTestSuite) SetupTest() {
- testrig.StandardDBSetup(suite.db)
+ testrig.StandardDBSetup(suite.db, nil)
}
// TearDownTest drops tables to make sure there's no data in the db