mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 06:52:26 -05:00 
			
		
		
		
	[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
This commit is contained in:
		
					parent
					
						
							
								3cc51d5072
							
						
					
				
			
			
				commit
				
					
						aa396c78d3
					
				
			
		
					 11 changed files with 392 additions and 22 deletions
				
			
		|  | @ -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. | 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. | 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! | ||||||
|  |  | ||||||
|  | @ -160,6 +160,8 @@ type Accountable interface { | ||||||
| 	WithFollowing | 	WithFollowing | ||||||
| 	WithFollowers | 	WithFollowers | ||||||
| 	WithFeatured | 	WithFeatured | ||||||
|  | 	WithMovedTo | ||||||
|  | 	WithAlsoKnownAs | ||||||
| 	WithManuallyApprovesFollowers | 	WithManuallyApprovesFollowers | ||||||
| 	WithEndpoints | 	WithEndpoints | ||||||
| 	WithTag | 	WithTag | ||||||
|  | @ -327,7 +329,7 @@ type TypeOrIRI interface { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Property represents the minimum interface for an ActivityStreams property with IRIs. | // Property represents the minimum interface for an ActivityStreams property with IRIs. | ||||||
| type Property[T TypeOrIRI] interface { | type Property[T WithIRI] interface { | ||||||
| 	Len() int | 	Len() int | ||||||
| 	At(int) T | 	At(int) T | ||||||
| 
 | 
 | ||||||
|  | @ -441,6 +443,18 @@ type WithFeatured interface { | ||||||
| 	SetTootFeatured(vocab.TootFeaturedProperty) | 	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 | // WithAttributedTo represents an activity with ActivityStreamsAttributedToProperty | ||||||
| type WithAttributedTo interface { | type WithAttributedTo interface { | ||||||
| 	GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty | 	GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty | ||||||
|  | @ -551,6 +565,12 @@ type WithObject interface { | ||||||
| 	SetActivityStreamsObject(vocab.ActivityStreamsObjectProperty) | 	SetActivityStreamsObject(vocab.ActivityStreamsObjectProperty) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // WithTarget represents an activity with ActivityStreamsTargetProperty | ||||||
|  | type WithTarget interface { | ||||||
|  | 	GetActivityStreamsTarget() vocab.ActivityStreamsTargetProperty | ||||||
|  | 	SetActivityStreamsTarget(vocab.ActivityStreamsTargetProperty) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // WithNext represents an activity with ActivityStreamsNextProperty | // WithNext represents an activity with ActivityStreamsNextProperty | ||||||
| type WithNext interface { | type WithNext interface { | ||||||
| 	GetActivityStreamsNext() vocab.ActivityStreamsNextProperty | 	GetActivityStreamsNext() vocab.ActivityStreamsNextProperty | ||||||
|  |  | ||||||
|  | @ -391,6 +391,36 @@ func NormalizeOutgoingAttachmentProp(item WithAttachment, rawJSON map[string]int | ||||||
| 	rawJSON["attachment"] = []interface{}{attachment} | 	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 | // NormalizeOutgoingContentProp normalizes go-fed's funky formatting of content and | ||||||
| // contentMap properties to a format better understood by other AP implementations. | // contentMap properties to a format better understood by other AP implementations. | ||||||
| // | // | ||||||
|  |  | ||||||
|  | @ -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. | // GetCc returns the IRIs contained in the Cc property of 'with'. Panics on entries with missing ID. | ||||||
| func GetCc(with WithCc) []*url.URL { | func GetCc(with WithCc) []*url.URL { | ||||||
| 	ccProp := with.GetActivityStreamsCc() | 	ccProp := with.GetActivityStreamsCc() | ||||||
| 	return getIRIs[vocab.ActivityStreamsCcPropertyIterator](ccProp) | 	return extractIRIs[vocab.ActivityStreamsCcPropertyIterator](ccProp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // AppendCc appends the given IRIs to the Cc property of 'with'. | // 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. | // GetBcc returns the IRIs contained in the Bcc property of 'with'. Panics on entries with missing ID. | ||||||
| func GetBcc(with WithBcc) []*url.URL { | func GetBcc(with WithBcc) []*url.URL { | ||||||
| 	bccProp := with.GetActivityStreamsBcc() | 	bccProp := with.GetActivityStreamsBcc() | ||||||
| 	return getIRIs[vocab.ActivityStreamsBccPropertyIterator](bccProp) | 	return extractIRIs[vocab.ActivityStreamsBccPropertyIterator](bccProp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // AppendBcc appends the given IRIs to the Bcc property of 'with'. | // 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'. | // GetActorIRIs returns the IRIs contained in the Actor property of 'with'. | ||||||
| func GetActorIRIs(with WithActor) []*url.URL { | func GetActorIRIs(with WithActor) []*url.URL { | ||||||
| 	actorProp := with.GetActivityStreamsActor() | 	actorProp := with.GetActivityStreamsActor() | ||||||
| 	return getIRIs[vocab.ActivityStreamsActorPropertyIterator](actorProp) | 	return extractIRIs[vocab.ActivityStreamsActorPropertyIterator](actorProp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // AppendActorIRIs appends the given IRIs to the Actor property of 'with'. | // 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'. | // GetObjectIRIs returns the IRIs contained in the Object property of 'with'. | ||||||
| func GetObjectIRIs(with WithObject) []*url.URL { | func GetObjectIRIs(with WithObject) []*url.URL { | ||||||
| 	objectProp := with.GetActivityStreamsObject() | 	objectProp := with.GetActivityStreamsObject() | ||||||
| 	return getIRIs[vocab.ActivityStreamsObjectPropertyIterator](objectProp) | 	return extractIRIs[vocab.ActivityStreamsObjectPropertyIterator](objectProp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // AppendObjectIRIs appends the given IRIs to the Object property of 'with'. | // 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'. | // GetAttributedTo returns the IRIs contained in the AttributedTo property of 'with'. | ||||||
| func GetAttributedTo(with WithAttributedTo) []*url.URL { | func GetAttributedTo(with WithAttributedTo) []*url.URL { | ||||||
| 	attribProp := with.GetActivityStreamsAttributedTo() | 	attribProp := with.GetActivityStreamsAttributedTo() | ||||||
| 	return getIRIs[vocab.ActivityStreamsAttributedToPropertyIterator](attribProp) | 	return extractIRIs[vocab.ActivityStreamsAttributedToPropertyIterator](attribProp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // AppendAttributedTo appends the given IRIs to the AttributedTo property of 'with'. | // 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'. | // GetInReplyTo returns the IRIs contained in the InReplyTo property of 'with'. | ||||||
| func GetInReplyTo(with WithInReplyTo) []*url.URL { | func GetInReplyTo(with WithInReplyTo) []*url.URL { | ||||||
| 	replyProp := with.GetActivityStreamsInReplyTo() | 	replyProp := with.GetActivityStreamsInReplyTo() | ||||||
| 	return getIRIs[vocab.ActivityStreamsInReplyToPropertyIterator](replyProp) | 	return extractIRIs[vocab.ActivityStreamsInReplyToPropertyIterator](replyProp) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // AppendInReplyTo appends the given IRIs to the InReplyTo property of 'with'. | // 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) | 	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'. | // GetPublished returns the time contained in the Published property of 'with'. | ||||||
| func GetPublished(with WithPublished) time.Time { | func GetPublished(with WithPublished) time.Time { | ||||||
| 	publishProp := with.GetActivityStreamsPublished() | 	publishProp := with.GetActivityStreamsPublished() | ||||||
|  | @ -465,7 +520,12 @@ func SetManuallyApprovesFollowers(with WithManuallyApprovesFollowers, manuallyAp | ||||||
| 	mafProp.Set(manuallyApprovesFollowers) | 	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 { | 	if prop == nil || prop.Len() == 0 { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  | @ -490,7 +550,29 @@ func getIRIs[T TypeOrIRI](prop Property[T]) []*url.URL { | ||||||
| 	return ids | 	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 { | 	if len(iri) == 0 { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -90,13 +90,12 @@ func serializeWithOrderedItems(t vocab.Type) (map[string]interface{}, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SerializeAccountable is a custom serializer for any Accountable type. | // SerializeAccountable is a custom serializer for any Accountable type. | ||||||
| // This serializer rewrites the 'attachment' value of the Accountable, if | // This serializer rewrites certain values of the Accountable, if present, | ||||||
| // present, to always be an array/slice. | // to always be an array/slice. | ||||||
| // | // | ||||||
| // While this is not strictly necessary in json-ld terms, most other fedi | // While this may not always be strictly necessary in json-ld terms, most other | ||||||
| // implementations look for attachment to be an array of PropertyValue (field) | // fedi implementations look for certain fields to be an array and will not parse | ||||||
| // entries, and will not parse single-entry, non-array attachments on accounts | // single-entry, non-array fields on accounts properly. | ||||||
| // properly. |  | ||||||
| // | // | ||||||
| // If the accountable is being serialized as a top-level object (eg., for serving | // 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 | // 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) | 	NormalizeOutgoingAttachmentProp(accountable, data) | ||||||
|  | 	NormalizeOutgoingAlsoKnownAsProp(accountable, data) | ||||||
| 
 | 
 | ||||||
| 	return data, nil | 	return data, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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 | 		// Account alsoKnownAs accounts are | ||||||
| 		// out-of-date with URIs, repopulate. | 		// out-of-date with URIs, repopulate. | ||||||
| 		alsoKnownAs := make([]*gtsmodel.Account, 0) | 		alsoKnownAs := make([]*gtsmodel.Account, 0) | ||||||
|  |  | ||||||
|  | @ -198,7 +198,25 @@ func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable a | ||||||
| 
 | 
 | ||||||
| 	// TODO: FeaturedTagsURI | 	// 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. | 	// Extract account public key and verify ownership to account. | ||||||
| 	pkey, pkeyURL, pkeyOwnerID, err := ap.ExtractPublicKey(accountable) | 	pkey, pkeyURL, pkeyOwnerID, err := ap.ExtractPublicKey(accountable) | ||||||
|  |  | ||||||
|  | @ -146,6 +146,7 @@ func (suite *ASToInternalTestSuite) TestParseGargron() { | ||||||
| 	acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "") | 	acct, err := suite.typeconverter.ASRepresentationToAccount(context.Background(), rep, "") | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 	suite.Equal("https://mastodon.social/inbox", *acct.SharedInboxURI) | 	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()) | 	suite.Equal(int64(1458086400), acct.CreatedAt.Unix()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -171,7 +171,30 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab | ||||||
| 
 | 
 | ||||||
| 	// alsoKnownAs | 	// alsoKnownAs | ||||||
| 	// Required for Move activity. | 	// 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 | 	// publicKey | ||||||
| 	// Required for signatures. | 	// Required for signatures. | ||||||
|  |  | ||||||
|  | @ -138,6 +138,72 @@ func (suite *InternalToASTestSuite) TestAccountToASWithFields() { | ||||||
| }`, trimmed) | }`, 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() { | func (suite *InternalToASTestSuite) TestAccountToASWithOneField() { | ||||||
| 	testAccount := >smodel.Account{} | 	testAccount := >smodel.Account{} | ||||||
| 	*testAccount = *suite.testAccounts["local_account_2"] | 	*testAccount = *suite.testAccounts["local_account_2"] | ||||||
|  |  | ||||||
|  | @ -2799,6 +2799,8 @@ func NewTestFediPeople() map[string]vocab.ActivityStreamsPerson { | ||||||
| 			nil, | 			nil, | ||||||
| 			URLMustParse("https://unknown-instance.com/users/brand_new_person/outbox"), | 			URLMustParse("https://unknown-instance.com/users/brand_new_person/outbox"), | ||||||
| 			URLMustParse("https://unknown-instance.com/users/brand_new_person/collections/featured"), | 			URLMustParse("https://unknown-instance.com/users/brand_new_person/collections/featured"), | ||||||
|  | 			nil, | ||||||
|  | 			nil, | ||||||
| 			"brand_new_person", | 			"brand_new_person", | ||||||
| 			"Geoff Brando New Personson", | 			"Geoff Brando New Personson", | ||||||
| 			"hey I'm a new person, your instance hasn't seen me yet uwu", | 			"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/sharedInbox"), | ||||||
| 			URLMustParse("https://turnip.farm/users/turniplover6969/outbox"), | 			URLMustParse("https://turnip.farm/users/turniplover6969/outbox"), | ||||||
| 			URLMustParse("https://turnip.farm/users/turniplover6969/collections/featured"), | 			URLMustParse("https://turnip.farm/users/turniplover6969/collections/featured"), | ||||||
|  | 			nil, | ||||||
|  | 			nil, | ||||||
| 			"turniplover6969", | 			"turniplover6969", | ||||||
| 			"Turnip Lover 6969", | 			"Turnip Lover 6969", | ||||||
| 			"I just think they're neat", | 			"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/sharedInbox"), | ||||||
| 			URLMustParse("http://example.org/users/Some_User/outbox"), | 			URLMustParse("http://example.org/users/Some_User/outbox"), | ||||||
| 			URLMustParse("http://example.org/users/Some_User/collections/featured"), | 			URLMustParse("http://example.org/users/Some_User/collections/featured"), | ||||||
|  | 			nil, | ||||||
|  | 			nil, | ||||||
| 			"Some_User", | 			"Some_User", | ||||||
| 			"just some user, don't mind me", | 			"just some user, don't mind me", | ||||||
| 			"Peepee poo poo", | 			"Peepee poo poo", | ||||||
|  | @ -3335,6 +3341,8 @@ func newAPPerson( | ||||||
| 	sharedInboxIRI *url.URL, | 	sharedInboxIRI *url.URL, | ||||||
| 	outboxURI *url.URL, | 	outboxURI *url.URL, | ||||||
| 	featuredURI *url.URL, | 	featuredURI *url.URL, | ||||||
|  | 	movedToURI *url.URL, | ||||||
|  | 	alsoKnownAsURIs []*url.URL, | ||||||
| 	username string, | 	username string, | ||||||
| 	displayName string, | 	displayName string, | ||||||
| 	note string, | 	note string, | ||||||
|  | @ -3444,9 +3452,15 @@ func newAPPerson( | ||||||
| 	// devices | 	// devices | ||||||
| 	// NOT IMPLEMENTED, probably won't implement | 	// NOT IMPLEMENTED, probably won't implement | ||||||
| 
 | 
 | ||||||
| 	// alsoKnownAs | 	// alsoKnownAs, movedTo | ||||||
| 	// Required for Move activity. | 	// 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 | 	// publicKey | ||||||
| 	// Required for signatures. | 	// Required for signatures. | ||||||
|  | @ -3628,7 +3642,7 @@ func newAPGroup( | ||||||
| 	// devices | 	// devices | ||||||
| 	// NOT IMPLEMENTED, probably won't implement | 	// NOT IMPLEMENTED, probably won't implement | ||||||
| 
 | 
 | ||||||
| 	// alsoKnownAs | 	// AlsoKnownAsURI | ||||||
| 	// Required for Move activity. | 	// 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 | 	// 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 | 	// devices | ||||||
| 	// NOT IMPLEMENTED, probably won't implement | 	// NOT IMPLEMENTED, probably won't implement | ||||||
| 
 | 
 | ||||||
| 	// alsoKnownAs | 	// AlsoKnownAsURI | ||||||
| 	// Required for Move activity. | 	// 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 | 	// 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 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue