summaryrefslogtreecommitdiff
path: root/internal/processing/workers
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-07-26 12:04:28 +0200
committerLibravatar GitHub <noreply@github.com>2024-07-26 12:04:28 +0200
commit8ab2b19a946251f258446d22f420d401f61d22f6 (patch)
tree39fb674f135fd1cfcf4de5b319913f0d0c17d11a /internal/processing/workers
parent[docs] Add separate migration section + instructions for moving to GtS and no... (diff)
downloadgotosocial-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.go217
-rw-r--r--internal/processing/workers/fromclientapi.go192
-rw-r--r--internal/processing/workers/fromfediapi.go227
-rw-r--r--internal/processing/workers/surfacenotify.go222
-rw-r--r--internal/processing/workers/util.go129
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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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
+}