2021-05-08 15:25:55 +03:00
|
|
|
/*
|
|
|
|
GoToSocial
|
2023-01-05 14:43:00 +03:00
|
|
|
Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
|
2021-05-08 15:25:55 +03:00
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// Package ap contains models and utilities for working with activitypub/activitystreams representations.
|
|
|
|
//
|
|
|
|
// It is built on top of go-fed/activity.
|
|
|
|
package ap
|
2021-05-08 15:25:55 +03:00
|
|
|
|
|
|
|
import (
|
2022-12-01 01:13:13 +03:00
|
|
|
"crypto"
|
2021-05-08 15:25:55 +03:00
|
|
|
"crypto/rsa"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/pem"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/url"
|
2021-05-15 12:58:11 +03:00
|
|
|
"strings"
|
|
|
|
"time"
|
2021-05-08 15:25:55 +03:00
|
|
|
|
2021-11-13 19:29:43 +03:00
|
|
|
"github.com/superseriousbusiness/activity/pub"
|
2021-05-15 12:58:11 +03:00
|
|
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
|
|
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
2021-05-08 15:25:55 +03:00
|
|
|
)
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractPreferredUsername returns a string representation of an interface's preferredUsername property.
|
|
|
|
func ExtractPreferredUsername(i WithPreferredUsername) (string, error) {
|
2021-05-08 15:25:55 +03:00
|
|
|
u := i.GetActivityStreamsPreferredUsername()
|
|
|
|
if u == nil || !u.IsXMLSchemaString() {
|
|
|
|
return "", errors.New("preferredUsername was not a string")
|
|
|
|
}
|
|
|
|
if u.GetXMLSchemaString() == "" {
|
|
|
|
return "", errors.New("preferredUsername was empty")
|
|
|
|
}
|
|
|
|
return u.GetXMLSchemaString(), nil
|
|
|
|
}
|
|
|
|
|
2023-02-02 18:41:02 +03:00
|
|
|
// ExtractName returns a string representation of an interface's name property,
|
|
|
|
// or an empty string if this is not found.
|
|
|
|
func ExtractName(i WithName) string {
|
2021-05-08 15:25:55 +03:00
|
|
|
nameProp := i.GetActivityStreamsName()
|
|
|
|
if nameProp == nil {
|
2023-02-02 18:41:02 +03:00
|
|
|
return ""
|
2021-05-08 15:25:55 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// take the first name string we can find
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := nameProp.Begin(); iter != nameProp.End(); iter = iter.Next() {
|
|
|
|
if iter.IsXMLSchemaString() && iter.GetXMLSchemaString() != "" {
|
2023-02-02 18:41:02 +03:00
|
|
|
return iter.GetXMLSchemaString()
|
2021-05-08 15:25:55 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-02 18:41:02 +03:00
|
|
|
return ""
|
2021-05-08 15:25:55 +03:00
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractInReplyToURI extracts the inReplyToURI property (if present) from an interface.
|
|
|
|
func ExtractInReplyToURI(i WithInReplyTo) *url.URL {
|
2021-05-15 12:58:11 +03:00
|
|
|
inReplyToProp := i.GetActivityStreamsInReplyTo()
|
2021-05-23 19:07:04 +03:00
|
|
|
if inReplyToProp == nil {
|
2021-08-10 14:32:39 +03:00
|
|
|
// the property just wasn't set
|
|
|
|
return nil
|
2021-05-23 19:07:04 +03:00
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := inReplyToProp.Begin(); iter != inReplyToProp.End(); iter = iter.Next() {
|
|
|
|
if iter.IsIRI() {
|
|
|
|
if iter.GetIRI() != nil {
|
2021-08-10 14:32:39 +03:00
|
|
|
return iter.GetIRI()
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-08-10 14:32:39 +03:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
for iter := items.Begin(); iter != items.End(); iter = iter.Next() {
|
|
|
|
if iter.IsIRI() {
|
|
|
|
urls = append(urls, iter.GetIRI())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return urls
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractTos returns a list of URIs that the activity addresses as To.
|
|
|
|
func ExtractTos(i WithTo) ([]*url.URL, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
to := []*url.URL{}
|
|
|
|
toProp := i.GetActivityStreamsTo()
|
2021-05-23 19:07:04 +03:00
|
|
|
if toProp == nil {
|
|
|
|
return nil, errors.New("toProp was nil")
|
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractCCs returns a list of URIs that the activity addresses as CC.
|
|
|
|
func ExtractCCs(i WithCC) ([]*url.URL, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
cc := []*url.URL{}
|
|
|
|
ccProp := i.GetActivityStreamsCc()
|
2021-05-23 19:07:04 +03:00
|
|
|
if ccProp == nil {
|
|
|
|
return cc, nil
|
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractAttributedTo returns the URL of the actor that the withAttributedTo is attributed to.
|
|
|
|
func ExtractAttributedTo(i WithAttributedTo) (*url.URL, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
attributedToProp := i.GetActivityStreamsAttributedTo()
|
2021-05-23 19:07:04 +03:00
|
|
|
if attributedToProp == nil {
|
|
|
|
return nil, errors.New("attributedToProp was nil")
|
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
|
|
|
|
if iter.IsIRI() {
|
|
|
|
if iter.GetIRI() != nil {
|
|
|
|
return iter.GetIRI(), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, errors.New("couldn't find iri for attributed to")
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractPublished extracts the publication time of an activity.
|
|
|
|
func ExtractPublished(i WithPublished) (time.Time, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
publishedProp := i.GetActivityStreamsPublished()
|
|
|
|
if publishedProp == nil {
|
|
|
|
return time.Time{}, errors.New("published prop was nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
if !publishedProp.IsXMLSchemaDateTime() {
|
|
|
|
return time.Time{}, errors.New("published prop was not date time")
|
|
|
|
}
|
|
|
|
|
|
|
|
t := publishedProp.Get()
|
|
|
|
if t.IsZero() {
|
|
|
|
return time.Time{}, errors.New("published time was zero")
|
|
|
|
}
|
|
|
|
return t, nil
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractIconURL extracts a URL to a supported image file from something like:
|
2022-09-28 20:30:40 +03:00
|
|
|
//
|
|
|
|
// "icon": {
|
|
|
|
// "mediaType": "image/jpeg",
|
|
|
|
// "type": "Image",
|
|
|
|
// "url": "http://example.org/path/to/some/file.jpeg"
|
|
|
|
// },
|
2021-08-10 14:32:39 +03:00
|
|
|
func ExtractIconURL(i WithIcon) (*url.URL, error) {
|
2021-05-08 15:25:55 +03:00
|
|
|
iconProp := i.GetActivityStreamsIcon()
|
|
|
|
if iconProp == nil {
|
|
|
|
return nil, errors.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
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := iconProp.Begin(); iter != iconProp.End(); iter = iter.Next() {
|
2021-05-08 15:25:55 +03:00
|
|
|
// 1. is an image
|
2021-05-15 12:58:11 +03:00
|
|
|
if !iter.IsActivityStreamsImage() {
|
2021-05-08 15:25:55 +03:00
|
|
|
continue
|
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
imageValue := iter.GetActivityStreamsImage()
|
2021-05-08 15:25:55 +03:00
|
|
|
if imageValue == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// 2. has a URL so we can grab it
|
2021-08-10 14:32:39 +03:00
|
|
|
url, err := ExtractURL(imageValue)
|
2021-05-08 15:25:55 +03:00
|
|
|
if err == nil && url != nil {
|
|
|
|
return url, 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")
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractImageURL extracts a URL to a supported image file from something like:
|
2022-09-28 20:30:40 +03:00
|
|
|
//
|
|
|
|
// "image": {
|
|
|
|
// "mediaType": "image/jpeg",
|
|
|
|
// "type": "Image",
|
|
|
|
// "url": "http://example.org/path/to/some/file.jpeg"
|
|
|
|
// },
|
2021-08-10 14:32:39 +03:00
|
|
|
func ExtractImageURL(i WithImage) (*url.URL, error) {
|
2021-05-08 15:25:55 +03:00
|
|
|
imageProp := i.GetActivityStreamsImage()
|
|
|
|
if imageProp == nil {
|
|
|
|
return nil, errors.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
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := imageProp.Begin(); iter != imageProp.End(); iter = iter.Next() {
|
2021-05-08 15:25:55 +03:00
|
|
|
// 1. is an image
|
2021-05-15 12:58:11 +03:00
|
|
|
if !iter.IsActivityStreamsImage() {
|
2021-05-08 15:25:55 +03:00
|
|
|
continue
|
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
imageValue := iter.GetActivityStreamsImage()
|
2021-05-08 15:25:55 +03:00
|
|
|
if imageValue == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// 2. has a URL so we can grab it
|
2021-08-10 14:32:39 +03:00
|
|
|
url, err := ExtractURL(imageValue)
|
2021-05-08 15:25:55 +03:00
|
|
|
if err == nil && url != nil {
|
|
|
|
return url, 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")
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractSummary extracts the summary/content warning of an interface.
|
2023-02-02 18:41:02 +03:00
|
|
|
// Will return an empty string if no summary was present.
|
|
|
|
func ExtractSummary(i WithSummary) string {
|
2021-05-08 15:25:55 +03:00
|
|
|
summaryProp := i.GetActivityStreamsSummary()
|
2021-08-29 13:03:08 +03:00
|
|
|
if summaryProp == nil || summaryProp.Len() == 0 {
|
|
|
|
// no summary to speak of
|
2023-02-02 18:41:02 +03:00
|
|
|
return ""
|
2021-05-08 15:25:55 +03:00
|
|
|
}
|
|
|
|
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := summaryProp.Begin(); iter != summaryProp.End(); iter = iter.Next() {
|
2022-04-26 11:47:21 +03:00
|
|
|
switch {
|
|
|
|
case iter.IsIRI():
|
2023-02-02 18:41:02 +03:00
|
|
|
return iter.GetIRI().String()
|
2022-04-26 11:47:21 +03:00
|
|
|
case iter.IsXMLSchemaString():
|
2023-02-02 18:41:02 +03:00
|
|
|
return iter.GetXMLSchemaString()
|
2021-05-08 15:25:55 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-02 18:41:02 +03:00
|
|
|
return ""
|
2021-05-08 15:25:55 +03:00
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractDiscoverable extracts the Discoverable boolean of an interface.
|
|
|
|
func ExtractDiscoverable(i WithDiscoverable) (bool, error) {
|
2021-05-08 15:25:55 +03:00
|
|
|
if i.GetTootDiscoverable() == nil {
|
|
|
|
return false, errors.New("discoverable was nil")
|
|
|
|
}
|
|
|
|
return i.GetTootDiscoverable().Get(), nil
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractURL extracts the URL property of an interface.
|
|
|
|
func ExtractURL(i WithURL) (*url.URL, error) {
|
2021-05-08 15:25:55 +03:00
|
|
|
urlProp := i.GetActivityStreamsUrl()
|
|
|
|
if urlProp == nil {
|
|
|
|
return nil, errors.New("url property was nil")
|
|
|
|
}
|
|
|
|
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := urlProp.Begin(); iter != urlProp.End(); iter = iter.Next() {
|
|
|
|
if iter.IsIRI() && iter.GetIRI() != nil {
|
|
|
|
return iter.GetIRI(), nil
|
2021-05-08 15:25:55 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, errors.New("could not extract url")
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractPublicKeyForOwner extracts the public key from an interface, as long as it belongs to the specified owner.
|
|
|
|
// It will return the public key itself, the id/URL of the public key, or an error if something goes wrong.
|
|
|
|
func ExtractPublicKeyForOwner(i WithPublicKey, forOwner *url.URL) (*rsa.PublicKey, *url.URL, error) {
|
2021-05-08 15:25:55 +03:00
|
|
|
publicKeyProp := i.GetW3IDSecurityV1PublicKey()
|
|
|
|
if publicKeyProp == nil {
|
|
|
|
return nil, nil, errors.New("public key property was nil")
|
|
|
|
}
|
|
|
|
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := publicKeyProp.Begin(); iter != publicKeyProp.End(); iter = iter.Next() {
|
|
|
|
pkey := iter.Get()
|
2021-05-08 15:25:55 +03:00
|
|
|
if pkey == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
pkeyID, err := pub.GetId(pkey)
|
|
|
|
if err != nil || pkeyID == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if pkey.GetW3IDSecurityV1Owner() == nil || pkey.GetW3IDSecurityV1Owner().Get() == nil || pkey.GetW3IDSecurityV1Owner().Get().String() != forOwner.String() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if pkey.GetW3IDSecurityV1PublicKeyPem() == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
pkeyPem := pkey.GetW3IDSecurityV1PublicKeyPem().Get()
|
|
|
|
if pkeyPem == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
block, _ := pem.Decode([]byte(pkeyPem))
|
2022-12-01 01:13:13 +03:00
|
|
|
if block == nil {
|
|
|
|
return nil, nil, errors.New("could not decode publicKeyPem: no PEM data")
|
|
|
|
}
|
|
|
|
var p crypto.PublicKey
|
|
|
|
switch block.Type {
|
|
|
|
case "PUBLIC KEY":
|
|
|
|
p, err = x509.ParsePKIXPublicKey(block.Bytes)
|
|
|
|
case "RSA PUBLIC KEY":
|
|
|
|
p, err = x509.ParsePKCS1PublicKey(block.Bytes)
|
|
|
|
default:
|
|
|
|
return nil, nil, fmt.Errorf("could not parse public key: unknown block type: %q", block.Type)
|
2021-05-08 15:25:55 +03:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
|
|
|
|
}
|
|
|
|
if p == nil {
|
|
|
|
return nil, nil, errors.New("returned public key was empty")
|
|
|
|
}
|
|
|
|
if publicKey, ok := p.(*rsa.PublicKey); ok {
|
|
|
|
return publicKey, pkeyID, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, nil, errors.New("couldn't find public key")
|
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
|
2022-05-23 18:12:46 +03:00
|
|
|
// ExtractContent returns a string representation of the interface's Content property,
|
|
|
|
// or an empty string if no Content is found.
|
|
|
|
func ExtractContent(i WithContent) string {
|
2021-05-15 12:58:11 +03:00
|
|
|
contentProperty := i.GetActivityStreamsContent()
|
|
|
|
if contentProperty == nil {
|
2022-05-23 18:12:46 +03:00
|
|
|
return ""
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
2022-05-23 18:12:46 +03:00
|
|
|
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := contentProperty.Begin(); iter != contentProperty.End(); iter = iter.Next() {
|
2022-05-23 18:12:46 +03:00
|
|
|
if iter.IsXMLSchemaString() {
|
|
|
|
return iter.GetXMLSchemaString()
|
|
|
|
}
|
|
|
|
if iter.IsIRI() && iter.GetIRI() != nil {
|
|
|
|
return iter.GetIRI().String()
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
|
|
|
}
|
2022-05-23 18:12:46 +03:00
|
|
|
|
|
|
|
return ""
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractAttachment returns a gts model of an attachment from an attachmentable interface.
|
|
|
|
func ExtractAttachment(i Attachmentable) (*gtsmodel.MediaAttachment, error) {
|
2023-02-02 18:41:02 +03:00
|
|
|
attachment := >smodel.MediaAttachment{}
|
2021-05-15 12:58:11 +03:00
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
attachmentURL, err := ExtractURL(i)
|
2021-05-15 12:58:11 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
attachment.RemoteURL = attachmentURL.String()
|
|
|
|
|
|
|
|
mediaType := i.GetActivityStreamsMediaType()
|
2023-02-02 18:41:02 +03:00
|
|
|
if mediaType != nil {
|
|
|
|
attachment.File.ContentType = mediaType.Get()
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
|
|
|
attachment.Type = gtsmodel.FileTypeImage
|
|
|
|
|
2023-02-02 18:41:02 +03:00
|
|
|
attachment.Description = ExtractName(i)
|
2022-01-09 20:41:22 +03:00
|
|
|
attachment.Blurhash = ExtractBlurhash(i)
|
|
|
|
|
2021-05-17 20:06:58 +03:00
|
|
|
attachment.Processing = gtsmodel.ProcessingStatusReceived
|
2021-05-15 12:58:11 +03:00
|
|
|
|
|
|
|
return attachment, nil
|
|
|
|
}
|
|
|
|
|
2022-01-09 20:41:22 +03:00
|
|
|
// ExtractBlurhash extracts the blurhash value (if present) from a WithBlurhash interface.
|
|
|
|
func ExtractBlurhash(i WithBlurhash) string {
|
|
|
|
if i.GetTootBlurhash() == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return i.GetTootBlurhash().Get()
|
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractHashtags returns a slice of tags on the interface.
|
|
|
|
func ExtractHashtags(i WithTag) ([]*gtsmodel.Tag, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
tags := []*gtsmodel.Tag{}
|
|
|
|
tagsProp := i.GetActivityStreamsTag()
|
2021-05-23 19:07:04 +03:00
|
|
|
if tagsProp == nil {
|
|
|
|
return tags, nil
|
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
|
|
|
|
t := iter.GetType()
|
|
|
|
if t == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if t.GetTypeName() != "Hashtag" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
hashtaggable, ok := t.(Hashtaggable)
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
tag, err := ExtractHashtag(hashtaggable)
|
2021-05-15 12:58:11 +03:00
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
tags = append(tags, tag)
|
|
|
|
}
|
|
|
|
return tags, nil
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractHashtag returns a gtsmodel tag from a hashtaggable.
|
|
|
|
func ExtractHashtag(i Hashtaggable) (*gtsmodel.Tag, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
tag := >smodel.Tag{}
|
|
|
|
|
|
|
|
hrefProp := i.GetActivityStreamsHref()
|
|
|
|
if hrefProp == nil || !hrefProp.IsIRI() {
|
|
|
|
return nil, errors.New("no href prop")
|
|
|
|
}
|
|
|
|
tag.URL = hrefProp.GetIRI().String()
|
|
|
|
|
2023-02-02 18:41:02 +03:00
|
|
|
name := ExtractName(i)
|
|
|
|
if name == "" {
|
|
|
|
return nil, errors.New("name prop empty")
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
2023-02-02 18:41:02 +03:00
|
|
|
|
2021-05-15 12:58:11 +03:00
|
|
|
tag.Name = strings.TrimPrefix(name, "#")
|
|
|
|
|
|
|
|
return tag, nil
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractEmojis returns a slice of emojis on the interface.
|
|
|
|
func ExtractEmojis(i WithTag) ([]*gtsmodel.Emoji, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
emojis := []*gtsmodel.Emoji{}
|
2023-02-20 18:27:41 +03:00
|
|
|
emojiMap := make(map[string]*gtsmodel.Emoji)
|
2021-05-15 12:58:11 +03:00
|
|
|
tagsProp := i.GetActivityStreamsTag()
|
2021-05-23 19:07:04 +03:00
|
|
|
if tagsProp == nil {
|
|
|
|
return emojis, nil
|
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
for iter := tagsProp.Begin(); iter != tagsProp.End(); iter = iter.Next() {
|
|
|
|
t := iter.GetType()
|
|
|
|
if t == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if t.GetTypeName() != "Emoji" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
emojiable, ok := t.(Emojiable)
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
emoji, err := ExtractEmoji(emojiable)
|
2021-05-15 12:58:11 +03:00
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-02-20 18:27:41 +03:00
|
|
|
emojiMap[emoji.URI] = emoji
|
|
|
|
}
|
|
|
|
for _, emoji := range emojiMap {
|
2021-05-15 12:58:11 +03:00
|
|
|
emojis = append(emojis, emoji)
|
|
|
|
}
|
|
|
|
return emojis, nil
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractEmoji ...
|
|
|
|
func ExtractEmoji(i Emojiable) (*gtsmodel.Emoji, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
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
|
|
|
|
|
2023-02-02 18:41:02 +03:00
|
|
|
name := ExtractName(i)
|
|
|
|
if name == "" {
|
|
|
|
return nil, errors.New("name prop empty")
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
|
|
|
emoji.Shortcode = strings.Trim(name, ":")
|
|
|
|
|
|
|
|
if i.GetActivityStreamsIcon() == nil {
|
|
|
|
return nil, errors.New("no icon for emoji")
|
|
|
|
}
|
2021-08-10 14:32:39 +03:00
|
|
|
imageURL, err := ExtractIconURL(i)
|
2021-05-15 12:58:11 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, errors.New("no url for emoji image")
|
|
|
|
}
|
|
|
|
emoji.ImageRemoteURL = imageURL.String()
|
|
|
|
|
2022-08-15 13:35:05 +03:00
|
|
|
// assume false for both to begin
|
|
|
|
emoji.Disabled = new(bool)
|
|
|
|
emoji.VisibleInPicker = new(bool)
|
|
|
|
|
2022-10-13 16:16:24 +03:00
|
|
|
updatedProp := i.GetActivityStreamsUpdated()
|
|
|
|
if updatedProp != nil && updatedProp.IsXMLSchemaDateTime() {
|
|
|
|
emoji.UpdatedAt = updatedProp.Get()
|
|
|
|
}
|
|
|
|
|
2021-05-15 12:58:11 +03:00
|
|
|
return emoji, nil
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractMentions extracts a slice of gtsmodel Mentions from a WithTag interface.
|
|
|
|
func ExtractMentions(i WithTag) ([]*gtsmodel.Mention, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
mentions := []*gtsmodel.Mention{}
|
|
|
|
tagsProp := i.GetActivityStreamsTag()
|
2021-05-23 19:07:04 +03:00
|
|
|
if tagsProp == nil {
|
|
|
|
return mentions, nil
|
|
|
|
}
|
2021-05-15 12:58:11 +03:00
|
|
|
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 {
|
2021-08-29 13:03:08 +03:00
|
|
|
return nil, errors.New("mention was not convertable to ap.Mentionable")
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
mention, err := ExtractMention(mentionable)
|
2021-05-15 12:58:11 +03:00
|
|
|
if err != nil {
|
2021-08-29 13:03:08 +03:00
|
|
|
return nil, err
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
mentions = append(mentions, mention)
|
|
|
|
}
|
|
|
|
return mentions, nil
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractMention extracts a gts model mention from a Mentionable.
|
|
|
|
func ExtractMention(i Mentionable) (*gtsmodel.Mention, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
mention := >smodel.Mention{}
|
|
|
|
|
2023-02-02 18:41:02 +03:00
|
|
|
mentionString := ExtractName(i)
|
|
|
|
if mentionString == "" {
|
|
|
|
return nil, errors.New("name prop empty")
|
2021-05-15 12:58:11 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// just make sure the mention string is valid so we can handle it properly later on...
|
2023-02-02 18:41:02 +03:00
|
|
|
if _, _, err := util.ExtractNamestringParts(mentionString); err != nil {
|
2021-05-15 12:58:11 +03:00
|
|
|
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
|
|
|
|
hrefProp := i.GetActivityStreamsHref()
|
|
|
|
if hrefProp == nil || !hrefProp.IsIRI() {
|
|
|
|
return nil, errors.New("no href prop")
|
|
|
|
}
|
2021-08-20 13:26:56 +03:00
|
|
|
mention.TargetAccountURI = hrefProp.GetIRI().String()
|
2021-05-15 12:58:11 +03:00
|
|
|
return mention, nil
|
|
|
|
}
|
|
|
|
|
2021-08-10 14:32:39 +03:00
|
|
|
// ExtractActor extracts the actor ID/IRI from an interface WithActor.
|
|
|
|
func ExtractActor(i WithActor) (*url.URL, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
actorProp := i.GetActivityStreamsActor()
|
|
|
|
if actorProp == nil {
|
|
|
|
return nil, errors.New("actor property was nil")
|
|
|
|
}
|
|
|
|
for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() {
|
|
|
|
if iter.IsIRI() && iter.GetIRI() != nil {
|
|
|
|
return iter.GetIRI(), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, errors.New("no iri found for actor prop")
|
|
|
|
}
|
|
|
|
|
2023-01-27 16:48:11 +03:00
|
|
|
// ExtractObject extracts the first URL object from a WithObject interface.
|
2021-08-10 14:32:39 +03:00
|
|
|
func ExtractObject(i WithObject) (*url.URL, error) {
|
2021-05-15 12:58:11 +03:00
|
|
|
objectProp := i.GetActivityStreamsObject()
|
|
|
|
if objectProp == nil {
|
|
|
|
return nil, errors.New("object property was nil")
|
|
|
|
}
|
|
|
|
for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
|
|
|
|
if iter.IsIRI() && iter.GetIRI() != nil {
|
|
|
|
return iter.GetIRI(), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, errors.New("no iri found for object prop")
|
|
|
|
}
|
2021-10-06 19:18:02 +03:00
|
|
|
|
2023-01-25 13:12:27 +03:00
|
|
|
// ExtractObjects extracts a slice of URL objects from a WithObject interface.
|
|
|
|
func ExtractObjects(i WithObject) ([]*url.URL, error) {
|
|
|
|
objectProp := i.GetActivityStreamsObject()
|
|
|
|
if objectProp == nil {
|
|
|
|
return nil, errors.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())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return urls, nil
|
|
|
|
}
|
|
|
|
|
2021-10-06 19:18:02 +03:00
|
|
|
// 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.
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(to) == 0 && len(cc) == 0 {
|
|
|
|
return "", errors.New("deriveVisibility: message wasn't TO or CC anyone")
|
|
|
|
}
|
|
|
|
|
|
|
|
// for visibility derivation, we start by assuming most restrictive, and work our way to least restrictive
|
|
|
|
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) {
|
|
|
|
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) {
|
|
|
|
visibility = gtsmodel.VisibilityUnlocked
|
|
|
|
}
|
|
|
|
|
|
|
|
// if it's To public, it's just straight up public
|
|
|
|
if isPublic(to) {
|
|
|
|
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
|
|
|
|
}
|
2021-11-13 19:29:43 +03:00
|
|
|
|
|
|
|
// 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.
|
|
|
|
func ExtractSensitive(withSensitive WithSensitive) bool {
|
|
|
|
sensitiveProp := withSensitive.GetActivityStreamsSensitive()
|
|
|
|
if sensitiveProp == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
for iter := sensitiveProp.Begin(); iter != sensitiveProp.End(); iter = iter.Next() {
|
|
|
|
if iter.IsXMLSchemaBoolean() {
|
|
|
|
return iter.Get()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
2022-09-23 22:27:35 +03:00
|
|
|
|
|
|
|
// ExtractSharedInbox extracts the sharedInbox URI properly from an Actor.
|
|
|
|
// Returns nil if this property is not set.
|
|
|
|
func ExtractSharedInbox(withEndpoints WithEndpoints) *url.URL {
|
|
|
|
endpointsProp := withEndpoints.GetActivityStreamsEndpoints()
|
|
|
|
if endpointsProp == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|