mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-03 19:22:24 -06:00 
			
		
		
		
	* [feature] Process `Reject` of interaction via fedi API, put rejected statuses in the "sin bin" * update test * move nil check back to `rejectStatusIRI`
		
			
				
	
	
		
			502 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			502 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// GoToSocial
 | 
						|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
 | 
						|
// SPDX-License-Identifier: AGPL-3.0-or-later
 | 
						|
//
 | 
						|
// This program is free software: you can redistribute it and/or modify
 | 
						|
// it under the terms of the GNU Affero General Public License as published by
 | 
						|
// the Free Software Foundation, either version 3 of the License, or
 | 
						|
// (at your option) any later version.
 | 
						|
//
 | 
						|
// This program is distributed in the hope that it will be useful,
 | 
						|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
						|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
						|
// GNU Affero General Public License for more details.
 | 
						|
//
 | 
						|
// You should have received a copy of the GNU Affero General Public License
 | 
						|
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
						|
 | 
						|
package federatingdb
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"errors"
 | 
						|
	"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"
 | 
						|
)
 | 
						|
 | 
						|
func (f *federatingDB) Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error {
 | 
						|
	if log.Level() >= level.DEBUG {
 | 
						|
		i, err := marshalItem(reject)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		l := log.WithContext(ctx).
 | 
						|
			WithField("reject", i)
 | 
						|
		l.Debug("entering Reject")
 | 
						|
	}
 | 
						|
 | 
						|
	activityContext := getActivityContext(ctx)
 | 
						|
	if activityContext.internal {
 | 
						|
		return nil // Already processed.
 | 
						|
	}
 | 
						|
 | 
						|
	requestingAcct := activityContext.requestingAcct
 | 
						|
	receivingAcct := activityContext.receivingAcct
 | 
						|
 | 
						|
	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() {
 | 
						|
 | 
						|
			// 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(); {
 | 
						|
 | 
						|
			// REJECT FOLLOW
 | 
						|
			case uris.IsFollowPath(objIRI):
 | 
						|
				if err := f.rejectFollowIRI(
 | 
						|
					ctx,
 | 
						|
					objIRI.String(),
 | 
						|
					receivingAcct,
 | 
						|
					requestingAcct,
 | 
						|
				); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
 | 
						|
			// REJECT STATUS (reply/boost)
 | 
						|
			case uris.IsStatusesPath(objIRI):
 | 
						|
				if err := f.rejectStatusIRI(
 | 
						|
					ctx,
 | 
						|
					activityID.String(),
 | 
						|
					objIRI.String(),
 | 
						|
					receivingAcct,
 | 
						|
					requestingAcct,
 | 
						|
				); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
 | 
						|
			// REJECT LIKE
 | 
						|
			case uris.IsLikePath(objIRI):
 | 
						|
				if err := f.rejectLikeIRI(
 | 
						|
					ctx,
 | 
						|
					activityID.String(),
 | 
						|
					objIRI.String(),
 | 
						|
					receivingAcct,
 | 
						|
					requestingAcct,
 | 
						|
				); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
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)
 | 
						|
 | 
						|
	// 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)
 | 
						|
	}
 | 
						|
 | 
						|
	// 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
 | 
						|
	}
 | 
						|
 | 
						|
	// 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 = >smodel.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 = >smodel.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
 | 
						|
}
 |