[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
|
@ -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…
Reference in New Issue