diff options
Diffstat (limited to 'internal/processing')
| -rw-r--r-- | internal/processing/account/follow.go | 5 | ||||
| -rw-r--r-- | internal/processing/admin/rule.go | 9 | ||||
| -rw-r--r-- | internal/processing/application/create.go | 5 | ||||
| -rw-r--r-- | internal/processing/fedi/accept.go | 42 | ||||
| -rw-r--r-- | internal/processing/fedi/authorization.go | 57 | ||||
| -rw-r--r-- | internal/processing/fedi/common.go | 50 | ||||
| -rw-r--r-- | internal/processing/interactionrequests/accept.go | 14 | ||||
| -rw-r--r-- | internal/processing/interactionrequests/accept_test.go | 2 | ||||
| -rw-r--r-- | internal/processing/interactionrequests/reject.go | 4 | ||||
| -rw-r--r-- | internal/processing/search/get.go | 2 | ||||
| -rw-r--r-- | internal/processing/workers/federate.go | 18 | ||||
| -rw-r--r-- | internal/processing/workers/fromclientapi.go | 93 | ||||
| -rw-r--r-- | internal/processing/workers/fromfediapi.go | 503 | ||||
| -rw-r--r-- | internal/processing/workers/fromfediapi_test.go | 120 | ||||
| -rw-r--r-- | internal/processing/workers/util.go | 36 |
15 files changed, 767 insertions, 193 deletions
diff --git a/internal/processing/account/follow.go b/internal/processing/account/follow.go index 26d552bbb..91955eaa7 100644 --- a/internal/processing/account/follow.go +++ b/internal/processing/account/follow.go @@ -82,10 +82,7 @@ func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmode // Neither follows nor follow requests, so // create and store a new follow request. - followID, err := id.NewRandomULID() - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } + followID := id.NewRandomULID() followURI := uris.GenerateURIForFollow(requestingAccount.Username, followID) fr := >smodel.FollowRequest{ diff --git a/internal/processing/admin/rule.go b/internal/processing/admin/rule.go index a665da9a1..de19cba0b 100644 --- a/internal/processing/admin/rule.go +++ b/internal/processing/admin/rule.go @@ -64,17 +64,12 @@ func (p *Processor) RuleGet(ctx context.Context, id string) (*apimodel.AdminInst // RuleCreate adds a new rule to the instance. func (p *Processor) RuleCreate(ctx context.Context, form *apimodel.InstanceRuleCreateRequest) (*apimodel.AdminInstanceRule, gtserror.WithCode) { - ruleID, err := id.NewRandomULID() - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating id for new instance rule: %s", err), "error creating rule ID") - } - rule := >smodel.Rule{ - ID: ruleID, + ID: id.NewRandomULID(), Text: form.Text, } - if err = p.state.DB.PutRule(ctx, rule); err != nil { + if err := p.state.DB.PutRule(ctx, rule); err != nil { return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/application/create.go b/internal/processing/application/create.go index 7ee6fb6b9..d63b682d6 100644 --- a/internal/processing/application/create.go +++ b/internal/processing/application/create.go @@ -86,10 +86,7 @@ func (p *Processor) Create( } // Generate random client ID. - clientID, err := id.NewRandomULID() - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } + clientID := id.NewRandomULID() // Generate + store app // to put in the database. diff --git a/internal/processing/fedi/accept.go b/internal/processing/fedi/accept.go index e63b460db..97e36fbb3 100644 --- a/internal/processing/fedi/accept.go +++ b/internal/processing/fedi/accept.go @@ -19,10 +19,8 @@ package fedi import ( "context" - "errors" "code.superseriousbusiness.org/gotosocial/internal/ap" - "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtserror" ) @@ -34,44 +32,18 @@ import ( func (p *Processor) AcceptGet( ctx context.Context, requestedUser string, - reqID string, -) (interface{}, gtserror.WithCode) { - // Authenticate incoming request, getting related accounts. - auth, errWithCode := p.authenticate(ctx, requestedUser) + intReqID string, +) (any, gtserror.WithCode) { + // Ensure valid request, intReq exists, etc. + intReq, errWithCode := p.validateIntReqRequest(ctx, requestedUser, intReqID) if errWithCode != nil { return nil, errWithCode } - if auth.handshakingURI != nil { - // We're currently handshaking, which means - // we don't know this account yet. This should - // be a very rare race condition. - err := gtserror.Newf("network race handshaking %s", auth.handshakingURI) - return nil, gtserror.NewErrorInternalError(err) - } - - receivingAcct := auth.receivingAcct - - req, err := p.state.DB.GetInteractionRequestByID(ctx, reqID) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - err := gtserror.Newf("db error getting interaction request %s: %w", reqID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - if req == nil || !req.IsAccepted() { - // Request doesn't exist or hasn't been accepted. - err := gtserror.Newf("interaction request %s not found", reqID) - return nil, gtserror.NewErrorNotFound(err) - } - - if req.TargetAccountID != receivingAcct.ID { - const text = "interaction request does not belong to receiving account" - return nil, gtserror.NewErrorNotFound(errors.New(text)) - } - - accept, err := p.converter.InteractionReqToASAccept(ctx, req) + // Convert + serialize the Accept. + accept, err := p.converter.InteractionReqToASAccept(ctx, intReq) if err != nil { - err := gtserror.Newf("error converting accept: %w", err) + err := gtserror.Newf("error converting to accept: %w", err) return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/fedi/authorization.go b/internal/processing/fedi/authorization.go new file mode 100644 index 000000000..bbba6a2d8 --- /dev/null +++ b/internal/processing/fedi/authorization.go @@ -0,0 +1,57 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// 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 fedi + +import ( + "context" + + "code.superseriousbusiness.org/gotosocial/internal/ap" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" +) + +// AuthorizationGet handles the getting of a fedi/activitypub +// representation of a local interaction authorization. +// +// It performs appropriate authentication before +// returning a JSON serializable interface. +func (p *Processor) AuthorizationGet( + ctx context.Context, + requestedUser string, + intReqID string, +) (any, gtserror.WithCode) { + // Ensure valid request, intReq exists, etc. + intReq, errWithCode := p.validateIntReqRequest(ctx, requestedUser, intReqID) + if errWithCode != nil { + return nil, errWithCode + } + + // Convert + serialize the Authorization. + authorization, err := p.converter.InteractionReqToASAuthorization(ctx, intReq) + if err != nil { + err := gtserror.Newf("error converting to authorization: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + data, err := ap.Serialize(authorization) + if err != nil { + err := gtserror.Newf("error serializing accept: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} diff --git a/internal/processing/fedi/common.go b/internal/processing/fedi/common.go index 9059aef39..fc783f93e 100644 --- a/internal/processing/fedi/common.go +++ b/internal/processing/fedi/common.go @@ -20,6 +20,7 @@ package fedi import ( "context" "errors" + "fmt" "net/url" "code.superseriousbusiness.org/gotosocial/internal/db" @@ -81,3 +82,52 @@ func (p *Processor) authenticate(ctx context.Context, requestedUser string) (*co receivingAcct: receiver, }, nil } + +// validateIntReqRequest is a shortcut function +// for returning an accepted interaction request +// targeting `requestedUser`. +func (p *Processor) validateIntReqRequest( + ctx context.Context, + requestedUser string, + intReqID string, +) (*gtsmodel.InteractionRequest, gtserror.WithCode) { + // Authenticate incoming request, getting related accounts. + auth, errWithCode := p.authenticate(ctx, requestedUser) + if errWithCode != nil { + return nil, errWithCode + } + + if auth.handshakingURI != nil { + // We're currently handshaking, which means we don't know + // this account yet. This should be a very rare race condition. + err := gtserror.Newf("network race handshaking %s", auth.handshakingURI) + return nil, gtserror.NewErrorInternalError(err) + } + + // Fetch interaction request with the given ID. + req, err := p.state.DB.GetInteractionRequestByID(ctx, intReqID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting interaction request %s: %w", intReqID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + // Ensure that this is an existing + // and *accepted* interaction request. + if req == nil || !req.IsAccepted() { + const text = "interaction request not found" + return nil, gtserror.NewErrorNotFound(errors.New(text)) + } + + // Ensure interaction request was accepted + // by the account in the request path. + if req.TargetAccountID != auth.receivingAcct.ID { + text := fmt.Sprintf( + "account %s is not targeted by interaction request %s and therefore can't accept it", + requestedUser, intReqID, + ) + return nil, gtserror.NewErrorNotFound(errors.New(text)) + } + + // All fine. + return req, nil +} diff --git a/internal/processing/interactionrequests/accept.go b/internal/processing/interactionrequests/accept.go index ce682380b..7efd1f373 100644 --- a/internal/processing/interactionrequests/accept.go +++ b/internal/processing/interactionrequests/accept.go @@ -65,14 +65,16 @@ func (p *Processor) Accept( defer unlock() // Mark the request as accepted - // and generate a URI for it. + // and generate URIs for it. req.AcceptedAt = time.Now() - req.URI = uris.GenerateURIForAccept(acct.Username, req.ID) + req.ResponseURI = uris.GenerateURIForAccept(acct.Username, req.ID) + req.AuthorizationURI = uris.GenerateURIForAuthorization(acct.Username, req.ID) if err := p.state.DB.UpdateInteractionRequest( ctx, req, "accepted_at", - "uri", + "response_uri", + "authorization_uri", ); err != nil { err := gtserror.Newf("db error updating interaction request: %w", err) return nil, gtserror.NewErrorInternalError(err) @@ -132,7 +134,7 @@ func (p *Processor) acceptLike( // Update the Like. req.Like.PendingApproval = util.Ptr(false) req.Like.PreApproved = false - req.Like.ApprovedByURI = req.URI + req.Like.ApprovedByURI = req.AuthorizationURI if err := p.state.DB.UpdateStatusFave( ctx, req.Like, @@ -173,7 +175,7 @@ func (p *Processor) acceptReply( // Update the Reply. req.Reply.PendingApproval = util.Ptr(false) req.Reply.PreApproved = false - req.Reply.ApprovedByURI = req.URI + req.Reply.ApprovedByURI = req.AuthorizationURI if err := p.state.DB.UpdateStatus( ctx, req.Reply, @@ -214,7 +216,7 @@ func (p *Processor) acceptAnnounce( // Update the Announce. req.Announce.PendingApproval = util.Ptr(false) req.Announce.PreApproved = false - req.Announce.ApprovedByURI = req.URI + req.Announce.ApprovedByURI = req.AuthorizationURI if err := p.state.DB.UpdateStatus( ctx, req.Announce, diff --git a/internal/processing/interactionrequests/accept_test.go b/internal/processing/interactionrequests/accept_test.go index b48978f2c..cb4212c24 100644 --- a/internal/processing/interactionrequests/accept_test.go +++ b/internal/processing/interactionrequests/accept_test.go @@ -67,7 +67,7 @@ func (suite *AcceptTestSuite) TestAccept() { suite.FailNow(err.Error()) } suite.False(*dbStatus.PendingApproval) - suite.Equal(dbReq.URI, dbStatus.ApprovedByURI) + suite.Equal(dbReq.AuthorizationURI, dbStatus.ApprovedByURI) // Wait for a notification // for interacting status. diff --git a/internal/processing/interactionrequests/reject.go b/internal/processing/interactionrequests/reject.go index 3ceaa47d9..4db52e260 100644 --- a/internal/processing/interactionrequests/reject.go +++ b/internal/processing/interactionrequests/reject.go @@ -66,12 +66,12 @@ func (p *Processor) Reject( // Mark the request as rejected // and generate a URI for it. req.RejectedAt = time.Now() - req.URI = uris.GenerateURIForReject(acct.Username, req.ID) + req.ResponseURI = uris.GenerateURIForReject(acct.Username, req.ID) if err := p.state.DB.UpdateInteractionRequest( ctx, req, "rejected_at", - "uri", + "response_uri", ); err != nil { err := gtserror.Newf("db error updating interaction request: %w", err) return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/search/get.go b/internal/processing/search/get.go index 2e956b049..64aefd23c 100644 --- a/internal/processing/search/get.go +++ b/internal/processing/search/get.go @@ -524,7 +524,7 @@ func (p *Processor) byURI( switch { case gtserror.IsUnretrievable(err), gtserror.IsWrongType(err), - gtserror.NotPermitted(err): + gtserror.IsNotPermitted(err): log.Debugf(ctx, "semi-expected error type looking up %s as status: %v", uri, err, diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go index 3cc3d130c..7459f0114 100644 --- a/internal/processing/workers/federate.go +++ b/internal/processing/workers/federate.go @@ -115,7 +115,7 @@ func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account) // Address the delete CC public. deleteCC := streams.NewActivityStreamsCcProperty() - deleteCC.AppendIRI(ap.PublicURI()) + deleteCC.AppendIRI(ap.PublicIRI()) delete.SetActivityStreamsCc(deleteCC) // Send the Delete via the Actor's outbox. @@ -491,12 +491,7 @@ func (f *federate) UndoAnnounce(ctx context.Context, boost *gtsmodel.Status) err } // Recreate the ActivityStreams Announce. - asAnnounce, err := f.converter.BoostToAS( - ctx, - boost, - boost.Account, - boost.BoostOfAccount, - ) + asAnnounce, err := f.converter.BoostToAS(ctx, boost) if err != nil { return gtserror.Newf("error converting boost to AS: %w", err) } @@ -767,12 +762,7 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error { } // Create the ActivityStreams Announce. - announce, err := f.converter.BoostToAS( - ctx, - boost, - boost.Account, - boost.BoostOfAccount, - ) + announce, err := f.converter.BoostToAS(ctx, boost) if err != nil { return gtserror.Newf("error converting boost to AS: %w", err) } @@ -1104,7 +1094,7 @@ func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) e ap.AppendTo(move, followersIRI) // Address the move CC public. - ap.AppendCc(move, ap.PublicURI()) + ap.AppendCc(move, ap.PublicIRI()) // Send the Move via the Actor's outbox. if _, err := f.FederatingActor().Send( diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go index 22e7780f6..9cdbcc548 100644 --- a/internal/processing/workers/fromclientapi.go +++ b/internal/processing/workers/fromclientapi.go @@ -287,7 +287,7 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA // and/or notify the account that's being // interacted with (if it's local): they can // approve or deny the interaction later. - if err := p.utils.requestReply(ctx, status); err != nil { + if err := p.utils.impoliteReplyRequest(ctx, status); err != nil { return gtserror.Newf("error pending reply: %w", err) } @@ -310,19 +310,22 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA // URI attached. // Store an already-accepted interaction request. - id := id.NewULID() + requestID := id.NewULID() approval := >smodel.InteractionRequest{ - ID: id, - StatusID: status.InReplyToID, - TargetAccountID: status.InReplyToAccountID, - TargetAccount: status.InReplyToAccount, - InteractingAccountID: status.AccountID, - InteractingAccount: status.Account, - InteractionURI: status.URI, - InteractionType: gtsmodel.InteractionReply, - Reply: status, - URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id), - AcceptedAt: time.Now(), + ID: requestID, + TargetStatusID: status.InReplyToID, + TargetAccountID: status.InReplyToAccountID, + TargetAccount: status.InReplyToAccount, + InteractingAccountID: status.AccountID, + InteractingAccount: status.Account, + InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix), + InteractionURI: status.URI, + InteractionType: gtsmodel.InteractionReply, + Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests. + Reply: status, + ResponseURI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, requestID), + AuthorizationURI: uris.GenerateURIForAuthorization(status.InReplyToAccount.Username, requestID), + AcceptedAt: time.Now(), } if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil { return gtserror.Newf("db error putting pre-approved interaction request: %w", err) @@ -331,7 +334,7 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA // Mark the status as now approved. status.PendingApproval = util.Ptr(false) status.PreApproved = false - status.ApprovedByURI = approval.URI + status.ApprovedByURI = approval.AuthorizationURI if err := p.state.DB.UpdateStatus( ctx, status, @@ -494,7 +497,7 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI // and/or notify the account that's being // interacted with (if it's local): they can // approve or deny the interaction later. - if err := p.utils.requestFave(ctx, fave); err != nil { + if err := p.utils.impoliteFaveRequest(ctx, fave); err != nil { return gtserror.Newf("error pending fave: %w", err) } @@ -517,19 +520,22 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI // URI attached. // Store an already-accepted interaction request. - id := id.NewULID() + requestID := id.NewULID() approval := >smodel.InteractionRequest{ - ID: id, - StatusID: fave.StatusID, - TargetAccountID: fave.TargetAccountID, - TargetAccount: fave.TargetAccount, - InteractingAccountID: fave.AccountID, - InteractingAccount: fave.Account, - InteractionURI: fave.URI, - InteractionType: gtsmodel.InteractionLike, - Like: fave, - URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id), - AcceptedAt: time.Now(), + ID: requestID, + TargetStatusID: fave.StatusID, + TargetAccountID: fave.TargetAccountID, + TargetAccount: fave.TargetAccount, + InteractingAccountID: fave.AccountID, + InteractingAccount: fave.Account, + InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix), + InteractionURI: fave.URI, + InteractionType: gtsmodel.InteractionLike, + Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests. + Like: fave, + ResponseURI: uris.GenerateURIForAccept(fave.TargetAccount.Username, requestID), + AuthorizationURI: uris.GenerateURIForAuthorization(fave.TargetAccount.Username, requestID), + AcceptedAt: time.Now(), } if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil { return gtserror.Newf("db error putting pre-approved interaction request: %w", err) @@ -538,7 +544,7 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI // Mark the fave itself as now approved. fave.PendingApproval = util.Ptr(false) fave.PreApproved = false - fave.ApprovedByURI = approval.URI + fave.ApprovedByURI = approval.AuthorizationURI if err := p.state.DB.UpdateStatusFave( ctx, fave, @@ -589,7 +595,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien // and/or notify the account that's being // interacted with (if it's local): they can // approve or deny the interaction later. - if err := p.utils.requestAnnounce(ctx, boost); err != nil { + if err := p.utils.impoliteAnnounceRequest(ctx, boost); err != nil { return gtserror.Newf("error pending boost: %w", err) } @@ -612,19 +618,22 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien // URI attached. // Store an already-accepted interaction request. - id := id.NewULID() + requestID := id.NewULID() approval := >smodel.InteractionRequest{ - ID: id, - StatusID: boost.BoostOfID, - TargetAccountID: boost.BoostOfAccountID, - TargetAccount: boost.BoostOfAccount, - InteractingAccountID: boost.AccountID, - InteractingAccount: boost.Account, - InteractionURI: boost.URI, - InteractionType: gtsmodel.InteractionAnnounce, - Announce: boost, - URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id), - AcceptedAt: time.Now(), + ID: requestID, + TargetStatusID: boost.BoostOfID, + TargetAccountID: boost.BoostOfAccountID, + TargetAccount: boost.BoostOfAccount, + InteractingAccountID: boost.AccountID, + InteractingAccount: boost.Account, + InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(boost.URI, gtsmodel.AnnounceRequestSuffix), + InteractionURI: boost.URI, + InteractionType: gtsmodel.InteractionAnnounce, + Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests. + Announce: boost, + ResponseURI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, requestID), + AuthorizationURI: uris.GenerateURIForAuthorization(boost.BoostOfAccount.Username, requestID), + AcceptedAt: time.Now(), } if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil { return gtserror.Newf("db error putting pre-approved interaction request: %w", err) @@ -633,7 +642,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien // Mark the boost itself as now approved. boost.PendingApproval = util.Ptr(false) boost.PreApproved = false - boost.ApprovedByURI = approval.URI + boost.ApprovedByURI = approval.AuthorizationURI if err := p.state.DB.UpdateStatus( ctx, boost, diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go index 09c1df480..797a2d9c6 100644 --- a/internal/processing/workers/fromfediapi.go +++ b/internal/processing/workers/fromfediapi.go @@ -88,6 +88,10 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF case ap.ObjectNote: return p.fediAPI.CreateStatus(ctx, fMsg) + // REQUEST TO REPLY TO A STATUS + case ap.ActivityReplyRequest: + return p.fediAPI.CreateReplyRequest(ctx, fMsg) + // CREATE FOLLOW (request) case ap.ActivityFollow: return p.fediAPI.CreateFollowReq(ctx, fMsg) @@ -96,10 +100,18 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF case ap.ActivityLike: return p.fediAPI.CreateLike(ctx, fMsg) + // REQUEST TO LIKE A STATUS + case ap.ActivityLikeRequest: + return p.fediAPI.CreateLikeRequest(ctx, fMsg) + // CREATE ANNOUNCE/BOOST case ap.ActivityAnnounce: return p.fediAPI.CreateAnnounce(ctx, fMsg) + // REQUEST TO BOOST A STATUS + case ap.ActivityAnnounceRequest: + return p.fediAPI.CreateAnnounceRequest(ctx, fMsg) + // CREATE BLOCK case ap.ActivityBlock: return p.fediAPI.CreateBlock(ctx, fMsg) @@ -146,11 +158,15 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF case ap.ObjectNote: return p.fediAPI.AcceptReply(ctx, fMsg) + // ACCEPT (pending) POLITE REPLY REQUEST + case ap.ActivityReplyRequest: + return p.fediAPI.AcceptPoliteReplyRequest(ctx, fMsg) + // ACCEPT (pending) ANNOUNCE case ap.ActivityAnnounce: return p.fediAPI.AcceptAnnounce(ctx, fMsg) - // ACCEPT (remote) REPLY or ANNOUNCE + // ACCEPT (remote) IMPOLITE REPLY or ANNOUNCE case ap.ObjectUnknown: return p.fediAPI.AcceptRemoteStatus(ctx, fMsg) } @@ -219,6 +235,9 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF return gtserror.Newf("unhandled: %s %s", fMsg.APActivityType, fMsg.APObjectType) } +// CreateStatus handles the creation of a status/post sent as a Create message. +// It is also capable of handling impolite reply requests to local + remote statuses, +// ie., replies sent directly without doing the ReplyRequest process first. func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI) error { var ( status *gtsmodel.Status @@ -291,7 +310,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI) // preapproved, then just notify the account // that's being interacted with: they can // approve or deny the interaction later. - if err := p.utils.requestReply(ctx, status); err != nil { + if err := p.utils.impoliteReplyRequest(ctx, status); err != nil { return gtserror.Newf("error pending reply: %w", err) } @@ -306,20 +325,24 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI) // collection. Do the Accept immediately and // then process everything else as normal. - // Store an already-accepted interaction request. - id := id.NewULID() + // Store an already-accepted + // impolite interaction request. + requestID := id.NewULID() approval := >smodel.InteractionRequest{ - ID: id, - StatusID: status.InReplyToID, - TargetAccountID: status.InReplyToAccountID, - TargetAccount: status.InReplyToAccount, - InteractingAccountID: status.AccountID, - InteractingAccount: status.Account, - InteractionURI: status.URI, - InteractionType: gtsmodel.InteractionReply, - Reply: status, - URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id), - AcceptedAt: time.Now(), + ID: requestID, + TargetStatusID: status.InReplyToID, + TargetAccountID: status.InReplyToAccountID, + TargetAccount: status.InReplyToAccount, + InteractingAccountID: status.AccountID, + InteractingAccount: status.Account, + InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix), + InteractionURI: status.URI, + InteractionType: gtsmodel.InteractionReply, + Polite: util.Ptr(false), + Reply: status, + ResponseURI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, requestID), + AuthorizationURI: uris.GenerateURIForAuthorization(status.InReplyToAccount.Username, requestID), + AcceptedAt: time.Now(), } if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil { return gtserror.Newf("db error putting pre-approved interaction request: %w", err) @@ -328,7 +351,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI) // Mark the status as now approved. status.PendingApproval = util.Ptr(false) status.PreApproved = false - status.ApprovedByURI = approval.URI + status.ApprovedByURI = approval.AuthorizationURI if err := p.state.DB.UpdateStatus( ctx, status, @@ -365,6 +388,118 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI) return nil } +// CreateReplyRequest handles a polite ReplyRequest. +// This is distinct from CreateStatus, which is capable +// of handling both "normal" top-level status creation, +// in addition to *impolite* reply requests. +func (p *fediAPI) CreateReplyRequest(ctx context.Context, fMsg *messages.FromFediAPI) error { + // Extract the ap model Statusable + // set by the federating db. + statusable, ok := fMsg.APObject.(ap.Statusable) + if !ok { + return gtserror.Newf("cannot cast %T -> ap.Statusable", fMsg.APObject) + } + + // Call RefreshStatus to parse and process the + // statusable. This will also check permissions. + replyURI := ap.GetJSONLDId(statusable).String() + reply, _, err := p.federate.RefreshStatus(ctx, + fMsg.Receiving.Username, + >smodel.Status{ + URI: replyURI, + Local: util.Ptr(false), + }, + statusable, + // Force refresh within 5min window. + dereferencing.Fresh, + ) + + switch { + case err == nil: + // All fine. + + case gtserror.IsNotPermitted(err): + // Reply is straight up not permitted by + // the interaction policy of the status + // it's replying to. Nothing more to do. + log.Debugf(ctx, + "dropping unpermitted ReplyRequest with instrument %s", + replyURI, + ) + return nil + + default: + // There's some real error. + return gtserror.Newf( + "error processing ReplyRequest with instrument %s: %w", + replyURI, err, + ) + } + + // The reply is permitted. Check if we + // should send out an Accept immediately. + manualApproval := *reply.PendingApproval && !reply.PreApproved + if manualApproval { + // The reply requires manual approval. + // + // Just notify target account about + // the requested interaction. + if err := p.surface.notifyPendingReply(ctx, reply); err != nil { + return gtserror.Newf("error notifying pending reply: %w", err) + } + + return nil + } + + // The reply is automatically approved, + // handle side effects of this. + req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest) + if !ok { + return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel) + } + + // Mark the request as accepted. + req.AcceptedAt = time.Now() + req.ResponseURI = uris.GenerateURIForAccept( + req.TargetAccount.Username, req.ID, + ) + req.AuthorizationURI = uris.GenerateURIForAuthorization( + req.TargetAccount.Username, req.ID, + ) + + // Update in the db. + if err := p.state.DB.UpdateInteractionRequest( + ctx, + req, + "accepted_at", + "response_uri", + "authorization_uri", + ); err != nil { + return gtserror.Newf("db error updating interaction request: %w", err) + } + + // Send out the accept. + if err := p.federate.AcceptInteraction(ctx, req); err != nil { + log.Errorf(ctx, "error federating accept: %v", err) + } + + // Update stats for the replying account. + if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, reply); err != nil { + log.Errorf(ctx, "error updating account stats: %v", err) + } + + // Timeline the reply + notify recipient(s). + if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil { + log.Errorf(ctx, "error timelining and notifying status: %v", err) + } + + // Interaction counts changed on the replied status; + // uncache the prepared version from all timelines. + p.surface.invalidateStatusFromTimelines(reply.InReplyToID) + + return nil +} + func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI) error { // Cast poll vote type from the worker message. vote, ok := fMsg.GTSModel.(*gtsmodel.PollVote) @@ -430,18 +565,18 @@ func (p *fediAPI) UpdatePollVote(ctx context.Context, fMsg *messages.FromFediAPI } // Get the origin status. - status := vote.Poll.Status + reply := vote.Poll.Status - if *status.Local { + if *reply.Local { // These were poll votes in a local status, we need to // federate the updated status model with latest vote counts. - if err := p.federate.UpdateStatus(ctx, status); err != nil { + if err := p.federate.UpdateStatus(ctx, reply); err != nil { log.Errorf(ctx, "error federating status update: %v", err) } } // Interaction counts changed, uncache from timelines. - p.surface.invalidateStatusFromTimelines(status.ID) + p.surface.invalidateStatusFromTimelines(reply.ID) return nil } @@ -503,6 +638,8 @@ func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg *messages.FromFediAP return nil } +// CreateLike handles an impolite Like, ie., a Like sent directly. +// This is different from the CreateLikeRequest function, which handles polite LikeRequests. func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) error { fave, ok := fMsg.GTSModel.(*gtsmodel.StatusFave) if !ok { @@ -525,7 +662,7 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er // preapproved, then just notify the account // that's being interacted with: they can // approve or deny the interaction later. - if err := p.utils.requestFave(ctx, fave); err != nil { + if err := p.utils.impoliteFaveRequest(ctx, fave); err != nil { return gtserror.Newf("error pending fave: %w", err) } @@ -540,20 +677,24 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er // collection. Do the Accept immediately and // then process everything else as normal. - // Store an already-accepted interaction request. - id := id.NewULID() + // Store an already-accepted + // impolite interaction request. + requestID := id.NewULID() approval := >smodel.InteractionRequest{ - ID: id, - StatusID: fave.StatusID, - TargetAccountID: fave.TargetAccountID, - TargetAccount: fave.TargetAccount, - InteractingAccountID: fave.AccountID, - InteractingAccount: fave.Account, - InteractionURI: fave.URI, - InteractionType: gtsmodel.InteractionLike, - Like: fave, - URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id), - AcceptedAt: time.Now(), + ID: requestID, + TargetStatusID: fave.StatusID, + TargetAccountID: fave.TargetAccountID, + TargetAccount: fave.TargetAccount, + InteractingAccountID: fave.AccountID, + InteractingAccount: fave.Account, + InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix), + InteractionURI: fave.URI, + InteractionType: gtsmodel.InteractionLike, + Polite: util.Ptr(false), + Like: fave, + ResponseURI: uris.GenerateURIForAccept(fave.TargetAccount.Username, requestID), + AuthorizationURI: uris.GenerateURIForAuthorization(fave.TargetAccount.Username, requestID), + AcceptedAt: time.Now(), } if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil { return gtserror.Newf("db error putting pre-approved interaction request: %w", err) @@ -562,7 +703,7 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er // Mark the fave itself as now approved. fave.PendingApproval = util.Ptr(false) fave.PreApproved = false - fave.ApprovedByURI = approval.URI + fave.ApprovedByURI = approval.AuthorizationURI if err := p.state.DB.UpdateStatusFave( ctx, fave, @@ -591,6 +732,87 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er return nil } +// CreateLikeRequest handles a polite LikeRequest, as +// opposed to CreateLike, which handles *impolite* like +// requests (ie., Likes sent directly). +func (p *fediAPI) CreateLikeRequest(ctx context.Context, fMsg *messages.FromFediAPI) error { + req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest) + if !ok { + return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel) + } + + // At this point the not-yet-approved + // interaction request, and the pending + // fave, are both in the database. + + if !req.Like.PreApproved { + // The fave is *not* pre-approved, and + // therefore requires manual approval. + // + // Just notify target account about + // the requested interaction. + if err := p.surface.notifyPendingFave(ctx, req.Like); err != nil { + return gtserror.Newf("error notifying pending like: %w", err) + } + + return nil + } + + // If it's pre-approved on the other hand + // we can handle everything immediately. + + // Mark the request as accepted. + req.AcceptedAt = time.Now() + req.ResponseURI = uris.GenerateURIForAccept( + req.TargetAccount.Username, req.ID, + ) + req.AuthorizationURI = uris.GenerateURIForAuthorization( + req.TargetAccount.Username, req.ID, + ) + + // Update in the db. + if err := p.state.DB.UpdateInteractionRequest( + ctx, + req, + "accepted_at", + "response_uri", + "authorization_uri", + ); err != nil { + return gtserror.Newf("db error updating interaction request: %w", err) + } + + // Send out the accept. + if err := p.federate.AcceptInteraction(ctx, req); err != nil { + log.Errorf(ctx, "error federating accept: %v", err) + } + + // Mark the fave as approved. + req.Like.PendingApproval = util.Ptr(false) + req.Like.ApprovedByURI = req.AuthorizationURI + req.Like.PreApproved = false + + // Update in the db. + if err := p.state.DB.UpdateStatusFave( + ctx, + req.Like, + "pending_approval", + "approved_by_uri", + ); err != nil { + return gtserror.Newf("db error updating status fave: %w", err) + } + + // Notify the faved account. + if err := p.surface.notifyFave(ctx, req.Like); err != nil { + log.Errorf(ctx, "error notifying fave: %v", err) + } + + // Interaction counts changed on the faved status; + // uncache the prepared version from all timelines. + p.surface.invalidateStatusFromTimelines(req.Like.StatusID) + + return nil +} + func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI) error { boost, ok := fMsg.GTSModel.(*gtsmodel.Status) if !ok { @@ -610,7 +832,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI ) if err != nil { if gtserror.IsUnretrievable(err) || - gtserror.NotPermitted(err) { + gtserror.IsNotPermitted(err) { // Boosted status domain blocked, or // otherwise not permitted, nothing to do. log.Debugf(ctx, "skipping announce: %v", err) @@ -632,7 +854,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI // preapproved, then just notify the account // that's being interacted with: they can // approve or deny the interaction later. - if err := p.utils.requestAnnounce(ctx, boost); err != nil { + if err := p.utils.impoliteAnnounceRequest(ctx, boost); err != nil { return gtserror.Newf("error pending boost: %w", err) } @@ -647,20 +869,24 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI // collection. Do the Accept immediately and // then process everything else as normal. - // Store an already-accepted interaction request. - id := id.NewULID() + // Store an already-accepted + // impolite interaction request. + requestID := id.NewULID() approval := >smodel.InteractionRequest{ - ID: id, - StatusID: boost.BoostOfID, - TargetAccountID: boost.BoostOfAccountID, - TargetAccount: boost.BoostOfAccount, - InteractingAccountID: boost.AccountID, - InteractingAccount: boost.Account, - InteractionURI: boost.URI, - InteractionType: gtsmodel.InteractionAnnounce, - Announce: boost, - URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id), - AcceptedAt: time.Now(), + ID: requestID, + TargetStatusID: boost.BoostOfID, + TargetAccountID: boost.BoostOfAccountID, + TargetAccount: boost.BoostOfAccount, + InteractingAccountID: boost.AccountID, + InteractingAccount: boost.Account, + InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(boost.URI, gtsmodel.AnnounceRequestSuffix), + InteractionURI: boost.URI, + InteractionType: gtsmodel.InteractionAnnounce, + Polite: util.Ptr(false), + Announce: boost, + ResponseURI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, requestID), + AuthorizationURI: uris.GenerateURIForAuthorization(boost.BoostOfAccount.Username, requestID), + AcceptedAt: time.Now(), } if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil { return gtserror.Newf("db error putting pre-approved interaction request: %w", err) @@ -669,7 +895,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI // Mark the boost itself as now approved. boost.PendingApproval = util.Ptr(false) boost.PreApproved = false - boost.ApprovedByURI = approval.URI + boost.ApprovedByURI = approval.AuthorizationURI if err := p.state.DB.UpdateStatus( ctx, boost, @@ -708,6 +934,103 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI return nil } +func (p *fediAPI) CreateAnnounceRequest(ctx context.Context, fMsg *messages.FromFediAPI) error { + req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest) + if !ok { + return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel) + } + + // At this point the not-yet-handled interaction req + // is in the database, but the announce isn't yet. + // + // We can check permissions for the announce *and* + // put it in the db (if acceptable) by doing Enrich. + boost, err := p.federate.EnrichAnnounce( + ctx, + req.Announce, + fMsg.Receiving.Username, + ) + + switch { + case err == nil: + // All fine. + + case gtserror.IsNotPermitted(err): + // Announce is straight up not permitted + // by the interaction policy of the status + // it's targeting. Nothing more to do. + log.Debugf(ctx, + "dropping unpermitted AnnounceRequest with instrument %s", + req.Announce.URI, + ) + return nil + + default: + // There's some real error. + return gtserror.Newf( + "error processing AnnounceRequest with instrument %s: %w", + req.Announce.URI, err, + ) + } + + // The announce is permitted. Check if we + // should send out an Accept immediately. + manualApproval := *boost.PendingApproval && !boost.PreApproved + if manualApproval { + // The announce requires manual approval. + // + // Just notify target account about + // the requested interaction. + if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil { + return gtserror.Newf("error notifying pending announce: %w", err) + } + + return nil + } + + // The announce is automatically approved, + // mark the request as accepted. + req.AcceptedAt = time.Now() + req.ResponseURI = uris.GenerateURIForAccept( + req.TargetAccount.Username, req.ID, + ) + req.AuthorizationURI = uris.GenerateURIForAuthorization( + req.TargetAccount.Username, req.ID, + ) + + // Update in the db. + if err := p.state.DB.UpdateInteractionRequest( + ctx, + req, + "accepted_at", + "response_uri", + "authorization_uri", + ); err != nil { + return gtserror.Newf("db error updating interaction request: %w", err) + } + + // Send out the accept. + if err := p.federate.AcceptInteraction(ctx, req); err != nil { + log.Errorf(ctx, "error federating accept: %v", err) + } + + // Update stats for the boosting account. + if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, boost); err != nil { + log.Errorf(ctx, "error updating account stats: %v", err) + } + + // Timeline the boost + notify recipient(s). + if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil { + log.Errorf(ctx, "error timelining and notifying status: %v", err) + } + + // Interaction counts changed on the boosted status; + // uncache the prepared version from all timelines. + p.surface.invalidateStatusFromTimelines(boost.BoostOfID) + + return nil +} + func (p *fediAPI) CreateBlock(ctx context.Context, fMsg *messages.FromFediAPI) error { block, ok := fMsg.GTSModel.(*gtsmodel.Block) if !ok { @@ -842,29 +1165,29 @@ func (p *fediAPI) AcceptLike(ctx context.Context, fMsg *messages.FromFediAPI) er } func (p *fediAPI) AcceptReply(ctx context.Context, fMsg *messages.FromFediAPI) error { - status, ok := fMsg.GTSModel.(*gtsmodel.Status) + reply, ok := fMsg.GTSModel.(*gtsmodel.Status) if !ok { return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel) } // Update stats for the actor account. - if err := p.utils.incrementStatusesCount(ctx, status.Account, status); err != nil { + if err := p.utils.incrementStatusesCount(ctx, reply.Account, reply); err != nil { log.Errorf(ctx, "error updating account stats: %v", err) } // Timeline and notify the status. - if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil { + if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil { log.Errorf(ctx, "error timelining and notifying status: %v", err) } // Send out the reply again, fully this time. - if err := p.federate.CreateStatus(ctx, status); err != nil { + if err := p.federate.CreateStatus(ctx, reply); err != nil { log.Errorf(ctx, "error federating announce: %v", err) } // Interaction counts changed on the replied-to status; // uncache the prepared version from all timelines. - p.surface.invalidateStatusFromTimelines(status.InReplyToID) + p.surface.invalidateStatusFromTimelines(reply.InReplyToID) return nil } @@ -893,9 +1216,9 @@ func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFed // barebones status and insert it into the database, // if indeed it's actually a status URI we can fetch. // - // This will also check whether the given AcceptIRI + // This will also check whether the given approvedByURI // actually grants permission for this status. - status, _, err := p.federate.RefreshStatus(ctx, + reply, _, err := p.federate.RefreshStatus(ctx, fMsg.Receiving.Username, bareStatus, nil, nil, @@ -906,20 +1229,70 @@ func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFed // No error means it was indeed a remote status, and the // given approvedByURI permitted it. Timeline and notify it. - if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil { + if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil { log.Errorf(ctx, "error timelining and notifying status: %v", err) } // Interaction counts changed on the interacted status; // uncache the prepared version from all timelines. - if status.InReplyToID != "" { - p.surface.invalidateStatusFromTimelines(status.InReplyToID) + if reply.InReplyToID != "" { + p.surface.invalidateStatusFromTimelines(reply.InReplyToID) + } + + if reply.BoostOfID != "" { + p.surface.invalidateStatusFromTimelines(reply.BoostOfID) + } + + return nil +} + +func (p *fediAPI) AcceptPoliteReplyRequest(ctx context.Context, fMsg *messages.FromFediAPI) error { + if util.IsNil(fMsg.GTSModel) { + // If the interaction request is nil, this + // must be an accept of a remote ReplyRequest + // not targeting one of our statuses. + // + // Just pass it to the AcceptRemoteStatus + // func to do dereferencing + side effects. + log.Debug(ctx, "accepting remote ReplyRequest for remote reply") + return p.AcceptRemoteStatus(ctx, fMsg) + } + + // If the interaction request is not nil, this will + // be an accept of one of our replies to a remote. + // + // Since the int req + reply have already been updated + // in the federatingDB, we just need to do side effects. + intReq, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest) + if !ok { + return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel) } - if status.BoostOfID != "" { - p.surface.invalidateStatusFromTimelines(status.BoostOfID) + // Ensure reply populated. + reply := intReq.Reply + if err := p.state.DB.PopulateStatus(ctx, reply); err != nil { + return gtserror.Newf("error populating status: %w", err) } + // Update stats for the actor account. + if err := p.utils.incrementStatusesCount(ctx, reply.Account, reply); err != nil { + log.Errorf(ctx, "error updating account stats: %v", err) + } + + // Timeline and notify the status. + if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil { + log.Errorf(ctx, "error timelining and notifying status: %v", err) + } + + // Send out the reply with approval attached. + if err := p.federate.CreateStatus(ctx, reply); err != nil { + log.Errorf(ctx, "error federating announce: %v", err) + } + + // Interaction counts changed on the replied-to status; + // uncache the prepared version from all timelines. + p.surface.invalidateStatusFromTimelines(reply.InReplyToID) + return nil } @@ -1169,7 +1542,7 @@ func (p *fediAPI) RejectReply(ctx context.Context, fMsg *messages.FromFediAPI) e // be in the database, we just need to do side effects. // Get the rejected status. - status, err := p.state.DB.GetStatusByURI( + reply, err := p.state.DB.GetStatusByURI( gtscontext.SetBarebones(ctx), req.InteractionURI, ) @@ -1189,7 +1562,7 @@ func (p *fediAPI) RejectReply(ctx context.Context, fMsg *messages.FromFediAPI) e // Perform the actual status deletion. if err := p.utils.wipeStatus( ctx, - status, + reply, deleteAttachments, copyToSinBin, ); err != nil { diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go index 7811e9f3d..790c78b70 100644 --- a/internal/processing/workers/fromfediapi_test.go +++ b/internal/processing/workers/fromfediapi_test.go @@ -18,6 +18,7 @@ package workers_test import ( + "bytes" "context" "encoding/json" "errors" @@ -26,11 +27,13 @@ import ( "testing" "time" + "code.superseriousbusiness.org/activity/streams/vocab" "code.superseriousbusiness.org/gotosocial/internal/ap" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/id" "code.superseriousbusiness.org/gotosocial/internal/messages" "code.superseriousbusiness.org/gotosocial/internal/stream" "code.superseriousbusiness.org/gotosocial/internal/util" @@ -781,6 +784,123 @@ func (suite *FromFediAPITestSuite) TestUpdateNote() { } } +func (suite *FromFediAPITestSuite) TestCreateReplyRequest() { + var ( + ctx = suite.T().Context() + testStructs = testrig.SetupTestStructs(rMediaPath, rTemplatePath) + requesting = suite.testAccounts["remote_account_1"] + receiving = suite.testAccounts["admin_account"] + testStatus = suite.testStatuses["admin_account_status_1"] + intReqURI = "http://fossbros-anonymous.io/requests/87fb1478-ac46-406a-8463-96ce05645219" + intURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/87fb1478-ac46-406a-8463-96ce05645219" + jsonStr = `{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://gotosocial.org/ns", + { + "sensitive": "as:sensitive" + } + ], + "type": "ReplyRequest", + "id": "` + intReqURI + `", + "actor": "` + requesting.URI + `", + "object": "` + testStatus.URI + `", + "to": "` + receiving.URI + `", + "instrument": { + "attributedTo": "` + requesting.URI + `", + "cc": "` + requesting.FollowersURI + `", + "content": "\u003cp\u003ethis is a reply!\u003c/p\u003e", + "id": "` + intURI + `", + "inReplyTo": "` + testStatus.URI + `", + "tag": { + "href": "` + receiving.URI + `", + "name": "@` + receiving.Username + `@localhost:8080", + "type": "Mention" + }, + "to": "https://www.w3.org/ns/activitystreams#Public", + "type": "Note" + } +}` + ) + defer testrig.TearDownTestStructs(testStructs) + + suite.T().Logf("testing reply request:\n\n%s", jsonStr) + + // Decode the reply request + embedded statusable. + t, err := ap.DecodeType(ctx, io.NopCloser(bytes.NewBufferString(jsonStr))) + if err != nil { + suite.FailNow(err.Error()) + } + replyReq := t.(vocab.GoToSocialReplyRequest) + statusable := replyReq.GetActivityStreamsInstrument().At(0).GetActivityStreamsNote().(ap.Statusable) + + // Create a pending interaction request in the + // database, as though the reply req had already + // passed through the federatingdb function. + intReq := >smodel.InteractionRequest{ + ID: id.NewULID(), + TargetStatusID: testStatus.ID, + TargetStatus: testStatus, + TargetAccountID: receiving.ID, + TargetAccount: receiving, + InteractingAccountID: requesting.ID, + InteractingAccount: requesting, + InteractionRequestURI: intReqURI, + InteractionURI: ap.GetJSONLDId(statusable).String(), + InteractionType: gtsmodel.InteractionReply, + Polite: util.Ptr(true), + Reply: nil, // Not settable yet. + } + if err := testStructs.State.DB.PutInteractionRequest(ctx, intReq); err != nil { + suite.FailNow(err.Error()) + } + + // Process the message. + if err = testStructs.Processor.Workers().ProcessFromFediAPI( + ctx, + &messages.FromFediAPI{ + APObjectType: ap.ActivityReplyRequest, + APActivityType: ap.ActivityCreate, + GTSModel: intReq, + APObject: statusable, + Receiving: receiving, + Requesting: requesting, + }, + ); err != nil { + suite.FailNow(err.Error()) + } + + // The interaction request should be accepted. + intReq, err = testStructs.State.DB.GetInteractionRequestByID(ctx, intReq.ID) + if err != nil { + suite.FailNow(err.Error()) + } + suite.WithinDuration(time.Now(), intReq.AcceptedAt, 1*time.Minute) + suite.NotEmpty(intReq.AuthorizationURI) + suite.NotEmpty(intReq.ResponseURI) + + // Federator should send out an Accept that looks something like: + // + // { + // "@context": [ + // "https://gotosocial.org/ns", + // "https://www.w3.org/ns/activitystreams" + // ], + // "actor": "http://localhost:8080/users/admin", + // "id": "http://localhost:8080/users/admin/accepts/01K2CV90660VRPZM39R35NMSG9", + // "object": { + // "actor": "http://fossbros-anonymous.io/users/foss_satan", + // "id": "http://fossbros-anonymous.io/requests/87fb1478-ac46-406a-8463-96ce05645219", + // "instrument": "http://fossbros-anonymous.io/users/foss_satan/statuses/87fb1478-ac46-406a-8463-96ce05645219", + // "object": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", + // "type": "ReplyRequest" + // }, + // "result": "http://localhost:8080/users/admin/authorizations/01K2CV90660VRPZM39R35NMSG9", + // "to": "http://fossbros-anonymous.io/users/foss_satan", + // "type": "Accept" + // } +} + func TestFromFederatorTestSuite(t *testing.T) { suite.Run(t, &FromFediAPITestSuite{}) } diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go index 3c17eaaf5..6382887eb 100644 --- a/internal/processing/workers/util.go +++ b/internal/processing/workers/util.go @@ -526,9 +526,13 @@ func (u *utils) decrementFollowRequestsCount( return nil } -// requestFave stores an interaction request +// impoliteFaveRequest stores an interaction request // for the given fave, and notifies the interactee. -func (u *utils) requestFave( +// +// It should be used only when an actor has sent a Like +// directly in response to a post that requires approval +// for it, instead of sending a LikeRequest. +func (u *utils) impoliteFaveRequest( ctx context.Context, fave *gtsmodel.StatusFave, ) error { @@ -555,8 +559,8 @@ func (u *utils) requestFave( return nil } - // Create + store new interaction request. - req = typeutils.StatusFaveToInteractionRequest(fave) + // Create + store new impolite interaction request. + req = typeutils.StatusFaveToImpoliteInteractionRequest(fave) if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil { return gtserror.Newf("db error storing interaction request: %w", err) } @@ -569,9 +573,13 @@ func (u *utils) requestFave( return nil } -// requestReply stores an interaction request +// impoliteReplyRequest stores an interaction request // for the given reply, and notifies the interactee. -func (u *utils) requestReply( +// +// It should be used only when an actor has sent a reply +// directly in response to a post that requires approval +// for it, instead of sending a ReplyRequest. +func (u *utils) impoliteReplyRequest( ctx context.Context, reply *gtsmodel.Status, ) error { @@ -598,8 +606,8 @@ func (u *utils) requestReply( return nil } - // Create + store interaction request. - req = typeutils.StatusToInteractionRequest(reply) + // Create + store impolite interaction request. + req = typeutils.StatusToImpoliteInteractionRequest(reply) if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil { return gtserror.Newf("db error storing interaction request: %w", err) } @@ -612,9 +620,13 @@ func (u *utils) requestReply( return nil } -// requestAnnounce stores an interaction request +// impoliteAnnounceRequest stores an interaction request // for the given announce, and notifies the interactee. -func (u *utils) requestAnnounce( +// +// It should be used only when an actor has sent an Announce +// directly in response to a post that requires approval +// for it, instead of sending an AnnounceRequest. +func (u *utils) impoliteAnnounceRequest( ctx context.Context, boost *gtsmodel.Status, ) error { @@ -641,8 +653,8 @@ func (u *utils) requestAnnounce( return nil } - // Create + store interaction request. - req = typeutils.StatusToInteractionRequest(boost) + // Create + store impolite interaction request. + req = typeutils.StatusToImpoliteInteractionRequest(boost) if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil { return gtserror.Newf("db error storing interaction request: %w", err) } |
