From aa396c78d30c129bb2145765d3990571dbc025bb Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:45:46 +0100 Subject: [PATCH] [feature] serdes for moved/also_known_as (#2600) * [feature] serdes for moved/also_known_as * document `alsoKnownAs` and `movedTo` properties * only implicitly populate AKA uris from DB for local accounts * don't let remotes store more than 20 AKA uris to avoid shenanigans --- docs/federation/federating_with_gotosocial.md | 111 ++++++++++++++++++ internal/ap/interfaces.go | 22 +++- internal/ap/normalize.go | 30 +++++ internal/ap/properties.go | 98 ++++++++++++++-- internal/ap/serialize.go | 12 +- internal/db/bundb/account.go | 7 +- internal/typeutils/astointernal.go | 20 +++- internal/typeutils/astointernal_test.go | 1 + internal/typeutils/internaltoas.go | 25 +++- internal/typeutils/internaltoas_test.go | 66 +++++++++++ testrig/testmodels.go | 22 +++- 11 files changed, 392 insertions(+), 22 deletions(-) diff --git a/docs/federation/federating_with_gotosocial.md b/docs/federation/federating_with_gotosocial.md index 563536556..0b513f93f 100644 --- a/docs/federation/federating_with_gotosocial.md +++ b/docs/federation/federating_with_gotosocial.md @@ -722,3 +722,114 @@ Here's an example of a "Create", in which user "https://sample.com/users/willy_n GoToSocial expects to receive poll votes in much the same manner that it sends them out. They will only ever expect to be received as part of a "Create" activity. In particular, GoToSocial recognizes votes as different to other "Note" objects by the inclusion of a "name" field, missing "content" field, and the "inReplyTo" field being an IRI pointing to a status with attached poll. If any of these conditions are not met, GoToSocial will consider the provided "Note" to be a malformed status object. + +## Actor Migration / Aliasing + +GoToSocial supports account migration from one instance/server to another through a combination of the `Move` activity, and the Actor Object properties `alsoKnownAs` and `movedTo`. + +### `alsoKnownAs` + +GoToSocial supports account aliasing using the `alsoKnownAs` Actor property, which is an [accepted ActivityPub extension](https://www.w3.org/wiki/Activity_Streams_extensions#as:alsoKnownAs_property). + +#### Incoming + +On incoming AP messages, GoToSocial looks for the `alsoKnownAs` property on an Actor to be an array of ActivityPub IDs/URIs of other Actors by which the Actor is also known. + +For example: + +```json +{ + "@context": [ + "http://joinmastodon.org/ns", + "https://w3id.org/security/v1", + "https://www.w3.org/ns/activitystreams", + "http://schema.org" + ], + "featured": "http://example.org/users/1happyturtle/collections/featured", + "followers": "http://example.org/users/1happyturtle/followers", + "following": "http://example.org/users/1happyturtle/following", + "id": "http://example.org/users/1happyturtle", + "inbox": "http://example.org/users/1happyturtle/inbox", + "manuallyApprovesFollowers": true, + "name": "happy little turtle :3", + "outbox": "http://example.org/users/1happyturtle/outbox", + "preferredUsername": "1happyturtle", + "publicKey": {...}, + "summary": "\u003cp\u003ei post about things that concern me\u003c/p\u003e", + "type": "Person", + "url": "http://example.org/@1happyturtle", + "alsoKnownAs": [ + "https://another-server.com/users/1happyturtle", + "https://somewhere-else.org/users/originalTurtle" + ] +} +``` + +In the above AP JSON, the Actor `http://example.org/users/1happyturtle` is aliased to the other Actors `https://another-server.com/users/1happyturtle` and `https://somewhere-else.org/users/originalTurtle`. + +GoToSocial will store incoming `alsoKnownAs` URIs in the database, but does not (currently) use them for anything except verifying a `Move` Activity (see below). + +#### Outgoing + +GoToSocial users can set multiple `alsoKnownAs` URIs on their account via the GoToSocial client API. GoToSocial will verify that these `alsoKnownAs` aliases are valid Actor URIs before storing them in the database and before serializing them in outgoing AP messages. + +However, GoToSocial does not verify *ownership* of those `alsoKnownAs` URIs by the user setting the aliases before serializing them in outgoing messages; it expects remote servers to do their own verification before trusting any transmitted `alsoKnownAs` values. + +As an example, the user `http://example.org/users/1happyturtle`, from their GoToSocial instance, might set `alsoKnownAs: [ "https://unrelated-server.com/users/someone_else" ]` on their account, and GoToSocial will duly transmit this alias to other servers. + +In this case, though, `https://unrelated-server.com/users/someone_else` may not be the same person as `1happyturtle`. `1happyturtle` may have set this alias by mistake, or maliciously. To properly verify ownership of `someone_else` by `1happyturtle`, a remote server should check that the `alsoKnownAs` property of the Actor `https://unrelated-server.com/users/someone_else` contains an entry `http://example.org/users/1happyturtle`. + +In other words, remote servers should not trust `alsoKnownAs` aliases by default, and should instead ensure that a **two-way alias** exists between Actors before treating the alias as valid. + +!!! info + The reason that GoToSocial does not perform verification of `alsoKnownAs` values before sending them out to other servers is to avoid a chicken and egg problem. Say that `1happyturtle` and `someone_else` *are* the same person, one of the two Actors must be able to set `alsoKnownAs` first, so that the instance of the other Actor can begin processing the alias. If both servers prevent an unverified alias from being serialized in the `alsoKnownAs` property, then it becomes impossible for either `1happyturtle` or `someone_else` to alias to one another. + +### `movedTo` + +GoToSocial marks accounts as moved using the `movedTo` property. Unlike `alsoKnownAs` this is not an accepted ActivityPub extension, but it has been widely popularized by Mastodon, which also uses it in connection with the `Move` activity. [See the Mastodon docs for more info](https://documentation.sig.gy/spec/activitypub/#namespaces). + +#### Incoming + +For incoming AP messages, GoToSocial looks for the `movedTo` property on an Actor to be set to a single ActivityPub Actor URI/ID. + +For example: + +```json +{ + "@context": [ + "http://joinmastodon.org/ns", + "https://w3id.org/security/v1", + "https://www.w3.org/ns/activitystreams", + "http://schema.org" + ], + "featured": "http://example.org/users/1happyturtle/collections/featured", + "followers": "http://example.org/users/1happyturtle/followers", + "following": "http://example.org/users/1happyturtle/following", + "id": "http://example.org/users/1happyturtle", + "inbox": "http://example.org/users/1happyturtle/inbox", + "manuallyApprovesFollowers": true, + "name": "happy little turtle :3", + "outbox": "http://example.org/users/1happyturtle/outbox", + "preferredUsername": "1happyturtle", + "publicKey": {...}, + "summary": "\u003cp\u003ei post about things that concern me\u003c/p\u003e", + "type": "Person", + "url": "http://example.org/@1happyturtle", + "alsoKnownAs": [ + "https://another-server.com/users/1happyturtle" + ], + "movedTo": "https://another-server.com/users/1happyturtle" +} +``` + +In the above JSON, the Actor `http://example.org/users/1happyturtle` has been aliased to the Actor `https://another-server.com/users/1happyturtle` and has also moved/migrated to that account. + +GoToSocial stores incoming `movedTo` values in the database, but does not consider an account migration to have been processed unless the Actor doing the Move had previously transmitted a Move activity (see below). + +#### Outgoing + +GoToSocial will only set `movedTo` on outgoing Actors when an account `Move` has been verified and processed. + +### `Move` Activity + +TODO: document how `Move` works! 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"] diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 2f7e53666..b350cafe5 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -2799,6 +2799,8 @@ func NewTestFediPeople() map[string]vocab.ActivityStreamsPerson { nil, URLMustParse("https://unknown-instance.com/users/brand_new_person/outbox"), URLMustParse("https://unknown-instance.com/users/brand_new_person/collections/featured"), + nil, + nil, "brand_new_person", "Geoff Brando New Personson", "hey I'm a new person, your instance hasn't seen me yet uwu", @@ -2820,6 +2822,8 @@ func NewTestFediPeople() map[string]vocab.ActivityStreamsPerson { URLMustParse("https://turnip.farm/sharedInbox"), URLMustParse("https://turnip.farm/users/turniplover6969/outbox"), URLMustParse("https://turnip.farm/users/turniplover6969/collections/featured"), + nil, + nil, "turniplover6969", "Turnip Lover 6969", "I just think they're neat", @@ -2841,6 +2845,8 @@ func NewTestFediPeople() map[string]vocab.ActivityStreamsPerson { URLMustParse("http://example.org/sharedInbox"), URLMustParse("http://example.org/users/Some_User/outbox"), URLMustParse("http://example.org/users/Some_User/collections/featured"), + nil, + nil, "Some_User", "just some user, don't mind me", "Peepee poo poo", @@ -3335,6 +3341,8 @@ func newAPPerson( sharedInboxIRI *url.URL, outboxURI *url.URL, featuredURI *url.URL, + movedToURI *url.URL, + alsoKnownAsURIs []*url.URL, username string, displayName string, note string, @@ -3444,9 +3452,15 @@ func newAPPerson( // devices // NOT IMPLEMENTED, probably won't implement - // alsoKnownAs + // alsoKnownAs, movedTo // 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 len(alsoKnownAsURIs) != 0 { + ap.SetAlsoKnownAs(person, alsoKnownAsURIs) + } + + if movedToURI != nil { + ap.SetMovedTo(person, movedToURI) + } // publicKey // Required for signatures. @@ -3628,7 +3642,7 @@ func newAPGroup( // devices // NOT IMPLEMENTED, probably won't implement - // alsoKnownAs + // AlsoKnownAsURI // 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 @@ -3812,7 +3826,7 @@ func newAPService( // devices // NOT IMPLEMENTED, probably won't implement - // alsoKnownAs + // AlsoKnownAsURI // 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