gtsocial-umbx

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

extract.go (24397B)


      1 // GoToSocial
      2 // Copyright (C) GoToSocial Authors admin@gotosocial.org
      3 // SPDX-License-Identifier: AGPL-3.0-or-later
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18 package ap
     19 
     20 import (
     21 	"crypto"
     22 	"crypto/rsa"
     23 	"crypto/x509"
     24 	"encoding/pem"
     25 	"fmt"
     26 	"net/url"
     27 	"strings"
     28 	"time"
     29 
     30 	"github.com/superseriousbusiness/activity/pub"
     31 	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
     32 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
     33 	"github.com/superseriousbusiness/gotosocial/internal/util"
     34 )
     35 
     36 // ExtractPreferredUsername returns a string representation of
     37 // an interface's preferredUsername property. Will return an
     38 // error if preferredUsername is nil, not a string, or empty.
     39 func ExtractPreferredUsername(i WithPreferredUsername) (string, error) {
     40 	u := i.GetActivityStreamsPreferredUsername()
     41 	if u == nil || !u.IsXMLSchemaString() {
     42 		return "", gtserror.New("preferredUsername nil or not a string")
     43 	}
     44 
     45 	if u.GetXMLSchemaString() == "" {
     46 		return "", gtserror.New("preferredUsername was empty")
     47 	}
     48 
     49 	return u.GetXMLSchemaString(), nil
     50 }
     51 
     52 // ExtractName returns the first string representation it
     53 // can find of an interface's name property, or an empty
     54 // string if this is not found.
     55 func ExtractName(i WithName) string {
     56 	nameProp := i.GetActivityStreamsName()
     57 	if nameProp == nil {
     58 		return ""
     59 	}
     60 
     61 	for iter := nameProp.Begin(); iter != nameProp.End(); iter = iter.Next() {
     62 		// Name may be parsed as IRI, depending on
     63 		// how it's formatted, so account for this.
     64 		switch {
     65 		case iter.IsXMLSchemaString():
     66 			return iter.GetXMLSchemaString()
     67 		case iter.IsIRI():
     68 			return iter.GetIRI().String()
     69 		}
     70 	}
     71 
     72 	return ""
     73 }
     74 
     75 // ExtractInReplyToURI extracts the first inReplyTo URI
     76 // property it can find from an interface. Will return
     77 // nil if no valid URI can be found.
     78 func ExtractInReplyToURI(i WithInReplyTo) *url.URL {
     79 	inReplyToProp := i.GetActivityStreamsInReplyTo()
     80 	if inReplyToProp == nil {
     81 		return nil
     82 	}
     83 
     84 	for iter := inReplyToProp.Begin(); iter != inReplyToProp.End(); iter = iter.Next() {
     85 		iri, err := pub.ToId(iter)
     86 		if err == nil && iri != nil {
     87 			// Found one we can use.
     88 			return iri
     89 		}
     90 	}
     91 
     92 	return nil
     93 }
     94 
     95 // ExtractItemsURIs extracts each URI it can
     96 // find for an item from the provided WithItems.
     97 func ExtractItemsURIs(i WithItems) []*url.URL {
     98 	itemsProp := i.GetActivityStreamsItems()
     99 	if itemsProp == nil {
    100 		return nil
    101 	}
    102 
    103 	uris := make([]*url.URL, 0, itemsProp.Len())
    104 	for iter := itemsProp.Begin(); iter != itemsProp.End(); iter = iter.Next() {
    105 		uri, err := pub.ToId(iter)
    106 		if err == nil {
    107 			// Found one we can use.
    108 			uris = append(uris, uri)
    109 		}
    110 	}
    111 
    112 	return uris
    113 }
    114 
    115 // ExtractToURIs returns a slice of URIs
    116 // that the given WithTo addresses as To.
    117 func ExtractToURIs(i WithTo) []*url.URL {
    118 	toProp := i.GetActivityStreamsTo()
    119 	if toProp == nil {
    120 		return nil
    121 	}
    122 
    123 	uris := make([]*url.URL, 0, toProp.Len())
    124 	for iter := toProp.Begin(); iter != toProp.End(); iter = iter.Next() {
    125 		uri, err := pub.ToId(iter)
    126 		if err == nil {
    127 			// Found one we can use.
    128 			uris = append(uris, uri)
    129 		}
    130 	}
    131 
    132 	return uris
    133 }
    134 
    135 // ExtractCcURIs returns a slice of URIs
    136 // that the given WithCC addresses as Cc.
    137 func ExtractCcURIs(i WithCC) []*url.URL {
    138 	ccProp := i.GetActivityStreamsCc()
    139 	if ccProp == nil {
    140 		return nil
    141 	}
    142 
    143 	urls := make([]*url.URL, 0, ccProp.Len())
    144 	for iter := ccProp.Begin(); iter != ccProp.End(); iter = iter.Next() {
    145 		uri, err := pub.ToId(iter)
    146 		if err == nil {
    147 			// Found one we can use.
    148 			urls = append(urls, uri)
    149 		}
    150 	}
    151 
    152 	return urls
    153 }
    154 
    155 // ExtractAttributedToURI returns the first URI it can find in the
    156 // given WithAttributedTo, or an error if no URI can be found.
    157 func ExtractAttributedToURI(i WithAttributedTo) (*url.URL, error) {
    158 	attributedToProp := i.GetActivityStreamsAttributedTo()
    159 	if attributedToProp == nil {
    160 		return nil, gtserror.New("attributedToProp was nil")
    161 	}
    162 
    163 	for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
    164 		id, err := pub.ToId(iter)
    165 		if err == nil {
    166 			return id, nil
    167 		}
    168 	}
    169 
    170 	return nil, gtserror.New("couldn't find iri for attributed to")
    171 }
    172 
    173 // ExtractPublished extracts the published time from the given
    174 // WithPublished. Will return an error if the published property
    175 // is not set, is not a time.Time, or is zero.
    176 func ExtractPublished(i WithPublished) (time.Time, error) {
    177 	t := time.Time{}
    178 
    179 	publishedProp := i.GetActivityStreamsPublished()
    180 	if publishedProp == nil {
    181 		return t, gtserror.New("published prop was nil")
    182 	}
    183 
    184 	if !publishedProp.IsXMLSchemaDateTime() {
    185 		return t, gtserror.New("published prop was not date time")
    186 	}
    187 
    188 	t = publishedProp.Get()
    189 	if t.IsZero() {
    190 		return t, gtserror.New("published time was zero")
    191 	}
    192 
    193 	return t, nil
    194 }
    195 
    196 // ExtractIconURI extracts the first URI it can find from
    197 // the given WithIcon which links to a supported image file.
    198 // Input will look something like this:
    199 //
    200 //	"icon": {
    201 //	  "mediaType": "image/jpeg",
    202 //	  "type": "Image",
    203 //	  "url": "http://example.org/path/to/some/file.jpeg"
    204 //	},
    205 //
    206 // If no valid URI can be found, this will return an error.
    207 func ExtractIconURI(i WithIcon) (*url.URL, error) {
    208 	iconProp := i.GetActivityStreamsIcon()
    209 	if iconProp == nil {
    210 		return nil, gtserror.New("icon property was nil")
    211 	}
    212 
    213 	// Icon can potentially contain multiple entries,
    214 	// so we iterate through all of them here in order
    215 	// to find the first one that meets these criteria:
    216 	//
    217 	//   1. Is an image.
    218 	//   2. Has a URL that we can use to derefereince it.
    219 	for iter := iconProp.Begin(); iter != iconProp.End(); iter = iter.Next() {
    220 		if !iter.IsActivityStreamsImage() {
    221 			continue
    222 		}
    223 
    224 		image := iter.GetActivityStreamsImage()
    225 		if image == nil {
    226 			continue
    227 		}
    228 
    229 		imageURL, err := ExtractURL(image)
    230 		if err == nil && imageURL != nil {
    231 			return imageURL, nil
    232 		}
    233 	}
    234 
    235 	return nil, gtserror.New("could not extract valid image URI from icon")
    236 }
    237 
    238 // ExtractImageURI extracts the first URI it can find from
    239 // the given WithImage which links to a supported image file.
    240 // Input will look something like this:
    241 //
    242 //	"image": {
    243 //	  "mediaType": "image/jpeg",
    244 //	  "type": "Image",
    245 //	  "url": "http://example.org/path/to/some/file.jpeg"
    246 //	},
    247 //
    248 // If no valid URI can be found, this will return an error.
    249 func ExtractImageURI(i WithImage) (*url.URL, error) {
    250 	imageProp := i.GetActivityStreamsImage()
    251 	if imageProp == nil {
    252 		return nil, gtserror.New("image property was nil")
    253 	}
    254 
    255 	// Image can potentially contain multiple entries,
    256 	// so we iterate through all of them here in order
    257 	// to find the first one that meets these criteria:
    258 	//
    259 	//   1. Is an image.
    260 	//   2. Has a URL that we can use to derefereince it.
    261 	for iter := imageProp.Begin(); iter != imageProp.End(); iter = iter.Next() {
    262 		if !iter.IsActivityStreamsImage() {
    263 			continue
    264 		}
    265 
    266 		image := iter.GetActivityStreamsImage()
    267 		if image == nil {
    268 			continue
    269 		}
    270 
    271 		imageURL, err := ExtractURL(image)
    272 		if err == nil && imageURL != nil {
    273 			return imageURL, nil
    274 		}
    275 	}
    276 
    277 	return nil, gtserror.New("could not extract valid image URI from image")
    278 }
    279 
    280 // ExtractSummary extracts the summary/content warning of
    281 // the given WithSummary interface. Will return an empty
    282 // string if no summary/content warning was present.
    283 func ExtractSummary(i WithSummary) string {
    284 	summaryProp := i.GetActivityStreamsSummary()
    285 	if summaryProp == nil {
    286 		return ""
    287 	}
    288 
    289 	for iter := summaryProp.Begin(); iter != summaryProp.End(); iter = iter.Next() {
    290 		// Summary may be parsed as IRI, depending on
    291 		// how it's formatted, so account for this.
    292 		switch {
    293 		case iter.IsXMLSchemaString():
    294 			return iter.GetXMLSchemaString()
    295 		case iter.IsIRI():
    296 			return iter.GetIRI().String()
    297 		}
    298 	}
    299 
    300 	return ""
    301 }
    302 
    303 // ExtractFields extracts property/value fields from the given
    304 // WithAttachment interface. Will return an empty slice if no
    305 // property/value fields can be found. Attachments that are not
    306 // (well-formed) PropertyValues will be ignored.
    307 func ExtractFields(i WithAttachment) []*gtsmodel.Field {
    308 	attachmentProp := i.GetActivityStreamsAttachment()
    309 	if attachmentProp == nil {
    310 		// Nothing to do.
    311 		return nil
    312 	}
    313 
    314 	l := attachmentProp.Len()
    315 	if l == 0 {
    316 		// Nothing to do.
    317 		return nil
    318 	}
    319 
    320 	fields := make([]*gtsmodel.Field, 0, l)
    321 	for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() {
    322 		if !iter.IsSchemaPropertyValue() {
    323 			continue
    324 		}
    325 
    326 		propertyValue := iter.GetSchemaPropertyValue()
    327 		if propertyValue == nil {
    328 			continue
    329 		}
    330 
    331 		nameProp := propertyValue.GetActivityStreamsName()
    332 		if nameProp == nil || nameProp.Len() != 1 {
    333 			continue
    334 		}
    335 
    336 		name := nameProp.At(0).GetXMLSchemaString()
    337 		if name == "" {
    338 			continue
    339 		}
    340 
    341 		valueProp := propertyValue.GetSchemaValue()
    342 		if valueProp == nil || !valueProp.IsXMLSchemaString() {
    343 			continue
    344 		}
    345 
    346 		value := valueProp.Get()
    347 		if value == "" {
    348 			continue
    349 		}
    350 
    351 		fields = append(fields, &gtsmodel.Field{
    352 			Name:  name,
    353 			Value: value,
    354 		})
    355 	}
    356 
    357 	return fields
    358 }
    359 
    360 // ExtractDiscoverable extracts the Discoverable boolean
    361 // of the given WithDiscoverable interface. Will return
    362 // an error if Discoverable was nil.
    363 func ExtractDiscoverable(i WithDiscoverable) (bool, error) {
    364 	discoverableProp := i.GetTootDiscoverable()
    365 	if discoverableProp == nil {
    366 		return false, gtserror.New("discoverable was nil")
    367 	}
    368 
    369 	return discoverableProp.Get(), nil
    370 }
    371 
    372 // ExtractURL extracts the first URI it can find from the
    373 // given WithURL interface, or an error if no URL was set.
    374 // The ID of a type will not work, this function wants a URI
    375 // specifically.
    376 func ExtractURL(i WithURL) (*url.URL, error) {
    377 	urlProp := i.GetActivityStreamsUrl()
    378 	if urlProp == nil {
    379 		return nil, gtserror.New("url property was nil")
    380 	}
    381 
    382 	for iter := urlProp.Begin(); iter != urlProp.End(); iter = iter.Next() {
    383 		if !iter.IsIRI() {
    384 			continue
    385 		}
    386 
    387 		// Found it.
    388 		return iter.GetIRI(), nil
    389 	}
    390 
    391 	return nil, gtserror.New("no valid URL property found")
    392 }
    393 
    394 // ExtractPublicKey extracts the public key, public key ID, and public
    395 // key owner ID from an interface, or an error if something goes wrong.
    396 func ExtractPublicKey(i WithPublicKey) (
    397 	*rsa.PublicKey, // pubkey
    398 	*url.URL, // pubkey ID
    399 	*url.URL, // pubkey owner
    400 	error,
    401 ) {
    402 	pubKeyProp := i.GetW3IDSecurityV1PublicKey()
    403 	if pubKeyProp == nil {
    404 		return nil, nil, nil, gtserror.New("public key property was nil")
    405 	}
    406 
    407 	for iter := pubKeyProp.Begin(); iter != pubKeyProp.End(); iter = iter.Next() {
    408 		if !iter.IsW3IDSecurityV1PublicKey() {
    409 			continue
    410 		}
    411 
    412 		pkey := iter.Get()
    413 		if pkey == nil {
    414 			continue
    415 		}
    416 
    417 		pubKeyID, err := pub.GetId(pkey)
    418 		if err != nil {
    419 			continue
    420 		}
    421 
    422 		pubKeyOwnerProp := pkey.GetW3IDSecurityV1Owner()
    423 		if pubKeyOwnerProp == nil {
    424 			continue
    425 		}
    426 
    427 		pubKeyOwner := pubKeyOwnerProp.GetIRI()
    428 		if pubKeyOwner == nil {
    429 			continue
    430 		}
    431 
    432 		pubKeyPemProp := pkey.GetW3IDSecurityV1PublicKeyPem()
    433 		if pubKeyPemProp == nil {
    434 			continue
    435 		}
    436 
    437 		pkeyPem := pubKeyPemProp.Get()
    438 		if pkeyPem == "" {
    439 			continue
    440 		}
    441 
    442 		block, _ := pem.Decode([]byte(pkeyPem))
    443 		if block == nil {
    444 			continue
    445 		}
    446 
    447 		var p crypto.PublicKey
    448 		switch block.Type {
    449 		case "PUBLIC KEY":
    450 			p, err = x509.ParsePKIXPublicKey(block.Bytes)
    451 		case "RSA PUBLIC KEY":
    452 			p, err = x509.ParsePKCS1PublicKey(block.Bytes)
    453 		default:
    454 			err = fmt.Errorf("unknown block type: %q", block.Type)
    455 		}
    456 		if err != nil {
    457 			err = gtserror.Newf("could not parse public key from block bytes: %w", err)
    458 			return nil, nil, nil, err
    459 		}
    460 
    461 		if p == nil {
    462 			return nil, nil, nil, gtserror.New("returned public key was empty")
    463 		}
    464 
    465 		pubKey, ok := p.(*rsa.PublicKey)
    466 		if !ok {
    467 			continue
    468 		}
    469 
    470 		return pubKey, pubKeyID, pubKeyOwner, nil
    471 	}
    472 
    473 	return nil, nil, nil, gtserror.New("couldn't find public key")
    474 }
    475 
    476 // ExtractContent returns a string representation of the
    477 // given interface's Content property, or an empty string
    478 // if no Content is found.
    479 func ExtractContent(i WithContent) string {
    480 	contentProperty := i.GetActivityStreamsContent()
    481 	if contentProperty == nil {
    482 		return ""
    483 	}
    484 
    485 	for iter := contentProperty.Begin(); iter != contentProperty.End(); iter = iter.Next() {
    486 		switch {
    487 		// Content may be parsed as IRI, depending on
    488 		// how it's formatted, so account for this.
    489 		case iter.IsXMLSchemaString():
    490 			return iter.GetXMLSchemaString()
    491 		case iter.IsIRI():
    492 			return iter.GetIRI().String()
    493 		}
    494 	}
    495 
    496 	return ""
    497 }
    498 
    499 // ExtractAttachment extracts a minimal gtsmodel.Attachment
    500 // (just remote URL, description, and blurhash) from the given
    501 // Attachmentable interface, or an error if no remote URL is set.
    502 func ExtractAttachment(i Attachmentable) (*gtsmodel.MediaAttachment, error) {
    503 	// Get the URL for the attachment file.
    504 	// If no URL is set, we can't do anything.
    505 	remoteURL, err := ExtractURL(i)
    506 	if err != nil {
    507 		return nil, gtserror.Newf("error extracting attachment URL: %w", err)
    508 	}
    509 
    510 	return &gtsmodel.MediaAttachment{
    511 		RemoteURL:   remoteURL.String(),
    512 		Description: ExtractName(i),
    513 		Blurhash:    ExtractBlurhash(i),
    514 		Processing:  gtsmodel.ProcessingStatusReceived,
    515 	}, nil
    516 }
    517 
    518 // ExtractBlurhash extracts the blurhash string value
    519 // from the given WithBlurhash interface, or returns
    520 // an empty string if nothing is found.
    521 func ExtractBlurhash(i WithBlurhash) string {
    522 	blurhashProp := i.GetTootBlurhash()
    523 	if blurhashProp == nil {
    524 		return ""
    525 	}
    526 
    527 	return blurhashProp.Get()
    528 }
    529 
    530 // ExtractHashtags extracts a slice of minimal gtsmodel.Tags
    531 // from a WithTag. If an entry in the WithTag is not a hashtag,
    532 // it will be quietly ignored.
    533 //
    534 // TODO: find a better heuristic for determining if something
    535 // is a hashtag or not, since looking for type name "Hashtag"
    536 // is non-normative. Perhaps look for things that are either
    537 // type "Hashtag" or have no type name set at all?
    538 func ExtractHashtags(i WithTag) ([]*gtsmodel.Tag, error) {
    539 	tagsProp := i.GetActivityStreamsTag()
    540 	if tagsProp == nil {
    541 		return nil, nil
    542 	}
    543 
    544 	var (
    545 		l    = tagsProp.Len()
    546 		tags = make([]*gtsmodel.Tag, 0, l)
    547 		keys = make(map[string]any, l) // Use map to dedupe items.
    548 	)
    549 
    550 	for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
    551 		t := iter.GetType()
    552 		if t == nil {
    553 			continue
    554 		}
    555 
    556 		if t.GetTypeName() != TagHashtag {
    557 			continue
    558 		}
    559 
    560 		hashtaggable, ok := t.(Hashtaggable)
    561 		if !ok {
    562 			continue
    563 		}
    564 
    565 		tag, err := ExtractHashtag(hashtaggable)
    566 		if err != nil {
    567 			continue
    568 		}
    569 
    570 		// Only append this tag if we haven't
    571 		// seen it already, to avoid duplicates
    572 		// in the slice.
    573 		if _, set := keys[tag.URL]; !set {
    574 			keys[tag.URL] = nil // Value doesn't matter.
    575 			tags = append(tags, tag)
    576 			tags = append(tags, tag)
    577 			tags = append(tags, tag)
    578 		}
    579 	}
    580 
    581 	return tags, nil
    582 }
    583 
    584 // ExtractEmoji extracts a minimal gtsmodel.Tag
    585 // from the given Hashtaggable.
    586 func ExtractHashtag(i Hashtaggable) (*gtsmodel.Tag, error) {
    587 	// Extract href/link for this tag.
    588 	hrefProp := i.GetActivityStreamsHref()
    589 	if hrefProp == nil || !hrefProp.IsIRI() {
    590 		return nil, gtserror.New("no href prop")
    591 	}
    592 	tagURL := hrefProp.GetIRI().String()
    593 
    594 	// Extract name for the tag; trim leading hash
    595 	// character, so '#example' becomes 'example'.
    596 	name := ExtractName(i)
    597 	if name == "" {
    598 		return nil, gtserror.New("name prop empty")
    599 	}
    600 	tagName := strings.TrimPrefix(name, "#")
    601 
    602 	return &gtsmodel.Tag{
    603 		URL:  tagURL,
    604 		Name: tagName,
    605 	}, nil
    606 }
    607 
    608 // ExtractEmojis extracts a slice of minimal gtsmodel.Emojis
    609 // from a WithTag. If an entry in the WithTag is not an emoji,
    610 // it will be quietly ignored.
    611 func ExtractEmojis(i WithTag) ([]*gtsmodel.Emoji, error) {
    612 	tagsProp := i.GetActivityStreamsTag()
    613 	if tagsProp == nil {
    614 		return nil, nil
    615 	}
    616 
    617 	var (
    618 		l      = tagsProp.Len()
    619 		emojis = make([]*gtsmodel.Emoji, 0, l)
    620 		keys   = make(map[string]any, l) // Use map to dedupe items.
    621 	)
    622 
    623 	for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
    624 		if !iter.IsTootEmoji() {
    625 			continue
    626 		}
    627 
    628 		tootEmoji := iter.GetTootEmoji()
    629 		if tootEmoji == nil {
    630 			continue
    631 		}
    632 
    633 		emoji, err := ExtractEmoji(tootEmoji)
    634 		if err != nil {
    635 			return nil, err
    636 		}
    637 
    638 		// Only append this emoji if we haven't
    639 		// seen it already, to avoid duplicates
    640 		// in the slice.
    641 		if _, set := keys[emoji.URI]; !set {
    642 			keys[emoji.URI] = nil // Value doesn't matter.
    643 			emojis = append(emojis, emoji)
    644 		}
    645 	}
    646 
    647 	return emojis, nil
    648 }
    649 
    650 // ExtractEmoji extracts a minimal gtsmodel.Emoji
    651 // from the given Emojiable.
    652 func ExtractEmoji(i Emojiable) (*gtsmodel.Emoji, error) {
    653 	// Use AP ID as emoji URI.
    654 	idProp := i.GetJSONLDId()
    655 	if idProp == nil || !idProp.IsIRI() {
    656 		return nil, gtserror.New("no id for emoji")
    657 	}
    658 	uri := idProp.GetIRI()
    659 
    660 	// Extract emoji last updated time (optional).
    661 	var updatedAt time.Time
    662 	updatedProp := i.GetActivityStreamsUpdated()
    663 	if updatedProp != nil && updatedProp.IsXMLSchemaDateTime() {
    664 		updatedAt = updatedProp.Get()
    665 	}
    666 
    667 	// Extract emoji name aka shortcode.
    668 	name := ExtractName(i)
    669 	if name == "" {
    670 		return nil, gtserror.New("name prop empty")
    671 	}
    672 	shortcode := strings.Trim(name, ":")
    673 
    674 	// Extract emoji image URL from Icon property.
    675 	imageRemoteURL, err := ExtractIconURI(i)
    676 	if err != nil {
    677 		return nil, gtserror.New("no url for emoji image")
    678 	}
    679 	imageRemoteURLStr := imageRemoteURL.String()
    680 
    681 	return &gtsmodel.Emoji{
    682 		UpdatedAt:       updatedAt,
    683 		Shortcode:       shortcode,
    684 		Domain:          uri.Host,
    685 		ImageRemoteURL:  imageRemoteURLStr,
    686 		URI:             uri.String(),
    687 		Disabled:        new(bool), // Assume false by default.
    688 		VisibleInPicker: new(bool), // Assume false by default.
    689 	}, nil
    690 }
    691 
    692 // ExtractMentions extracts a slice of minimal gtsmodel.Mentions
    693 // from a WithTag. If an entry in the WithTag is not a mention,
    694 // it will be quietly ignored.
    695 func ExtractMentions(i WithTag) ([]*gtsmodel.Mention, error) {
    696 	tagsProp := i.GetActivityStreamsTag()
    697 	if tagsProp == nil {
    698 		return nil, nil
    699 	}
    700 
    701 	var (
    702 		l        = tagsProp.Len()
    703 		mentions = make([]*gtsmodel.Mention, 0, l)
    704 		keys     = make(map[string]any, l) // Use map to dedupe items.
    705 	)
    706 
    707 	for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
    708 		if !iter.IsActivityStreamsMention() {
    709 			continue
    710 		}
    711 
    712 		asMention := iter.GetActivityStreamsMention()
    713 		if asMention == nil {
    714 			continue
    715 		}
    716 
    717 		mention, err := ExtractMention(asMention)
    718 		if err != nil {
    719 			return nil, err
    720 		}
    721 
    722 		// Only append this mention if we haven't
    723 		// seen it already, to avoid duplicates
    724 		// in the slice.
    725 		if _, set := keys[mention.TargetAccountURI]; !set {
    726 			keys[mention.TargetAccountURI] = nil // Value doesn't matter.
    727 			mentions = append(mentions, mention)
    728 		}
    729 	}
    730 
    731 	return mentions, nil
    732 }
    733 
    734 // ExtractMention extracts a minimal gtsmodel.Mention from a Mentionable.
    735 func ExtractMention(i Mentionable) (*gtsmodel.Mention, error) {
    736 	nameString := ExtractName(i)
    737 	if nameString == "" {
    738 		return nil, gtserror.New("name prop empty")
    739 	}
    740 
    741 	// Ensure namestring is valid so we
    742 	// can handle it properly later on.
    743 	if _, _, err := util.ExtractNamestringParts(nameString); err != nil {
    744 		return nil, err
    745 	}
    746 
    747 	// The href prop should be the AP URI
    748 	// of the target account.
    749 	hrefProp := i.GetActivityStreamsHref()
    750 	if hrefProp == nil || !hrefProp.IsIRI() {
    751 		return nil, gtserror.New("no href prop")
    752 	}
    753 
    754 	return &gtsmodel.Mention{
    755 		NameString:       nameString,
    756 		TargetAccountURI: hrefProp.GetIRI().String(),
    757 	}, nil
    758 }
    759 
    760 // ExtractActorURI extracts the first Actor URI
    761 // it can find from a WithActor interface.
    762 func ExtractActorURI(withActor WithActor) (*url.URL, error) {
    763 	actorProp := withActor.GetActivityStreamsActor()
    764 	if actorProp == nil {
    765 		return nil, gtserror.New("actor property was nil")
    766 	}
    767 
    768 	for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() {
    769 		id, err := pub.ToId(iter)
    770 		if err == nil {
    771 			// Found one we can use.
    772 			return id, nil
    773 		}
    774 	}
    775 
    776 	return nil, gtserror.New("no iri found for actor prop")
    777 }
    778 
    779 // ExtractObjectURI extracts the first Object URI
    780 // it can find from a WithObject interface.
    781 func ExtractObjectURI(withObject WithObject) (*url.URL, error) {
    782 	objectProp := withObject.GetActivityStreamsObject()
    783 	if objectProp == nil {
    784 		return nil, gtserror.New("object property was nil")
    785 	}
    786 
    787 	for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
    788 		id, err := pub.ToId(iter)
    789 		if err == nil {
    790 			// Found one we can use.
    791 			return id, nil
    792 		}
    793 	}
    794 
    795 	return nil, gtserror.New("no iri found for object prop")
    796 }
    797 
    798 // ExtractObjectURIs extracts the URLs of each Object
    799 // it can find from a WithObject interface.
    800 func ExtractObjectURIs(withObject WithObject) ([]*url.URL, error) {
    801 	objectProp := withObject.GetActivityStreamsObject()
    802 	if objectProp == nil {
    803 		return nil, gtserror.New("object property was nil")
    804 	}
    805 
    806 	urls := make([]*url.URL, 0, objectProp.Len())
    807 	for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
    808 		id, err := pub.ToId(iter)
    809 		if err == nil {
    810 			// Found one we can use.
    811 			urls = append(urls, id)
    812 		}
    813 	}
    814 
    815 	return urls, nil
    816 }
    817 
    818 // ExtractVisibility extracts the gtsmodel.Visibility
    819 // of a given addressable with a To and CC property.
    820 //
    821 // ActorFollowersURI is needed to check whether the
    822 // visibility is FollowersOnly or not. The passed-in
    823 // value should just be the string value representation
    824 // of the followers URI of the actor who created the activity,
    825 // eg., `https://example.org/users/whoever/followers`.
    826 func ExtractVisibility(addressable Addressable, actorFollowersURI string) (gtsmodel.Visibility, error) {
    827 	var (
    828 		to = ExtractToURIs(addressable)
    829 		cc = ExtractCcURIs(addressable)
    830 	)
    831 
    832 	if len(to) == 0 && len(cc) == 0 {
    833 		return "", gtserror.Newf("message wasn't TO or CC anyone")
    834 	}
    835 
    836 	// Assume most restrictive visibility,
    837 	// and work our way up from there.
    838 	visibility := gtsmodel.VisibilityDirect
    839 
    840 	if isFollowers(to, actorFollowersURI) {
    841 		// Followers in TO: it's at least followers only.
    842 		visibility = gtsmodel.VisibilityFollowersOnly
    843 	}
    844 
    845 	if isPublic(cc) {
    846 		// CC'd to public: it's at least unlocked.
    847 		visibility = gtsmodel.VisibilityUnlocked
    848 	}
    849 
    850 	if isPublic(to) {
    851 		// TO'd to public: it's a public post.
    852 		visibility = gtsmodel.VisibilityPublic
    853 	}
    854 
    855 	return visibility, nil
    856 }
    857 
    858 // ExtractSensitive extracts whether or not an item should
    859 // be marked as sensitive according to its ActivityStreams
    860 // sensitive property.
    861 //
    862 // If no sensitive property is set on the item at all, or
    863 // if this property isn't a boolean, then false will be
    864 // returned by default.
    865 func ExtractSensitive(withSensitive WithSensitive) bool {
    866 	sensitiveProp := withSensitive.GetActivityStreamsSensitive()
    867 	if sensitiveProp == nil {
    868 		return false
    869 	}
    870 
    871 	for iter := sensitiveProp.Begin(); iter != sensitiveProp.End(); iter = iter.Next() {
    872 		if iter.IsXMLSchemaBoolean() {
    873 			return iter.Get()
    874 		}
    875 	}
    876 
    877 	return false
    878 }
    879 
    880 // ExtractSharedInbox extracts the sharedInbox URI property
    881 // from an Actor. Returns nil if this property is not set.
    882 func ExtractSharedInbox(withEndpoints WithEndpoints) *url.URL {
    883 	endpointsProp := withEndpoints.GetActivityStreamsEndpoints()
    884 	if endpointsProp == nil {
    885 		return nil
    886 	}
    887 
    888 	for iter := endpointsProp.Begin(); iter != endpointsProp.End(); iter = iter.Next() {
    889 		if !iter.IsActivityStreamsEndpoints() {
    890 			continue
    891 		}
    892 
    893 		endpoints := iter.Get()
    894 		if endpoints == nil {
    895 			continue
    896 		}
    897 
    898 		sharedInboxProp := endpoints.GetActivityStreamsSharedInbox()
    899 		if sharedInboxProp == nil || !sharedInboxProp.IsIRI() {
    900 			continue
    901 		}
    902 
    903 		return sharedInboxProp.GetIRI()
    904 	}
    905 
    906 	return nil
    907 }
    908 
    909 // isPublic checks if at least one entry in the given
    910 // uris slice equals the activitystreams public uri.
    911 func isPublic(uris []*url.URL) bool {
    912 	for _, uri := range uris {
    913 		if pub.IsPublic(uri.String()) {
    914 			return true
    915 		}
    916 	}
    917 
    918 	return false
    919 }
    920 
    921 // isFollowers checks if at least one entry in the given
    922 // uris slice equals the given followersURI.
    923 func isFollowers(uris []*url.URL, followersURI string) bool {
    924 	for _, uri := range uris {
    925 		if strings.EqualFold(uri.String(), followersURI) {
    926 			return true
    927 		}
    928 	}
    929 
    930 	return false
    931 }