diff --git a/docs/federation/actors.md b/docs/federation/actors.md index d5dc61e19..ba2283ee9 100644 --- a/docs/federation/actors.md +++ b/docs/federation/actors.md @@ -1,5 +1,13 @@ # Actors and Actor Properties +## `Service` vs `Person` actors + +GoToSocial serves most accounts as the ActivityStreams `Person` type described [here](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person). + +Accounts that users have selected to mark as bot accounts, however, will use the `Service` type described [here](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service). + +This type distinction can be used by remote servers to distinguish between bot accounts and "regular" user accounts. + ## Inbox GoToSocial implements Inboxes for Actors following the ActivityPub specification [here](https://www.w3.org/TR/activitypub/#inbox). diff --git a/internal/ap/activitystreams.go b/internal/ap/activitystreams.go index 8c53ae501..50955ce2c 100644 --- a/internal/ap/activitystreams.go +++ b/internal/ap/activitystreams.go @@ -17,6 +17,22 @@ package ap +import ( + "net/url" + + "github.com/superseriousbusiness/activity/pub" +) + +// PublicURI returns a fresh copy of the *url.URL version of the +// magic ActivityPub URI https://www.w3.org/ns/activitystreams#Public +func PublicURI() *url.URL { + publicURI, err := url.Parse(pub.PublicActivityPubIRI) + if err != nil { + panic(err) + } + return publicURI +} + // https://www.w3.org/TR/activitystreams-vocabulary const ( ActivityAccept = "Accept" // ActivityStreamsAccept https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accept diff --git a/internal/ap/ap_test.go b/internal/ap/ap_test.go index f982e4443..3738c8c9c 100644 --- a/internal/ap/ap_test.go +++ b/internal/ap/ap_test.go @@ -24,7 +24,6 @@ "io" "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" @@ -111,7 +110,7 @@ func noteWithMentions1() vocab.ActivityStreamsNote { // Anyone can like. canLikeAlwaysProp := streams.NewGoToSocialAlwaysProperty() - canLikeAlwaysProp.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) + canLikeAlwaysProp.AppendIRI(ap.PublicURI()) canLike.SetGoToSocialAlways(canLikeAlwaysProp) // Empty approvalRequired. @@ -128,7 +127,7 @@ func noteWithMentions1() vocab.ActivityStreamsNote { // Anyone can reply. canReplyAlwaysProp := streams.NewGoToSocialAlwaysProperty() - canReplyAlwaysProp.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) + canReplyAlwaysProp.AppendIRI(ap.PublicURI()) canReply.SetGoToSocialAlways(canReplyAlwaysProp) // Set empty approvalRequired. @@ -151,7 +150,7 @@ func noteWithMentions1() vocab.ActivityStreamsNote { // Public requires approval to announce. canAnnounceApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty() - canAnnounceApprovalRequiredProp.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) + canAnnounceApprovalRequiredProp.AppendIRI(ap.PublicURI()) canAnnounce.SetGoToSocialApprovalRequired(canAnnounceApprovalRequiredProp) // Set canAnnounce on the policy. @@ -266,7 +265,7 @@ func addressable1() ap.Addressable { note := streams.NewActivityStreamsNote() toProp := streams.NewActivityStreamsToProperty() - toProp.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) + toProp.AppendIRI(ap.PublicURI()) note.SetActivityStreamsTo(toProp) @@ -288,7 +287,7 @@ func addressable2() ap.Addressable { note.SetActivityStreamsTo(toProp) ccProp := streams.NewActivityStreamsCcProperty() - ccProp.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) + ccProp.AppendIRI(ap.PublicURI()) note.SetActivityStreamsCc(ccProp) diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index 1f08fde37..fdd5e4a0b 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -188,6 +188,7 @@ type Accountable interface { WithTag WithPublished WithUpdated + WithImage } // Statusable represents the minimum activitypub interface for representing a 'status'. @@ -439,6 +440,7 @@ type WithValue interface { // WithImage represents an activity with ActivityStreamsImageProperty type WithImage interface { GetActivityStreamsImage() vocab.ActivityStreamsImageProperty + SetActivityStreamsImage(vocab.ActivityStreamsImageProperty) } // WithSummary represents an activity with ActivityStreamsSummaryProperty diff --git a/internal/api/activitypub/users/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go index 64c9f7e6c..f87224d65 100644 --- a/internal/api/activitypub/users/inboxpost_test.go +++ b/internal/api/activitypub/users/inboxpost_test.go @@ -177,38 +177,6 @@ func (suite *InboxPostTestSuite) newUndo( return undo } -func (suite *InboxPostTestSuite) newUpdatePerson(person vocab.ActivityStreamsPerson, cc string, updateIRI string) vocab.ActivityStreamsUpdate { - // create an update - update := streams.NewActivityStreamsUpdate() - - // set the appropriate actor on it - updateActor := streams.NewActivityStreamsActorProperty() - updateActor.AppendIRI(person.GetJSONLDId().Get()) - update.SetActivityStreamsActor(updateActor) - - // Set the person as the 'object' property. - updateObject := streams.NewActivityStreamsObjectProperty() - updateObject.AppendActivityStreamsPerson(person) - update.SetActivityStreamsObject(updateObject) - - // Set the To of the update as public - updateTo := streams.NewActivityStreamsToProperty() - updateTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) - update.SetActivityStreamsTo(updateTo) - - // set the cc of the update to the receivingAccount - updateCC := streams.NewActivityStreamsCcProperty() - updateCC.AppendIRI(testrig.URLMustParse(cc)) - update.SetActivityStreamsCc(updateCC) - - // set some random-ass ID for the activity - updateID := streams.NewJSONLDIdProperty() - updateID.SetIRI(testrig.URLMustParse(updateIRI)) - update.SetJSONLDId(updateID) - - return update -} - func (suite *InboxPostTestSuite) newDelete(actorIRI string, objectIRI string, deleteIRI string) vocab.ActivityStreamsDelete { // create a delete delete := streams.NewActivityStreamsDelete() @@ -225,7 +193,7 @@ func (suite *InboxPostTestSuite) newDelete(actorIRI string, objectIRI string, de // Set the To of the delete as public deleteTo := streams.NewActivityStreamsToProperty() - deleteTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) + deleteTo.AppendIRI(ap.PublicURI()) delete.SetActivityStreamsTo(deleteTo) // set some random-ass ID for the activity @@ -329,7 +297,6 @@ func (suite *InboxPostTestSuite) TestPostUpdate() { var ( requestingAccount = new(gtsmodel.Account) targetAccount = suite.testAccounts["local_account_1"] - activityID = "http://fossbros-anonymous.io/72cc96a3-f742-4daf-b9f5-3407667260c5" updatedDisplayName = "updated display name!" ) @@ -348,11 +315,19 @@ func (suite *InboxPostTestSuite) TestPostUpdate() { requestingAccount.Emojis = []*gtsmodel.Emoji{testEmoji} // Create an update from the account. - asAccount, err := suite.tc.AccountToAS(context.Background(), requestingAccount) + accountable, err := suite.tc.AccountToAS(context.Background(), requestingAccount) if err != nil { suite.FailNow(err.Error()) } - update := suite.newUpdatePerson(asAccount, targetAccount.URI, activityID) + update, err := suite.tc.WrapAccountableInUpdate(accountable) + if err != nil { + suite.FailNow(err.Error()) + } + + // Set the ID to something from fossbros anonymous. + idProp := streams.NewJSONLDIdProperty() + idProp.SetIRI(testrig.URLMustParse("https://fossbros-anonymous.io/updates/waaaaaaaaaaaaaaaaa")) + update.SetJSONLDId(idProp) // Update. suite.inboxPost( @@ -540,17 +515,20 @@ func (suite *InboxPostTestSuite) TestPostFromBlockedAccount() { var ( requestingAccount = suite.testAccounts["remote_account_1"] targetAccount = suite.testAccounts["local_account_2"] - activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3" ) - person, err := suite.tc.AccountToAS(context.Background(), requestingAccount) + // Create an update from the account. + accountable, err := suite.tc.AccountToAS(context.Background(), requestingAccount) + if err != nil { + suite.FailNow(err.Error()) + } + update, err := suite.tc.WrapAccountableInUpdate(accountable) if err != nil { suite.FailNow(err.Error()) } - // Post an update from foss satan to turtle, who blocks him. - update := suite.newUpdatePerson(person, targetAccount.URI, activityID) - + // Post an update from foss satan + // to turtle, who blocks him. suite.inboxPost( update, requestingAccount, diff --git a/internal/federation/federator_test.go b/internal/federation/federator_test.go index e10c7576c..a4f8c4683 100644 --- a/internal/federation/federator_test.go +++ b/internal/federation/federator_test.go @@ -75,12 +75,12 @@ func (suite *FederatorStandardTestSuite) SetupTest() { // Ensure it's possible to deref // main key of foss satan. - fossSatanPerson, err := suite.typeconverter.AccountToAS(context.Background(), suite.testAccounts["remote_account_1"]) + fossSatanAS, err := suite.typeconverter.AccountToAS(context.Background(), suite.testAccounts["remote_account_1"]) if err != nil { suite.FailNow(err.Error()) } - suite.httpClient = testrig.NewMockHTTPClient(nil, "../../testrig/media", fossSatanPerson) + suite.httpClient = testrig.NewMockHTTPClient(nil, "../../testrig/media", fossSatanAS) suite.httpClient.TestRemotePeople = testrig.NewTestFediPeople() suite.httpClient.TestRemoteStatuses = testrig.NewTestFediStatuses() diff --git a/internal/processing/fedi/user.go b/internal/processing/fedi/user.go index bf14554cf..79c1b4fdb 100644 --- a/internal/processing/fedi/user.go +++ b/internal/processing/fedi/user.go @@ -23,7 +23,6 @@ "fmt" "net/url" - "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" @@ -72,7 +71,7 @@ func (p *Processor) UserGet(ctx context.Context, requestedUsername string, reque } // Auth passed, generate the proper AP representation. - person, err := p.converter.AccountToAS(ctx, receiver) + accountable, err := p.converter.AccountToAS(ctx, receiver) if err != nil { err := gtserror.Newf("error converting account: %w", err) return nil, gtserror.NewErrorInternalError(err) @@ -91,7 +90,7 @@ func (p *Processor) UserGet(ctx context.Context, requestedUsername string, reque // Instead, we end up in an 'I'll show you mine if you show me // yours' situation, where we sort of agree to reveal each // other's profiles at the same time. - return data(person) + return data(accountable) } // Get requester from auth. @@ -107,13 +106,13 @@ func (p *Processor) UserGet(ctx context.Context, requestedUsername string, reque return nil, gtserror.NewErrorForbidden(errors.New(text)) } - return data(person) + return data(accountable) } -func data(requestedPerson vocab.ActivityStreamsPerson) (interface{}, gtserror.WithCode) { - data, err := ap.Serialize(requestedPerson) +func data(accountable ap.Accountable) (interface{}, gtserror.WithCode) { + data, err := ap.Serialize(accountable) if err != nil { - err := gtserror.Newf("error serializing person: %w", err) + err := gtserror.Newf("error serializing accountable: %w", err) return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go index 8c08c42b7..d6dec6691 100644 --- a/internal/processing/workers/federate.go +++ b/internal/processing/workers/federate.go @@ -21,7 +21,6 @@ "context" "net/url" - "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" @@ -93,11 +92,6 @@ func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account) return err } - publicIRI, err := parseURI(pub.PublicActivityPubIRI) - if err != nil { - return err - } - // Create a new delete. // todo: tc.AccountToASDelete delete := streams.NewActivityStreamsDelete() @@ -121,7 +115,7 @@ func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account) // Address the delete CC public. deleteCC := streams.NewActivityStreamsCcProperty() - deleteCC.AppendIRI(publicIRI) + deleteCC.AppendIRI(ap.PublicURI()) delete.SetActivityStreamsCc(deleteCC) // Send the Delete via the Actor's outbox. @@ -877,14 +871,14 @@ func (f *federate) UpdateAccount(ctx context.Context, account *gtsmodel.Account) return err } - // Convert account to ActivityStreams Person. - person, err := f.converter.AccountToAS(ctx, account) + // Convert account to Accountable. + accountable, err := f.converter.AccountToAS(ctx, account) if err != nil { return gtserror.Newf("error converting account to Person: %w", err) } - // Use ActivityStreams Person as Object of Update. - update, err := f.converter.WrapPersonInUpdate(person, account) + // Use Accountable as Object of Update. + update, err := f.converter.WrapAccountableInUpdate(accountable) if err != nil { return gtserror.Newf("error wrapping Person in Update: %w", err) } @@ -1089,11 +1083,6 @@ func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) e return err } - publicIRI, err := parseURI(pub.PublicActivityPubIRI) - if err != nil { - return err - } - // Create a new move. move := streams.NewActivityStreamsMove() @@ -1115,7 +1104,7 @@ func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) e ap.AppendTo(move, followersIRI) // Address the move CC public. - ap.AppendCc(move, publicIRI) + ap.AppendCc(move, ap.PublicURI()) // Send the Move via the Actor's outbox. if _, err := f.FederatingActor().Send( diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index de1badb5c..ce5187bde 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -36,12 +36,24 @@ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" + "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/util/xslices" ) -// AccountToAS converts a gts model account into an activity streams person, suitable for federation -func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) { - person := streams.NewActivityStreamsPerson() +// AccountToAS converts a gts model account +// into an activity streams person or service. +func (c *Converter) AccountToAS( + ctx context.Context, + a *gtsmodel.Account, +) (ap.Accountable, error) { + // accountable is a service if this + // is a bot account, otherwise a person. + var accountable ap.Accountable + if util.PtrOrZero(a.Bot) { + accountable = streams.NewActivityStreamsService() + } else { + accountable = streams.NewActivityStreamsPerson() + } // id should be the activitypub URI of this user // something like https://example.org/users/example_user @@ -51,13 +63,13 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab } idProp := streams.NewJSONLDIdProperty() idProp.SetIRI(profileIDURI) - person.SetJSONLDId(idProp) + accountable.SetJSONLDId(idProp) // published // The moment when the account was created. publishedProp := streams.NewActivityStreamsPublishedProperty() publishedProp.Set(a.CreatedAt) - person.SetActivityStreamsPublished(publishedProp) + accountable.SetActivityStreamsPublished(publishedProp) // following // The URI for retrieving a list of accounts this user is following @@ -67,7 +79,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab } followingProp := streams.NewActivityStreamsFollowingProperty() followingProp.SetIRI(followingURI) - person.SetActivityStreamsFollowing(followingProp) + accountable.SetActivityStreamsFollowing(followingProp) // followers // The URI for retrieving a list of this user's followers @@ -77,7 +89,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab } followersProp := streams.NewActivityStreamsFollowersProperty() followersProp.SetIRI(followersURI) - person.SetActivityStreamsFollowers(followersProp) + accountable.SetActivityStreamsFollowers(followersProp) // inbox // the activitypub inbox of this user for accepting messages @@ -87,7 +99,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab } inboxProp := streams.NewActivityStreamsInboxProperty() inboxProp.SetIRI(inboxURI) - person.SetActivityStreamsInbox(inboxProp) + accountable.SetActivityStreamsInbox(inboxProp) // shared inbox -- only add this if we know for sure it has one if a.SharedInboxURI != nil && *a.SharedInboxURI != "" { @@ -101,7 +113,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab sharedInboxProp.SetIRI(sharedInboxURI) endpoints.SetActivityStreamsSharedInbox(sharedInboxProp) endpointsProp.AppendActivityStreamsEndpoints(endpoints) - person.SetActivityStreamsEndpoints(endpointsProp) + accountable.SetActivityStreamsEndpoints(endpointsProp) } // outbox @@ -112,7 +124,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab } outboxProp := streams.NewActivityStreamsOutboxProperty() outboxProp.SetIRI(outboxURI) - person.SetActivityStreamsOutbox(outboxProp) + accountable.SetActivityStreamsOutbox(outboxProp) // featured posts // Pinned posts. @@ -122,7 +134,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab } featuredProp := streams.NewTootFeaturedProperty() featuredProp.SetIRI(featuredURI) - person.SetTootFeatured(featuredProp) + accountable.SetTootFeatured(featuredProp) // featuredTags // NOT IMPLEMENTED @@ -131,7 +143,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab // Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI. preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty() preferredUsernameProp.SetXMLSchemaString(a.Username) - person.SetActivityStreamsPreferredUsername(preferredUsernameProp) + accountable.SetActivityStreamsPreferredUsername(preferredUsernameProp) // name // Used as profile display name. @@ -141,14 +153,14 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab } else { nameProp.AppendXMLSchemaString(a.Username) } - person.SetActivityStreamsName(nameProp) + accountable.SetActivityStreamsName(nameProp) // summary // Used as profile bio. if a.Note != "" { summaryProp := streams.NewActivityStreamsSummaryProperty() summaryProp.AppendXMLSchemaString(a.Note) - person.SetActivityStreamsSummary(summaryProp) + accountable.SetActivityStreamsSummary(summaryProp) } // url @@ -159,19 +171,19 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab } urlProp := streams.NewActivityStreamsUrlProperty() urlProp.AppendIRI(profileURL) - person.SetActivityStreamsUrl(urlProp) + accountable.SetActivityStreamsUrl(urlProp) // manuallyApprovesFollowers // Will be shown as a locked account. manuallyApprovesFollowersProp := streams.NewActivityStreamsManuallyApprovesFollowersProperty() manuallyApprovesFollowersProp.Set(*a.Locked) - person.SetActivityStreamsManuallyApprovesFollowers(manuallyApprovesFollowersProp) + accountable.SetActivityStreamsManuallyApprovesFollowers(manuallyApprovesFollowersProp) // discoverable // Will be shown in the profile directory. discoverableProp := streams.NewTootDiscoverableProperty() discoverableProp.Set(*a.Discoverable) - person.SetTootDiscoverable(discoverableProp) + accountable.SetTootDiscoverable(discoverableProp) // devices // NOT IMPLEMENTED, probably won't implement @@ -189,7 +201,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab alsoKnownAsURIs[i] = uri } - ap.SetAlsoKnownAs(person, alsoKnownAsURIs) + ap.SetAlsoKnownAs(accountable, alsoKnownAsURIs) } // movedTo @@ -200,7 +212,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab return nil, err } - ap.SetMovedTo(person, movedTo) + ap.SetMovedTo(accountable, movedTo) } // publicKey @@ -241,7 +253,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey) // set the public key property on the Person - person.SetW3IDSecurityV1PublicKey(publicKeyProp) + accountable.SetW3IDSecurityV1PublicKey(publicKeyProp) // tags tagProp := streams.NewActivityStreamsTagProperty() @@ -269,7 +281,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab // tag -- hashtags // TODO - person.SetActivityStreamsTag(tagProp) + accountable.SetActivityStreamsTag(tagProp) // attachment // Used for profile fields. @@ -290,7 +302,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab attachmentProp.AppendSchemaPropertyValue(propertyValue) } - person.SetActivityStreamsAttachment(attachmentProp) + accountable.SetActivityStreamsAttachment(attachmentProp) } // endpoints @@ -326,7 +338,7 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab iconImage.SetActivityStreamsUrl(avatarURLProperty) iconProperty.AppendActivityStreamsImage(iconImage) - person.SetActivityStreamsIcon(iconProperty) + accountable.SetActivityStreamsIcon(iconProperty) } } @@ -360,20 +372,32 @@ func (c *Converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab headerImage.SetActivityStreamsUrl(headerURLProperty) headerProperty.AppendActivityStreamsImage(headerImage) - person.SetActivityStreamsImage(headerProperty) + accountable.SetActivityStreamsImage(headerProperty) } } - return person, nil + return accountable, nil } -// AccountToASMinimal converts a gts model account into an activity streams person, suitable for federation. +// AccountToASMinimal converts a gts model account +// into an activity streams person or service. // -// The returned account will just have the Type, Username, PublicKey, and ID properties set. This is -// suitable for serving to requesters to whom we want to give as little information as possible because -// we don't trust them (yet). -func (c *Converter) AccountToASMinimal(ctx context.Context, a *gtsmodel.Account) (vocab.ActivityStreamsPerson, error) { - person := streams.NewActivityStreamsPerson() +// The returned account will just have the Type, Username, +// PublicKey, and ID properties set. This is suitable for +// serving to requesters to whom we want to give as little +// information as possible because we don't trust them (yet). +func (c *Converter) AccountToASMinimal( + ctx context.Context, + a *gtsmodel.Account, +) (ap.Accountable, error) { + // accountable is a service if this + // is a bot account, otherwise a person. + var accountable ap.Accountable + if util.PtrOrZero(a.Bot) { + accountable = streams.NewActivityStreamsService() + } else { + accountable = streams.NewActivityStreamsPerson() + } // id should be the activitypub URI of this user // something like https://example.org/users/example_user @@ -383,13 +407,13 @@ func (c *Converter) AccountToASMinimal(ctx context.Context, a *gtsmodel.Account) } idProp := streams.NewJSONLDIdProperty() idProp.SetIRI(profileIDURI) - person.SetJSONLDId(idProp) + accountable.SetJSONLDId(idProp) // preferredUsername // Used for Webfinger lookup. Must be unique on the domain, and must correspond to a Webfinger acct: URI. preferredUsernameProp := streams.NewActivityStreamsPreferredUsernameProperty() preferredUsernameProp.SetXMLSchemaString(a.Username) - person.SetActivityStreamsPreferredUsername(preferredUsernameProp) + accountable.SetActivityStreamsPreferredUsername(preferredUsernameProp) // publicKey // Required for signatures. @@ -429,9 +453,9 @@ func (c *Converter) AccountToASMinimal(ctx context.Context, a *gtsmodel.Account) publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKey) // set the public key property on the Person - person.SetW3IDSecurityV1PublicKey(publicKeyProp) + accountable.SetW3IDSecurityV1PublicKey(publicKeyProp) - return person, nil + return accountable, nil } // StatusToAS converts a gts model status into an ActivityStreams Statusable implementation, suitable for federation diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go index 344a42798..4d0d95641 100644 --- a/internal/typeutils/internaltoas_test.go +++ b/internal/typeutils/internaltoas_test.go @@ -27,6 +27,7 @@ "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -38,10 +39,10 @@ func (suite *InternalToASTestSuite) TestAccountToAS() { testAccount := >smodel.Account{} *testAccount = *suite.testAccounts["local_account_1"] // take zork for this test - asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) + accountable, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) suite.NoError(err) - ser, err := ap.Serialize(asPerson) + ser, err := ap.Serialize(accountable) suite.NoError(err) bytes, err := json.MarshalIndent(ser, "", " ") @@ -94,14 +95,80 @@ func (suite *InternalToASTestSuite) TestAccountToAS() { }`, string(bytes)) } +func (suite *InternalToASTestSuite) TestAccountToASBot() { + testAccount := >smodel.Account{} + *testAccount = *suite.testAccounts["local_account_1"] // take zork for this test + + // Update zork to be a bot. + testAccount.Bot = util.Ptr(true) + if err := suite.state.DB.UpdateAccount(context.Background(), testAccount); err != nil { + suite.FailNow(err.Error()) + } + + accountable, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) + suite.NoError(err) + + ser, err := ap.Serialize(accountable) + suite.NoError(err) + + bytes, err := json.MarshalIndent(ser, "", " ") + suite.NoError(err) + + suite.Equal(`{ + "@context": [ + "https://w3id.org/security/v1", + "https://www.w3.org/ns/activitystreams", + { + "discoverable": "toot:discoverable", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "toot": "http://joinmastodon.org/ns#" + } + ], + "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, + "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" + }, + "published": "2022-05-20T11:09:18Z", + "summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e", + "tag": [], + "type": "Service", + "url": "http://localhost:8080/@the_mighty_zork" +}`, string(bytes)) +} + func (suite *InternalToASTestSuite) TestAccountToASWithFields() { testAccount := >smodel.Account{} *testAccount = *suite.testAccounts["local_account_2"] - asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) + accountable, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) suite.NoError(err) - ser, err := ap.Serialize(asPerson) + ser, err := ap.Serialize(accountable) suite.NoError(err) bytes, err := json.MarshalIndent(ser, "", " ") @@ -176,10 +243,10 @@ func (suite *InternalToASTestSuite) TestAccountToASAliasedAndMoved() { suite.FailNow(err.Error()) } - asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) + accountable, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) suite.NoError(err) - ser, err := ap.Serialize(asPerson) + ser, err := ap.Serialize(accountable) suite.NoError(err) bytes, err := json.MarshalIndent(ser, "", " ") @@ -246,10 +313,10 @@ func (suite *InternalToASTestSuite) TestAccountToASWithOneField() { *testAccount = *suite.testAccounts["local_account_2"] testAccount.Fields = testAccount.Fields[0:1] // Take only one field. - asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) + accountable, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) suite.NoError(err) - ser, err := ap.Serialize(asPerson) + ser, err := ap.Serialize(accountable) suite.NoError(err) bytes, err := json.MarshalIndent(ser, "", " ") @@ -308,10 +375,10 @@ func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() { *testAccount = *suite.testAccounts["local_account_1"] // take zork for this test testAccount.Emojis = []*gtsmodel.Emoji{suite.testEmojis["rainbow"]} - asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) + accountable, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) suite.NoError(err) - ser, err := ap.Serialize(asPerson) + ser, err := ap.Serialize(accountable) suite.NoError(err) bytes, err := json.MarshalIndent(ser, "", " ") @@ -381,10 +448,10 @@ func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() { sharedInbox := "http://localhost:8080/sharedInbox" testAccount.SharedInboxURI = &sharedInbox - asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) + accountable, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) suite.NoError(err) - ser, err := ap.Serialize(asPerson) + ser, err := ap.Serialize(accountable) suite.NoError(err) bytes, err := json.MarshalIndent(ser, "", " ") diff --git a/internal/typeutils/wrap.go b/internal/typeutils/wrap.go index 89bcdfc09..1230981d4 100644 --- a/internal/typeutils/wrap.go +++ b/internal/typeutils/wrap.go @@ -18,68 +18,45 @@ package typeutils import ( - "net/url" - - "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/uris" ) -// WrapPersonInUpdate ... -func (c *Converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, originAccount *gtsmodel.Account) (vocab.ActivityStreamsUpdate, error) { +// WrapAccountableInUpdate wraps the given accountable +// in an Update activity with the accountable as the object. +// +// The Update will be addressed to Public and bcc followers. +func (c *Converter) WrapAccountableInUpdate(accountable ap.Accountable) (vocab.ActivityStreamsUpdate, error) { update := streams.NewActivityStreamsUpdate() - // set the actor - actorURI, err := url.Parse(originAccount.URI) - if err != nil { - return nil, gtserror.Newf("error parsing url %s: %w", originAccount.URI, err) - } - actorProp := streams.NewActivityStreamsActorProperty() - actorProp.AppendIRI(actorURI) - update.SetActivityStreamsActor(actorProp) + // Set actor IRI to this accountable's IRI. + ap.AppendActorIRIs(update, ap.GetJSONLDId(accountable)) - // set the ID - newID, err := id.NewRandomULID() - if err != nil { - return nil, err - } + // Set the update ID + updateURI := uris.GenerateURIForUpdate(ap.ExtractPreferredUsername(accountable), id.NewULID()) + ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(update), updateURI) - idString := uris.GenerateURIForUpdate(originAccount.Username, newID) - idURI, err := url.Parse(idString) - if err != nil { - return nil, gtserror.Newf("error parsing url %s: %w", idString, err) - } - idProp := streams.NewJSONLDIdProperty() - idProp.SetIRI(idURI) - update.SetJSONLDId(idProp) - - // set the person as the object here + // Set the accountable as the object of the update. objectProp := streams.NewActivityStreamsObjectProperty() - objectProp.AppendActivityStreamsPerson(person) + switch t := accountable.(type) { + case vocab.ActivityStreamsPerson: + objectProp.AppendActivityStreamsPerson(t) + case vocab.ActivityStreamsService: + objectProp.AppendActivityStreamsService(t) + default: + log.Panicf(nil, "%T was neither person nor service", t) + } update.SetActivityStreamsObject(objectProp) - // to should be public - toURI, err := url.Parse(pub.PublicActivityPubIRI) - if err != nil { - return nil, gtserror.Newf("error parsing url %s: %w", pub.PublicActivityPubIRI, err) - } - toProp := streams.NewActivityStreamsToProperty() - toProp.AppendIRI(toURI) - update.SetActivityStreamsTo(toProp) + // to should be public. + ap.AppendTo(update, ap.PublicURI()) - // bcc followers - followersURI, err := url.Parse(originAccount.FollowersURI) - if err != nil { - return nil, gtserror.Newf("error parsing url %s: %w", originAccount.FollowersURI, err) - } - bccProp := streams.NewActivityStreamsBccProperty() - bccProp.AppendIRI(followersURI) - update.SetActivityStreamsBcc(bccProp) + // bcc should be followers. + ap.AppendBcc(update, ap.GetFollowers(accountable)) return update, nil } diff --git a/internal/typeutils/wrap_test.go b/internal/typeutils/wrap_test.go index 1085c8c66..8c8af7506 100644 --- a/internal/typeutils/wrap_test.go +++ b/internal/typeutils/wrap_test.go @@ -139,6 +139,86 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() { }`, string(bytes)) } +func (suite *WrapTestSuite) TestWrapAccountableInUpdate() { + testAccount := suite.testAccounts["local_account_1"] + + accountable, err := suite.typeconverter.AccountToAS(context.Background(), testAccount) + if err != nil { + suite.FailNow(err.Error()) + } + + create, err := suite.typeconverter.WrapAccountableInUpdate(accountable) + if err != nil { + suite.FailNow(err.Error()) + } + + createI, err := ap.Serialize(create) + if err != nil { + suite.FailNow(err.Error()) + } + + // Get the ID as it's not determinate. + createID := ap.GetJSONLDId(create) + + bytes, err := json.MarshalIndent(createI, "", " ") + if err != nil { + suite.FailNow(err.Error()) + } + + suite.Equal(`{ + "@context": [ + "https://w3id.org/security/v1", + "https://www.w3.org/ns/activitystreams", + { + "discoverable": "toot:discoverable", + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "toot": "http://joinmastodon.org/ns#" + } + ], + "actor": "http://localhost:8080/users/the_mighty_zork", + "bcc": "http://localhost:8080/users/the_mighty_zork/followers", + "id": "`+createID.String()+`", + "object": { + "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, + "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" + }, + "published": "2022-05-20T11:09:18Z", + "summary": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e", + "tag": [], + "type": "Person", + "url": "http://localhost:8080/@the_mighty_zork" + }, + "to": "https://www.w3.org/ns/activitystreams#Public", + "type": "Update" +}`, string(bytes)) +} + func TestWrapTestSuite(t *testing.T) { suite.Run(t, new(WrapTestSuite)) } diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 81c3a85c5..da4202eed 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -2853,7 +2853,7 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit "this is a public status, please forward it!", "", URLMustParse("http://example.org/users/Some_User"), - []*url.URL{URLMustParse(pub.PublicActivityPubIRI)}, + []*url.URL{ap.PublicURI()}, nil, false, []vocab.ActivityStreamsMention{}, @@ -3207,7 +3207,7 @@ func NewTestFediStatuses() map[string]vocab.ActivityStreamsNote { "this is a public status, please forward it!", "", URLMustParse("http://example.org/users/Some_User"), - []*url.URL{URLMustParse(pub.PublicActivityPubIRI)}, + []*url.URL{ap.PublicURI()}, nil, false, []vocab.ActivityStreamsMention{}, @@ -3228,7 +3228,7 @@ func NewTestFediStatuses() map[string]vocab.ActivityStreamsNote { "", URLMustParse("https://unknown-instance.com/users/brand_new_person"), []*url.URL{ - URLMustParse(pub.PublicActivityPubIRI), + ap.PublicURI(), }, []*url.URL{}, false, @@ -3244,7 +3244,7 @@ func NewTestFediStatuses() map[string]vocab.ActivityStreamsNote { "", URLMustParse("https://unknown-instance.com/users/brand_new_person"), []*url.URL{ - URLMustParse(pub.PublicActivityPubIRI), + ap.PublicURI(), }, []*url.URL{}, false, @@ -3265,7 +3265,7 @@ func NewTestFediStatuses() map[string]vocab.ActivityStreamsNote { "", URLMustParse("https://unknown-instance.com/users/brand_new_person"), []*url.URL{ - URLMustParse(pub.PublicActivityPubIRI), + ap.PublicURI(), }, []*url.URL{}, false, @@ -3286,7 +3286,7 @@ func NewTestFediStatuses() map[string]vocab.ActivityStreamsNote { "", URLMustParse("https://turnip.farm/users/turniplover6969"), []*url.URL{ - URLMustParse(pub.PublicActivityPubIRI), + ap.PublicURI(), }, []*url.URL{}, false, @@ -3309,7 +3309,7 @@ func NewTestFediStatuses() map[string]vocab.ActivityStreamsNote { "", URLMustParse("http://fossbros-anonymous.io/users/foss_satan"), []*url.URL{ - URLMustParse(pub.PublicActivityPubIRI), + ap.PublicURI(), }, []*url.URL{}, false, diff --git a/testrig/transportcontroller.go b/testrig/transportcontroller.go index 3bc8752e0..b886e5c40 100644 --- a/testrig/transportcontroller.go +++ b/testrig/transportcontroller.go @@ -29,6 +29,7 @@ "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -81,7 +82,7 @@ type MockHTTPClient struct { // to customize how the client is mocked. // // Note that you should never ever make ACTUAL http calls with this thing. -func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relativeMediaPath string, extraPeople ...vocab.ActivityStreamsPerson) *MockHTTPClient { +func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error), relativeMediaPath string, extraPeople ...ap.Accountable) *MockHTTPClient { mockHTTPClient := &MockHTTPClient{} if do != nil {