summaryrefslogtreecommitdiff
path: root/internal/federation/federatingdb/reject.go
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-09-10 14:34:49 +0200
committerLibravatar GitHub <noreply@github.com>2024-09-10 12:34:49 +0000
commit307d98e3862b6e867eea524b81d5428b03e6607c (patch)
treeb990378c5452f5779b85bd0d769db77a78f93600 /internal/federation/federatingdb/reject.go
parent[chore] status dereferencing improvements (#3255) (diff)
downloadgotosocial-307d98e3862b6e867eea524b81d5428b03e6607c.tar.xz
[feature] Process `Reject` of interaction via fedi API, put rejected statuses in the "sin bin" 😈 (#3271)
* [feature] Process `Reject` of interaction via fedi API, put rejected statuses in the "sin bin" * update test * move nil check back to `rejectStatusIRI`
Diffstat (limited to 'internal/federation/federatingdb/reject.go')
-rw-r--r--internal/federation/federatingdb/reject.go476
1 files changed, 434 insertions, 42 deletions
diff --git a/internal/federation/federatingdb/reject.go b/internal/federation/federatingdb/reject.go
index 929559031..404e19c4c 100644
--- a/internal/federation/federatingdb/reject.go
+++ b/internal/federation/federatingdb/reject.go
@@ -20,12 +20,17 @@ package federatingdb
import (
"context"
"errors"
- "fmt"
+ "time"
"codeberg.org/gruf/go-logger/v2/level"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "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/messages"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
@@ -48,63 +53,450 @@ func (f *federatingDB) Reject(ctx context.Context, reject vocab.ActivityStreamsR
requestingAcct := activityContext.requestingAcct
receivingAcct := activityContext.receivingAcct
- for _, obj := range ap.ExtractObjects(reject) {
+ activityID := ap.GetJSONLDId(reject)
+ if activityID == nil {
+ // We need an ID.
+ const text = "Reject had no id property"
+ return gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+
+ for _, object := range ap.ExtractObjects(reject) {
+ if asType := object.GetType(); asType != nil {
+ // Check and handle any
+ // vocab.Type objects.
+ // nolint:gocritic
+ switch asType.GetTypeName() {
- if obj.IsIRI() {
- // we have just the URI of whatever is being rejected, so we need to find out what it is
- rejectedObjectIRI := obj.GetIRI()
- if uris.IsFollowPath(rejectedObjectIRI) {
- // REJECT FOLLOW
- followReq, err := f.state.DB.GetFollowRequestByURI(ctx, rejectedObjectIRI.String())
- if err != nil {
- return fmt.Errorf("Reject: couldn't get follow request with id %s from the database: %s", rejectedObjectIRI.String(), err)
+ // REJECT FOLLOW
+ case ap.ActivityFollow:
+ if err := f.rejectFollowType(
+ ctx,
+ asType,
+ receivingAcct,
+ requestingAcct,
+ ); err != nil {
+ return err
}
+ }
+
+ } else if object.IsIRI() {
+ // Check and handle any
+ // IRI type objects.
+ switch objIRI := object.GetIRI(); {
- // Make sure the creator of the original follow
- // is the same as whatever inbox this landed in.
- if followReq.AccountID != receivingAcct.ID {
- return errors.New("Reject: follow account and inbox account were not the same")
+ // REJECT FOLLOW
+ case uris.IsFollowPath(objIRI):
+ if err := f.rejectFollowIRI(
+ ctx,
+ objIRI.String(),
+ receivingAcct,
+ requestingAcct,
+ ); err != nil {
+ return err
}
- // Make sure the target of the original follow
- // is the same as the account making the request.
- if followReq.TargetAccountID != requestingAcct.ID {
- return errors.New("Reject: follow target account and requesting account were not the same")
+ // REJECT STATUS (reply/boost)
+ case uris.IsStatusesPath(objIRI):
+ if err := f.rejectStatusIRI(
+ ctx,
+ activityID.String(),
+ objIRI.String(),
+ receivingAcct,
+ requestingAcct,
+ ); err != nil {
+ return err
}
- return f.state.DB.RejectFollowRequest(ctx, followReq.AccountID, followReq.TargetAccountID)
+ // REJECT LIKE
+ case uris.IsLikePath(objIRI):
+ if err := f.rejectLikeIRI(
+ ctx,
+ activityID.String(),
+ objIRI.String(),
+ receivingAcct,
+ requestingAcct,
+ ); err != nil {
+ return err
+ }
}
}
+ }
- if t := obj.GetType(); t != nil {
- // we have the whole object so we can figure out what we're rejecting
- // REJECT FOLLOW
- asFollow, ok := t.(vocab.ActivityStreamsFollow)
- if !ok {
- return errors.New("Reject: couldn't parse follow into vocab.ActivityStreamsFollow")
- }
+ return nil
+}
- // convert the follow to something we can understand
- gtsFollow, err := f.converter.ASFollowToFollow(ctx, asFollow)
- if err != nil {
- return fmt.Errorf("Reject: error converting asfollow to gtsfollow: %s", err)
- }
+func (f *federatingDB) rejectFollowType(
+ ctx context.Context,
+ asType vocab.Type,
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+) error {
+ // Cast the vocab.Type object to known AS type.
+ asFollow := asType.(vocab.ActivityStreamsFollow)
- // Make sure the creator of the original follow
- // is the same as whatever inbox this landed in.
- if gtsFollow.AccountID != receivingAcct.ID {
- return errors.New("Reject: follow account and inbox account were not the same")
- }
+ // Reconstruct the follow.
+ follow, err := f.converter.ASFollowToFollow(ctx, asFollow)
+ if err != nil {
+ err := gtserror.Newf("error converting Follow to *gtsmodel.Follow: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
- // Make sure the target of the original follow
- // is the same as the account making the request.
- if gtsFollow.TargetAccountID != requestingAcct.ID {
- return errors.New("Reject: follow target account and requesting account were not the same")
- }
+ // Lock on the Follow URI
+ // as we may be updating it.
+ unlock := f.state.FedLocks.Lock(follow.URI)
+ defer unlock()
+
+ // Make sure the creator of the original follow
+ // is the same as whatever inbox this landed in.
+ if follow.AccountID != receivingAcct.ID {
+ const text = "Follow account and inbox account were not the same"
+ return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
+ }
+
+ // Make sure the target of the original follow
+ // is the same as the account making the request.
+ if follow.TargetAccountID != requestingAcct.ID {
+ const text = "Follow target account and requesting account were not the same"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // Reject the follow.
+ err = f.state.DB.RejectFollowRequest(
+ ctx,
+ follow.AccountID,
+ follow.TargetAccountID,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error rejecting follow request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ return nil
+}
+
+func (f *federatingDB) rejectFollowIRI(
+ ctx context.Context,
+ objectIRI string,
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+) error {
+ // Lock on this potential Follow
+ // URI as we may be updating it.
+ unlock := f.state.FedLocks.Lock(objectIRI)
+ defer unlock()
+
+ // Get the follow req from the db.
+ followReq, err := f.state.DB.GetFollowRequestByURI(ctx, objectIRI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting follow request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if followReq == nil {
+ // We didn't have a follow request
+ // with this URI, so nothing to do.
+ // Just return.
+ //
+ // TODO: Handle Reject Follow to remove
+ // an already-accepted follow relationship.
+ return nil
+ }
+
+ // Make sure the creator of the original follow
+ // is the same as whatever inbox this landed in.
+ if followReq.AccountID != receivingAcct.ID {
+ const text = "Follow account and inbox account were not the same"
+ return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
+ }
+
+ // Make sure the target of the original follow
+ // is the same as the account making the request.
+ if followReq.TargetAccountID != requestingAcct.ID {
+ const text = "Follow target account and requesting account were not the same"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // Reject the follow.
+ err = f.state.DB.RejectFollowRequest(
+ ctx,
+ followReq.AccountID,
+ followReq.TargetAccountID,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error rejecting follow request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ return nil
+}
+
+func (f *federatingDB) rejectStatusIRI(
+ ctx context.Context,
+ activityID string,
+ objectIRI string,
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+) error {
+ // Lock on this potential status URI.
+ unlock := f.state.FedLocks.Lock(objectIRI)
+ defer unlock()
+
+ // Get the status from the db.
+ status, err := f.state.DB.GetStatusByURI(ctx, objectIRI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting status: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if status == nil {
+ // We didn't have a status with
+ // this URI, so nothing to do.
+ // Just return.
+ return nil
+ }
+
+ if !status.IsLocal() {
+ // We don't process Rejects of statuses
+ // that weren't created on our instance.
+ // Just return.
+ //
+ // TODO: Handle Reject to remove *remote*
+ // posts replying-to or boosting the
+ // Rejecting account.
+ return nil
+ }
- return f.state.DB.RejectFollowRequest(ctx, gtsFollow.AccountID, gtsFollow.TargetAccountID)
+ // Make sure the creator of the original status
+ // is the same as the inbox processing the Reject;
+ // this also ensures the status is local.
+ if status.AccountID != receivingAcct.ID {
+ const text = "status author account and inbox account were not the same"
+ return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
+ }
+
+ // Check if we're dealing with a reply
+ // or an announce, and make sure the
+ // requester is permitted to Reject.
+ var apObjectType string
+ if status.InReplyToID != "" {
+ // Rejecting a Reply.
+ apObjectType = ap.ObjectNote
+ if status.InReplyToAccountID != requestingAcct.ID {
+ const text = "status reply to account and requesting account were not the same"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // You can't mention an account and then Reject replies from that
+ // same account (harassment vector); don't process these Rejects.
+ if status.InReplyTo != nil && status.InReplyTo.MentionsAccount(status.AccountID) {
+ const text = "refusing to process Reject of a reply from a mentioned account"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ } else {
+ // Rejecting an Announce.
+ apObjectType = ap.ActivityAnnounce
+ if status.BoostOfAccountID != requestingAcct.ID {
+ const text = "status boost of account and requesting account were not the same"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+ }
+
+ // Check if there's an interaction request in the db for this status.
+ req, err := f.state.DB.GetInteractionRequestByInteractionURI(ctx, status.URI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ switch {
+ case req == nil:
+ // No interaction request existed yet for this
+ // status, create a pre-rejected request now.
+ req = &gtsmodel.InteractionRequest{
+ ID: id.NewULID(),
+ TargetAccountID: requestingAcct.ID,
+ TargetAccount: requestingAcct,
+ InteractingAccountID: receivingAcct.ID,
+ InteractingAccount: receivingAcct,
+ InteractionURI: status.URI,
+ URI: activityID,
+ RejectedAt: time.Now(),
+ }
+
+ if apObjectType == ap.ObjectNote {
+ // Reply.
+ req.InteractionType = gtsmodel.InteractionReply
+ req.StatusID = status.InReplyToID
+ req.Status = status.InReplyTo
+ req.Reply = status
+ } else {
+ // Announce.
+ req.InteractionType = gtsmodel.InteractionAnnounce
+ req.StatusID = status.BoostOfID
+ req.Status = status.BoostOf
+ req.Announce = status
+ }
+
+ if err := f.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ err := gtserror.Newf("db error inserting interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ case req.IsRejected():
+ // Interaction has already been rejected. Just
+ // update to this Reject URI and then return early.
+ req.URI = activityID
+ if err := f.state.DB.UpdateInteractionRequest(ctx, req, "uri"); err != nil {
+ err := gtserror.Newf("db error updating interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+
+ default:
+ // Mark existing interaction request as
+ // Rejected, even if previously Accepted.
+ req.AcceptedAt = time.Time{}
+ req.RejectedAt = time.Now()
+ req.URI = activityID
+ if err := f.state.DB.UpdateInteractionRequest(ctx, req,
+ "accepted_at",
+ "rejected_at",
+ "uri",
+ ); err != nil {
+ err := gtserror.Newf("db error updating interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ // Send the rejected request through to
+ // the fedi worker to process side effects.
+ f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
+ APObjectType: apObjectType,
+ APActivityType: ap.ActivityReject,
+ GTSModel: req,
+ Receiving: receivingAcct,
+ Requesting: requestingAcct,
+ })
+
+ return nil
+}
+
+func (f *federatingDB) rejectLikeIRI(
+ ctx context.Context,
+ activityID string,
+ objectIRI string,
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+) error {
+ // Lock on this potential Like
+ // URI as we may be updating it.
+ unlock := f.state.FedLocks.Lock(objectIRI)
+ defer unlock()
+
+ // Get the fave from the db.
+ fave, err := f.state.DB.GetStatusFaveByURI(ctx, objectIRI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting fave: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if fave == nil {
+ // We didn't have a fave with
+ // this URI, so nothing to do.
+ // Just return.
+ return nil
+ }
+
+ if !fave.Account.IsLocal() {
+ // We don't process Rejects of Likes
+ // that weren't created on our instance.
+ // Just return.
+ //
+ // TODO: Handle Reject to remove *remote*
+ // likes targeting the Rejecting account.
+ return nil
+ }
+
+ // Make sure the creator of the original Like
+ // is the same as the inbox processing the Reject;
+ // this also ensures the Like is local.
+ if fave.AccountID != receivingAcct.ID {
+ const text = "fave creator account and inbox account were not the same"
+ return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
+ }
+
+ // Make sure the target of the Like is the
+ // same as the account doing the Reject.
+ if fave.TargetAccountID != requestingAcct.ID {
+ const text = "status fave target account and requesting account were not the same"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // Check if there's an interaction request in the db for this like.
+ req, err := f.state.DB.GetInteractionRequestByInteractionURI(ctx, fave.URI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ switch {
+ case req == nil:
+ // No interaction request existed yet for this
+ // fave, create a pre-rejected request now.
+ req = &gtsmodel.InteractionRequest{
+ ID: id.NewULID(),
+ TargetAccountID: requestingAcct.ID,
+ TargetAccount: requestingAcct,
+ InteractingAccountID: receivingAcct.ID,
+ InteractingAccount: receivingAcct,
+ InteractionURI: fave.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Like: fave,
+ URI: activityID,
+ RejectedAt: time.Now(),
+ }
+
+ if err := f.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ err := gtserror.Newf("db error inserting interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ case req.IsRejected():
+ // Interaction has already been rejected. Just
+ // update to this Reject URI and then return early.
+ req.URI = activityID
+ if err := f.state.DB.UpdateInteractionRequest(ctx, req, "uri"); err != nil {
+ err := gtserror.Newf("db error updating interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+
+ default:
+ // Mark existing interaction request as
+ // Rejected, even if previously Accepted.
+ req.AcceptedAt = time.Time{}
+ req.RejectedAt = time.Now()
+ req.URI = activityID
+ if err := f.state.DB.UpdateInteractionRequest(ctx, req,
+ "accepted_at",
+ "rejected_at",
+ "uri",
+ ); err != nil {
+ err := gtserror.Newf("db error updating interaction request: %w", err)
+ return gtserror.NewErrorInternalError(err)
}
}
+ // Send the rejected request through to
+ // the fedi worker to process side effects.
+ f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
+ APObjectType: ap.ActivityLike,
+ APActivityType: ap.ActivityReject,
+ GTSModel: req,
+ Receiving: receivingAcct,
+ Requesting: requestingAcct,
+ })
+
return nil
}