diff options
Diffstat (limited to 'internal/processing/workers')
| -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 |
5 files changed, 637 insertions, 133 deletions
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) } |
