mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-02 20:02:25 -06:00 
			
		
		
		
	after seeing a potential reported federating worker lockup i decided to start digging into the federatingdb code. this PR encompasses:
- removes one of our last unused interface types `federatingdb.DB{}`, replacing it with a struct type `*federatingdb.DB{}`
- in `transport.dereferenceLocal()` differentiates between an unsupported lookup type and ErrNoEntries to reduce unnecessary calls, and reduce potential lockups that may occur while trying to call our own endpoints that then call `federatingdb.Lock()`
- removes a bunch of the locks on follow state changes since the DB already synchronizes that
- removes the unnecessary `pub.Clock{}` struct field and type passed to the transport controller
frankly it would be great if we could remove the locking in `federatingdb.Lock()` and instead handle it ourselves as it gets very confusing trying to figure out what functions will have locks held. but i guess that's one for when we move further away from the go-fed/activity/pub package usage.
Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4178
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
		
	
			
		
			
				
	
	
		
			636 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			636 lines
		
	
	
	
		
			17 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"
 | 
						|
	"fmt"
 | 
						|
	"net/url"
 | 
						|
 | 
						|
	"code.superseriousbusiness.org/activity/streams/vocab"
 | 
						|
	"code.superseriousbusiness.org/gotosocial/internal/ap"
 | 
						|
	"code.superseriousbusiness.org/gotosocial/internal/config"
 | 
						|
	"code.superseriousbusiness.org/gotosocial/internal/db"
 | 
						|
	"code.superseriousbusiness.org/gotosocial/internal/gtserror"
 | 
						|
	"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
 | 
						|
	"code.superseriousbusiness.org/gotosocial/internal/log"
 | 
						|
	"code.superseriousbusiness.org/gotosocial/internal/messages"
 | 
						|
	"code.superseriousbusiness.org/gotosocial/internal/uris"
 | 
						|
	"code.superseriousbusiness.org/gotosocial/internal/util"
 | 
						|
)
 | 
						|
 | 
						|
func (f *DB) GetAccept(
 | 
						|
	ctx context.Context,
 | 
						|
	acceptIRI *url.URL,
 | 
						|
) (vocab.ActivityStreamsAccept, error) {
 | 
						|
	approval, err := f.state.DB.GetInteractionRequestByURI(ctx, acceptIRI.String())
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return f.converter.InteractionReqToASAccept(ctx, approval)
 | 
						|
}
 | 
						|
 | 
						|
func (f *DB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
 | 
						|
	log.DebugKV(ctx, "accept", serialize{accept})
 | 
						|
 | 
						|
	activityContext := getActivityContext(ctx)
 | 
						|
	if activityContext.internal {
 | 
						|
		return nil // Already processed.
 | 
						|
	}
 | 
						|
 | 
						|
	requestingAcct := activityContext.requestingAcct
 | 
						|
	receivingAcct := activityContext.receivingAcct
 | 
						|
 | 
						|
	if requestingAcct.IsMoving() {
 | 
						|
		// A Moving account
 | 
						|
		// can't do this.
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	acceptID := ap.GetJSONLDId(accept)
 | 
						|
	if acceptID == nil {
 | 
						|
		// We need an ID.
 | 
						|
		const text = "Accept had no id property"
 | 
						|
		return gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
						|
	}
 | 
						|
 | 
						|
	// Ensure requester is the same as the
 | 
						|
	// Actor of the Accept; you can't Accept
 | 
						|
	// something on someone else's behalf.
 | 
						|
	actorURI, err := ap.ExtractActorURI(accept)
 | 
						|
	if err != nil {
 | 
						|
		const text = "Accept had empty or invalid actor property"
 | 
						|
		return gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
						|
	}
 | 
						|
 | 
						|
	if requestingAcct.URI != actorURI.String() {
 | 
						|
		const text = "Accept actor and requesting account were not the same"
 | 
						|
		return gtserror.NewErrorBadRequest(errors.New(text), text)
 | 
						|
	}
 | 
						|
 | 
						|
	// Iterate all provided objects in the activity,
 | 
						|
	// handling the ones we know how to handle.
 | 
						|
	for _, object := range ap.ExtractObjects(accept) {
 | 
						|
		if asType := object.GetType(); asType != nil {
 | 
						|
			// Check and handle any vocab.Type objects.
 | 
						|
			switch name := asType.GetTypeName(); {
 | 
						|
 | 
						|
			// ACCEPT FOLLOW
 | 
						|
			case name == ap.ActivityFollow:
 | 
						|
				if err := f.acceptFollowType(
 | 
						|
					ctx,
 | 
						|
					asType,
 | 
						|
					receivingAcct,
 | 
						|
					requestingAcct,
 | 
						|
				); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
 | 
						|
			// ACCEPT TYPE-HINTED LIKE
 | 
						|
			//
 | 
						|
			// ie., a Like with just `id`
 | 
						|
			// and `type` properties set.
 | 
						|
			case name == ap.ActivityLike:
 | 
						|
				objIRI := ap.GetJSONLDId(asType)
 | 
						|
				if objIRI == nil {
 | 
						|
					log.Debugf(ctx, "could not retrieve id of inlined Accept object %s", name)
 | 
						|
					continue
 | 
						|
				}
 | 
						|
 | 
						|
				if err := f.acceptLikeIRI(
 | 
						|
					ctx,
 | 
						|
					acceptID,
 | 
						|
					accept,
 | 
						|
					objIRI.String(),
 | 
						|
					receivingAcct,
 | 
						|
					requestingAcct,
 | 
						|
				); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
 | 
						|
			// ACCEPT TYPE-HINTED REPLY OR ANNOUNCE.
 | 
						|
			//
 | 
						|
			// ie., a statusable or Announce with
 | 
						|
			// just `id` and `type` properties set.
 | 
						|
			case name == ap.ActivityAnnounce || ap.IsStatusable(name):
 | 
						|
				objIRI := ap.GetJSONLDId(asType)
 | 
						|
				if objIRI == nil {
 | 
						|
					log.Debugf(ctx, "could not retrieve id of inlined Accept object %s", name)
 | 
						|
					continue
 | 
						|
				}
 | 
						|
 | 
						|
				if err := f.acceptOtherIRI(
 | 
						|
					ctx,
 | 
						|
					acceptID,
 | 
						|
					accept,
 | 
						|
					objIRI,
 | 
						|
					receivingAcct,
 | 
						|
					requestingAcct,
 | 
						|
				); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
 | 
						|
			// UNHANDLED
 | 
						|
			default:
 | 
						|
				log.Debugf(ctx, "unhandled object type: %s", name)
 | 
						|
			}
 | 
						|
 | 
						|
		} else if object.IsIRI() {
 | 
						|
			// Check and handle any
 | 
						|
			// IRI type objects.
 | 
						|
			switch objIRI := object.GetIRI(); {
 | 
						|
 | 
						|
			// ACCEPT FOLLOW
 | 
						|
			case uris.IsFollowPath(objIRI):
 | 
						|
				if err := f.acceptFollowIRI(
 | 
						|
					ctx,
 | 
						|
					objIRI.String(),
 | 
						|
					receivingAcct,
 | 
						|
					requestingAcct,
 | 
						|
				); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
 | 
						|
			// ACCEPT LIKE
 | 
						|
			case uris.IsLikePath(objIRI):
 | 
						|
				if err := f.acceptLikeIRI(
 | 
						|
					ctx,
 | 
						|
					acceptID,
 | 
						|
					accept,
 | 
						|
					objIRI.String(),
 | 
						|
					receivingAcct,
 | 
						|
					requestingAcct,
 | 
						|
				); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
 | 
						|
			// ACCEPT OTHER (reply? announce?)
 | 
						|
			//
 | 
						|
			// Don't check on IsStatusesPath
 | 
						|
			// as this may be a remote status.
 | 
						|
			default:
 | 
						|
				if err := f.acceptOtherIRI(
 | 
						|
					ctx,
 | 
						|
					acceptID,
 | 
						|
					accept,
 | 
						|
					objIRI,
 | 
						|
					receivingAcct,
 | 
						|
					requestingAcct,
 | 
						|
				); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (f *DB) acceptFollowType(
 | 
						|
	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)
 | 
						|
	}
 | 
						|
 | 
						|
	// 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)
 | 
						|
	}
 | 
						|
 | 
						|
	// Accept and get the populated follow back.
 | 
						|
	follow, err = f.state.DB.AcceptFollowRequest(ctx,
 | 
						|
		follow.AccountID,
 | 
						|
		follow.TargetAccountID,
 | 
						|
	)
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		err := gtserror.Newf("db error accepting follow request: %w", err)
 | 
						|
		return gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if follow == nil {
 | 
						|
		// There was no follow request
 | 
						|
		// to accept, just return 202.
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Send the accepted follow through
 | 
						|
	// the processor to do side effects.
 | 
						|
	f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
 | 
						|
		APObjectType:   ap.ActivityFollow,
 | 
						|
		APActivityType: ap.ActivityAccept,
 | 
						|
		GTSModel:       follow,
 | 
						|
		Receiving:      receivingAcct,
 | 
						|
		Requesting:     requestingAcct,
 | 
						|
	})
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (f *DB) acceptFollowIRI(
 | 
						|
	ctx context.Context,
 | 
						|
	objectIRI string,
 | 
						|
	receivingAcct *gtsmodel.Account,
 | 
						|
	requestingAcct *gtsmodel.Account,
 | 
						|
) error {
 | 
						|
	// 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.
 | 
						|
		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)
 | 
						|
	}
 | 
						|
 | 
						|
	// Accept and get the populated follow back.
 | 
						|
	follow, err := f.state.DB.AcceptFollowRequest(ctx,
 | 
						|
		followReq.AccountID,
 | 
						|
		followReq.TargetAccountID,
 | 
						|
	)
 | 
						|
	if err != nil && !errors.Is(err, db.ErrNoEntries) {
 | 
						|
		err := gtserror.Newf("db error accepting follow request: %w", err)
 | 
						|
		return gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if follow == nil {
 | 
						|
		// There was no follow request
 | 
						|
		// to accept, just return 202.
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Send the accepted follow through
 | 
						|
	// the processor to do side effects.
 | 
						|
	f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
 | 
						|
		APObjectType:   ap.ActivityFollow,
 | 
						|
		APActivityType: ap.ActivityAccept,
 | 
						|
		GTSModel:       follow,
 | 
						|
		Receiving:      receivingAcct,
 | 
						|
		Requesting:     requestingAcct,
 | 
						|
	})
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (f *DB) acceptOtherIRI(
 | 
						|
	ctx context.Context,
 | 
						|
	acceptID *url.URL,
 | 
						|
	accept vocab.ActivityStreamsAccept,
 | 
						|
	objectIRI *url.URL,
 | 
						|
	receivingAcct *gtsmodel.Account,
 | 
						|
	requestingAcct *gtsmodel.Account,
 | 
						|
) error {
 | 
						|
	// See if we can get a status from the db.
 | 
						|
	status, err := f.state.DB.GetStatusByURI(ctx, objectIRI.String())
 | 
						|
	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 had a status stored with this
 | 
						|
		// objectIRI, proceed to accept it.
 | 
						|
		return f.acceptStoredStatus(
 | 
						|
			ctx,
 | 
						|
			acceptID,
 | 
						|
			accept,
 | 
						|
			status,
 | 
						|
			receivingAcct,
 | 
						|
			requestingAcct,
 | 
						|
		)
 | 
						|
	}
 | 
						|
 | 
						|
	if objectIRI.Host == config.GetHost() ||
 | 
						|
		objectIRI.Host == config.GetAccountDomain() {
 | 
						|
		// Claims to be Accepting something of ours,
 | 
						|
		// but we don't have a status stored for this
 | 
						|
		// URI, so most likely it's been deleted in
 | 
						|
		// the meantime, just bail.
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// This must be an Accept of a remote Activity
 | 
						|
	// or Object. Ensure relevance of this message
 | 
						|
	// by checking that receiver follows requester.
 | 
						|
	following, err := f.state.DB.IsFollowing(
 | 
						|
		ctx,
 | 
						|
		receivingAcct.ID,
 | 
						|
		requestingAcct.ID,
 | 
						|
	)
 | 
						|
	if err != nil {
 | 
						|
		err := gtserror.Newf("db error checking following: %w", err)
 | 
						|
		return gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	if !following {
 | 
						|
		// If we don't follow this person, and
 | 
						|
		// they're not Accepting something we know
 | 
						|
		// about, then we don't give a good goddamn.
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// This may be a reply, or it may be a boost,
 | 
						|
	// we can't know yet without dereferencing it,
 | 
						|
	// but let the processor worry about that.
 | 
						|
	//
 | 
						|
	// TODO: do something with type hinting here.
 | 
						|
	apObjectType := ap.ObjectUnknown
 | 
						|
 | 
						|
	// Extract appropriate approvedByURI from the Accept.
 | 
						|
	approvedByURI, err := approvedByURI(acceptID, accept)
 | 
						|
	if err != nil {
 | 
						|
		return gtserror.NewErrorForbidden(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	// Pass to the processor and let them handle side effects.
 | 
						|
	f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
 | 
						|
		APObjectType:   apObjectType,
 | 
						|
		APActivityType: ap.ActivityAccept,
 | 
						|
		APIRI:          approvedByURI,
 | 
						|
		APObject:       objectIRI,
 | 
						|
		Receiving:      receivingAcct,
 | 
						|
		Requesting:     requestingAcct,
 | 
						|
	})
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (f *DB) acceptStoredStatus(
 | 
						|
	ctx context.Context,
 | 
						|
	acceptID *url.URL,
 | 
						|
	accept vocab.ActivityStreamsAccept,
 | 
						|
	status *gtsmodel.Status,
 | 
						|
	receivingAcct *gtsmodel.Account,
 | 
						|
	requestingAcct *gtsmodel.Account,
 | 
						|
) error {
 | 
						|
	// Lock on this status URI
 | 
						|
	// as we may be updating it.
 | 
						|
	unlock := f.state.FedLocks.Lock(status.URI)
 | 
						|
	defer unlock()
 | 
						|
 | 
						|
	pendingApproval := util.PtrOrValue(status.PendingApproval, false)
 | 
						|
	if !pendingApproval {
 | 
						|
		// Status doesn't need approval or it's
 | 
						|
		// already been approved by an Accept.
 | 
						|
		// Just return.
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Make sure the target of the interaction (reply/boost)
 | 
						|
	// is the same as the account doing the Accept.
 | 
						|
	if status.BoostOfAccountID != requestingAcct.ID &&
 | 
						|
		status.InReplyToAccountID != requestingAcct.ID {
 | 
						|
		const text = "status reply to or boost of account and requesting account were not the same"
 | 
						|
		return gtserror.NewErrorForbidden(errors.New(text), text)
 | 
						|
	}
 | 
						|
 | 
						|
	// Extract appropriate approvedByURI from the Accept.
 | 
						|
	approvedByURI, err := approvedByURI(acceptID, accept)
 | 
						|
	if err != nil {
 | 
						|
		return gtserror.NewErrorForbidden(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	// Mark the status as approved by this URI.
 | 
						|
	status.PendingApproval = util.Ptr(false)
 | 
						|
	status.ApprovedByURI = approvedByURI.String()
 | 
						|
	if err := f.state.DB.UpdateStatus(
 | 
						|
		ctx,
 | 
						|
		status,
 | 
						|
		"pending_approval",
 | 
						|
		"approved_by_uri",
 | 
						|
	); err != nil {
 | 
						|
		err := gtserror.Newf("db error accepting status: %w", err)
 | 
						|
		return gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	var apObjectType string
 | 
						|
	if status.InReplyToID != "" {
 | 
						|
		// Accepting a Reply.
 | 
						|
		apObjectType = ap.ObjectNote
 | 
						|
	} else {
 | 
						|
		// Accepting an Announce.
 | 
						|
		apObjectType = ap.ActivityAnnounce
 | 
						|
	}
 | 
						|
 | 
						|
	// Send the now-approved status through to the
 | 
						|
	// fedi worker again to process side effects.
 | 
						|
	f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
 | 
						|
		APObjectType:   apObjectType,
 | 
						|
		APActivityType: ap.ActivityAccept,
 | 
						|
		GTSModel:       status,
 | 
						|
		Receiving:      receivingAcct,
 | 
						|
		Requesting:     requestingAcct,
 | 
						|
	})
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (f *DB) acceptLikeIRI(
 | 
						|
	ctx context.Context,
 | 
						|
	acceptID *url.URL,
 | 
						|
	accept vocab.ActivityStreamsAccept,
 | 
						|
	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 Accepts of Likes
 | 
						|
		// that weren't created on our instance.
 | 
						|
		// Just return.
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	pendingApproval := util.PtrOrValue(fave.PendingApproval, false)
 | 
						|
	if !pendingApproval {
 | 
						|
		// Like doesn't need approval or it's
 | 
						|
		// already been approved by an Accept.
 | 
						|
		// Just return.
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	// Make sure the creator of the original Like
 | 
						|
	// is the same as the inbox processing the Accept;
 | 
						|
	// 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 Accept.
 | 
						|
	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)
 | 
						|
	}
 | 
						|
 | 
						|
	// Extract appropriate approvedByURI from the Accept.
 | 
						|
	approvedByURI, err := approvedByURI(acceptID, accept)
 | 
						|
	if err != nil {
 | 
						|
		return gtserror.NewErrorForbidden(err, err.Error())
 | 
						|
	}
 | 
						|
 | 
						|
	// Mark the fave as approved by this URI.
 | 
						|
	fave.PendingApproval = util.Ptr(false)
 | 
						|
	fave.ApprovedByURI = approvedByURI.String()
 | 
						|
	if err := f.state.DB.UpdateStatusFave(
 | 
						|
		ctx,
 | 
						|
		fave,
 | 
						|
		"pending_approval",
 | 
						|
		"approved_by_uri",
 | 
						|
	); err != nil {
 | 
						|
		err := gtserror.Newf("db error accepting status: %w", err)
 | 
						|
		return gtserror.NewErrorInternalError(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Send the now-approved fave through to the
 | 
						|
	// fedi worker again to process side effects.
 | 
						|
	f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
 | 
						|
		APObjectType:   ap.ActivityLike,
 | 
						|
		APActivityType: ap.ActivityAccept,
 | 
						|
		GTSModel:       fave,
 | 
						|
		Receiving:      receivingAcct,
 | 
						|
		Requesting:     requestingAcct,
 | 
						|
	})
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// approvedByURI extracts the appropriate *url.URL
 | 
						|
// to use as an interaction's approvedBy value by
 | 
						|
// checking to see if the Accept has a result URL set.
 | 
						|
// If that result URL exists, is an IRI (not a type),
 | 
						|
// and is on the same host as the Accept ID, then the
 | 
						|
// result URI will be returned. In all other cases,
 | 
						|
// the Accept ID is returned unchanged.
 | 
						|
//
 | 
						|
// Error is only returned if the result URI is set
 | 
						|
// but the host differs from the Accept ID host.
 | 
						|
//
 | 
						|
// TODO: This function should be updated at some point
 | 
						|
// to check for inlined result type, and see if type is
 | 
						|
// a LikeApproval, ReplyApproval, or AnnounceApproval,
 | 
						|
// and check the attributedTo, object, and target of
 | 
						|
// the approval as well. But this'll do for now.
 | 
						|
func approvedByURI(
 | 
						|
	acceptID *url.URL,
 | 
						|
	accept vocab.ActivityStreamsAccept,
 | 
						|
) (*url.URL, error) {
 | 
						|
	// Check if the Accept has a `result` property
 | 
						|
	// set on it (which should be an approval).
 | 
						|
	resultProp := accept.GetActivityStreamsResult()
 | 
						|
	if resultProp == nil {
 | 
						|
		// No result,
 | 
						|
		// use AcceptID.
 | 
						|
		return acceptID, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if resultProp.Len() != 1 {
 | 
						|
		// Result was unexpected
 | 
						|
		// length, can't use this.
 | 
						|
		return acceptID, nil
 | 
						|
	}
 | 
						|
 | 
						|
	result := resultProp.At(0)
 | 
						|
	if result == nil {
 | 
						|
		// Result entry
 | 
						|
		// was nil, huh!
 | 
						|
		return acceptID, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if !result.IsIRI() {
 | 
						|
		// Can't handle
 | 
						|
		// inlined yet.
 | 
						|
		return acceptID, nil
 | 
						|
	}
 | 
						|
 | 
						|
	resultIRI := result.GetIRI()
 | 
						|
	if resultIRI == nil {
 | 
						|
		// Result entry
 | 
						|
		// was nil, huh!
 | 
						|
		return acceptID, nil
 | 
						|
	}
 | 
						|
 | 
						|
	if resultIRI.Host != acceptID.Host {
 | 
						|
		// What the boobs is this?
 | 
						|
		err := fmt.Errorf(
 | 
						|
			"host of result %s differed from host of Accept %s",
 | 
						|
			resultIRI, accept,
 | 
						|
		)
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// Use the result IRI we've been
 | 
						|
	// given instead of the acceptID.
 | 
						|
	return resultIRI, nil
 | 
						|
}
 |