gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit d8e16a226a570a7d262bdeb067273ce35b03cc7c
parent 0fa06c0cde19ba5a4b77aa8372509cf9116fc546
Author: tobi <31960611+tsmethurst@users.noreply.github.com>
Date:   Sat, 17 Jun 2023 17:49:11 +0200

[chore/bugfix] Refactor `ap/extract.go` functions, return URIs more reliably (#1897)


Diffstat:
Minternal/ap/activitystreams.go | 7+++++++
Minternal/ap/extract.go | 686++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Minternal/ap/extractattachments_test.go | 2+-
Minternal/federation/dereferencing/status.go | 4++--
Minternal/federation/federatingdb/announce.go | 9+++++----
Minternal/federation/federatingprotocol.go | 20+++++---------------
Minternal/typeutils/astointernal.go | 144+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Minternal/typeutils/util.go | 18++++++++++++++++++
Minternal/typeutils/wrap.go | 49++++++++++++++++++++++++++++++++-----------------
9 files changed, 562 insertions(+), 377 deletions(-)

diff --git a/internal/ap/activitystreams.go b/internal/ap/activitystreams.go @@ -69,4 +69,11 @@ const ( ObjectCollection = "Collection" // ActivityStreamsCollection https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collection ObjectCollectionPage = "CollectionPage" // ActivityStreamsCollectionPage https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage ObjectOrderedCollection = "OrderedCollection" // ActivityStreamsOrderedCollection https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection + + // Hashtag is not in the AS spec per se, but it tends to get used + // as though 'Hashtag' is a named type under the Tag property. + // + // See https://www.w3.org/TR/activitystreams-vocabulary/#microsyntaxes + // and https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag + TagHashtag = "Hashtag" ) diff --git a/internal/ap/extract.go b/internal/ap/extract.go @@ -15,9 +15,6 @@ // 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 ap contains models and utilities for working with activitypub/activitystreams representations. -// -// It is built on top of go-fed/activity. package ap import ( @@ -25,7 +22,6 @@ import ( "crypto/rsa" "crypto/x509" "encoding/pem" - "errors" "fmt" "net/url" "strings" @@ -37,28 +33,34 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/util" ) -// ExtractPreferredUsername returns a string representation of an interface's preferredUsername property. +// ExtractPreferredUsername returns a string representation of +// an interface's preferredUsername property. Will return an +// error if preferredUsername is nil, not a string, or empty. func ExtractPreferredUsername(i WithPreferredUsername) (string, error) { u := i.GetActivityStreamsPreferredUsername() if u == nil || !u.IsXMLSchemaString() { - return "", errors.New("preferredUsername was not a string") + return "", gtserror.New("preferredUsername nil or not a string") } + if u.GetXMLSchemaString() == "" { - return "", errors.New("preferredUsername was empty") + return "", gtserror.New("preferredUsername was empty") } + return u.GetXMLSchemaString(), nil } -// ExtractName returns a string representation of an interface's name property, -// or an empty string if this is not found. +// ExtractName returns the first string representation it +// can find of an interface's name property, or an empty +// string if this is not found. func ExtractName(i WithName) string { nameProp := i.GetActivityStreamsName() if nameProp == nil { return "" } - // Take the first useful value for the name string we can find. for iter := nameProp.Begin(); iter != nameProp.End(); iter = iter.Next() { + // Name may be parsed as IRI, depending on + // how it's formatted, so account for this. switch { case iter.IsXMLSchemaString(): return iter.GetXMLSchemaString() @@ -70,192 +72,223 @@ func ExtractName(i WithName) string { return "" } -// ExtractInReplyToURI extracts the inReplyToURI property (if present) from an interface. +// ExtractInReplyToURI extracts the first inReplyTo URI +// property it can find from an interface. Will return +// nil if no valid URI can be found. func ExtractInReplyToURI(i WithInReplyTo) *url.URL { inReplyToProp := i.GetActivityStreamsInReplyTo() if inReplyToProp == nil { - // the property just wasn't set return nil } + for iter := inReplyToProp.Begin(); iter != inReplyToProp.End(); iter = iter.Next() { - if iter.IsIRI() { - if iter.GetIRI() != nil { - return iter.GetIRI() - } + iri, err := pub.ToId(iter) + if err == nil && iri != nil { + // Found one we can use. + return iri } } - // couldn't find a URI + return nil } -// ExtractURLItems extracts a slice of URLs from a property that has withItems. -func ExtractURLItems(i WithItems) []*url.URL { - urls := []*url.URL{} - items := i.GetActivityStreamsItems() - if items == nil || items.Len() == 0 { - return urls +// ExtractItemsURIs extracts each URI it can +// find for an item from the provided WithItems. +func ExtractItemsURIs(i WithItems) []*url.URL { + itemsProp := i.GetActivityStreamsItems() + if itemsProp == nil { + return nil } - for iter := items.Begin(); iter != items.End(); iter = iter.Next() { - if iter.IsIRI() { - urls = append(urls, iter.GetIRI()) + uris := make([]*url.URL, 0, itemsProp.Len()) + for iter := itemsProp.Begin(); iter != itemsProp.End(); iter = iter.Next() { + uri, err := pub.ToId(iter) + if err == nil { + // Found one we can use. + uris = append(uris, uri) } } - return urls + + return uris } -// ExtractTos returns a list of URIs that the activity addresses as To. -func ExtractTos(i WithTo) ([]*url.URL, error) { - to := []*url.URL{} +// ExtractToURIs returns a slice of URIs +// that the given WithTo addresses as To. +func ExtractToURIs(i WithTo) []*url.URL { toProp := i.GetActivityStreamsTo() if toProp == nil { - return nil, errors.New("toProp was nil") + return nil } + + uris := make([]*url.URL, 0, toProp.Len()) for iter := toProp.Begin(); iter != toProp.End(); iter = iter.Next() { - if iter.IsIRI() { - if iter.GetIRI() != nil { - to = append(to, iter.GetIRI()) - } + uri, err := pub.ToId(iter) + if err == nil { + // Found one we can use. + uris = append(uris, uri) } } - return to, nil + + return uris } -// ExtractCCs returns a list of URIs that the activity addresses as CC. -func ExtractCCs(i WithCC) ([]*url.URL, error) { - cc := []*url.URL{} +// ExtractCcURIs returns a slice of URIs +// that the given WithCC addresses as Cc. +func ExtractCcURIs(i WithCC) []*url.URL { ccProp := i.GetActivityStreamsCc() if ccProp == nil { - return cc, nil + return nil } + + urls := make([]*url.URL, 0, ccProp.Len()) for iter := ccProp.Begin(); iter != ccProp.End(); iter = iter.Next() { - if iter.IsIRI() { - if iter.GetIRI() != nil { - cc = append(cc, iter.GetIRI()) - } + uri, err := pub.ToId(iter) + if err == nil { + // Found one we can use. + urls = append(urls, uri) } } - return cc, nil + + return urls } -// ExtractAttributedTo returns the URL of the actor that the withAttributedTo is attributed to. -func ExtractAttributedTo(i WithAttributedTo) (*url.URL, error) { +// ExtractAttributedToURI returns the first URI it can find in the +// given WithAttributedTo, or an error if no URI can be found. +func ExtractAttributedToURI(i WithAttributedTo) (*url.URL, error) { attributedToProp := i.GetActivityStreamsAttributedTo() if attributedToProp == nil { - return nil, errors.New("attributedToProp was nil") + return nil, gtserror.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 - } + id, err := pub.ToId(iter) + if err == nil { + return id, nil } } - return nil, errors.New("couldn't find iri for attributed to") + + return nil, gtserror.New("couldn't find iri for attributed to") } -// ExtractPublished extracts the publication time of an activity. +// ExtractPublished extracts the published time from the given +// WithPublished. Will return an error if the published property +// is not set, is not a time.Time, or is zero. func ExtractPublished(i WithPublished) (time.Time, error) { + t := time.Time{} + publishedProp := i.GetActivityStreamsPublished() if publishedProp == nil { - return time.Time{}, errors.New("published prop was nil") + return t, gtserror.New("published prop was nil") } if !publishedProp.IsXMLSchemaDateTime() { - return time.Time{}, errors.New("published prop was not date time") + return t, gtserror.New("published prop was not date time") } - t := publishedProp.Get() + t = publishedProp.Get() if t.IsZero() { - return time.Time{}, errors.New("published time was zero") + return t, gtserror.New("published time was zero") } + return t, nil } -// ExtractIconURL extracts a URL to a supported image file from something like: +// ExtractIconURI extracts the first URI it can find from +// the given WithIcon which links to a supported image file. +// Input will look something like this: // // "icon": { // "mediaType": "image/jpeg", // "type": "Image", // "url": "http://example.org/path/to/some/file.jpeg" // }, -func ExtractIconURL(i WithIcon) (*url.URL, error) { +// +// If no valid URI can be found, this will return an error. +func ExtractIconURI(i WithIcon) (*url.URL, error) { iconProp := i.GetActivityStreamsIcon() if iconProp == nil { - return nil, errors.New("icon property was nil") + return nil, gtserror.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 + // 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 that we can use to derefereince 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 { + + image := iter.GetActivityStreamsImage() + if image == nil { continue } - // 2. has a URL so we can grab it - url, err := ExtractURL(imageValue) - if err == nil && url != nil { - return url, nil + imageURL, err := ExtractURL(image) + if err == nil && imageURL != nil { + return imageURL, 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") + + return nil, gtserror.New("could not extract valid image URI from icon") } -// ExtractImageURL extracts a URL to a supported image file from something like: +// ExtractImageURI extracts the first URI it can find from +// the given WithImage which links to a supported image file. +// Input will look something like this: // // "image": { // "mediaType": "image/jpeg", // "type": "Image", // "url": "http://example.org/path/to/some/file.jpeg" // }, -func ExtractImageURL(i WithImage) (*url.URL, error) { +// +// If no valid URI can be found, this will return an error. +func ExtractImageURI(i WithImage) (*url.URL, error) { imageProp := i.GetActivityStreamsImage() if imageProp == nil { - return nil, errors.New("icon property was nil") + return nil, gtserror.New("image 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 + // Image 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 that we can use to derefereince 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 { + + image := iter.GetActivityStreamsImage() + if image == nil { continue } - // 2. has a URL so we can grab it - url, err := ExtractURL(imageValue) - if err == nil && url != nil { - return url, nil + imageURL, err := ExtractURL(image) + if err == nil && imageURL != nil { + return imageURL, 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") + + return nil, gtserror.New("could not extract valid image URI from image") } -// ExtractSummary extracts the summary/content warning of an interface. -// Will return an empty string if no summary was present. +// ExtractSummary extracts the summary/content warning of +// the given WithSummary interface. Will return an empty +// string if no summary/content warning was present. func ExtractSummary(i WithSummary) string { summaryProp := i.GetActivityStreamsSummary() - if summaryProp == nil || summaryProp.Len() == 0 { - // no summary to speak of + if summaryProp == nil { return "" } for iter := summaryProp.Begin(); iter != summaryProp.End(); iter = iter.Next() { + // Summary may be parsed as IRI, depending on + // how it's formatted, so account for this. switch { case iter.IsXMLSchemaString(): return iter.GetXMLSchemaString() @@ -267,6 +300,10 @@ func ExtractSummary(i WithSummary) string { return "" } +// ExtractFields extracts property/value fields from the given +// WithAttachment interface. Will return an empty slice if no +// property/value fields can be found. Attachments that are not +// (well-formed) PropertyValues will be ignored. func ExtractFields(i WithAttachment) []*gtsmodel.Field { attachmentProp := i.GetActivityStreamsAttachment() if attachmentProp == nil { @@ -320,28 +357,38 @@ func ExtractFields(i WithAttachment) []*gtsmodel.Field { return fields } -// ExtractDiscoverable extracts the Discoverable boolean of an interface. +// ExtractDiscoverable extracts the Discoverable boolean +// of the given WithDiscoverable interface. Will return +// an error if Discoverable was nil. func ExtractDiscoverable(i WithDiscoverable) (bool, error) { - if i.GetTootDiscoverable() == nil { - return false, errors.New("discoverable was nil") + discoverableProp := i.GetTootDiscoverable() + if discoverableProp == nil { + return false, gtserror.New("discoverable was nil") } - return i.GetTootDiscoverable().Get(), nil + + return discoverableProp.Get(), nil } -// ExtractURL extracts the URL property of an interface. +// ExtractURL extracts the first URI it can find from the +// given WithURL interface, or an error if no URL was set. +// The ID of a type will not work, this function wants a URI +// specifically. func ExtractURL(i WithURL) (*url.URL, error) { urlProp := i.GetActivityStreamsUrl() if urlProp == nil { - return nil, errors.New("url property was nil") + return nil, gtserror.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 + if !iter.IsIRI() { + continue } + + // Found it. + return iter.GetIRI(), nil } - return nil, errors.New("could not extract url") + return nil, gtserror.New("no valid URL property found") } // ExtractPublicKey extracts the public key, public key ID, and public @@ -426,8 +473,9 @@ func ExtractPublicKey(i WithPublicKey) ( return nil, nil, nil, gtserror.New("couldn't find public key") } -// ExtractContent returns a string representation of the interface's Content property, -// or an empty string if no Content is found. +// ExtractContent returns a string representation of the +// given interface's Content property, or an empty string +// if no Content is found. func ExtractContent(i WithContent) string { contentProperty := i.GetActivityStreamsContent() if contentProperty == nil { @@ -436,6 +484,8 @@ func ExtractContent(i WithContent) string { for iter := contentProperty.Begin(); iter != contentProperty.End(); iter = iter.Next() { switch { + // Content may be parsed as IRI, depending on + // how it's formatted, so account for this. case iter.IsXMLSchemaString(): return iter.GetXMLSchemaString() case iter.IsIRI(): @@ -446,52 +496,64 @@ func ExtractContent(i WithContent) string { return "" } -// ExtractAttachment returns a gts model of an attachment from an attachmentable interface. +// ExtractAttachment extracts a minimal gtsmodel.Attachment +// (just remote URL, description, and blurhash) from the given +// Attachmentable interface, or an error if no remote URL is set. func ExtractAttachment(i Attachmentable) (*gtsmodel.MediaAttachment, error) { - attachment := &gtsmodel.MediaAttachment{} - - attachmentURL, err := ExtractURL(i) + // Get the URL for the attachment file. + // If no URL is set, we can't do anything. + remoteURL, err := ExtractURL(i) if err != nil { - return nil, err - } - attachment.RemoteURL = attachmentURL.String() - - mediaType := i.GetActivityStreamsMediaType() - if mediaType != nil { - attachment.File.ContentType = mediaType.Get() + return nil, gtserror.Newf("error extracting attachment URL: %w", err) } - attachment.Type = gtsmodel.FileTypeImage - - attachment.Description = ExtractName(i) - attachment.Blurhash = ExtractBlurhash(i) - attachment.Processing = gtsmodel.ProcessingStatusReceived - - return attachment, nil + return &gtsmodel.MediaAttachment{ + RemoteURL: remoteURL.String(), + Description: ExtractName(i), + Blurhash: ExtractBlurhash(i), + Processing: gtsmodel.ProcessingStatusReceived, + }, nil } -// ExtractBlurhash extracts the blurhash value (if present) from a WithBlurhash interface. +// ExtractBlurhash extracts the blurhash string value +// from the given WithBlurhash interface, or returns +// an empty string if nothing is found. func ExtractBlurhash(i WithBlurhash) string { - if i.GetTootBlurhash() == nil { + blurhashProp := i.GetTootBlurhash() + if blurhashProp == nil { return "" } - return i.GetTootBlurhash().Get() + + return blurhashProp.Get() } -// ExtractHashtags returns a slice of tags on the interface. +// ExtractHashtags extracts a slice of minimal gtsmodel.Tags +// from a WithTag. If an entry in the WithTag is not a hashtag, +// it will be quietly ignored. +// +// TODO: find a better heuristic for determining if something +// is a hashtag or not, since looking for type name "Hashtag" +// is non-normative. Perhaps look for things that are either +// type "Hashtag" or have no type name set at all? func ExtractHashtags(i WithTag) ([]*gtsmodel.Tag, error) { - tags := []*gtsmodel.Tag{} tagsProp := i.GetActivityStreamsTag() if tagsProp == nil { - return tags, nil + return nil, nil } + + var ( + l = tagsProp.Len() + tags = make([]*gtsmodel.Tag, 0, l) + keys = make(map[string]any, l) // Use map to dedupe items. + ) + for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { t := iter.GetType() if t == nil { continue } - if t.GetTypeName() != "Hashtag" { + if t.GetTypeName() != TagHashtag { continue } @@ -505,274 +567,301 @@ func ExtractHashtags(i WithTag) ([]*gtsmodel.Tag, error) { continue } - tags = append(tags, tag) + // Only append this tag if we haven't + // seen it already, to avoid duplicates + // in the slice. + if _, set := keys[tag.URL]; !set { + keys[tag.URL] = nil // Value doesn't matter. + tags = append(tags, tag) + tags = append(tags, tag) + tags = append(tags, tag) + } } + return tags, nil } -// ExtractHashtag returns a gtsmodel tag from a hashtaggable. +// ExtractEmoji extracts a minimal gtsmodel.Tag +// from the given Hashtaggable. func ExtractHashtag(i Hashtaggable) (*gtsmodel.Tag, error) { - tag := &gtsmodel.Tag{} - + // Extract href/link for this tag. hrefProp := i.GetActivityStreamsHref() if hrefProp == nil || !hrefProp.IsIRI() { - return nil, errors.New("no href prop") + return nil, gtserror.New("no href prop") } - tag.URL = hrefProp.GetIRI().String() + tagURL := hrefProp.GetIRI().String() + // Extract name for the tag; trim leading hash + // character, so '#example' becomes 'example'. name := ExtractName(i) if name == "" { - return nil, errors.New("name prop empty") + return nil, gtserror.New("name prop empty") } + tagName := strings.TrimPrefix(name, "#") - tag.Name = strings.TrimPrefix(name, "#") - - return tag, nil + return &gtsmodel.Tag{ + URL: tagURL, + Name: tagName, + }, nil } -// ExtractEmojis returns a slice of emojis on the interface. +// ExtractEmojis extracts a slice of minimal gtsmodel.Emojis +// from a WithTag. If an entry in the WithTag is not an emoji, +// it will be quietly ignored. func ExtractEmojis(i WithTag) ([]*gtsmodel.Emoji, error) { - emojis := []*gtsmodel.Emoji{} - emojiMap := make(map[string]*gtsmodel.Emoji) tagsProp := i.GetActivityStreamsTag() if tagsProp == nil { - return emojis, nil + return nil, nil } - for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { - t := iter.GetType() - if t == nil { - continue - } - if t.GetTypeName() != "Emoji" { + var ( + l = tagsProp.Len() + emojis = make([]*gtsmodel.Emoji, 0, l) + keys = make(map[string]any, l) // Use map to dedupe items. + ) + + for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { + if !iter.IsTootEmoji() { continue } - emojiable, ok := t.(Emojiable) - if !ok { + tootEmoji := iter.GetTootEmoji() + if tootEmoji == nil { continue } - emoji, err := ExtractEmoji(emojiable) + emoji, err := ExtractEmoji(tootEmoji) if err != nil { - continue + return nil, err } - emojiMap[emoji.URI] = emoji - } - for _, emoji := range emojiMap { - emojis = append(emojis, emoji) + // Only append this emoji if we haven't + // seen it already, to avoid duplicates + // in the slice. + if _, set := keys[emoji.URI]; !set { + keys[emoji.URI] = nil // Value doesn't matter. + emojis = append(emojis, emoji) + } } + return emojis, nil } -// ExtractEmoji ... +// ExtractEmoji extracts a minimal gtsmodel.Emoji +// from the given Emojiable. func ExtractEmoji(i Emojiable) (*gtsmodel.Emoji, error) { - emoji := &gtsmodel.Emoji{} - + // Use AP ID as emoji URI. idProp := i.GetJSONLDId() if idProp == nil || !idProp.IsIRI() { - return nil, errors.New("no id for emoji") + return nil, gtserror.New("no id for emoji") } uri := idProp.GetIRI() - emoji.URI = uri.String() - emoji.Domain = uri.Host + // Extract emoji last updated time (optional). + var updatedAt time.Time + updatedProp := i.GetActivityStreamsUpdated() + if updatedProp != nil && updatedProp.IsXMLSchemaDateTime() { + updatedAt = updatedProp.Get() + } + + // Extract emoji name aka shortcode. name := ExtractName(i) if name == "" { - return nil, errors.New("name prop empty") + return nil, gtserror.New("name prop empty") } - emoji.Shortcode = strings.Trim(name, ":") + shortcode := strings.Trim(name, ":") - if i.GetActivityStreamsIcon() == nil { - return nil, errors.New("no icon for emoji") - } - imageURL, err := ExtractIconURL(i) + // Extract emoji image URL from Icon property. + imageRemoteURL, err := ExtractIconURI(i) if err != nil { - return nil, errors.New("no url for emoji image") + return nil, gtserror.New("no url for emoji image") } - emoji.ImageRemoteURL = imageURL.String() - - // assume false for both to begin - emoji.Disabled = new(bool) - emoji.VisibleInPicker = new(bool) + imageRemoteURLStr := imageRemoteURL.String() - updatedProp := i.GetActivityStreamsUpdated() - if updatedProp != nil && updatedProp.IsXMLSchemaDateTime() { - emoji.UpdatedAt = updatedProp.Get() - } - - return emoji, nil + return &gtsmodel.Emoji{ + UpdatedAt: updatedAt, + Shortcode: shortcode, + Domain: uri.Host, + ImageRemoteURL: imageRemoteURLStr, + URI: uri.String(), + Disabled: new(bool), // Assume false by default. + VisibleInPicker: new(bool), // Assume false by default. + }, nil } -// ExtractMentions extracts a slice of gtsmodel Mentions from a WithTag interface. +// ExtractMentions extracts a slice of minimal gtsmodel.Mentions +// from a WithTag. If an entry in the WithTag is not a mention, +// it will be quietly ignored. func ExtractMentions(i WithTag) ([]*gtsmodel.Mention, error) { - mentions := []*gtsmodel.Mention{} tagsProp := i.GetActivityStreamsTag() if tagsProp == nil { - return mentions, nil + return nil, nil } + + var ( + l = tagsProp.Len() + mentions = make([]*gtsmodel.Mention, 0, l) + keys = make(map[string]any, l) // Use map to dedupe items. + ) + for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() { - t := iter.GetType() - if t == nil { + if !iter.IsActivityStreamsMention() { continue } - if t.GetTypeName() != "Mention" { + asMention := iter.GetActivityStreamsMention() + if asMention == nil { continue } - mentionable, ok := t.(Mentionable) - if !ok { - return nil, errors.New("mention was not convertable to ap.Mentionable") - } - - mention, err := ExtractMention(mentionable) + mention, err := ExtractMention(asMention) if err != nil { return nil, err } - mentions = append(mentions, mention) + // Only append this mention if we haven't + // seen it already, to avoid duplicates + // in the slice. + if _, set := keys[mention.TargetAccountURI]; !set { + keys[mention.TargetAccountURI] = nil // Value doesn't matter. + mentions = append(mentions, mention) + } } + return mentions, nil } -// ExtractMention extracts a gts model mention from a Mentionable. +// ExtractMention extracts a minimal gtsmodel.Mention from a Mentionable. func ExtractMention(i Mentionable) (*gtsmodel.Mention, error) { - mention := &gtsmodel.Mention{} - - mentionString := ExtractName(i) - if mentionString == "" { - return nil, errors.New("name prop empty") + nameString := ExtractName(i) + if nameString == "" { + return nil, gtserror.New("name prop empty") } - // just make sure the mention string is valid so we can handle it properly later on... - if _, _, err := util.ExtractNamestringParts(mentionString); err != nil { + // Ensure namestring is valid so we + // can handle it properly later on. + if _, _, err := util.ExtractNamestringParts(nameString); err != nil { return nil, err } - mention.NameString = mentionString - // the href prop should be the AP URI of a user we know, eg https://example.org/users/whatever_user + // The href prop should be the AP URI + // of the target account. hrefProp := i.GetActivityStreamsHref() if hrefProp == nil || !hrefProp.IsIRI() { - return nil, errors.New("no href prop") + return nil, gtserror.New("no href prop") } - mention.TargetAccountURI = hrefProp.GetIRI().String() - return mention, nil + + return &gtsmodel.Mention{ + NameString: nameString, + TargetAccountURI: hrefProp.GetIRI().String(), + }, nil } -// ExtractActor extracts the actor ID/IRI from an interface WithActor. -func ExtractActor(i WithActor) (*url.URL, error) { - actorProp := i.GetActivityStreamsActor() +// ExtractActorURI extracts the first Actor URI +// it can find from a WithActor interface. +func ExtractActorURI(withActor WithActor) (*url.URL, error) { + actorProp := withActor.GetActivityStreamsActor() if actorProp == nil { - return nil, errors.New("actor property was nil") + return nil, gtserror.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 + id, err := pub.ToId(iter) + if err == nil { + // Found one we can use. + return id, nil } } - return nil, errors.New("no iri found for actor prop") + + return nil, gtserror.New("no iri found for actor prop") } -// ExtractObject extracts the first URL object from a WithObject interface. -func ExtractObject(i WithObject) (*url.URL, error) { - objectProp := i.GetActivityStreamsObject() +// ExtractObjectURI extracts the first Object URI +// it can find from a WithObject interface. +func ExtractObjectURI(withObject WithObject) (*url.URL, error) { + objectProp := withObject.GetActivityStreamsObject() if objectProp == nil { - return nil, errors.New("object property was nil") + return nil, gtserror.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 + id, err := pub.ToId(iter) + if err == nil { + // Found one we can use. + return id, nil } } - return nil, errors.New("no iri found for object prop") + + return nil, gtserror.New("no iri found for object prop") } -// ExtractObjects extracts a slice of URL objects from a WithObject interface. -func ExtractObjects(i WithObject) ([]*url.URL, error) { - objectProp := i.GetActivityStreamsObject() +// ExtractObjectURIs extracts the URLs of each Object +// it can find from a WithObject interface. +func ExtractObjectURIs(withObject WithObject) ([]*url.URL, error) { + objectProp := withObject.GetActivityStreamsObject() if objectProp == nil { - return nil, errors.New("object property was nil") + return nil, gtserror.New("object property was nil") } urls := make([]*url.URL, 0, objectProp.Len()) for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() { - if iter.IsIRI() && iter.GetIRI() != nil { - urls = append(urls, iter.GetIRI()) + id, err := pub.ToId(iter) + if err == nil { + // Found one we can use. + urls = append(urls, id) } } return urls, nil } -// ExtractVisibility extracts the gtsmodel.Visibility of a given addressable with a To and CC property. +// ExtractVisibility extracts the gtsmodel.Visibility +// of a given addressable with a To and CC property. // -// ActorFollowersURI is needed to check whether the visibility is FollowersOnly or not. The passed-in value -// should just be the string value representation of the followers URI of the actor who created the activity, -// eg https://example.org/users/whoever/followers. +// ActorFollowersURI is needed to check whether the +// visibility is FollowersOnly or not. The passed-in +// value should just be the string value representation +// of the followers URI of the actor who created the activity, +// eg., `https://example.org/users/whoever/followers`. func ExtractVisibility(addressable Addressable, actorFollowersURI string) (gtsmodel.Visibility, error) { - to, err := ExtractTos(addressable) - if err != nil { - return "", fmt.Errorf("deriveVisibility: error extracting TO values: %s", err) - } - - cc, err := ExtractCCs(addressable) - if err != nil { - return "", fmt.Errorf("deriveVisibility: error extracting CC values: %s", err) - } + var ( + to = ExtractToURIs(addressable) + cc = ExtractCcURIs(addressable) + ) if len(to) == 0 && len(cc) == 0 { - return "", errors.New("deriveVisibility: message wasn't TO or CC anyone") + return "", gtserror.Newf("message wasn't TO or CC anyone") } - // for visibility derivation, we start by assuming most restrictive, and work our way to least restrictive + // Assume most restrictive visibility, + // and work our way up from there. visibility := gtsmodel.VisibilityDirect - // if it's got followers in TO and it's not also CC'ed to public, it's followers only if isFollowers(to, actorFollowersURI) { + // Followers in TO: it's at least followers only. visibility = gtsmodel.VisibilityFollowersOnly } - // if it's CC'ed to public, it's unlocked - // mentioned SPECIFIC ACCOUNTS also get added to CC'es if it's not a direct message if isPublic(cc) { + // CC'd to public: it's at least unlocked. visibility = gtsmodel.VisibilityUnlocked } - // if it's To public, it's just straight up public if isPublic(to) { + // TO'd to public: it's a public post. visibility = gtsmodel.VisibilityPublic } return visibility, nil } -// isPublic checks if at least one entry in the given uris slice equals -// the activitystreams public uri. -func isPublic(uris []*url.URL) bool { - for _, entry := range uris { - if strings.EqualFold(entry.String(), pub.PublicActivityPubIRI) { - return true - } - } - return false -} - -// isFollowers checks if at least one entry in the given uris slice equals -// the given followersURI. -func isFollowers(uris []*url.URL, followersURI string) bool { - for _, entry := range uris { - if strings.EqualFold(entry.String(), followersURI) { - return true - } - } - return false -} - -// ExtractSensitive extracts whether or not an item is 'sensitive'. -// If no sensitive property is set on the item at all, or if this property -// isn't a boolean, then false will be returned by default. +// ExtractSensitive extracts whether or not an item should +// be marked as sensitive according to its ActivityStreams +// sensitive property. +// +// If no sensitive property is set on the item at all, or +// if this property isn't a boolean, then false will be +// returned by default. func ExtractSensitive(withSensitive WithSensitive) bool { sensitiveProp := withSensitive.GetActivityStreamsSensitive() if sensitiveProp == nil { @@ -788,8 +877,8 @@ func ExtractSensitive(withSensitive WithSensitive) bool { return false } -// ExtractSharedInbox extracts the sharedInbox URI properly from an Actor. -// Returns nil if this property is not set. +// ExtractSharedInbox extracts the sharedInbox URI property +// from an Actor. Returns nil if this property is not set. func ExtractSharedInbox(withEndpoints WithEndpoints) *url.URL { endpointsProp := withEndpoints.GetActivityStreamsEndpoints() if endpointsProp == nil { @@ -797,23 +886,46 @@ func ExtractSharedInbox(withEndpoints WithEndpoints) *url.URL { } for iter := endpointsProp.Begin(); iter != endpointsProp.End(); iter = iter.Next() { - if iter.IsActivityStreamsEndpoints() { - endpoints := iter.Get() - if endpoints == nil { - return nil - } - sharedInboxProp := endpoints.GetActivityStreamsSharedInbox() - if sharedInboxProp == nil { - return nil - } + if !iter.IsActivityStreamsEndpoints() { + continue + } - if !sharedInboxProp.IsIRI() { - return nil - } + endpoints := iter.Get() + if endpoints == nil { + continue + } - return sharedInboxProp.GetIRI() + sharedInboxProp := endpoints.GetActivityStreamsSharedInbox() + if sharedInboxProp == nil || !sharedInboxProp.IsIRI() { + continue } + + return sharedInboxProp.GetIRI() } return nil } + +// isPublic checks if at least one entry in the given +// uris slice equals the activitystreams public uri. +func isPublic(uris []*url.URL) bool { + for _, uri := range uris { + if pub.IsPublic(uri.String()) { + return true + } + } + + return false +} + +// isFollowers checks if at least one entry in the given +// uris slice equals the given followersURI. +func isFollowers(uris []*url.URL, followersURI string) bool { + for _, uri := range uris { + if strings.EqualFold(uri.String(), followersURI) { + return true + } + } + + return false +} diff --git a/internal/ap/extractattachments_test.go b/internal/ap/extractattachments_test.go @@ -34,7 +34,7 @@ func (suite *ExtractAttachmentsTestSuite) TestExtractAttachmentMissingURL() { d1.SetActivityStreamsUrl(streams.NewActivityStreamsUrlProperty()) attachment, err := ap.ExtractAttachment(d1) - suite.EqualError(err, "could not extract url") + suite.EqualError(err, "ExtractAttachment: error extracting attachment URL: ExtractURL: no valid URL property found") suite.Nil(attachment) } diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go @@ -239,8 +239,8 @@ func (d *deref) enrichStatus(ctx context.Context, requestUser string, uri *url.U derefd = true } - // Get the attributed-to status in order to fetch profile. - attributedTo, err := ap.ExtractAttributedTo(apubStatus) + // Get the attributed-to account in order to fetch profile. + attributedTo, err := ap.ExtractAttributedToURI(apubStatus) if err != nil { return nil, nil, gtserror.New("attributedTo was empty") } diff --git a/internal/federation/federatingdb/announce.go b/internal/federation/federatingdb/announce.go @@ -19,11 +19,11 @@ package federatingdb import ( "context" - "fmt" "codeberg.org/gruf/go-logger/v2/level" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" ) @@ -46,15 +46,16 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre boost, isNew, err := f.typeConverter.ASAnnounceToStatus(ctx, announce) if err != nil { - return fmt.Errorf("Announce: error converting announce to boost: %s", err) + return gtserror.Newf("error converting announce to boost: %w", err) } if !isNew { - // nothing to do here if this isn't a new announce + // We've already seen this boost; + // nothing else to do here. return nil } - // it's a new announce so pass it back to the processor async for dereferencing etc + // This is a new boost. Process side effects asynchronously. f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{ APObjectType: ap.ActivityAnnounce, APActivityType: ap.ActivityCreate, diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go @@ -110,15 +110,10 @@ func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Reques } } - // Check for TOs and CCs on the Activity. + // Check for TO and CC URIs on the Activity. if addressable, ok := activity.(ap.Addressable); ok { - if toURIs, err := ap.ExtractTos(addressable); err == nil { - otherIRIs = append(otherIRIs, toURIs...) - } - - if ccURIs, err := ap.ExtractCCs(addressable); err == nil { - otherIRIs = append(otherIRIs, ccURIs...) - } + otherIRIs = append(otherIRIs, ap.ExtractToURIs(addressable)...) + otherIRIs = append(otherIRIs, ap.ExtractCcURIs(addressable)...) } // Now perform the same checks, but for the Object(s) of the Activity. @@ -146,13 +141,8 @@ func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Reques } if addressable, ok := t.(ap.Addressable); ok { - if toURIs, err := ap.ExtractTos(addressable); err == nil { - otherIRIs = append(otherIRIs, toURIs...) - } - - if ccURIs, err := ap.ExtractCCs(addressable); err == nil { - otherIRIs = append(otherIRIs, ccURIs...) - } + otherIRIs = append(otherIRIs, ap.ExtractToURIs(addressable)...) + otherIRIs = append(otherIRIs, ap.ExtractCcURIs(addressable)...) } } diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go @@ -27,6 +27,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" @@ -61,13 +62,13 @@ func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable a // avatar aka icon // if this one isn't extractable in a format we recognise we'll just skip it - if avatarURL, err := ap.ExtractIconURL(accountable); err == nil { + if avatarURL, err := ap.ExtractIconURI(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 := ap.ExtractImageURL(accountable); err == nil { + if headerURL, err := ap.ExtractImageURI(accountable); err == nil { acct.HeaderRemoteURL = headerURL.String() } @@ -310,7 +311,7 @@ func (c *converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // which account posted this status? // if we don't know the account yet we can dereference it later - attributedTo, err := ap.ExtractAttributedTo(statusable) + attributedTo, err := ap.ExtractAttributedToURI(statusable) if err != nil { return nil, errors.New("ASStatusToStatus: attributedTo was empty") } @@ -386,7 +387,7 @@ func (c *converter) ASFollowToFollowRequest(ctx context.Context, followable ap.F } uri := idProp.GetIRI().String() - origin, err := ap.ExtractActor(followable) + origin, err := ap.ExtractActorURI(followable) if err != nil { return nil, errors.New("error extracting actor property from follow") } @@ -395,7 +396,7 @@ func (c *converter) ASFollowToFollowRequest(ctx context.Context, followable ap.F return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } - target, err := ap.ExtractObject(followable) + target, err := ap.ExtractObjectURI(followable) if err != nil { return nil, errors.New("error extracting object property from follow") } @@ -420,7 +421,7 @@ func (c *converter) ASFollowToFollow(ctx context.Context, followable ap.Followab } uri := idProp.GetIRI().String() - origin, err := ap.ExtractActor(followable) + origin, err := ap.ExtractActorURI(followable) if err != nil { return nil, errors.New("error extracting actor property from follow") } @@ -429,7 +430,7 @@ func (c *converter) ASFollowToFollow(ctx context.Context, followable ap.Followab return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } - target, err := ap.ExtractObject(followable) + target, err := ap.ExtractObjectURI(followable) if err != nil { return nil, errors.New("error extracting object property from follow") } @@ -454,7 +455,7 @@ func (c *converter) ASLikeToFave(ctx context.Context, likeable ap.Likeable) (*gt } uri := idProp.GetIRI().String() - origin, err := ap.ExtractActor(likeable) + origin, err := ap.ExtractActorURI(likeable) if err != nil { return nil, errors.New("error extracting actor property from like") } @@ -463,7 +464,7 @@ func (c *converter) ASLikeToFave(ctx context.Context, likeable ap.Likeable) (*gt return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } - target, err := ap.ExtractObject(likeable) + target, err := ap.ExtractObjectURI(likeable) if err != nil { return nil, errors.New("error extracting object property from like") } @@ -502,7 +503,7 @@ func (c *converter) ASBlockToBlock(ctx context.Context, blockable ap.Blockable) } uri := idProp.GetIRI().String() - origin, err := ap.ExtractActor(blockable) + origin, err := ap.ExtractActorURI(blockable) if err != nil { return nil, errors.New("ASBlockToBlock: error extracting actor property from block") } @@ -511,7 +512,7 @@ func (c *converter) ASBlockToBlock(ctx context.Context, blockable ap.Blockable) return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) } - target, err := ap.ExtractObject(blockable) + target, err := ap.ExtractObjectURI(blockable) if err != nil { return nil, errors.New("ASBlockToBlock: error extracting object property from block") } @@ -530,72 +531,113 @@ func (c *converter) ASBlockToBlock(ctx context.Context, blockable ap.Blockable) }, nil } +// Implementation note: this function creates and returns a boost WRAPPER +// status which references the boosted status in its BoostOf field. No +// dereferencing is done on the boosted status by this function. Callers +// should look at `status.BoostOf` to see the status being boosted, and do +// dereferencing on it as appropriate. +// +// The returned boolean indicates whether or not the boost has already been +// seen before by this instance. If it was, then status.BoostOf should be a +// fully filled-out status. If not, then only status.BoostOf.URI will be set. func (c *converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Announceable) (*gtsmodel.Status, bool, error) { - status := &gtsmodel.Status{} - isNew := true + // Ensure item has an ID URI set. + _, statusURIStr, err := getURI(announceable) + if err != nil { + err = gtserror.Newf("error extracting URI: %w", err) + return nil, false, err + } - // check if we already have the boost in the database - idProp := announceable.GetJSONLDId() - if idProp == nil || !idProp.IsIRI() { - return nil, isNew, errors.New("no id property set on announce, or was not an iri") + var ( + status *gtsmodel.Status + isNew bool + ) + + // Check if we already have this boost in the database. + status, err = c.db.GetStatusByURI(ctx, statusURIStr) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + // Real database error. + err = gtserror.Newf("db error trying to get status with uri %s: %w", statusURIStr, err) + return nil, isNew, err } - uri := idProp.GetIRI().String() - if status, err := c.db.GetStatusByURI(ctx, uri); err == nil { - // we already have it, great, just return it as-is :) - isNew = false + if status != nil { + // We already have this status, + // no need to proceed further. return status, isNew, nil } - status.URI = uri - // get the URI of the announced/boosted status - boostedStatusURI, err := ap.ExtractObject(announceable) + // If we reach here, we're dealing + // with a boost we haven't seen before. + isNew = true + + // Start assembling the new status + // (we already know the URI). + status = new(gtsmodel.Status) + status.URI = statusURIStr + + // Get the URI of the boosted status. + boostOfURI, err := ap.ExtractObjectURI(announceable) if err != nil { - return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error getting object from announce: %s", err) + err = gtserror.Newf("error extracting Object: %w", err) + return nil, isNew, err } - // set the URI on the new status for dereferencing later - status.BoostOf = &gtsmodel.Status{ - URI: boostedStatusURI.String(), + // Set the URI of the boosted status on + // the new status, for later dereferencing. + boostOf := &gtsmodel.Status{ + URI: boostOfURI.String(), } + status.BoostOf = boostOf - // get the published time for the announce + // Extract published time for the boost. published, err := ap.ExtractPublished(announceable) if err != nil { - return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error extracting published time: %s", err) + err = gtserror.Newf("error extracting published: %w", err) + return nil, isNew, err } status.CreatedAt = published status.UpdatedAt = published - // get the actor's IRI (ie., the person who boosted the status) - actor, err := ap.ExtractActor(announceable) + // Extract URI of the boosting account. + accountURI, err := ap.ExtractActorURI(announceable) if err != nil { - return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error extracting actor: %s", err) + err = gtserror.Newf("error extracting Actor: %w", err) + return nil, isNew, err } + accountURIStr := accountURI.String() - // get the boosting account based on the URI - // this should have been dereferenced already before we hit this point so we can confidently error out if we don't have it - boostingAccount, err := c.db.GetAccountByURI(ctx, actor.String()) + // Try to get the boosting account based on the URI. + // This should have been dereferenced already before + // we hit this point so we can confidently error out + // if we don't have it. + account, err := c.db.GetAccountByURI(ctx, accountURIStr) if err != nil { - return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error in db fetching account with uri %s: %s", actor.String(), err) + err = gtserror.Newf("db error trying to get account with uri %s: %w", accountURIStr, err) + return nil, isNew, err } - status.AccountID = boostingAccount.ID - status.AccountURI = boostingAccount.URI - status.Account = boostingAccount - - // these will all be wrapped in the boosted status so set them empty here - status.AttachmentIDs = []string{} - status.TagIDs = []string{} - status.MentionIDs = []string{} - status.EmojiIDs = []string{} + status.AccountID = account.ID + status.AccountURI = account.URI + status.Account = account - visibility, err := ap.ExtractVisibility(announceable, boostingAccount.FollowersURI) + // Calculate intended visibility of the boost. + visibility, err := ap.ExtractVisibility(announceable, account.FollowersURI) if err != nil { - return nil, isNew, fmt.Errorf("ASAnnounceToStatus: error extracting visibility: %s", err) + err = gtserror.Newf("error extracting visibility: %w", err) + return nil, isNew, err } status.Visibility = visibility - // the rest of the fields will be taken from the target status, but it's not our job to do the dereferencing here + // Below IDs will all be included in the + // boosted status, so set them empty here. + status.AttachmentIDs = make([]string, 0) + status.TagIDs = make([]string, 0) + status.MentionIDs = make([]string, 0) + status.EmojiIDs = make([]string, 0) + + // Remaining fields on the boost status will be taken + // from the boosted status; it's not our job to do all + // that dereferencing here. return status, isNew, nil } @@ -609,7 +651,7 @@ func (c *converter) ASFlagToReport(ctx context.Context, flaggable ap.Flaggable) // Extract account that created the flag / report. // This will usually be an instance actor. - actor, err := ap.ExtractActor(flaggable) + actor, err := ap.ExtractActorURI(flaggable) if err != nil { return nil, fmt.Errorf("ASFlagToReport: error extracting actor: %w", err) } @@ -637,7 +679,7 @@ func (c *converter) ASFlagToReport(ctx context.Context, flaggable ap.Flaggable) // maybe some statuses. // // Throw away anything that's not relevant to us. - objects, err := ap.ExtractObjects(flaggable) + objects, err := ap.ExtractObjectURIs(flaggable) if err != nil { return nil, fmt.Errorf("ASFlagToReport: error extracting objects: %w", err) } diff --git a/internal/typeutils/util.go b/internal/typeutils/util.go @@ -19,9 +19,11 @@ package typeutils import ( "context" + "errors" "fmt" "net/url" + "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/regexes" ) @@ -82,3 +84,19 @@ func misskeyReportInlineURLs(content string) []*url.URL { } return urls } + +// getURI is a shortcut/util function for extracting +// the JSONLDId URI of an Activity or Object. +func getURI(withID ap.WithJSONLDId) (*url.URL, string, error) { + idProp := withID.GetJSONLDId() + if idProp == nil { + return nil, "", errors.New("id prop was nil") + } + + if !idProp.IsIRI() { + return nil, "", errors.New("id prop was not an IRI") + } + + id := idProp.Get() + return id, id.String(), nil +} diff --git a/internal/typeutils/wrap.go b/internal/typeutils/wrap.go @@ -1,13 +1,30 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// 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 ( - "fmt" "net/url" "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/uris" @@ -19,7 +36,7 @@ func (c *converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi // set the actor actorURI, err := url.Parse(originAccount.URI) if err != nil { - return nil, fmt.Errorf("WrapPersonInUpdate: error parsing url %s: %s", originAccount.URI, err) + return nil, gtserror.Newf("error parsing url %s: %w", originAccount.URI, err) } actorProp := streams.NewActivityStreamsActorProperty() actorProp.AppendIRI(actorURI) @@ -35,7 +52,7 @@ func (c *converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi idString := uris.GenerateURIForUpdate(originAccount.Username, newID) idURI, err := url.Parse(idString) if err != nil { - return nil, fmt.Errorf("WrapPersonInUpdate: error parsing url %s: %s", idString, err) + return nil, gtserror.Newf("error parsing url %s: %w", idString, err) } idProp := streams.NewJSONLDIdProperty() idProp.SetIRI(idURI) @@ -49,7 +66,7 @@ func (c *converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi // to should be public toURI, err := url.Parse(pub.PublicActivityPubIRI) if err != nil { - return nil, fmt.Errorf("WrapPersonInUpdate: error parsing url %s: %s", pub.PublicActivityPubIRI, err) + return nil, gtserror.Newf("error parsing url %s: %w", pub.PublicActivityPubIRI, err) } toProp := streams.NewActivityStreamsToProperty() toProp.AppendIRI(toURI) @@ -58,7 +75,7 @@ func (c *converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi // bcc followers followersURI, err := url.Parse(originAccount.FollowersURI) if err != nil { - return nil, fmt.Errorf("WrapPersonInUpdate: error parsing url %s: %s", originAccount.FollowersURI, err) + return nil, gtserror.Newf("error parsing url %s: %w", originAccount.FollowersURI, err) } bccProp := streams.NewActivityStreamsBccProperty() bccProp.AppendIRI(followersURI) @@ -81,7 +98,7 @@ func (c *converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn // ID property idProp := streams.NewJSONLDIdProperty() - createID := fmt.Sprintf("%s/activity", note.GetJSONLDId().GetIRI().String()) + createID := note.GetJSONLDId().GetIRI().String() + "/activity" createIDIRI, err := url.Parse(createID) if err != nil { return nil, err @@ -91,9 +108,9 @@ func (c *converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn // Actor Property actorProp := streams.NewActivityStreamsActorProperty() - actorIRI, err := ap.ExtractAttributedTo(note) + actorIRI, err := ap.ExtractAttributedToURI(note) if err != nil { - return nil, fmt.Errorf("WrapNoteInCreate: couldn't extract AttributedTo: %s", err) + return nil, gtserror.Newf("couldn't extract AttributedTo: %w", err) } actorProp.AppendIRI(actorIRI) create.SetActivityStreamsActor(actorProp) @@ -102,27 +119,25 @@ func (c *converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn publishedProp := streams.NewActivityStreamsPublishedProperty() published, err := ap.ExtractPublished(note) if err != nil { - return nil, fmt.Errorf("WrapNoteInCreate: couldn't extract Published: %s", err) + return nil, gtserror.Newf("couldn't extract Published: %w", err) } publishedProp.Set(published) create.SetActivityStreamsPublished(publishedProp) // To Property toProp := streams.NewActivityStreamsToProperty() - tos, err := ap.ExtractTos(note) - if err == nil { - for _, to := range tos { - toProp.AppendIRI(to) + if toURIs := ap.ExtractToURIs(note); len(toURIs) != 0 { + for _, toURI := range toURIs { + toProp.AppendIRI(toURI) } create.SetActivityStreamsTo(toProp) } // Cc Property ccProp := streams.NewActivityStreamsCcProperty() - ccs, err := ap.ExtractCCs(note) - if err == nil { - for _, cc := range ccs { - ccProp.AppendIRI(cc) + if ccURIs := ap.ExtractCcURIs(note); len(ccURIs) != 0 { + for _, ccURI := range ccURIs { + ccProp.AppendIRI(ccURI) } create.SetActivityStreamsCc(ccProp) }