diff options
author | 2021-10-24 11:57:39 +0200 | |
---|---|---|
committer | 2021-10-24 11:57:39 +0200 | |
commit | 4b1d9d3780134098ff06877abc20c970c32d4aac (patch) | |
tree | a46deccd4cdf2ddf9d0ea92f32bd8669657a4687 /internal/processing | |
parent | pregenerate RSA keys for testrig accounts. If a user is added without a key,... (diff) | |
download | gotosocial-4b1d9d3780134098ff06877abc20c970c32d4aac.tar.xz |
Serve `outbox` for Actor (#289)
* add statusesvisible convenience function
* add minID + onlyPublic to account statuses get
* move swagger collection stuff to common
* start working on Outbox GETting
* move functions into federationProcessor
* outboxToASCollection
* add statusesvisible convenience function
* add minID + onlyPublic to account statuses get
* move swagger collection stuff to common
* start working on Outbox GETting
* move functions into federationProcessor
* outboxToASCollection
* bit more work on outbox paging
* wrapNoteInCreate function
* test + hook up the processor functions
* don't do prev + next links on empty reply
* test get outbox through api
* don't fail on no status entries
* add outbox implementation doc
* typo
Diffstat (limited to 'internal/processing')
-rw-r--r-- | internal/processing/account.go | 4 | ||||
-rw-r--r-- | internal/processing/account/account.go | 2 | ||||
-rw-r--r-- | internal/processing/account/delete.go | 2 | ||||
-rw-r--r-- | internal/processing/account/getstatuses.go | 4 | ||||
-rw-r--r-- | internal/processing/federation.go | 393 | ||||
-rw-r--r-- | internal/processing/federation/federation.go | 103 | ||||
-rw-r--r-- | internal/processing/federation/getfollowers.go | 74 | ||||
-rw-r--r-- | internal/processing/federation/getfollowing.go | 74 | ||||
-rw-r--r-- | internal/processing/federation/getnodeinfo.go | 69 | ||||
-rw-r--r-- | internal/processing/federation/getoutbox.go | 107 | ||||
-rw-r--r-- | internal/processing/federation/getstatus.go | 91 | ||||
-rw-r--r-- | internal/processing/federation/getstatusreplies.go | 164 | ||||
-rw-r--r-- | internal/processing/federation/getuser.go | 85 | ||||
-rw-r--r-- | internal/processing/federation/getwebfinger.go | 64 | ||||
-rw-r--r-- | internal/processing/federation/postinbox.go | 32 | ||||
-rw-r--r-- | internal/processing/fromclientapi.go | 7 | ||||
-rw-r--r-- | internal/processing/fromfederator_test.go | 2 | ||||
-rw-r--r-- | internal/processing/processor.go | 40 |
18 files changed, 907 insertions, 410 deletions
diff --git a/internal/processing/account.go b/internal/processing/account.go index 94ba596ac..f1c119083 100644 --- a/internal/processing/account.go +++ b/internal/processing/account.go @@ -38,8 +38,8 @@ func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form return p.accountProcessor.Update(ctx, authed.Account, form) } -func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) { - return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) +func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) { + return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, maxID, minID, pinnedOnly, mediaOnly, publicOnly) } func (p *processor) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index e88cd3a94..4e807540c 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -50,7 +50,7 @@ type Processor interface { Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) // StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for // the account given in authed. - StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) + StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) // FollowersGet fetches a list of the target account's followers. FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) // FollowingGet fetches a list of the accounts that target account is following. diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index 4be0c859e..20198e8ef 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -139,7 +139,7 @@ func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origi var maxID string selectStatusesLoop: for { - statuses, err := p.db.GetAccountStatuses(ctx, account.ID, 20, false, maxID, false, false) + statuses, err := p.db.GetAccountStatuses(ctx, account.ID, 20, false, maxID, "", false, false, false) if err != nil { if err == db.ErrNoEntries { // no statuses left for this instance so we're done diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go index 56b5b0eae..bd327cae6 100644 --- a/internal/processing/account/getstatuses.go +++ b/internal/processing/account/getstatuses.go @@ -28,7 +28,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinnedOnly bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) { +func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) { if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { return nil, gtserror.NewErrorInternalError(err) } else if blocked { @@ -37,7 +37,7 @@ func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel apiStatuses := []apimodel.Status{} - statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) + statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, maxID, minID, pinnedOnly, mediaOnly, publicOnly) if err != nil { if err == db.ErrNoEntries { return apiStatuses, nil diff --git a/internal/processing/federation.go b/internal/processing/federation.go index 1336a6e46..023930494 100644 --- a/internal/processing/federation.go +++ b/internal/processing/federation.go @@ -20,418 +20,49 @@ package processing import ( "context" - "errors" - "fmt" "net/http" "net/url" - "github.com/go-fed/activity/streams" - "github.com/go-fed/activity/streams/vocab" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" ) func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - var requestedPerson vocab.ActivityStreamsPerson - if util.IsPublicKeyPath(requestURL) { - // if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key - requestedPerson, err = p.tc.AccountToASMinimal(ctx, requestedAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - } else if util.IsUserPath(requestURL) { - // if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile - requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if err != nil || !authenticated { - return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") - } - - // if we're not already handshaking/dereferencing a remote account, dereference it now - if !p.federator.Handshaking(ctx, requestedUsername, requestingAccountURI) { - requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) - if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) - } - - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - } - - requestedPerson, err = p.tc.AccountToAS(ctx, requestedAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - } else { - return nil, gtserror.NewErrorBadRequest(fmt.Errorf("path was not public key path or user path")) - } - - data, err := streams.Serialize(requestedPerson) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil + return p.federationProcessor.GetUser(ctx, requestedUsername, requestURL) } func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // authenticate the request - requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if err != nil || !authenticated { - return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") - } - - requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) - if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) - } - - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - - requestedAccountURI, err := url.Parse(requestedAccount.URI) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) - } - - requestedFollowers, err := p.federator.FederatingDB().Followers(ctx, requestedAccountURI) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err)) - } - - data, err := streams.Serialize(requestedFollowers) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil + return p.federationProcessor.GetFollowers(ctx, requestedUsername, requestURL) } func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // authenticate the request - requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if err != nil || !authenticated { - return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") - } - - requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) - if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) - } - - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - - requestedAccountURI, err := url.Parse(requestedAccount.URI) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) - } - - requestedFollowing, err := p.federator.FederatingDB().Following(ctx, requestedAccountURI) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err)) - } - - data, err := streams.Serialize(requestedFollowing) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil + return p.federationProcessor.GetFollowing(ctx, requestedUsername, requestURL) } func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // authenticate the request - requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if err != nil || !authenticated { - return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") - } - - requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) - if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) - } - - // authorize the request: - // 1. check if a block exists between the requester and the requestee - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - - // get the status out of the database here - s := >smodel.Status{} - if err := p.db.GetWhere(ctx, []db.Where{ - {Key: "id", Value: requestedStatusID}, - {Key: "account_id", Value: requestedAccount.ID}, - }, s); err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) - } - - visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if !visible { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID)) - } - - // requester is authorized to view the status, so convert it to AP representation and serialize it - asStatus, err := p.tc.StatusToAS(ctx, s) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - data, err := streams.Serialize(asStatus) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil + return p.federationProcessor.GetStatus(ctx, requestedUsername, requestedStatusID, requestURL) } func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // authenticate the request - requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if err != nil || !authenticated { - return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") - } - - requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) - if err != nil { - return nil, gtserror.NewErrorNotAuthorized(err) - } - - // authorize the request: - // 1. check if a block exists between the requester and the requestee - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - - // get the status out of the database here - s := >smodel.Status{} - if err := p.db.GetWhere(ctx, []db.Where{ - {Key: "id", Value: requestedStatusID}, - {Key: "account_id", Value: requestedAccount.ID}, - }, s); err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) - } - - visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if !visible { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID)) - } - - var data map[string]interface{} - - // now there are three scenarios: - // 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page. - // 2. we're asked for a page but only_other_accounts has not been set in the query -- so we should just return the first page of the collection, with no items. - // 3. we're asked for a page, and only_other_accounts has been set, and min_id has optionally been set -- so we need to return some actual items! - - if !page { - // scenario 1 - - // get the collection - collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - data, err = streams.Serialize(collection) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - } else if page && requestURL.Query().Get("only_other_accounts") == "" { - // scenario 2 - - // get the collection - collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - // but only return the first page - data, err = streams.Serialize(collection.GetActivityStreamsFirst().GetActivityStreamsCollectionPage()) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - } else { - // scenario 3 - // get immediate children - replies, err := p.db.GetStatusChildren(ctx, s, true, minID) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - // filter children and extract URIs - replyURIs := map[string]*url.URL{} - for _, r := range replies { - // only show public or unlocked statuses as replies - if r.Visibility != gtsmodel.VisibilityPublic && r.Visibility != gtsmodel.VisibilityUnlocked { - continue - } - - // respect onlyOtherAccounts parameter - if onlyOtherAccounts && r.AccountID == requestedAccount.ID { - continue - } - - // only show replies that the status owner can see - visibleToStatusOwner, err := p.filter.StatusVisible(ctx, r, requestedAccount) - if err != nil || !visibleToStatusOwner { - continue - } - - // only show replies that the requester can see - visibleToRequester, err := p.filter.StatusVisible(ctx, r, requestingAccount) - if err != nil || !visibleToRequester { - continue - } - - rURI, err := url.Parse(r.URI) - if err != nil { - continue - } - - replyURIs[r.ID] = rURI - } - - repliesPage, err := p.tc.StatusURIsToASRepliesPage(ctx, s, onlyOtherAccounts, minID, replyURIs) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - data, err = streams.Serialize(repliesPage) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - } + return p.federationProcessor.GetStatusReplies(ctx, requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, requestURL) +} - return data, nil +func (p *processor) GetFediOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + return p.federationProcessor.GetOutbox(ctx, requestedUsername, page, maxID, minID, requestURL) } func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // return the webfinger representation - return &apimodel.WellKnownResponse{ - Subject: fmt.Sprintf("acct:%s@%s", requestedAccount.Username, p.config.AccountDomain), - Aliases: []string{ - requestedAccount.URI, - requestedAccount.URL, - }, - Links: []apimodel.Link{ - { - Rel: "http://webfinger.net/rel/profile-page", - Type: "text/html", - Href: requestedAccount.URL, - }, - { - Rel: "self", - Type: "application/activity+json", - Href: requestedAccount.URI, - }, - }, - }, nil + return p.federationProcessor.GetWebfingerAccount(ctx, requestedUsername) } func (p *processor) GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) { - return &apimodel.WellKnownResponse{ - Links: []apimodel.Link{ - { - Rel: "http://nodeinfo.diaspora.software/ns/schema/2.0", - Href: fmt.Sprintf("%s://%s/nodeinfo/2.0", p.config.Protocol, p.config.Host), - }, - }, - }, nil + return p.federationProcessor.GetNodeInfoRel(ctx, request) } func (p *processor) GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) { - return &apimodel.Nodeinfo{ - Version: "2.0", - Software: apimodel.NodeInfoSoftware{ - Name: "gotosocial", - Version: p.config.SoftwareVersion, - }, - Protocols: []string{"activitypub"}, - Services: apimodel.NodeInfoServices{ - Inbound: []string{}, - Outbound: []string{}, - }, - OpenRegistrations: p.config.AccountsConfig.OpenRegistration, - Usage: apimodel.NodeInfoUsage{ - Users: apimodel.NodeInfoUsers{}, - }, - Metadata: make(map[string]interface{}), - }, nil + return p.federationProcessor.GetNodeInfo(ctx, request) } func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { - contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator) - return p.federator.FederatingActor().PostInbox(contextWithChannel, w, r) + return p.federationProcessor.PostInbox(ctx, w, r) } diff --git a/internal/processing/federation/federation.go b/internal/processing/federation/federation.go new file mode 100644 index 000000000..b050406c5 --- /dev/null +++ b/internal/processing/federation/federation.go @@ -0,0 +1,103 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "net/http" + "net/url" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/messages" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/internal/visibility" +) + +// Processor wraps functions for processing federation API requests. +type Processor interface { + // GetUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication + // before returning a JSON serializable interface to the caller. + GetUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) + + // GetFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate + // authentication before returning a JSON serializable interface to the caller. + GetFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) + + // GetFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate + // authentication before returning a JSON serializable interface to the caller. + GetFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) + + // GetStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate + // authentication before returning a JSON serializable interface to the caller. + GetStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) + + // GetStatus handles the getting of a fedi/activitypub representation of replies to a status, performing appropriate + // authentication before returning a JSON serializable interface to the caller. + GetStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) + + // GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. + GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) + + // GetNodeInfoRel returns a well known response giving the path to node info. + GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) + + // GetNodeInfo returns a node info struct in response to a node info request. + GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) + + // GetOutbox returns the activitypub representation of a local user's outbox. + // This contains links to PUBLIC posts made by this user. + GetOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) + + // PostInbox handles POST requests to a user's inbox for new activitypub messages. + // + // PostInbox returns true if the request was handled as an ActivityPub POST to an actor's inbox. + // If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page. + // + // If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written. + // + // If the Actor was constructed with the Federated Protocol enabled, side effects will occur. + // + // If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur. + PostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) +} + +type processor struct { + db db.DB + config *config.Config + federator federation.Federator + tc typeutils.TypeConverter + filter visibility.Filter + fromFederator chan messages.FromFederator +} + +// New returns a new federation processor. +func New(db db.DB, tc typeutils.TypeConverter, config *config.Config, federator federation.Federator, fromFederator chan messages.FromFederator) Processor { + return &processor{ + db: db, + config: config, + federator: federator, + tc: tc, + filter: visibility.NewFilter(db), + fromFederator: fromFederator, + } +} diff --git a/internal/processing/federation/getfollowers.go b/internal/processing/federation/getfollowers.go new file mode 100644 index 000000000..b17c90e07 --- /dev/null +++ b/internal/processing/federation/getfollowers.go @@ -0,0 +1,74 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "errors" + "fmt" + "net/url" + + "github.com/go-fed/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +func (p *processor) GetFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if err != nil || !authenticated { + return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") + } + + requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) + if err != nil { + return nil, gtserror.NewErrorNotAuthorized(err) + } + + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + if blocked { + return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + requestedAccountURI, err := url.Parse(requestedAccount.URI) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) + } + + requestedFollowers, err := p.federator.FederatingDB().Followers(ctx, requestedAccountURI) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err)) + } + + data, err := streams.Serialize(requestedFollowers) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} diff --git a/internal/processing/federation/getfollowing.go b/internal/processing/federation/getfollowing.go new file mode 100644 index 000000000..e2d50d238 --- /dev/null +++ b/internal/processing/federation/getfollowing.go @@ -0,0 +1,74 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "errors" + "fmt" + "net/url" + + "github.com/go-fed/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +func (p *processor) GetFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if err != nil || !authenticated { + return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") + } + + requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) + if err != nil { + return nil, gtserror.NewErrorNotAuthorized(err) + } + + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + if blocked { + return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + requestedAccountURI, err := url.Parse(requestedAccount.URI) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) + } + + requestedFollowing, err := p.federator.FederatingDB().Following(ctx, requestedAccountURI) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err)) + } + + data, err := streams.Serialize(requestedFollowing) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} diff --git a/internal/processing/federation/getnodeinfo.go b/internal/processing/federation/getnodeinfo.go new file mode 100644 index 000000000..4eb8ad3b7 --- /dev/null +++ b/internal/processing/federation/getnodeinfo.go @@ -0,0 +1,69 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "fmt" + "net/http" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +const ( + nodeInfoVersion = "2.0" + nodeInfoSoftwareName = "gotosocial" +) + +var ( + nodeInfoRel = fmt.Sprintf("http://nodeinfo.diaspora.software/ns/schema/%s", nodeInfoVersion) + nodeInfoProtocols = []string{"activitypub"} +) + +func (p *processor) GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) { + return &apimodel.WellKnownResponse{ + Links: []apimodel.Link{ + { + Rel: nodeInfoRel, + Href: fmt.Sprintf("%s://%s/nodeinfo/%s", p.config.Protocol, p.config.Host, nodeInfoVersion), + }, + }, + }, nil +} + +func (p *processor) GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) { + return &apimodel.Nodeinfo{ + Version: nodeInfoVersion, + Software: apimodel.NodeInfoSoftware{ + Name: nodeInfoSoftwareName, + Version: p.config.SoftwareVersion, + }, + Protocols: nodeInfoProtocols, + Services: apimodel.NodeInfoServices{ + Inbound: []string{}, + Outbound: []string{}, + }, + OpenRegistrations: p.config.AccountsConfig.OpenRegistration, + Usage: apimodel.NodeInfoUsage{ + Users: apimodel.NodeInfoUsers{}, + }, + Metadata: make(map[string]interface{}), + }, nil +} diff --git a/internal/processing/federation/getoutbox.go b/internal/processing/federation/getoutbox.go new file mode 100644 index 000000000..a3b2cff3c --- /dev/null +++ b/internal/processing/federation/getoutbox.go @@ -0,0 +1,107 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "errors" + "fmt" + "net/url" + + "github.com/go-fed/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if err != nil || !authenticated { + return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") + } + + requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) + if err != nil { + return nil, gtserror.NewErrorNotAuthorized(err) + } + + // authorize the request: + // 1. check if a block exists between the requester and the requestee + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + if blocked { + return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + var data map[string]interface{} + // now there are two scenarios: + // 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page. + // 2. we're asked for a specific page; this can be either the first page or any other page + + if !page { + /* + scenario 1: return the collection with no items + we want something that looks like this: + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.org/users/whatever/outbox", + "type": "OrderedCollection", + "first": "https://example.org/users/whatever/outbox?page=true", + "last": "https://example.org/users/whatever/outbox?min_id=0&page=true" + } + */ + collection, err := p.tc.OutboxToASCollection(ctx, requestedAccount.OutboxURI) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + data, err = streams.Serialize(collection) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil + } + + // scenario 2 -- get the requested page + // limit pages to 30 entries per page + publicStatuses, err := p.db.GetAccountStatuses(ctx, requestedAccount.ID, 30, true, maxID, minID, false, false, true) + if err != nil && err != db.ErrNoEntries { + return nil, gtserror.NewErrorInternalError(err) + } + + outboxPage, err := p.tc.StatusesToASOutboxPage(ctx, requestedAccount.OutboxURI, maxID, minID, publicStatuses) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + data, err = streams.Serialize(outboxPage) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} diff --git a/internal/processing/federation/getstatus.go b/internal/processing/federation/getstatus.go new file mode 100644 index 000000000..a4f251023 --- /dev/null +++ b/internal/processing/federation/getstatus.go @@ -0,0 +1,91 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "errors" + "fmt" + "net/url" + + "github.com/go-fed/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (p *processor) GetStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if err != nil || !authenticated { + return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") + } + + requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) + if err != nil { + return nil, gtserror.NewErrorNotAuthorized(err) + } + + // authorize the request: + // 1. check if a block exists between the requester and the requestee + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + if blocked { + return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + // get the status out of the database here + s := >smodel.Status{} + if err := p.db.GetWhere(ctx, []db.Where{ + {Key: "id", Value: requestedStatusID}, + {Key: "account_id", Value: requestedAccount.ID}, + }, s); err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) + } + + visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + if !visible { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID)) + } + + // requester is authorized to view the status, so convert it to AP representation and serialize it + asStatus, err := p.tc.StatusToAS(ctx, s) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + data, err := streams.Serialize(asStatus) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} diff --git a/internal/processing/federation/getstatusreplies.go b/internal/processing/federation/getstatusreplies.go new file mode 100644 index 000000000..0fa0cc386 --- /dev/null +++ b/internal/processing/federation/getstatusreplies.go @@ -0,0 +1,164 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "errors" + "fmt" + "net/url" + + "github.com/go-fed/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if err != nil || !authenticated { + return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") + } + + requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) + if err != nil { + return nil, gtserror.NewErrorNotAuthorized(err) + } + + // authorize the request: + // 1. check if a block exists between the requester and the requestee + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + if blocked { + return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + // get the status out of the database here + s := >smodel.Status{} + if err := p.db.GetWhere(ctx, []db.Where{ + {Key: "id", Value: requestedStatusID}, + {Key: "account_id", Value: requestedAccount.ID}, + }, s); err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) + } + + visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + if !visible { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID)) + } + + var data map[string]interface{} + + // now there are three scenarios: + // 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page. + // 2. we're asked for a page but only_other_accounts has not been set in the query -- so we should just return the first page of the collection, with no items. + // 3. we're asked for a page, and only_other_accounts has been set, and min_id has optionally been set -- so we need to return some actual items! + + if !page { + // scenario 1 + + // get the collection + collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + data, err = streams.Serialize(collection) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + } else if page && requestURL.Query().Get("only_other_accounts") == "" { + // scenario 2 + + // get the collection + collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + // but only return the first page + data, err = streams.Serialize(collection.GetActivityStreamsFirst().GetActivityStreamsCollectionPage()) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + } else { + // scenario 3 + // get immediate children + replies, err := p.db.GetStatusChildren(ctx, s, true, minID) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + // filter children and extract URIs + replyURIs := map[string]*url.URL{} + for _, r := range replies { + // only show public or unlocked statuses as replies + if r.Visibility != gtsmodel.VisibilityPublic && r.Visibility != gtsmodel.VisibilityUnlocked { + continue + } + + // respect onlyOtherAccounts parameter + if onlyOtherAccounts && r.AccountID == requestedAccount.ID { + continue + } + + // only show replies that the status owner can see + visibleToStatusOwner, err := p.filter.StatusVisible(ctx, r, requestedAccount) + if err != nil || !visibleToStatusOwner { + continue + } + + // only show replies that the requester can see + visibleToRequester, err := p.filter.StatusVisible(ctx, r, requestingAccount) + if err != nil || !visibleToRequester { + continue + } + + rURI, err := url.Parse(r.URI) + if err != nil { + continue + } + + replyURIs[r.ID] = rURI + } + + repliesPage, err := p.tc.StatusURIsToASRepliesPage(ctx, s, onlyOtherAccounts, minID, replyURIs) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + data, err = streams.Serialize(repliesPage) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + } + + return data, nil +} diff --git a/internal/processing/federation/getuser.go b/internal/processing/federation/getuser.go new file mode 100644 index 000000000..0d80e528e --- /dev/null +++ b/internal/processing/federation/getuser.go @@ -0,0 +1,85 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "errors" + "fmt" + "net/url" + + "github.com/go-fed/activity/streams" + "github.com/go-fed/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +func (p *processor) GetUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + var requestedPerson vocab.ActivityStreamsPerson + if util.IsPublicKeyPath(requestURL) { + // if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key + requestedPerson, err = p.tc.AccountToASMinimal(ctx, requestedAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + } else if util.IsUserPath(requestURL) { + // if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile + requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if err != nil || !authenticated { + return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized") + } + + // if we're not already handshaking/dereferencing a remote account, dereference it now + if !p.federator.Handshaking(ctx, requestedUsername, requestingAccountURI) { + requestingAccount, _, err := p.federator.GetRemoteAccount(ctx, requestedUsername, requestingAccountURI, false) + if err != nil { + return nil, gtserror.NewErrorNotAuthorized(err) + } + + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + if blocked { + return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + } + + requestedPerson, err = p.tc.AccountToAS(ctx, requestedAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + } else { + return nil, gtserror.NewErrorBadRequest(fmt.Errorf("path was not public key path or user path")) + } + + data, err := streams.Serialize(requestedPerson) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} diff --git a/internal/processing/federation/getwebfinger.go b/internal/processing/federation/getwebfinger.go new file mode 100644 index 000000000..aaa7687be --- /dev/null +++ b/internal/processing/federation/getwebfinger.go @@ -0,0 +1,64 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +const ( + webfingerProfilePage = "http://webfinger.net/rel/profile-page" + webFingerProfilePageContentType = "text/html" + webfingerSelf = "self" + webFingerSelfContentType = "application/activity+json" + webfingerAccount = "acct" +) + +func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetLocalAccountByUsername(ctx, requestedUsername) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // return the webfinger representation + return &apimodel.WellKnownResponse{ + Subject: fmt.Sprintf("%s:%s@%s", webfingerAccount, requestedAccount.Username, p.config.AccountDomain), + Aliases: []string{ + requestedAccount.URI, + requestedAccount.URL, + }, + Links: []apimodel.Link{ + { + Rel: webfingerProfilePage, + Type: webFingerProfilePageContentType, + Href: requestedAccount.URL, + }, + { + Rel: webfingerSelf, + Type: webFingerSelfContentType, + Href: requestedAccount.URI, + }, + }, + }, nil +} diff --git a/internal/processing/federation/postinbox.go b/internal/processing/federation/postinbox.go new file mode 100644 index 000000000..df9da0a51 --- /dev/null +++ b/internal/processing/federation/postinbox.go @@ -0,0 +1,32 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package federation + +import ( + "context" + "net/http" + + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +func (p *processor) PostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { + // pass the fromFederator channel through to postInbox, since it'll be needed later + contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator) + return p.federator.FederatingActor().PostInbox(contextWithChannel, w, r) +} diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index d4e8f5fa5..3054fbf57 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -269,12 +269,17 @@ func (p *processor) federateStatus(ctx context.Context, status *gtsmodel.Status) return fmt.Errorf("federateStatus: error converting status to as format: %s", err) } + create, err := p.tc.WrapNoteInCreate(asStatus, false) + if err != nil { + return fmt.Errorf("federateStatus: error wrapping status in create: %s", err) + } + outboxIRI, err := url.Parse(status.Account.OutboxURI) if err != nil { return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.Account.OutboxURI, err) } - _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, asStatus) + _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, create) return err } diff --git a/internal/processing/fromfederator_test.go b/internal/processing/fromfederator_test.go index 09519d1d3..6846357d1 100644 --- a/internal/processing/fromfederator_test.go +++ b/internal/processing/fromfederator_test.go @@ -339,7 +339,7 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() { suite.False(zorkFollowsSatan) // no statuses from foss satan should be left in the database - dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, "", false, false) + dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, "", "", false, false, false) suite.ErrorIs(err, db.ErrNoEntries) suite.Empty(dbStatuses) diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 8fd372735..d0a636b27 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -36,6 +36,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/processing/account" "github.com/superseriousbusiness/gotosocial/internal/processing/admin" + federationProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/federation" mediaProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/media" "github.com/superseriousbusiness/gotosocial/internal/processing/status" "github.com/superseriousbusiness/gotosocial/internal/processing/streaming" @@ -78,7 +79,7 @@ type Processor interface { AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for // the account given in authed. - AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) + AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) ([]apimodel.Status, gtserror.WithCode) // AccountFollowersGet fetches a list of the target account's followers. AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) // AccountFollowingGet fetches a list of the accounts that target account is following. @@ -190,32 +191,26 @@ type Processor interface { // GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication // before returning a JSON serializable interface to the caller. GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate // authentication before returning a JSON serializable interface to the caller. GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate // authentication before returning a JSON serializable interface to the caller. GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate // authentication before returning a JSON serializable interface to the caller. GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetFediStatus handles the getting of a fedi/activitypub representation of replies to a status, performing appropriate // authentication before returning a JSON serializable interface to the caller. GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) - + // GetFediOutbox returns the public outbox of the requested user, with the given parameters. + GetFediOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) // GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) - // GetNodeInfoRel returns a well known response giving the path to node info. GetNodeInfoRel(ctx context.Context, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) - // GetNodeInfo returns a node info struct in response to a node info request. GetNodeInfo(ctx context.Context, request *http.Request) (*apimodel.Nodeinfo, gtserror.WithCode) - // InboxPost handles POST requests to a user's inbox for new activitypub messages. // // InboxPost returns true if the request was handled as an ActivityPub POST to an actor's inbox. @@ -248,12 +243,13 @@ type processor struct { SUB-PROCESSORS */ - accountProcessor account.Processor - adminProcessor admin.Processor - statusProcessor status.Processor - streamingProcessor streaming.Processor - mediaProcessor mediaProcessor.Processor - userProcessor user.Processor + accountProcessor account.Processor + adminProcessor admin.Processor + statusProcessor status.Processor + streamingProcessor streaming.Processor + mediaProcessor mediaProcessor.Processor + userProcessor user.Processor + federationProcessor federationProcessor.Processor } // NewProcessor returns a new Processor that uses the given federator @@ -267,6 +263,7 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f adminProcessor := admin.New(db, tc, mediaHandler, fromClientAPI, config) mediaProcessor := mediaProcessor.New(db, tc, mediaHandler, storage, config) userProcessor := user.New(db, config) + federationProcessor := federationProcessor.New(db, tc, config, federator, fromFederator) return &processor{ fromClientAPI: fromClientAPI, @@ -282,12 +279,13 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f db: db, filter: visibility.NewFilter(db), - accountProcessor: accountProcessor, - adminProcessor: adminProcessor, - statusProcessor: statusProcessor, - streamingProcessor: streamingProcessor, - mediaProcessor: mediaProcessor, - userProcessor: userProcessor, + accountProcessor: accountProcessor, + adminProcessor: adminProcessor, + statusProcessor: statusProcessor, + streamingProcessor: streamingProcessor, + mediaProcessor: mediaProcessor, + userProcessor: userProcessor, + federationProcessor: federationProcessor, } } |