diff options
Diffstat (limited to 'internal/typeutils')
-rw-r--r-- | internal/typeutils/accountable.go | 101 | ||||
-rw-r--r-- | internal/typeutils/asextractionutil.go | 367 | ||||
-rw-r--r-- | internal/typeutils/asinterfaces.go | 237 | ||||
-rw-r--r-- | internal/typeutils/astointernal.go | 201 | ||||
-rw-r--r-- | internal/typeutils/astointernal_test.go | 233 | ||||
-rw-r--r-- | internal/typeutils/converter.go | 4 | ||||
-rw-r--r-- | internal/typeutils/internaltoas.go | 16 |
7 files changed, 1032 insertions, 127 deletions
diff --git a/internal/typeutils/accountable.go b/internal/typeutils/accountable.go deleted file mode 100644 index ba5c4aa2a..000000000 --- a/internal/typeutils/accountable.go +++ /dev/null @@ -1,101 +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 - withGetTypeName - withPreferredUsername - withIcon - withDisplayName - withImage - withSummary - withDiscoverable - withURL - withPublicKey - withInbox - withOutbox - withFollowing - withFollowers - withFeatured -} - -type withJSONLDId interface { - GetJSONLDId() vocab.JSONLDIdProperty -} - -type withGetTypeName interface { - GetTypeName() string -} - -type withPreferredUsername interface { - GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty -} - -type withIcon interface { - GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty -} - -type withDisplayName 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 -} diff --git a/internal/typeutils/asextractionutil.go b/internal/typeutils/asextractionutil.go index 8d39be3ec..4ee3347bd 100644 --- a/internal/typeutils/asextractionutil.go +++ b/internal/typeutils/asextractionutil.go @@ -25,8 +25,12 @@ import ( "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) { @@ -40,22 +44,89 @@ func extractPreferredUsername(i withPreferredUsername) (string, error) { return u.GetXMLSchemaString(), nil } -func extractName(i withDisplayName) (string, error) { +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 nameIter := nameProp.Begin(); nameIter != nameProp.End(); nameIter = nameIter.Next() { - if nameIter.IsXMLSchemaString() && nameIter.GetXMLSchemaString() != "" { - return nameIter.GetXMLSchemaString(), nil + 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() + 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() + 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() + 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() + 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", @@ -72,12 +143,12 @@ func extractIconURL(i withIcon) (*url.URL, error) { // 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 iconIter := iconProp.Begin(); iconIter != iconProp.End(); iconIter = iconIter.Next() { + for iter := iconProp.Begin(); iter != iconProp.End(); iter = iter.Next() { // 1. is an image - if !iconIter.IsActivityStreamsImage() { + if !iter.IsActivityStreamsImage() { continue } - imageValue := iconIter.GetActivityStreamsImage() + imageValue := iter.GetActivityStreamsImage() if imageValue == nil { continue } @@ -108,12 +179,12 @@ func extractImageURL(i withImage) (*url.URL, error) { // 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 imageIter := imageProp.Begin(); imageIter != imageProp.End(); imageIter = imageIter.Next() { + for iter := imageProp.Begin(); iter != imageProp.End(); iter = iter.Next() { // 1. is an image - if !imageIter.IsActivityStreamsImage() { + if !iter.IsActivityStreamsImage() { continue } - imageValue := imageIter.GetActivityStreamsImage() + imageValue := iter.GetActivityStreamsImage() if imageValue == nil { continue } @@ -134,9 +205,9 @@ func extractSummary(i withSummary) (string, error) { return "", errors.New("summary property was nil") } - for summaryIter := summaryProp.Begin(); summaryIter != summaryProp.End(); summaryIter = summaryIter.Next() { - if summaryIter.IsXMLSchemaString() && summaryIter.GetXMLSchemaString() != "" { - return summaryIter.GetXMLSchemaString(), nil + for iter := summaryProp.Begin(); iter != summaryProp.End(); iter = iter.Next() { + if iter.IsXMLSchemaString() && iter.GetXMLSchemaString() != "" { + return iter.GetXMLSchemaString(), nil } } @@ -156,9 +227,9 @@ func extractURL(i withURL) (*url.URL, error) { return nil, errors.New("url property was nil") } - for urlIter := urlProp.Begin(); urlIter != urlProp.End(); urlIter = urlIter.Next() { - if urlIter.IsIRI() && urlIter.GetIRI() != nil { - return urlIter.GetIRI(), nil + for iter := urlProp.Begin(); iter != urlProp.End(); iter = iter.Next() { + if iter.IsIRI() && iter.GetIRI() != nil { + return iter.GetIRI(), nil } } @@ -171,8 +242,8 @@ func extractPublicKeyForOwner(i withPublicKey, forOwner *url.URL) (*rsa.PublicKe return nil, nil, errors.New("public key property was nil") } - for publicKeyIter := publicKeyProp.Begin(); publicKeyIter != publicKeyProp.End(); publicKeyIter = publicKeyIter.Next() { - pkey := publicKeyIter.Get() + for iter := publicKeyProp.Begin(); iter != publicKeyProp.End(); iter = iter.Next() { + pkey := iter.Get() if pkey == nil { continue } @@ -214,3 +285,263 @@ func extractPublicKeyForOwner(i withPublicKey, forOwner *url.URL) (*rsa.PublicKe } 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() + for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() { + attachmentable, ok := iter.(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 := >smodel.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 + } + + blurhash, err := extractBlurhash(i) + if err == nil { + attachment.Blurhash = blurhash + } + + 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() + 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 := >smodel.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() + 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 := >smodel.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() + 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 := >smodel.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 new file mode 100644 index 000000000..970ed2ecf --- /dev/null +++ b/internal/typeutils/asinterfaces.go @@ -0,0 +1,237 @@ +/* + 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 + withBlurhash + withFocalPoint +} + +// 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 +} + +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 7842411ea..7f0a4c1a4 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -21,6 +21,8 @@ package typeutils import ( "errors" "fmt" + "net/url" + "strings" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -157,3 +159,202 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable) (*gtsmode return acct, nil } + +func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error) { + status := >smodel.Status{} + + // uri at which this status is reachable + uriProp := statusable.GetJSONLDId() + if uriProp == nil || !uriProp.IsIRI() { + return nil, errors.New("no id property found, or id was not an iri") + } + status.URI = uriProp.GetIRI().String() + + // web url for viewing this status + if statusURL, err := extractURL(statusable); err == nil { + status.URL = statusURL.String() + } + + // the html-formatted content of this status + if content, err := 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 { + status.GTSMediaAttachments = attachments + } + + // hashtags to dereference later on + if hashtags, err := extractHashtags(statusable); err == nil { + status.GTSTags = hashtags + } + + // emojis to dereference and fetch later on + if emojis, err := extractEmojis(statusable); err == nil { + status.GTSEmojis = emojis + } + + // mentions to dereference later on + if mentions, err := extractMentions(statusable); err == nil { + status.GTSMentions = mentions + } + + // cw string for this status + if cw, err := extractSummary(statusable); err == nil { + status.ContentWarning = cw + } + + // when was this status created? + published, err := 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) + if err != nil { + return nil, errors.New("attributedTo was empty") + } + status.APStatusOwnerURI = attributedTo.String() + + statusOwner := >smodel.Account{} + if err := c.db.GetWhere("uri", attributedTo.String(), statusOwner); err != nil { + return nil, fmt.Errorf("couldn't get status owner from db: %s", err) + } + status.AccountID = statusOwner.ID + status.GTSAccount = statusOwner + + // check if there's a post that this is a reply to + inReplyToURI, err := extractInReplyToURI(statusable) + if err == 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.APReplyToStatusURI = inReplyToURI.String() + + // now we can check if we have the replied-to status in our db already + inReplyToStatus := >smodel.Status{} + if err := c.db.GetWhere("uri", inReplyToURI.String(), inReplyToStatus); err == nil { + // we have the status in our database already + // so we can set these fields here and then... + status.InReplyToID = inReplyToStatus.ID + status.InReplyToAccountID = inReplyToStatus.AccountID + status.GTSReplyToStatus = inReplyToStatus + + // ... check if we've seen the account already + inReplyToAccount := >smodel.Account{} + if err := c.db.GetByID(inReplyToStatus.AccountID, inReplyToAccount); err == nil { + status.GTSReplyToAccount = inReplyToAccount + } + } + } + + // visibility entry for this status + var visibility gtsmodel.Visibility + + to, err := extractTos(statusable) + if err != nil { + return nil, fmt.Errorf("error extracting TO values: %s", err) + } + + cc, err := extractCCs(statusable) + if err != nil { + return nil, fmt.Errorf("error extracting CC values: %s", err) + } + + if len(to) == 0 && len(cc) == 0 { + return nil, errors.New("message wasn't TO or CC anyone") + } + + // for visibility derivation, we start by assuming most restrictive, and work our way to least restrictive + + // if it's a DM then it's addressed to SPECIFIC ACCOUNTS and not followers or public + if len(to) != 0 && len(cc) == 0 { + visibility = gtsmodel.VisibilityDirect + } + + // if it's just got followers in TO and it's not also CC'ed to public, it's followers only + if isFollowers(to, statusOwner.FollowersURI) { + visibility = gtsmodel.VisibilityFollowersOnly + } + + // if it's CC'ed to public, it's public or unlocked + // mentioned SPECIFIC ACCOUNTS also get added to CC'es if it's not a direct message + if isPublic(to) { + visibility = gtsmodel.VisibilityPublic + } + + // we should have a visibility by now + if visibility == "" { + return nil, errors.New("couldn't derive visibility") + } + status.Visibility = visibility + + // advanced visibility for this status + // TODO: a lot of work to be done here -- a new type needs to be created for this in go-fed/activity using ASTOOL + + // sensitive + // TODO: this is a bool + + // language + // we might be able to extract this from the contentMap field + + // ActivityStreamsType + status.ActivityStreamsType = gtsmodel.ActivityStreamsObject(statusable.GetTypeName()) + + return status, nil +} + +func (c *converter) ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, 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) + if err != nil { + return nil, errors.New("error extracting actor property from follow") + } + originAccount := >smodel.Account{} + if err := c.db.GetWhere("uri", origin.String(), originAccount); err != nil { + return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + } + + target, err := extractObject(followable) + if err != nil { + return nil, errors.New("error extracting object property from follow") + } + targetAccount := >smodel.Account{} + if err := c.db.GetWhere("uri", target.String(), targetAccount); err != nil { + return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + } + + followRequest := >smodel.FollowRequest{ + URI: uri, + AccountID: originAccount.ID, + TargetAccountID: targetAccount.ID, + } + + return followRequest, nil +} + +func isPublic(tos []*url.URL) bool { + for _, entry := range tos { + if strings.EqualFold(entry.String(), "https://www.w3.org/ns/activitystreams#Public") { + return true + } + } + return false +} + +func isFollowers(ccs []*url.URL, followersURI string) bool { + for _, entry := range ccs { + if strings.EqualFold(entry.String(), followersURI) { + return true + } + } + return false +} diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go index 1cd66a0ab..f1287e027 100644 --- a/internal/typeutils/astointernal_test.go +++ b/internal/typeutils/astointernal_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/go-fed/activity/streams" + "github.com/go-fed/activity/streams/vocab" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/typeutils" @@ -36,6 +37,182 @@ type ASToInternalTestSuite struct { } const ( + statusWithMentionsActivityJson = `{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount" + } + ], + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552/activity", + "type": "Create", + "actor": "https://ondergrond.org/users/dumpsterqueer", + "published": "2021-05-12T09:58:38Z", + "to": [ + "https://ondergrond.org/users/dumpsterqueer/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://social.pixie.town/users/f0x" + ], + "object": { + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552", + "type": "Note", + "summary": null, + "inReplyTo": "https://social.pixie.town/users/f0x/statuses/106221628567855262", + "published": "2021-05-12T09:58:38Z", + "url": "https://ondergrond.org/@dumpsterqueer/106221634728637552", + "attributedTo": "https://ondergrond.org/users/dumpsterqueer", + "to": [ + "https://ondergrond.org/users/dumpsterqueer/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://social.pixie.town/users/f0x" + ], + "sensitive": false, + "atomUri": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552", + "inReplyToAtomUri": "https://social.pixie.town/users/f0x/statuses/106221628567855262", + "conversation": "tag:ondergrond.org,2021-05-12:objectId=1132361:objectType=Conversation", + "content": "<p><span class=\"h-card\"><a href=\"https://social.pixie.town/@f0x\" class=\"u-url mention\">@<span>f0x</span></a></span> nice there it is:</p><p><a href=\"https://social.pixie.town/users/f0x/statuses/106221628567855262/activity\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">social.pixie.town/users/f0x/st</span><span class=\"invisible\">atuses/106221628567855262/activity</span></a></p>", + "contentMap": { + "en": "<p><span class=\"h-card\"><a href=\"https://social.pixie.town/@f0x\" class=\"u-url mention\">@<span>f0x</span></a></span> nice there it is:</p><p><a href=\"https://social.pixie.town/users/f0x/statuses/106221628567855262/activity\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">social.pixie.town/users/f0x/st</span><span class=\"invisible\">atuses/106221628567855262/activity</span></a></p>" + }, + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": "https://social.pixie.town/users/f0x", + "name": "@f0x@pixie.town" + } + ], + "replies": { + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552/replies?only_other_accounts=true&page=true", + "partOf": "https://ondergrond.org/users/dumpsterqueer/statuses/106221634728637552/replies", + "items": [] + } + } + } + }` + statusWithEmojisAndTagsAsActivityJson = `{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "sensitive": "as:sensitive", + "toot": "http://joinmastodon.org/ns#", + "votersCount": "toot:votersCount", + "Hashtag": "as:Hashtag", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + } + } + ], + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704/activity", + "type": "Create", + "actor": "https://ondergrond.org/users/dumpsterqueer", + "published": "2021-05-12T09:41:38Z", + "to": [ + "https://ondergrond.org/users/dumpsterqueer/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "object": { + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704", + "type": "Note", + "summary": null, + "inReplyTo": null, + "published": "2021-05-12T09:41:38Z", + "url": "https://ondergrond.org/@dumpsterqueer/106221567884565704", + "attributedTo": "https://ondergrond.org/users/dumpsterqueer", + "to": [ + "https://ondergrond.org/users/dumpsterqueer/followers" + ], + "cc": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "sensitive": false, + "atomUri": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704", + "inReplyToAtomUri": null, + "conversation": "tag:ondergrond.org,2021-05-12:objectId=1132361:objectType=Conversation", + "content": "<p>just testing activitypub representations of <a href=\"https://ondergrond.org/tags/tags\" class=\"mention hashtag\" rel=\"tag\">#<span>tags</span></a> and <a href=\"https://ondergrond.org/tags/emoji\" class=\"mention hashtag\" rel=\"tag\">#<span>emoji</span></a> :party_parrot: :amaze: :blobsunglasses: </p><p>don't mind me....</p>", + "contentMap": { + "en": "<p>just testing activitypub representations of <a href=\"https://ondergrond.org/tags/tags\" class=\"mention hashtag\" rel=\"tag\">#<span>tags</span></a> and <a href=\"https://ondergrond.org/tags/emoji\" class=\"mention hashtag\" rel=\"tag\">#<span>emoji</span></a> :party_parrot: :amaze: :blobsunglasses: </p><p>don't mind me....</p>" + }, + "attachment": [], + "tag": [ + { + "type": "Hashtag", + "href": "https://ondergrond.org/tags/tags", + "name": "#tags" + }, + { + "type": "Hashtag", + "href": "https://ondergrond.org/tags/emoji", + "name": "#emoji" + }, + { + "id": "https://ondergrond.org/emojis/2390", + "type": "Emoji", + "name": ":party_parrot:", + "updated": "2020-11-06T13:42:11Z", + "icon": { + "type": "Image", + "mediaType": "image/gif", + "url": "https://ondergrond.org/system/custom_emojis/images/000/002/390/original/ef133aac7ab23341.gif" + } + }, + { + "id": "https://ondergrond.org/emojis/2395", + "type": "Emoji", + "name": ":amaze:", + "updated": "2020-09-26T12:29:56Z", + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://ondergrond.org/system/custom_emojis/images/000/002/395/original/2c7d9345e57367ed.png" + } + }, + { + "id": "https://ondergrond.org/emojis/764", + "type": "Emoji", + "name": ":blobsunglasses:", + "updated": "2020-09-26T12:13:23Z", + "icon": { + "type": "Image", + "mediaType": "image/png", + "url": "https://ondergrond.org/system/custom_emojis/images/000/000/764/original/3f8eef9de773c90d.png" + } + } + ], + "replies": { + "id": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704/replies", + "type": "Collection", + "first": { + "type": "CollectionPage", + "next": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704/replies?only_other_accounts=true&page=true", + "partOf": "https://ondergrond.org/users/dumpsterqueer/statuses/106221567884565704/replies", + "items": [] + } + } + } + }` gargronAsActivityJson = `{ "@context": [ "https://www.w3.org/ns/activitystreams", @@ -197,6 +374,62 @@ func (suite *ASToInternalTestSuite) TestParseGargron() { // TODO: write assertions here, rn we're just eyeballing the output } +func (suite *ASToInternalTestSuite) TestParseStatus() { + m := make(map[string]interface{}) + err := json.Unmarshal([]byte(statusWithEmojisAndTagsAsActivityJson), &m) + assert.NoError(suite.T(), err) + + t, err := streams.ToType(context.Background(), m) + assert.NoError(suite.T(), err) + + create, ok := t.(vocab.ActivityStreamsCreate) + assert.True(suite.T(), ok) + + obj := create.GetActivityStreamsObject() + assert.NotNil(suite.T(), obj) + + first := obj.Begin() + assert.NotNil(suite.T(), first) + + rep, ok := first.GetType().(typeutils.Statusable) + assert.True(suite.T(), ok) + + status, err := suite.typeconverter.ASStatusToStatus(rep) + assert.NoError(suite.T(), err) + + assert.Len(suite.T(), status.GTSEmojis, 3) + // assert.Len(suite.T(), status.GTSTags, 2) TODO: implement this first so that it can pick up tags +} + +func (suite *ASToInternalTestSuite) TestParseStatusWithMention() { + m := make(map[string]interface{}) + err := json.Unmarshal([]byte(statusWithMentionsActivityJson), &m) + assert.NoError(suite.T(), err) + + t, err := streams.ToType(context.Background(), m) + assert.NoError(suite.T(), err) + + create, ok := t.(vocab.ActivityStreamsCreate) + assert.True(suite.T(), ok) + + obj := create.GetActivityStreamsObject() + assert.NotNil(suite.T(), obj) + + first := obj.Begin() + assert.NotNil(suite.T(), first) + + rep, ok := first.GetType().(typeutils.Statusable) + assert.True(suite.T(), ok) + + status, err := suite.typeconverter.ASStatusToStatus(rep) + assert.NoError(suite.T(), err) + + fmt.Printf("%+v", status) + + assert.Len(suite.T(), status.GTSMentions, 1) + fmt.Println(status.GTSMentions[0]) +} + func (suite *ASToInternalTestSuite) TearDownTest() { testrig.StandardDBTeardown(suite.db) } diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index f269fa182..8f310c921 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -90,6 +90,10 @@ type TypeConverter interface { // ASPersonToAccount converts a remote account/person/application representation into a gts model account ASRepresentationToAccount(accountable Accountable) (*gtsmodel.Account, error) + // ASStatus converts a remote activitystreams 'status' representation into a gts model status. + ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, error) + // ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request. + ASFollowToFollowRequest(followable Followable) (*gtsmodel.FollowRequest, error) /* INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index 73c121155..0216dea5e 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -200,15 +200,15 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso // icon // Used as profile avatar. if a.AvatarMediaAttachmentID != "" { - iconProperty := streams.NewActivityStreamsIconProperty() - - iconImage := streams.NewActivityStreamsImage() - avatar := >smodel.MediaAttachment{} if err := c.db.GetByID(a.AvatarMediaAttachmentID, avatar); err != nil { return nil, err } + iconProperty := streams.NewActivityStreamsIconProperty() + + iconImage := streams.NewActivityStreamsImage() + mediaType := streams.NewActivityStreamsMediaTypeProperty() mediaType.Set(avatar.File.ContentType) iconImage.SetActivityStreamsMediaType(mediaType) @@ -228,15 +228,15 @@ func (c *converter) AccountToAS(a *gtsmodel.Account) (vocab.ActivityStreamsPerso // image // Used as profile header. if a.HeaderMediaAttachmentID != "" { - headerProperty := streams.NewActivityStreamsImageProperty() - - headerImage := streams.NewActivityStreamsImage() - header := >smodel.MediaAttachment{} if err := c.db.GetByID(a.HeaderMediaAttachmentID, header); err != nil { return nil, err } + headerProperty := streams.NewActivityStreamsImageProperty() + + headerImage := streams.NewActivityStreamsImage() + mediaType := streams.NewActivityStreamsMediaTypeProperty() mediaType.Set(header.File.ContentType) headerImage.SetActivityStreamsMediaType(mediaType) |