diff --git a/internal/ap/activitystreams.go b/internal/ap/activitystreams.go
index a434a6a07..e8a362800 100644
--- 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
index ee6a513f6..9a1b6aa4f 100644
--- 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 .
-// 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 uris
+}
+
+// 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
+ }
+
+ uris := make([]*url.URL, 0, toProp.Len())
+ for iter := toProp.Begin(); iter != toProp.End(); iter = iter.Next() {
+ uri, err := pub.ToId(iter)
+ if err == nil {
+ // Found one we can use.
+ uris = append(uris, uri)
+ }
+ }
+
+ return uris
+}
+
+// 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 nil
+ }
+
+ urls := make([]*url.URL, 0, ccProp.Len())
+ for iter := ccProp.Begin(); iter != ccProp.End(); iter = iter.Next() {
+ uri, err := pub.ToId(iter)
+ if err == nil {
+ // Found one we can use.
+ urls = append(urls, uri)
+ }
+ }
+
return urls
}
-// ExtractTos returns a list of URIs that the activity addresses as To.
-func ExtractTos(i WithTo) ([]*url.URL, error) {
- to := []*url.URL{}
- toProp := i.GetActivityStreamsTo()
- if toProp == nil {
- return nil, errors.New("toProp was nil")
- }
- for iter := toProp.Begin(); iter != toProp.End(); iter = iter.Next() {
- if iter.IsIRI() {
- if iter.GetIRI() != nil {
- to = append(to, iter.GetIRI())
- }
- }
- }
- return to, nil
-}
-
-// ExtractCCs returns a list of URIs that the activity addresses as CC.
-func ExtractCCs(i WithCC) ([]*url.URL, error) {
- cc := []*url.URL{}
- ccProp := i.GetActivityStreamsCc()
- if ccProp == nil {
- return cc, nil
- }
- for iter := ccProp.Begin(); iter != ccProp.End(); iter = iter.Next() {
- if iter.IsIRI() {
- if iter.GetIRI() != nil {
- cc = append(cc, iter.GetIRI())
- }
- }
- }
- return cc, nil
-}
-
-// 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 := >smodel.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
+ return nil, gtserror.Newf("error extracting attachment URL: %w", err)
}
- attachment.RemoteURL = attachmentURL.String()
- mediaType := i.GetActivityStreamsMediaType()
- if mediaType != nil {
- attachment.File.ContentType = mediaType.Get()
- }
- attachment.Type = gtsmodel.FileTypeImage
-
- attachment.Description = ExtractName(i)
- attachment.Blurhash = ExtractBlurhash(i)
-
- attachment.Processing = gtsmodel.ProcessingStatusReceived
-
- return attachment, nil
+ return >smodel.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 := >smodel.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 >smodel.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
}
+
+ 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() {
- t := iter.GetType()
- if t == nil {
+ if !iter.IsTootEmoji() {
continue
}
- if t.GetTypeName() != "Emoji" {
+ tootEmoji := iter.GetTootEmoji()
+ if tootEmoji == nil {
continue
}
- emojiable, ok := t.(Emojiable)
- if !ok {
- continue
- }
-
- emoji, err := ExtractEmoji(emojiable)
- if err != nil {
- continue
- }
-
- emojiMap[emoji.URI] = emoji
- }
- for _, emoji := range emojiMap {
- emojis = append(emojis, emoji)
- }
- return emojis, nil
-}
-
-// ExtractEmoji ...
-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 := ExtractName(i)
- if name == "" {
- return nil, errors.New("name prop empty")
- }
- 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()
-
- // assume false for both to begin
- emoji.Disabled = new(bool)
- emoji.VisibleInPicker = new(bool)
-
- updatedProp := i.GetActivityStreamsUpdated()
- if updatedProp != nil && updatedProp.IsXMLSchemaDateTime() {
- emoji.UpdatedAt = updatedProp.Get()
- }
-
- return emoji, nil
-}
-
-// ExtractMentions extracts a slice of gtsmodel Mentions from a WithTag interface.
-func ExtractMentions(i WithTag) ([]*gtsmodel.Mention, error) {
- mentions := []*gtsmodel.Mention{}
- tagsProp := i.GetActivityStreamsTag()
- if tagsProp == nil {
- return mentions, nil
- }
- for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
- t := iter.GetType()
- if t == nil {
- continue
- }
-
- if t.GetTypeName() != "Mention" {
- continue
- }
-
- mentionable, ok := t.(Mentionable)
- if !ok {
- return nil, errors.New("mention was not convertable to ap.Mentionable")
- }
-
- mention, err := ExtractMention(mentionable)
+ emoji, err := ExtractEmoji(tootEmoji)
if err != nil {
return nil, err
}
- mentions = append(mentions, mention)
+ // 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 extracts a minimal gtsmodel.Emoji
+// from the given Emojiable.
+func ExtractEmoji(i Emojiable) (*gtsmodel.Emoji, error) {
+ // Use AP ID as emoji URI.
+ idProp := i.GetJSONLDId()
+ if idProp == nil || !idProp.IsIRI() {
+ return nil, gtserror.New("no id for emoji")
+ }
+ uri := idProp.GetIRI()
+
+ // 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, gtserror.New("name prop empty")
+ }
+ shortcode := strings.Trim(name, ":")
+
+ // Extract emoji image URL from Icon property.
+ imageRemoteURL, err := ExtractIconURI(i)
+ if err != nil {
+ return nil, gtserror.New("no url for emoji image")
+ }
+ imageRemoteURLStr := imageRemoteURL.String()
+
+ return >smodel.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 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) {
+ tagsProp := i.GetActivityStreamsTag()
+ if tagsProp == 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() {
+ if !iter.IsActivityStreamsMention() {
+ continue
+ }
+
+ asMention := iter.GetActivityStreamsMention()
+ if asMention == nil {
+ continue
+ }
+
+ mention, err := ExtractMention(asMention)
+ if err != nil {
+ return nil, err
+ }
+
+ // 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 := >smodel.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 >smodel.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 !sharedInboxProp.IsIRI() {
- return nil
- }
-
- return sharedInboxProp.GetIRI()
+ if !iter.IsActivityStreamsEndpoints() {
+ continue
}
+
+ endpoints := iter.Get()
+ if endpoints == nil {
+ continue
+ }
+
+ 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
index 0005374ba..e3a40c7bb 100644
--- 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
index e4ecee639..11d6d7147 100644
--- 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
index ab230cdfb..0246cff7c 100644
--- 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
index 18feb2429..ec74de097 100644
--- 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
index aeb6a5917..62b2c138a 100644
--- 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 := >smodel.Status{}
- isNew := true
-
- // 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")
+ // 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
}
- 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
+ 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
+ }
+
+ 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 = >smodel.Status{
- URI: boostedStatusURI.String(),
+ // Set the URI of the boosted status on
+ // the new status, for later dereferencing.
+ boostOf := >smodel.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
+ status.AccountID = account.ID
+ status.AccountURI = account.URI
+ status.Account = account
- // 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{}
-
- 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
index bd6c33ee1..0100200dc 100644
--- 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
index c81bf14f1..71099cbee 100644
--- 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 .
+
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)
}