Dereference remote replies (#132)

* decided where to put reply dereferencing

* fiddling with dereferencing threads

* further adventures

* tidy up some stuff

* move dereferencing functionality

* a bunch of refactoring

* go fmt

* more refactoring

* bleep bloop

* docs and linting

* start implementing replies collection on gts side

* fiddling around

* allow dereferencing our replies

* lint, fmt
This commit is contained in:
Tobi Smethurst 2021-08-10 13:32:39 +02:00 committed by GitHub
commit 0f2de6394a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 2946 additions and 1393 deletions

View file

@ -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")
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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
*/

View file

@ -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
}

View file

@ -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
}

View file

@ -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