diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/ap/interfaces.go | 22 | ||||
| -rw-r--r-- | internal/ap/normalize.go | 30 | ||||
| -rw-r--r-- | internal/ap/properties.go | 98 | ||||
| -rw-r--r-- | internal/ap/serialize.go | 12 | ||||
| -rw-r--r-- | internal/db/bundb/account.go | 7 | ||||
| -rw-r--r-- | internal/typeutils/astointernal.go | 20 | ||||
| -rw-r--r-- | internal/typeutils/astointernal_test.go | 1 | ||||
| -rw-r--r-- | internal/typeutils/internaltoas.go | 25 | ||||
| -rw-r--r-- | internal/typeutils/internaltoas_test.go | 66 | 
9 files changed, 263 insertions, 18 deletions
| diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index 45ddbfba7..811e09125 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -160,6 +160,8 @@ type Accountable interface {  	WithFollowing  	WithFollowers  	WithFeatured +	WithMovedTo +	WithAlsoKnownAs  	WithManuallyApprovesFollowers  	WithEndpoints  	WithTag @@ -327,7 +329,7 @@ type TypeOrIRI interface {  }  // Property represents the minimum interface for an ActivityStreams property with IRIs. -type Property[T TypeOrIRI] interface { +type Property[T WithIRI] interface {  	Len() int  	At(int) T @@ -441,6 +443,18 @@ type WithFeatured interface {  	SetTootFeatured(vocab.TootFeaturedProperty)  } +// WithMovedTo represents an Object with ActivityStreamsMovedToProperty. +type WithMovedTo interface { +	GetActivityStreamsMovedTo() vocab.ActivityStreamsMovedToProperty +	SetActivityStreamsMovedTo(vocab.ActivityStreamsMovedToProperty) +} + +// WithAlsoKnownAs represents an Object with ActivityStreamsAlsoKnownAsProperty. +type WithAlsoKnownAs interface { +	GetActivityStreamsAlsoKnownAs() vocab.ActivityStreamsAlsoKnownAsProperty +	SetActivityStreamsAlsoKnownAs(vocab.ActivityStreamsAlsoKnownAsProperty) +} +  // WithAttributedTo represents an activity with ActivityStreamsAttributedToProperty  type WithAttributedTo interface {  	GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty @@ -551,6 +565,12 @@ type WithObject interface {  	SetActivityStreamsObject(vocab.ActivityStreamsObjectProperty)  } +// WithTarget represents an activity with ActivityStreamsTargetProperty +type WithTarget interface { +	GetActivityStreamsTarget() vocab.ActivityStreamsTargetProperty +	SetActivityStreamsTarget(vocab.ActivityStreamsTargetProperty) +} +  // WithNext represents an activity with ActivityStreamsNextProperty  type WithNext interface {  	GetActivityStreamsNext() vocab.ActivityStreamsNextProperty diff --git a/internal/ap/normalize.go b/internal/ap/normalize.go index a27527b84..8e0a022c1 100644 --- a/internal/ap/normalize.go +++ b/internal/ap/normalize.go @@ -391,6 +391,36 @@ func NormalizeOutgoingAttachmentProp(item WithAttachment, rawJSON map[string]int  	rawJSON["attachment"] = []interface{}{attachment}  } +// NormalizeOutgoingAlsoKnownAsProp replaces single-entry alsoKnownAs values with +// single-entry arrays, for better compatibility with other AP implementations. +// +// Ie: +// +//	"alsoKnownAs": "https://example.org/users/some_user" +// +// becomes: +// +//	"alsoKnownAs": ["https://example.org/users/some_user"] +// +// Noop for items with no attachments, or with attachments that are already a slice. +func NormalizeOutgoingAlsoKnownAsProp(item WithAlsoKnownAs, rawJSON map[string]interface{}) { +	alsoKnownAs, ok := rawJSON["alsoKnownAs"] +	if !ok { +		// No 'alsoKnownAs', +		// nothing to change. +		return +	} + +	if _, ok := alsoKnownAs.([]interface{}); ok { +		// Already slice, +		// nothing to change. +		return +	} + +	// Coerce single-object to slice. +	rawJSON["alsoKnownAs"] = []interface{}{alsoKnownAs} +} +  // NormalizeOutgoingContentProp normalizes go-fed's funky formatting of content and  // contentMap properties to a format better understood by other AP implementations.  // diff --git a/internal/ap/properties.go b/internal/ap/properties.go index 6103608d6..b77d20a02 100644 --- a/internal/ap/properties.go +++ b/internal/ap/properties.go @@ -102,7 +102,7 @@ func AppendTo(with WithTo, to ...*url.URL) {  // GetCc returns the IRIs contained in the Cc property of 'with'. Panics on entries with missing ID.  func GetCc(with WithCc) []*url.URL {  	ccProp := with.GetActivityStreamsCc() -	return getIRIs[vocab.ActivityStreamsCcPropertyIterator](ccProp) +	return extractIRIs[vocab.ActivityStreamsCcPropertyIterator](ccProp)  }  // AppendCc appends the given IRIs to the Cc property of 'with'. @@ -120,7 +120,7 @@ func AppendCc(with WithCc, cc ...*url.URL) {  // GetBcc returns the IRIs contained in the Bcc property of 'with'. Panics on entries with missing ID.  func GetBcc(with WithBcc) []*url.URL {  	bccProp := with.GetActivityStreamsBcc() -	return getIRIs[vocab.ActivityStreamsBccPropertyIterator](bccProp) +	return extractIRIs[vocab.ActivityStreamsBccPropertyIterator](bccProp)  }  // AppendBcc appends the given IRIs to the Bcc property of 'with'. @@ -170,7 +170,7 @@ func AppendURL(with WithURL, url ...*url.URL) {  // GetActorIRIs returns the IRIs contained in the Actor property of 'with'.  func GetActorIRIs(with WithActor) []*url.URL {  	actorProp := with.GetActivityStreamsActor() -	return getIRIs[vocab.ActivityStreamsActorPropertyIterator](actorProp) +	return extractIRIs[vocab.ActivityStreamsActorPropertyIterator](actorProp)  }  // AppendActorIRIs appends the given IRIs to the Actor property of 'with'. @@ -188,7 +188,7 @@ func AppendActorIRIs(with WithActor, actor ...*url.URL) {  // GetObjectIRIs returns the IRIs contained in the Object property of 'with'.  func GetObjectIRIs(with WithObject) []*url.URL {  	objectProp := with.GetActivityStreamsObject() -	return getIRIs[vocab.ActivityStreamsObjectPropertyIterator](objectProp) +	return extractIRIs[vocab.ActivityStreamsObjectPropertyIterator](objectProp)  }  // AppendObjectIRIs appends the given IRIs to the Object property of 'with'. @@ -203,10 +203,28 @@ func AppendObjectIRIs(with WithObject) {  	})  } +// GetTargetIRIs returns the IRIs contained in the Target property of 'with'. +func GetTargetIRIs(with WithTarget) []*url.URL { +	targetProp := with.GetActivityStreamsTarget() +	return extractIRIs[vocab.ActivityStreamsTargetPropertyIterator](targetProp) +} + +// AppendTargetIRIs appends the given IRIs to the Target property of 'with'. +func AppendTargetIRIs(with WithTarget) { +	appendIRIs(func() Property[vocab.ActivityStreamsTargetPropertyIterator] { +		targetProp := with.GetActivityStreamsTarget() +		if targetProp == nil { +			targetProp = streams.NewActivityStreamsTargetProperty() +			with.SetActivityStreamsTarget(targetProp) +		} +		return targetProp +	}) +} +  // GetAttributedTo returns the IRIs contained in the AttributedTo property of 'with'.  func GetAttributedTo(with WithAttributedTo) []*url.URL {  	attribProp := with.GetActivityStreamsAttributedTo() -	return getIRIs[vocab.ActivityStreamsAttributedToPropertyIterator](attribProp) +	return extractIRIs[vocab.ActivityStreamsAttributedToPropertyIterator](attribProp)  }  // AppendAttributedTo appends the given IRIs to the AttributedTo property of 'with'. @@ -224,7 +242,7 @@ func AppendAttributedTo(with WithAttributedTo, attribTo ...*url.URL) {  // GetInReplyTo returns the IRIs contained in the InReplyTo property of 'with'.  func GetInReplyTo(with WithInReplyTo) []*url.URL {  	replyProp := with.GetActivityStreamsInReplyTo() -	return getIRIs[vocab.ActivityStreamsInReplyToPropertyIterator](replyProp) +	return extractIRIs[vocab.ActivityStreamsInReplyToPropertyIterator](replyProp)  }  // AppendInReplyTo appends the given IRIs to the InReplyTo property of 'with'. @@ -334,6 +352,43 @@ func SetFeatured(with WithFeatured, featured *url.URL) {  	featuredProp.SetIRI(featured)  } +// GetMovedTo returns the IRI contained in the movedTo property of 'with'. +func GetMovedTo(with WithMovedTo) *url.URL { +	movedToProp := with.GetActivityStreamsMovedTo() +	if movedToProp == nil || !movedToProp.IsIRI() { +		return nil +	} +	return movedToProp.GetIRI() +} + +// SetMovedTo sets the given IRI on the movedTo property of 'with'. +func SetMovedTo(with WithMovedTo, movedTo *url.URL) { +	movedToProp := with.GetActivityStreamsMovedTo() +	if movedToProp == nil { +		movedToProp = streams.NewActivityStreamsMovedToProperty() +		with.SetActivityStreamsMovedTo(movedToProp) +	} +	movedToProp.SetIRI(movedTo) +} + +// GetAlsoKnownAs returns the IRI contained in the alsoKnownAs property of 'with'. +func GetAlsoKnownAs(with WithAlsoKnownAs) []*url.URL { +	alsoKnownAsProp := with.GetActivityStreamsAlsoKnownAs() +	return getIRIs[vocab.ActivityStreamsAlsoKnownAsPropertyIterator](alsoKnownAsProp) +} + +// SetAlsoKnownAs sets the given IRIs on the alsoKnownAs property of 'with'. +func SetAlsoKnownAs(with WithAlsoKnownAs, alsoKnownAs []*url.URL) { +	appendIRIs(func() Property[vocab.ActivityStreamsAlsoKnownAsPropertyIterator] { +		alsoKnownAsProp := with.GetActivityStreamsAlsoKnownAs() +		if alsoKnownAsProp == nil { +			alsoKnownAsProp = streams.NewActivityStreamsAlsoKnownAsProperty() +			with.SetActivityStreamsAlsoKnownAs(alsoKnownAsProp) +		} +		return alsoKnownAsProp +	}, alsoKnownAs...) +} +  // GetPublished returns the time contained in the Published property of 'with'.  func GetPublished(with WithPublished) time.Time {  	publishProp := with.GetActivityStreamsPublished() @@ -465,7 +520,12 @@ func SetManuallyApprovesFollowers(with WithManuallyApprovesFollowers, manuallyAp  	mafProp.Set(manuallyApprovesFollowers)  } -func getIRIs[T TypeOrIRI](prop Property[T]) []*url.URL { +// extractIRIs extracts just the AP IRIs from an iterable +// property that may contain types (with IRIs) or just IRIs. +// +// If you know the property contains only IRIs and no types, +// then use getIRIs instead, since it's slightly faster. +func extractIRIs[T TypeOrIRI](prop Property[T]) []*url.URL {  	if prop == nil || prop.Len() == 0 {  		return nil  	} @@ -490,7 +550,29 @@ func getIRIs[T TypeOrIRI](prop Property[T]) []*url.URL {  	return ids  } -func appendIRIs[T TypeOrIRI](getProp func() Property[T], iri ...*url.URL) { +// getIRIs gets AP IRIs from an iterable property of IRIs. +// +// Types will be ignored; to extract IRIs from an iterable +// that may contain types too, use extractIRIs. +func getIRIs[T WithIRI](prop Property[T]) []*url.URL { +	if prop == nil || prop.Len() == 0 { +		return nil +	} +	ids := make([]*url.URL, 0, prop.Len()) +	for i := 0; i < prop.Len(); i++ { +		at := prop.At(i) +		if at.IsIRI() { +			id := at.GetIRI() +			if id != nil { +				ids = append(ids, id) +				continue +			} +		} +	} +	return ids +} + +func appendIRIs[T WithIRI](getProp func() Property[T], iri ...*url.URL) {  	if len(iri) == 0 {  		return  	} diff --git a/internal/ap/serialize.go b/internal/ap/serialize.go index 774e95f2d..b13ebb340 100644 --- a/internal/ap/serialize.go +++ b/internal/ap/serialize.go @@ -90,13 +90,12 @@ func serializeWithOrderedItems(t vocab.Type) (map[string]interface{}, error) {  }  // SerializeAccountable is a custom serializer for any Accountable type. -// This serializer rewrites the 'attachment' value of the Accountable, if -// present, to always be an array/slice. +// This serializer rewrites certain values of the Accountable, if present, +// to always be an array/slice.  // -// While this is not strictly necessary in json-ld terms, most other fedi -// implementations look for attachment to be an array of PropertyValue (field) -// entries, and will not parse single-entry, non-array attachments on accounts -// properly. +// While this may not always be strictly necessary in json-ld terms, most other +// fedi implementations look for certain fields to be an array and will not parse +// single-entry, non-array fields on accounts properly.  //  // If the accountable is being serialized as a top-level object (eg., for serving  // in response to an account dereference request), then includeContext should be @@ -126,6 +125,7 @@ func serializeAccountable(t vocab.Type, includeContext bool) (map[string]interfa  	}  	NormalizeOutgoingAttachmentProp(accountable, data) +	NormalizeOutgoingAlsoKnownAsProp(accountable, data)  	return data, nil  } diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index cdb949efa..705e1b118 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -279,7 +279,12 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou  		}  	} -	if !account.AlsoKnownAsPopulated() { +	// Only try to populate AlsoKnownAs for local accounts, +	// since those are the only accounts to which it's relevant. +	// +	// AKA from remotes might have loads of random-ass values +	// set here, and we don't want to do lots of failing DB calls. +	if account.IsLocal() && !account.AlsoKnownAsPopulated() {  		// Account alsoKnownAs accounts are  		// out-of-date with URIs, repopulate.  		alsoKnownAs := make([]*gtsmodel.Account, 0) diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index fa2ae6a62..b262030de 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -198,7 +198,25 @@ func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable a  	// TODO: FeaturedTagsURI -	// TODO: alsoKnownAs +	// Moved and AlsoKnownAsURIs, +	// needed for account migrations. +	movedToURI := ap.GetMovedTo(accountable) +	if movedToURI != nil { +		acct.MovedToURI = movedToURI.String() +	} + +	alsoKnownAsURIs := ap.GetAlsoKnownAs(accountable) +	for i, uri := range alsoKnownAsURIs { +		// Don't store more than +		// 20 AKA URIs for remotes, +		// to prevent people playing +		// silly buggers. +		if i >= 20 { +			break +		} + +		acct.AlsoKnownAsURIs = append(acct.AlsoKnownAsURIs, uri.String()) +	}  	// Extract account public key and verify ownership to account.  	pkey, pkeyURL, pkeyOwnerID, err := ap.ExtractPublicKey(accountable) diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go index 627f9cac7..fc8cd19a0 100644 --- a/internal/typeutils/astointernal_test.go +++ b/internal/typeutils/astointernal_test.go @@ -146,6 +146,7 @@ func (suite *ASToInternalTestSuite) TestParseGargron() {  	acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "")  	suite.NoError(err)  	suite.Equal("https://mastodon.social/inbox", *acct.SharedInboxURI) +	suite.Equal([]string{"https://tooting.ai/users/Gargron"}, acct.AlsoKnownAsURIs)  	suite.Equal(int64(1458086400), acct.CreatedAt.Unix())  } diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index dc25babaa..a795541d0 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -171,7 +171,30 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab  	// alsoKnownAs  	// Required for Move activity. -	// TODO: NOT IMPLEMENTED **YET** -- this needs to be added as an activitypub extension to https://github.com/go-fed/activity, see https://github.com/go-fed/activity/tree/master/astool +	if l := len(a.AlsoKnownAsURIs); l != 0 { +		alsoKnownAsURIs := make([]*url.URL, l) +		for i, rawURL := range a.AlsoKnownAsURIs { +			uri, err := url.Parse(rawURL) +			if err != nil { +				return nil, err +			} + +			alsoKnownAsURIs[i] = uri +		} + +		ap.SetAlsoKnownAs(person, alsoKnownAsURIs) +	} + +	// movedTo +	// Required for Move activity. +	if a.MovedToURI != "" { +		movedTo, err := url.Parse(a.MovedToURI) +		if err != nil { +			return nil, err +		} + +		ap.SetMovedTo(person, movedTo) +	}  	// publicKey  	// Required for signatures. diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go index cbeaf3c8c..740938220 100644 --- a/internal/typeutils/internaltoas_test.go +++ b/internal/typeutils/internaltoas_test.go @@ -138,6 +138,72 @@ func (suite *InternalToASTestSuite) TestAccountToASWithFields() {  }`, trimmed)  } +func (suite *InternalToASTestSuite) TestAccountToASAliasedAndMoved() { +	testAccount := >smodel.Account{} +	*testAccount = *suite.testAccounts["local_account_1"] // take zork for this test + +	ctx := context.Background() + +	// Suppose zork has moved account to turtle. +	testAccount.AlsoKnownAsURIs = []string{"http://localhost:8080/users/1happyturtle"} +	testAccount.MovedToURI = "http://localhost:8080/users/1happyturtle" +	if err := suite.state.DB.UpdateAccount(ctx, +		testAccount, +		"also_known_as_uris", +		"moved_to_uri", +	); err != nil { +		suite.FailNow(err.Error()) +	} + +	asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) +	suite.NoError(err) + +	ser, err := ap.Serialize(asPerson) +	suite.NoError(err) + +	bytes, err := json.MarshalIndent(ser, "", "  ") +	suite.NoError(err) + +	// trim off everything up to 'alsoKnownAs'; +	// this is necessary because the order of multiple 'context' entries is not determinate +	trimmed := strings.Split(string(bytes), "\"alsoKnownAs\"")[1] + +	suite.Equal(`: [ +    "http://localhost:8080/users/1happyturtle" +  ], +  "discoverable": true, +  "featured": "http://localhost:8080/users/the_mighty_zork/collections/featured", +  "followers": "http://localhost:8080/users/the_mighty_zork/followers", +  "following": "http://localhost:8080/users/the_mighty_zork/following", +  "icon": { +    "mediaType": "image/jpeg", +    "type": "Image", +    "url": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg" +  }, +  "id": "http://localhost:8080/users/the_mighty_zork", +  "image": { +    "mediaType": "image/jpeg", +    "type": "Image", +    "url": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg" +  }, +  "inbox": "http://localhost:8080/users/the_mighty_zork/inbox", +  "manuallyApprovesFollowers": false, +  "movedTo": "http://localhost:8080/users/1happyturtle", +  "name": "original zork (he/they)", +  "outbox": "http://localhost:8080/users/the_mighty_zork/outbox", +  "preferredUsername": "the_mighty_zork", +  "publicKey": { +    "id": "http://localhost:8080/users/the_mighty_zork/main-key", +    "owner": "http://localhost:8080/users/the_mighty_zork", +    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwXTcOAvM1Jiw5Ffpk0qn\nr0cwbNvFe/5zQ+Tp7tumK/ZnT37o7X0FUEXrxNi+dkhmeJ0gsaiN+JQGNUewvpSk\nPIAXKvi908aSfCGjs7bGlJCJCuDuL5d6m7hZnP9rt9fJc70GElPpG0jc9fXwlz7T\nlsPb2ecatmG05Y4jPwdC+oN4MNCv9yQzEvCVMzl76EJaM602kIHC1CISn0rDFmYd\n9rSN7XPlNJw1F6PbpJ/BWQ+pXHKw3OEwNTETAUNYiVGnZU+B7a7bZC9f6/aPbJuV\nt8Qmg+UnDvW1Y8gmfHnxaWG2f5TDBvCHmcYtucIZPLQD4trAozC4ryqlmCWQNKbt\n0wIDAQAB\n-----END PUBLIC KEY-----\n" +  }, +  "summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e", +  "tag": [], +  "type": "Person", +  "url": "http://localhost:8080/@the_mighty_zork" +}`, trimmed) +} +  func (suite *InternalToASTestSuite) TestAccountToASWithOneField() {  	testAccount := >smodel.Account{}  	*testAccount = *suite.testAccounts["local_account_2"] | 
