From 71261c62c25548b23ca188f2970cfe1b13942bc2 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:08:42 +0200 Subject: [PATCH] [chore] Reject replies to rejected replies (#3291) * [chore] Reject replies to rejected replies * tweak * don't set URI for implicit Rejects --- internal/federation/dereferencing/status.go | 16 +-- .../dereferencing/status_permitted.go | 128 +++++++++++++++++- 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go index 2338f55f8..a3c1b7371 100644 --- a/internal/federation/dereferencing/status.go +++ b/internal/federation/dereferencing/status.go @@ -373,7 +373,7 @@ func (d *Dereferencer) enrichStatus( requestUser string, uri *url.URL, status *gtsmodel.Status, - apubStatus ap.Statusable, + statusable ap.Statusable, ) ( *gtsmodel.Status, ap.Statusable, @@ -393,7 +393,7 @@ func (d *Dereferencer) enrichStatus( return nil, nil, gtserror.SetUnretrievable(err) } - if apubStatus == nil { + if statusable == nil { // Dereference latest version of the status. rsp, err := tsport.Dereference(ctx, uri) if err != nil { @@ -402,7 +402,7 @@ func (d *Dereferencer) enrichStatus( } // Attempt to resolve ActivityPub status from response. - apubStatus, err = ap.ResolveStatusable(ctx, rsp.Body) + statusable, err = ap.ResolveStatusable(ctx, rsp.Body) // Tidy up now done. _ = rsp.Body.Close() @@ -444,7 +444,7 @@ func (d *Dereferencer) enrichStatus( } // Get the attributed-to account in order to fetch profile. - attributedTo, err := ap.ExtractAttributedToURI(apubStatus) + attributedTo, err := ap.ExtractAttributedToURI(statusable) if err != nil { return nil, nil, gtserror.New("attributedTo was empty") } @@ -460,7 +460,7 @@ func (d *Dereferencer) enrichStatus( // ActivityPub model was recently dereferenced, so assume passed status // may contain out-of-date information. Convert AP model to our GTS model. - latestStatus, err := d.converter.ASStatusToStatus(ctx, apubStatus) + latestStatus, err := d.converter.ASStatusToStatus(ctx, statusable) if err != nil { return nil, nil, gtserror.Newf("error converting statusable to gts model for status %s: %w", uri, err) } @@ -479,8 +479,8 @@ func (d *Dereferencer) enrichStatus( matches, err := util.URIMatches( uri, append( - ap.GetURL(apubStatus), // status URL(s) - ap.GetJSONLDId(apubStatus), // status URI + ap.GetURL(statusable), // status URL(s) + ap.GetJSONLDId(statusable), // status URI )..., ) if err != nil { @@ -591,7 +591,7 @@ func (d *Dereferencer) enrichStatus( } } - return latestStatus, apubStatus, nil + return latestStatus, statusable, nil } func (d *Dereferencer) fetchStatusMentions( 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 {