summaryrefslogtreecommitdiff
path: root/internal/ap/extract.go
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-06-17 17:49:11 +0200
committerLibravatar GitHub <noreply@github.com>2023-06-17 16:49:11 +0100
commitd8e16a226a570a7d262bdeb067273ce35b03cc7c (patch)
tree94a09ebe5501c01159efb8aa3e74d70da8db2f87 /internal/ap/extract.go
parent[bugfix] Accept non-multipart forms for account updates (#1896) (diff)
downloadgotosocial-d8e16a226a570a7d262bdeb067273ce35b03cc7c.tar.xz
[chore/bugfix] Refactor `ap/extract.go` functions, return URIs more reliably (#1897)
Diffstat (limited to 'internal/ap/extract.go')
-rw-r--r--internal/ap/extract.go686
1 files changed, 399 insertions, 287 deletions
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 <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
+}