diff options
Diffstat (limited to 'internal/federation/dereferencing/status_permitted.go')
-rw-r--r-- | internal/federation/dereferencing/status_permitted.go | 128 |
1 files changed, 122 insertions, 6 deletions
diff --git a/internal/federation/dereferencing/status_permitted.go b/internal/federation/dereferencing/status_permitted.go index 9bd74811e..2aecfc9b7 100644 --- a/internal/federation/dereferencing/status_permitted.go +++ b/internal/federation/dereferencing/status_permitted.go @@ -19,12 +19,18 @@ package dereferencing import ( "context" + "errors" "net/url" + "time" "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/db" + "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/uris" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -43,6 +49,14 @@ import ( // pending approval, then "PendingApproval" will be set // to "true" on status. Callers should check this // and handle it as appropriate. +// +// If status is a reply that is not permitted based on +// interaction policies, or status replies to a status +// that's been Rejected before (ie., it has a rejected +// InteractionRequest stored in the db) then the reply +// will also be rejected, and a pre-rejected interaction +// request will be stored for it before doing cleanup, +// if one didn't already exist. func (d *Dereferencer) isPermittedStatus( ctx context.Context, requestUser string, @@ -58,7 +72,7 @@ func (d *Dereferencer) isPermittedStatus( log.Warnf(ctx, "status author suspended: %s", status.AccountURI) permitted = false - case status.InReplyTo != nil: + case status.InReplyToURI != "": // Status is a reply, check permissivity. permitted, err = d.isPermittedReply(ctx, requestUser, @@ -101,8 +115,90 @@ func (d *Dereferencer) isPermittedReply( requestUser string, status *gtsmodel.Status, ) (bool, error) { - // Extract reply from status. - inReplyTo := status.InReplyTo + var ( + statusURI = status.URI // Definitely set. + inReplyToURI = status.InReplyToURI // Definitely set. + inReplyTo = status.InReplyTo // Might not yet be set. + ) + + // Check if status with this URI has previously been rejected. + req, err := d.state.DB.GetInteractionRequestByInteractionURI( + gtscontext.SetBarebones(ctx), + statusURI, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting interaction request: %w", err) + return false, err + } + + if req != nil && req.IsRejected() { + // This status has been + // rejected reviously, so + // it's not permitted now. + return false, nil + } + + // Check if replied-to status has previously been rejected. + req, err = d.state.DB.GetInteractionRequestByInteractionURI( + gtscontext.SetBarebones(ctx), + inReplyToURI, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("db error getting interaction request: %w", err) + return false, err + } + + if req != nil && req.IsRejected() { + // This status's parent was rejected, so + // implicitly this reply should be rejected too. + // + // We know already that we haven't inserted + // a rejected interaction request for this + // status yet so do it before returning. + id := id.NewULID() + + // To ensure the Reject chain stays coherent, + // borrow fields from the up-thread rejection. + // This collapses the chain beyond the first + // rejected reply and allows us to avoid derefing + // further replies we already know we don't want. + statusID := req.StatusID + targetAccountID := req.TargetAccountID + + // As nobody is actually Rejecting the reply + // directly, but it's an implicit Reject coming + // from our internal logic, don't bother setting + // a URI (it's not a required field anyway). + uri := "" + + rejection := >smodel.InteractionRequest{ + ID: id, + StatusID: statusID, + TargetAccountID: targetAccountID, + InteractingAccountID: status.AccountID, + InteractionURI: statusURI, + InteractionType: gtsmodel.InteractionReply, + URI: uri, + RejectedAt: time.Now(), + } + err := d.state.DB.PutInteractionRequest(ctx, rejection) + if err != nil && !errors.Is(err, db.ErrAlreadyExists) { + return false, gtserror.Newf("db error putting pre-rejected interaction request: %w", err) + } + + return false, nil + } + + if inReplyTo == nil { + // We didn't have the replied-to status in + // our database (yet) so we can't know if + // this reply is permitted or not. For now + // just return true; worst-case, the status + // sticks around on the instance for a couple + // hours until we try to dereference it again + // and realize it should be forbidden. + return true, nil + } if inReplyTo.BoostOfID != "" { // We do not permit replies to @@ -142,8 +238,28 @@ func (d *Dereferencer) isPermittedReply( } if replyable.Forbidden() { - // Replier is not permitted - // to do this interaction. + // Reply is not permitted. + // + // Insert a pre-rejected interaction request + // into the db and return. This ensures that + // replies to this now-rejected status aren't + // inadvertently permitted. + id := id.NewULID() + rejection := >smodel.InteractionRequest{ + ID: id, + StatusID: inReplyTo.ID, + TargetAccountID: inReplyTo.AccountID, + InteractingAccountID: status.AccountID, + InteractionURI: statusURI, + InteractionType: gtsmodel.InteractionReply, + URI: uris.GenerateURIForReject(inReplyTo.Account.Username, id), + RejectedAt: time.Now(), + } + err := d.state.DB.PutInteractionRequest(ctx, rejection) + if err != nil && !errors.Is(err, db.ErrAlreadyExists) { + return false, gtserror.Newf("db error putting pre-rejected interaction request: %w", err) + } + return false, nil } @@ -193,7 +309,7 @@ func (d *Dereferencer) isPermittedReply( ctx, requestUser, status.ApprovedByURI, - status.URI, + statusURI, inReplyTo.AccountURI, ); err != nil { |