diff options
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 +}  | 
