diff options
author | 2024-07-26 12:04:28 +0200 | |
---|---|---|
committer | 2024-07-26 12:04:28 +0200 | |
commit | 8ab2b19a946251f258446d22f420d401f61d22f6 (patch) | |
tree | 39fb674f135fd1cfcf4de5b319913f0d0c17d11a /internal/processing/workers | |
parent | [docs] Add separate migration section + instructions for moving to GtS and no... (diff) | |
download | gotosocial-8ab2b19a946251f258446d22f420d401f61d22f6.tar.xz |
[feature] Federate interaction policies + Accepts; enforce policies (#3138)
* [feature] Federate interaction policies + Accepts; enforce policies
* use Acceptable type
* fix index
* remove appendIRIStrs
* add GetAccept federatingdb function
* lock on object IRI
Diffstat (limited to 'internal/processing/workers')
-rw-r--r-- | internal/processing/workers/federate.go | 217 | ||||
-rw-r--r-- | internal/processing/workers/fromclientapi.go | 192 | ||||
-rw-r--r-- | internal/processing/workers/fromfediapi.go | 227 | ||||
-rw-r--r-- | internal/processing/workers/surfacenotify.go | 222 | ||||
-rw-r--r-- | internal/processing/workers/util.go | 129 |
5 files changed, 931 insertions, 56 deletions
diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go index 3538c9958..d71bb0a83 100644 --- a/internal/processing/workers/federate.go +++ b/internal/processing/workers/federate.go @@ -23,12 +23,14 @@ import ( "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" + "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/state" "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // federate wraps functions for federating @@ -135,6 +137,12 @@ func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account) return nil } +// CreateStatus sends the given status out to relevant +// recipients with the Outbox of the status creator. +// +// If the status is pending approval, then it will be +// sent **ONLY** to the inbox of the account it replies to, +// ignoring shared inboxes. func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) error { // Do nothing if the status // shouldn't be federated. @@ -153,18 +161,32 @@ func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) er return gtserror.Newf("error populating status: %w", err) } - // Parse the outbox URI of the status author. - outboxIRI, err := parseURI(status.Account.OutboxURI) - if err != nil { - return err - } - // Convert status to AS Statusable implementing type. statusable, err := f.converter.StatusToAS(ctx, status) if err != nil { return gtserror.Newf("error converting status to Statusable: %w", err) } + // If status is pending approval, + // it must be a reply. Deliver it + // **ONLY** to the account it replies + // to, on behalf of the replier. + if util.PtrOrValue(status.PendingApproval, false) { + return f.deliverToInboxOnly( + ctx, + status.Account, + status.InReplyToAccount, + // Status has to be wrapped in Create activity. + typeutils.WrapStatusableInCreate(statusable, false), + ) + } + + // Parse the outbox URI of the status author. + outboxIRI, err := parseURI(status.Account.OutboxURI) + if err != nil { + return err + } + // Send a Create activity with Statusable via the Actor's outbox. create := typeutils.WrapStatusableInCreate(statusable, false) if _, err := f.FederatingActor().Send(ctx, outboxIRI, create); err != nil { @@ -672,6 +694,12 @@ func (f *federate) RejectFollow(ctx context.Context, follow *gtsmodel.Follow) er return nil } +// Like sends the given fave out to relevant +// recipients with the Outbox of the status creator. +// +// If the fave is pending approval, then it will be +// sent **ONLY** to the inbox of the account it faves, +// ignoring shared inboxes. func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error { // Populate model. if err := f.state.DB.PopulateStatusFave(ctx, fave); err != nil { @@ -684,18 +712,30 @@ func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error { return nil } - // Parse relevant URI(s). - outboxIRI, err := parseURI(fave.Account.OutboxURI) - if err != nil { - return err - } - // Create the ActivityStreams Like. like, err := f.converter.FaveToAS(ctx, fave) if err != nil { return gtserror.Newf("error converting fave to AS Like: %w", err) } + // If fave is pending approval, + // deliver it **ONLY** to the account + // it faves, on behalf of the faver. + if util.PtrOrValue(fave.PendingApproval, false) { + return f.deliverToInboxOnly( + ctx, + fave.Account, + fave.TargetAccount, + like, + ) + } + + // Parse relevant URI(s). + outboxIRI, err := parseURI(fave.Account.OutboxURI) + if err != nil { + return err + } + // Send the Like via the Actor's outbox. if _, err := f.FederatingActor().Send( ctx, outboxIRI, like, @@ -709,6 +749,12 @@ func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error { return nil } +// Announce sends the given boost out to relevant +// recipients with the Outbox of the status creator. +// +// If the boost is pending approval, then it will be +// sent **ONLY** to the inbox of the account it boosts, +// ignoring shared inboxes. func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error { // Populate model. if err := f.state.DB.PopulateStatus(ctx, boost); err != nil { @@ -721,12 +767,6 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error { return nil } - // Parse relevant URI(s). - outboxIRI, err := parseURI(boost.Account.OutboxURI) - if err != nil { - return err - } - // Create the ActivityStreams Announce. announce, err := f.converter.BoostToAS( ctx, @@ -738,6 +778,24 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error { return gtserror.Newf("error converting boost to AS: %w", err) } + // If announce is pending approval, + // deliver it **ONLY** to the account + // it boosts, on behalf of the booster. + if util.PtrOrValue(boost.PendingApproval, false) { + return f.deliverToInboxOnly( + ctx, + boost.Account, + boost.BoostOfAccount, + announce, + ) + } + + // Parse relevant URI(s). + outboxIRI, err := parseURI(boost.Account.OutboxURI) + if err != nil { + return err + } + // Send the Announce via the Actor's outbox. if _, err := f.FederatingActor().Send( ctx, outboxIRI, announce, @@ -751,6 +809,57 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error { return nil } +// deliverToInboxOnly delivers the given Activity +// *only* to the inbox of targetAcct, on behalf of +// sendingAcct, regardless of the `to` and `cc` values +// set on the activity. This should be used specifically +// for sending "pending approval" activities. +func (f *federate) deliverToInboxOnly( + ctx context.Context, + sendingAcct *gtsmodel.Account, + targetAcct *gtsmodel.Account, + t vocab.Type, +) error { + if targetAcct.IsLocal() { + // If this is a local target, + // they've already received it. + return nil + } + + toInbox, err := url.Parse(targetAcct.InboxURI) + if err != nil { + return gtserror.Newf( + "error parsing target inbox uri: %w", + err, + ) + } + + tsport, err := f.TransportController().NewTransportForUsername( + ctx, + sendingAcct.Username, + ) + if err != nil { + return gtserror.Newf( + "error getting transport to deliver activity %T to target inbox %s: %w", + t, targetAcct.InboxURI, err, + ) + } + + m, err := ap.Serialize(t) + if err != nil { + return err + } + + if err := tsport.Deliver(ctx, m, toInbox); err != nil { + return gtserror.Newf( + "error delivering activity %T to target inbox %s: %w", + t, targetAcct.InboxURI, err, + ) + } + + return nil +} + func (f *federate) UpdateAccount(ctx context.Context, account *gtsmodel.Account) error { // Populate model. if err := f.state.DB.PopulateAccount(ctx, account); err != nil { @@ -1015,3 +1124,75 @@ func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) e return nil } + +func (f *federate) AcceptInteraction( + ctx context.Context, + approval *gtsmodel.InteractionApproval, +) error { + // Populate model. + if err := f.state.DB.PopulateInteractionApproval(ctx, approval); err != nil { + return gtserror.Newf("error populating approval: %w", err) + } + + // Bail if interacting account is ours: + // we've already accepted internally and + // shouldn't send an Accept to ourselves. + if approval.InteractingAccount.IsLocal() { + return nil + } + + // Bail if account isn't ours: + // we can't Accept on another + // instance's behalf. (This + // should never happen but...) + if approval.Account.IsRemote() { + return nil + } + + // Parse relevant URI(s). + outboxIRI, err := parseURI(approval.Account.OutboxURI) + if err != nil { + return err + } + + acceptingAcctIRI, err := parseURI(approval.Account.URI) + if err != nil { + return err + } + + interactingAcctURI, err := parseURI(approval.InteractingAccount.URI) + if err != nil { + return err + } + + interactionURI, err := parseURI(approval.InteractionURI) + if err != nil { + return err + } + + // Create a new Accept. + accept := streams.NewActivityStreamsAccept() + + // Set interacted-with account + // as Actor of the Accept. + ap.AppendActorIRIs(accept, acceptingAcctIRI) + + // Set the interacted-with object + // as Object of the Accept. + ap.AppendObjectIRIs(accept, interactionURI) + + // Address the Accept To the interacting acct. + ap.AppendTo(accept, interactingAcctURI) + + // Send the Accept via the Actor's outbox. + if _, err := f.FederatingActor().Send( + ctx, outboxIRI, accept, + ); err != nil { + return gtserror.Newf( + "error sending activity %T for %v via outbox %s: %w", + accept, approval.InteractionType, outboxIRI, err, + ) + } + + return nil +} diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go index d5d4265e1..7f1b5780c 100644 --- a/internal/processing/workers/fromclientapi.go +++ b/internal/processing/workers/fromclientapi.go @@ -135,6 +135,18 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro // ACCEPT USER (ie., new user+account sign-up) case ap.ObjectProfile: return p.clientAPI.AcceptUser(ctx, cMsg) + + // ACCEPT NOTE/STATUS (ie., accept a reply) + case ap.ObjectNote: + return p.clientAPI.AcceptReply(ctx, cMsg) + + // ACCEPT LIKE + case ap.ActivityLike: + return p.clientAPI.AcceptLike(ctx, cMsg) + + // ACCEPT BOOST + case ap.ActivityAnnounce: + return p.clientAPI.AcceptAnnounce(ctx, cMsg) } // REJECT SOMETHING @@ -236,6 +248,61 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel) } + // If pending approval is true then status must + // reply to a status (either one of ours or a + // remote) that requires approval for the reply. + pendingApproval := util.PtrOrValue( + status.PendingApproval, + false, + ) + + switch { + case pendingApproval && !status.PreApproved: + // If approval is required and status isn't + // preapproved, then send out the Create to + // only the replied-to account (if it's remote), + // and/or notify the account that's being + // interacted with (if it's local): they can + // approve or deny the interaction later. + + // Notify *local* account of pending reply. + if err := p.surface.notifyPendingReply(ctx, status); err != nil { + log.Errorf(ctx, "error notifying pending reply: %v", err) + } + + // Send Create to *remote* account inbox ONLY. + if err := p.federate.CreateStatus(ctx, status); err != nil { + log.Errorf(ctx, "error federating pending reply: %v", err) + } + + // Return early. + return nil + + case pendingApproval && status.PreApproved: + // If approval is required and status is + // preapproved, that means this is a reply + // to one of our statuses with permission + // that matched on a following/followers + // collection. Do the Accept immediately and + // then process everything else as normal, + // sending out the Create with the approval + // URI attached. + + // Put approval in the database and + // update the status with approvedBy URI. + approval, err := p.utils.approveReply(ctx, status) + if err != nil { + return gtserror.Newf("error pre-approving reply: %w", err) + } + + // Send out the approval as Accept. + if err := p.federate.AcceptInteraction(ctx, approval); err != nil { + return gtserror.Newf("error federating pre-approval of reply: %w", err) + } + + // Don't return, just continue as normal. + } + // Update stats for the actor account. if err := p.utils.incrementStatusesCount(ctx, cMsg.Origin, status); err != nil { log.Errorf(ctx, "error updating account stats: %v", err) @@ -362,6 +429,61 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI return gtserror.Newf("error populating status fave: %w", err) } + // If pending approval is true then fave must + // target a status (either one of ours or a + // remote) that requires approval for the fave. + pendingApproval := util.PtrOrValue( + fave.PendingApproval, + false, + ) + + switch { + case pendingApproval && !fave.PreApproved: + // If approval is required and fave isn't + // preapproved, then send out the Like to + // only the faved account (if it's remote), + // and/or notify the account that's being + // interacted with (if it's local): they can + // approve or deny the interaction later. + + // Notify *local* account of pending reply. + if err := p.surface.notifyPendingFave(ctx, fave); err != nil { + log.Errorf(ctx, "error notifying pending fave: %v", err) + } + + // Send Like to *remote* account inbox ONLY. + if err := p.federate.Like(ctx, fave); err != nil { + log.Errorf(ctx, "error federating pending Like: %v", err) + } + + // Return early. + return nil + + case pendingApproval && fave.PreApproved: + // If approval is required and fave is + // preapproved, that means this is a fave + // of one of our statuses with permission + // that matched on a following/followers + // collection. Do the Accept immediately and + // then process everything else as normal, + // sending out the Like with the approval + // URI attached. + + // Put approval in the database and + // update the fave with approvedBy URI. + approval, err := p.utils.approveFave(ctx, fave) + if err != nil { + return gtserror.Newf("error pre-approving fave: %w", err) + } + + // Send out the approval as Accept. + if err := p.federate.AcceptInteraction(ctx, approval); err != nil { + return gtserror.Newf("error federating pre-approval of fave: %w", err) + } + + // Don't return, just continue as normal. + } + if err := p.surface.notifyFave(ctx, fave); err != nil { log.Errorf(ctx, "error notifying fave: %v", err) } @@ -383,6 +505,61 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel) } + // If pending approval is true then status must + // boost a status (either one of ours or a + // remote) that requires approval for the boost. + pendingApproval := util.PtrOrValue( + boost.PendingApproval, + false, + ) + + switch { + case pendingApproval && !boost.PreApproved: + // If approval is required and boost isn't + // preapproved, then send out the Announce to + // only the boosted account (if it's remote), + // and/or notify the account that's being + // interacted with (if it's local): they can + // approve or deny the interaction later. + + // Notify *local* account of pending announce. + if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil { + log.Errorf(ctx, "error notifying pending boost: %v", err) + } + + // Send Announce to *remote* account inbox ONLY. + if err := p.federate.Announce(ctx, boost); err != nil { + log.Errorf(ctx, "error federating pending Announce: %v", err) + } + + // Return early. + return nil + + case pendingApproval && boost.PreApproved: + // If approval is required and boost is + // preapproved, that means this is a boost + // of one of our statuses with permission + // that matched on a following/followers + // collection. Do the Accept immediately and + // then process everything else as normal, + // sending out the Create with the approval + // URI attached. + + // Put approval in the database and + // update the boost with approvedBy URI. + approval, err := p.utils.approveAnnounce(ctx, boost) + if err != nil { + return gtserror.Newf("error pre-approving boost: %w", err) + } + + // Send out the approval as Accept. + if err := p.federate.AcceptInteraction(ctx, approval); err != nil { + return gtserror.Newf("error federating pre-approval of boost: %w", err) + } + + // Don't return, just continue as normal. + } + // Update stats for the actor account. if err := p.utils.incrementStatusesCount(ctx, cMsg.Origin, boost); err != nil { log.Errorf(ctx, "error updating account stats: %v", err) @@ -874,3 +1051,18 @@ func (p *clientAPI) RejectUser(ctx context.Context, cMsg *messages.FromClientAPI return nil } + +func (p *clientAPI) AcceptLike(ctx context.Context, cMsg *messages.FromClientAPI) error { + // TODO + return nil +} + +func (p *clientAPI) AcceptReply(ctx context.Context, cMsg *messages.FromClientAPI) error { + // TODO + return nil +} + +func (p *clientAPI) AcceptAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error { + // TODO + return nil +} diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go index ac4003f6a..63d1f0d16 100644 --- a/internal/processing/workers/fromfediapi.go +++ b/internal/processing/workers/fromfediapi.go @@ -122,11 +122,23 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF // ACCEPT SOMETHING case ap.ActivityAccept: - switch fMsg.APObjectType { //nolint:gocritic + switch fMsg.APObjectType { - // ACCEPT FOLLOW + // ACCEPT (pending) FOLLOW case ap.ActivityFollow: return p.fediAPI.AcceptFollow(ctx, fMsg) + + // ACCEPT (pending) LIKE + case ap.ActivityLike: + return p.fediAPI.AcceptLike(ctx, fMsg) + + // ACCEPT (pending) REPLY + case ap.ObjectNote: + return p.fediAPI.AcceptReply(ctx, fMsg) + + // ACCEPT (pending) ANNOUNCE + case ap.ActivityAnnounce: + return p.fediAPI.AcceptAnnounce(ctx, fMsg) } // DELETE SOMETHING @@ -216,6 +228,52 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI) return nil } + // If pending approval is true then + // status must reply to a LOCAL status + // that requires approval for the reply. + pendingApproval := util.PtrOrValue( + status.PendingApproval, + false, + ) + + switch { + case pendingApproval && !status.PreApproved: + // If approval is required and status isn't + // preapproved, then just notify the account + // that's being interacted with: they can + // approve or deny the interaction later. + + // Notify *local* account of pending reply. + if err := p.surface.notifyPendingReply(ctx, status); err != nil { + log.Errorf(ctx, "error notifying pending reply: %v", err) + } + + // Return early. + return nil + + case pendingApproval && status.PreApproved: + // If approval is required and status is + // preapproved, that means this is a reply + // to one of our statuses with permission + // that matched on a following/followers + // collection. Do the Accept immediately and + // then process everything else as normal. + + // Put approval in the database and + // update the status with approvedBy URI. + approval, err := p.utils.approveReply(ctx, status) + if err != nil { + return gtserror.Newf("error pre-approving reply: %w", err) + } + + // Send out the approval as Accept. + if err := p.federate.AcceptInteraction(ctx, approval); err != nil { + return gtserror.Newf("error federating pre-approval of reply: %w", err) + } + + // Don't return, just continue as normal. + } + // Update stats for the remote account. if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, status); err != nil { log.Errorf(ctx, "error updating account stats: %v", err) @@ -348,6 +406,52 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er return gtserror.Newf("error populating status fave: %w", err) } + // If pending approval is true then + // fave must target a LOCAL status + // that requires approval for the fave. + pendingApproval := util.PtrOrValue( + fave.PendingApproval, + false, + ) + + switch { + case pendingApproval && !fave.PreApproved: + // If approval is required and fave isn't + // preapproved, then just notify the account + // that's being interacted with: they can + // approve or deny the interaction later. + + // Notify *local* account of pending fave. + if err := p.surface.notifyPendingFave(ctx, fave); err != nil { + log.Errorf(ctx, "error notifying pending fave: %v", err) + } + + // Return early. + return nil + + case pendingApproval && fave.PreApproved: + // If approval is required and fave is + // preapproved, that means this is a fave + // of one of our statuses with permission + // that matched on a following/followers + // collection. Do the Accept immediately and + // then process everything else as normal. + + // Put approval in the database and + // update the fave with approvedBy URI. + approval, err := p.utils.approveFave(ctx, fave) + if err != nil { + return gtserror.Newf("error pre-approving fave: %w", err) + } + + // Send out the approval as Accept. + if err := p.federate.AcceptInteraction(ctx, approval); err != nil { + return gtserror.Newf("error federating pre-approval of fave: %w", err) + } + + // Don't return, just continue as normal. + } + if err := p.surface.notifyFave(ctx, fave); err != nil { log.Errorf(ctx, "error notifying fave: %v", err) } @@ -365,8 +469,9 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel) } - // Dereference status that this boosts, note - // that this will handle storing the boost in + // Dereference into a boost wrapper status. + // + // Note: this will handle storing the boost in // the db, and dereferencing the target status // ancestors / descendants where appropriate. var err error @@ -376,8 +481,10 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI fMsg.Receiving.Username, ) if err != nil { - if gtserror.IsUnretrievable(err) { - // Boosted status domain blocked, nothing to do. + if gtserror.IsUnretrievable(err) || + gtserror.NotPermitted(err) { + // Boosted status domain blocked, or + // otherwise not permitted, nothing to do. log.Debugf(ctx, "skipping announce: %v", err) return nil } @@ -386,6 +493,52 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI return gtserror.Newf("error dereferencing announce: %w", err) } + // If pending approval is true then + // boost must target a LOCAL status + // that requires approval for the boost. + pendingApproval := util.PtrOrValue( + boost.PendingApproval, + false, + ) + + switch { + case pendingApproval && !boost.PreApproved: + // If approval is required and boost isn't + // preapproved, then just notify the account + // that's being interacted with: they can + // approve or deny the interaction later. + + // Notify *local* account of pending announce. + if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil { + log.Errorf(ctx, "error notifying pending boost: %v", err) + } + + // Return early. + return nil + + case pendingApproval && boost.PreApproved: + // If approval is required and status is + // preapproved, that means this is a boost + // of one of our statuses with permission + // that matched on a following/followers + // collection. Do the Accept immediately and + // then process everything else as normal. + + // Put approval in the database and + // update the boost with approvedBy URI. + approval, err := p.utils.approveAnnounce(ctx, boost) + if err != nil { + return gtserror.Newf("error pre-approving boost: %w", err) + } + + // Send out the approval as Accept. + if err := p.federate.AcceptInteraction(ctx, approval); err != nil { + return gtserror.Newf("error federating pre-approval of boost: %w", err) + } + + // Don't return, just continue as normal. + } + // Update stats for the remote account. if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, boost); err != nil { log.Errorf(ctx, "error updating account stats: %v", err) @@ -549,6 +702,68 @@ func (p *fediAPI) AcceptFollow(ctx context.Context, fMsg *messages.FromFediAPI) return nil } +func (p *fediAPI) AcceptLike(ctx context.Context, fMsg *messages.FromFediAPI) error { + // TODO: Add something here if we ever implement sending out Likes to + // followers more broadly and not just the owner of the Liked status. + return nil +} + +func (p *fediAPI) AcceptReply(ctx context.Context, fMsg *messages.FromFediAPI) error { + status, 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 { + log.Errorf(ctx, "error updating account stats: %v", err) + } + + // Timeline and notify the status. + if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil { + log.Errorf(ctx, "error timelining and notifying status: %v", err) + } + + // Interaction counts changed on the replied-to status; + // uncache the prepared version from all timelines. + p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID) + + // Send out the reply again, fully this time. + if err := p.federate.CreateStatus(ctx, status); err != nil { + log.Errorf(ctx, "error federating announce: %v", err) + } + + return nil +} + +func (p *fediAPI) AcceptAnnounce(ctx context.Context, fMsg *messages.FromFediAPI) error { + boost, 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, boost.Account, boost); err != nil { + log.Errorf(ctx, "error updating account stats: %v", err) + } + + // Timeline and notify the boost wrapper status. + 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(ctx, boost.BoostOfID) + + // Send out the boost again, fully this time. + if err := p.federate.Announce(ctx, boost); err != nil { + log.Errorf(ctx, "error federating announce: %v", err) + } + + return nil +} + func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI) error { // Cast the existing Status model attached to msg. existing, ok := fMsg.GTSModel.(*gtsmodel.Status) diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go index edeb4b57e..872ccca65 100644 --- a/internal/processing/workers/surfacenotify.go +++ b/internal/processing/workers/surfacenotify.go @@ -32,6 +32,62 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/util" ) +// notifyPendingReply notifies the account replied-to +// by the given status that they have a new reply, +// and that approval is pending. +func (s *Surface) notifyPendingReply( + ctx context.Context, + status *gtsmodel.Status, +) error { + // Beforehand, ensure the passed status is fully populated. + if err := s.State.DB.PopulateStatus(ctx, status); err != nil { + return gtserror.Newf("error populating status %s: %w", status.ID, err) + } + + if status.InReplyToAccount.IsRemote() { + // Don't notify + // remote accounts. + return nil + } + + if status.AccountID == status.InReplyToAccountID { + // Don't notify + // self-replies. + return nil + } + + // Ensure thread not muted + // by replied-to account. + muted, err := s.State.DB.IsThreadMutedByAccount( + ctx, + status.ThreadID, + status.InReplyToAccountID, + ) + if err != nil { + return gtserror.Newf("error checking status thread mute %s: %w", status.ThreadID, err) + } + + if muted { + // The replied-to account + // has muted the thread. + // Don't pester them. + return nil + } + + // notify mentioned + // by status author. + if err := s.Notify(ctx, + gtsmodel.NotificationPendingReply, + status.InReplyToAccount, + status.Account, + status.ID, + ); err != nil { + return gtserror.Newf("error notifying replied-to account %s: %w", status.InReplyToAccountID, err) + } + + return nil +} + // notifyMentions iterates through mentions on the // given status, and notifies each mentioned account // that they have a new mention. @@ -181,20 +237,82 @@ func (s *Surface) notifyFave( ctx context.Context, fave *gtsmodel.StatusFave, ) error { + notifyable, err := s.notifyableFave(ctx, fave) + if err != nil { + return err + } + + if !notifyable { + // Nothing to do. + return nil + } + + // notify status author + // of fave by account. + if err := s.Notify(ctx, + gtsmodel.NotificationFave, + fave.TargetAccount, + fave.Account, + fave.StatusID, + ); err != nil { + return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err) + } + + return nil +} + +// notifyPendingFave notifies the target of the +// given fave that their status has been faved +// and that approval is required. +func (s *Surface) notifyPendingFave( + ctx context.Context, + fave *gtsmodel.StatusFave, +) error { + notifyable, err := s.notifyableFave(ctx, fave) + if err != nil { + return err + } + + if !notifyable { + // Nothing to do. + return nil + } + + // notify status author + // of fave by account. + if err := s.Notify(ctx, + gtsmodel.NotificationPendingFave, + fave.TargetAccount, + fave.Account, + fave.StatusID, + ); err != nil { + return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err) + } + + return nil +} + +// notifyableFave checks that the given +// fave should be notified, taking account +// of localness of receiving account, and mutes. +func (s *Surface) notifyableFave( + ctx context.Context, + fave *gtsmodel.StatusFave, +) (bool, error) { if fave.TargetAccountID == fave.AccountID { // Self-fave, nothing to do. - return nil + return false, nil } // Beforehand, ensure the passed status fave is fully populated. if err := s.State.DB.PopulateStatusFave(ctx, fave); err != nil { - return gtserror.Newf("error populating fave %s: %w", fave.ID, err) + return false, gtserror.Newf("error populating fave %s: %w", fave.ID, err) } if fave.TargetAccount.IsRemote() { // no need to notify // remote accounts. - return nil + return false, nil } // Ensure favee hasn't @@ -205,54 +323,105 @@ func (s *Surface) notifyFave( fave.TargetAccountID, ) if err != nil { - return gtserror.Newf("error checking status thread mute %s: %w", fave.StatusID, err) + return false, gtserror.Newf("error checking status thread mute %s: %w", fave.StatusID, err) } if muted { // Favee doesn't want // notifs for this thread. + return false, nil + } + + return true, nil +} + +// notifyAnnounce notifies the status boost target +// account that their status has been boosted. +func (s *Surface) notifyAnnounce( + ctx context.Context, + boost *gtsmodel.Status, +) error { + notifyable, err := s.notifyableAnnounce(ctx, boost) + if err != nil { + return err + } + + if !notifyable { + // Nothing to do. return nil } // notify status author - // of fave by account. + // of boost by account. if err := s.Notify(ctx, - gtsmodel.NotificationFave, - fave.TargetAccount, - fave.Account, - fave.StatusID, + gtsmodel.NotificationReblog, + boost.BoostOfAccount, + boost.Account, + boost.ID, ); err != nil { - return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err) + return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err) } return nil } -// notifyAnnounce notifies the status boost target -// account that their status has been boosted. -func (s *Surface) notifyAnnounce( +// notifyPendingAnnounce notifies the status boost +// target account that their status has been boosted, +// and that the boost requires approval. +func (s *Surface) notifyPendingAnnounce( ctx context.Context, - status *gtsmodel.Status, + boost *gtsmodel.Status, ) error { + notifyable, err := s.notifyableAnnounce(ctx, boost) + if err != nil { + return err + } + + if !notifyable { + // Nothing to do. + return nil + } + + // notify status author + // of boost by account. + if err := s.Notify(ctx, + gtsmodel.NotificationPendingReblog, + boost.BoostOfAccount, + boost.Account, + boost.ID, + ); err != nil { + return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err) + } + + return nil +} + +// notifyableAnnounce checks that the given +// announce should be notified, taking account +// of localness of receiving account, and mutes. +func (s *Surface) notifyableAnnounce( + ctx context.Context, + status *gtsmodel.Status, +) (bool, error) { if status.BoostOfID == "" { // Not a boost, nothing to do. - return nil + return false, nil } if status.BoostOfAccountID == status.AccountID { // Self-boost, nothing to do. - return nil + return false, nil } // Beforehand, ensure the passed status is fully populated. if err := s.State.DB.PopulateStatus(ctx, status); err != nil { - return gtserror.Newf("error populating status %s: %w", status.ID, err) + return false, gtserror.Newf("error populating status %s: %w", status.ID, err) } if status.BoostOfAccount.IsRemote() { // no need to notify // remote accounts. - return nil + return false, nil } // Ensure boostee hasn't @@ -264,27 +433,16 @@ func (s *Surface) notifyAnnounce( ) if err != nil { - return gtserror.Newf("error checking status thread mute %s: %w", status.BoostOfID, err) + return false, gtserror.Newf("error checking status thread mute %s: %w", status.BoostOfID, err) } if muted { // Boostee doesn't want // notifs for this thread. - return nil + return false, nil } - // notify status author - // of boost by account. - if err := s.Notify(ctx, - gtsmodel.NotificationReblog, - status.BoostOfAccount, - status.Account, - status.ID, - ); err != nil { - return gtserror.Newf("error notifying status author %s: %w", status.BoostOfAccountID, err) - } - - return nil + return true, nil } func (s *Surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) error { diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go index 915370976..994242d37 100644 --- a/internal/processing/workers/util.go +++ b/internal/processing/workers/util.go @@ -26,10 +26,13 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtscontext" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/processing/account" "github.com/superseriousbusiness/gotosocial/internal/processing/media" "github.com/superseriousbusiness/gotosocial/internal/state" + "github.com/superseriousbusiness/gotosocial/internal/uris" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // util provides util functions used by both @@ -498,3 +501,129 @@ func (u *utils) decrementFollowRequestsCount( return nil } + +// approveFave stores + returns an +// interactionApproval for a fave. +func (u *utils) approveFave( + ctx context.Context, + fave *gtsmodel.StatusFave, +) (*gtsmodel.InteractionApproval, error) { + id := id.NewULID() + + approval := >smodel.InteractionApproval{ + ID: id, + AccountID: fave.TargetAccountID, + Account: fave.TargetAccount, + InteractingAccountID: fave.AccountID, + InteractingAccount: fave.Account, + InteractionURI: fave.URI, + InteractionType: gtsmodel.InteractionLike, + URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id), + } + + if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil { + err := gtserror.Newf("db error inserting interaction approval: %w", err) + return nil, err + } + + // Mark the fave itself as now approved. + fave.PendingApproval = util.Ptr(false) + fave.PreApproved = false + fave.ApprovedByURI = approval.URI + + if err := u.state.DB.UpdateStatusFave( + ctx, + fave, + "pending_approval", + "approved_by_uri", + ); err != nil { + err := gtserror.Newf("db error updating status fave: %w", err) + return nil, err + } + + return approval, nil +} + +// approveReply stores + returns an +// interactionApproval for a reply. +func (u *utils) approveReply( + ctx context.Context, + status *gtsmodel.Status, +) (*gtsmodel.InteractionApproval, error) { + id := id.NewULID() + + approval := >smodel.InteractionApproval{ + ID: id, + AccountID: status.InReplyToAccountID, + Account: status.InReplyToAccount, + InteractingAccountID: status.AccountID, + InteractingAccount: status.Account, + InteractionURI: status.URI, + InteractionType: gtsmodel.InteractionReply, + URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id), + } + + if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil { + err := gtserror.Newf("db error inserting interaction approval: %w", err) + return nil, err + } + + // Mark the status itself as now approved. + status.PendingApproval = util.Ptr(false) + status.PreApproved = false + status.ApprovedByURI = approval.URI + + if err := u.state.DB.UpdateStatus( + ctx, + status, + "pending_approval", + "approved_by_uri", + ); err != nil { + err := gtserror.Newf("db error updating status: %w", err) + return nil, err + } + + return approval, nil +} + +// approveAnnounce stores + returns an +// interactionApproval for an announce. +func (u *utils) approveAnnounce( + ctx context.Context, + boost *gtsmodel.Status, +) (*gtsmodel.InteractionApproval, error) { + id := id.NewULID() + + approval := >smodel.InteractionApproval{ + ID: id, + AccountID: boost.BoostOfAccountID, + Account: boost.BoostOfAccount, + InteractingAccountID: boost.AccountID, + InteractingAccount: boost.Account, + InteractionURI: boost.URI, + InteractionType: gtsmodel.InteractionReply, + URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id), + } + + if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil { + err := gtserror.Newf("db error inserting interaction approval: %w", err) + return nil, err + } + + // Mark the status itself as now approved. + boost.PendingApproval = util.Ptr(false) + boost.PreApproved = false + boost.ApprovedByURI = approval.URI + + if err := u.state.DB.UpdateStatus( + ctx, + boost, + "pending_approval", + "approved_by_uri", + ); err != nil { + err := gtserror.Newf("db error updating boost wrapper status: %w", err) + return nil, err + } + + return approval, nil +} |