diff options
Diffstat (limited to 'internal/processing/account/follow.go')
-rw-r--r-- | internal/processing/account/follow.go | 227 |
1 files changed, 119 insertions, 108 deletions
diff --git a/internal/processing/account/follow.go b/internal/processing/account/follow.go index 191b1f02d..8d053e92a 100644 --- a/internal/processing/account/follow.go +++ b/internal/processing/account/follow.go @@ -19,6 +19,7 @@ package account import ( "context" + "errors" "fmt" "github.com/superseriousbusiness/gotosocial/internal/ap" @@ -33,172 +34,182 @@ import ( // FollowCreate handles a follow request to an account, either remote or local. func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) { - // if there's a block between the accounts we shouldn't create the request ofc - if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, form.ID, true); err != nil { - return nil, gtserror.NewErrorInternalError(err) - } else if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) + targetAccount, errWithCode := p.getFollowTarget(ctx, requestingAccount.ID, form.ID) + if errWithCode != nil { + return nil, errWithCode } - // make sure the target account actually exists in our db - targetAcct, err := p.state.DB.GetAccountByID(ctx, form.ID) - if err != nil { - if err == db.ErrNoEntries { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.ID, err)) - } + // Check if a follow exists already. + if follows, err := p.state.DB.IsFollowing(ctx, requestingAccount, targetAccount); err != nil { + err = fmt.Errorf("FollowCreate: db error checking follow: %w", err) return nil, gtserror.NewErrorInternalError(err) - } - - // check if a follow exists already - if follows, err := p.state.DB.IsFollowing(ctx, requestingAccount, targetAcct); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err)) } else if follows { - // already follows so just return the relationship + // Already follows, just return current relationship. return p.RelationshipGet(ctx, requestingAccount, form.ID) } - // check if a follow request exists already - if followRequested, err := p.state.DB.IsFollowRequested(ctx, requestingAccount, targetAcct); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err)) + // Check if a follow request exists already. + if followRequested, err := p.state.DB.IsFollowRequested(ctx, requestingAccount, targetAccount); err != nil { + err = fmt.Errorf("FollowCreate: db error checking follow request: %w", err) + return nil, gtserror.NewErrorInternalError(err) } else if followRequested { - // already follow requested so just return the relationship + // Already follow requested, just return current relationship. return p.RelationshipGet(ctx, requestingAccount, form.ID) } - // check for attempt to follow self - if requestingAccount.ID == targetAcct.ID { - return nil, gtserror.NewErrorNotAcceptable(fmt.Errorf("accountfollowcreate: account %s cannot follow itself", requestingAccount.ID)) - } - - // make the follow request - newFollowID, err := id.NewRandomULID() + // Create and store a new follow request. + followID, err := id.NewRandomULID() if err != nil { return nil, gtserror.NewErrorInternalError(err) } + followURI := uris.GenerateURIForFollow(requestingAccount.Username, followID) - showReblogs := true - notify := false fr := >smodel.FollowRequest{ - ID: newFollowID, + ID: followID, + URI: followURI, AccountID: requestingAccount.ID, + Account: requestingAccount, TargetAccountID: form.ID, - ShowReblogs: &showReblogs, - URI: uris.GenerateURIForFollow(requestingAccount.Username, newFollowID), - Notify: ¬ify, - } - if form.Reblogs != nil { - fr.ShowReblogs = form.Reblogs - } - if form.Notify != nil { - fr.Notify = form.Notify + TargetAccount: targetAccount, + ShowReblogs: form.Reblogs, + Notify: form.Notify, } - // whack it in the database if err := p.state.DB.Put(ctx, fr); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err)) + err = fmt.Errorf("FollowCreate: error creating follow request in db: %s", err) + return nil, gtserror.NewErrorInternalError(err) } - // if it's a local account that's not locked we can just straight up accept the follow request - if !*targetAcct.Locked && targetAcct.Domain == "" { + if targetAccount.IsLocal() && !*targetAccount.Locked { + // If the target account is local and not locked, + // we can already accept the follow request and + // skip any further processing. + // + // Because we know the requestingAccount is also + // local, we don't need to federate the accept out. if _, err := p.state.DB.AcceptFollowRequest(ctx, requestingAccount.ID, form.ID); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err)) + err = fmt.Errorf("FollowCreate: error accepting follow request for local unlocked account: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - // return the new relationship - return p.RelationshipGet(ctx, requestingAccount, form.ID) + } else if targetAccount.IsRemote() { + // Otherwise we leave the follow request as it is, + // and we handle the rest of the process async. + p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityCreate, + GTSModel: fr, + OriginAccount: requestingAccount, + TargetAccount: targetAccount, + }) } - // otherwise we leave the follow request as it is and we handle the rest of the process asynchronously - p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ - APObjectType: ap.ActivityFollow, - APActivityType: ap.ActivityCreate, - GTSModel: fr, - OriginAccount: requestingAccount, - TargetAccount: targetAcct, - }) - - // return whatever relationship results from this return p.RelationshipGet(ctx, requestingAccount, form.ID) } // FollowRemove handles the removal of a follow/follow request to an account, either remote or local. func (p *Processor) FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - // if there's a block between the accounts we shouldn't do anything - blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts")) + targetAccount, errWithCode := p.getFollowTarget(ctx, requestingAccount.ID, targetAccountID) + if errWithCode != nil { + return nil, errWithCode } - // make sure the target account actually exists in our db - targetAcct, err := p.state.DB.GetAccountByID(ctx, targetAccountID) + // Unfollow and deal with side effects. + msgs, err := p.unfollow(ctx, requestingAccount, targetAccount) if err != nil { - if err == db.ErrNoEntries { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err)) - } + return nil, gtserror.NewErrorNotFound(fmt.Errorf("FollowRemove: account %s not found in the db: %s", targetAccountID, err)) } - // check if a follow request exists, and remove it if it does (storing the URI for later) - var frChanged bool - var frURI string - fr := >smodel.FollowRequest{} - if err := p.state.DB.GetWhere(ctx, []db.Where{ - {Key: "account_id", Value: requestingAccount.ID}, - {Key: "target_account_id", Value: targetAccountID}, - }, fr); err == nil { - frURI = fr.URI - if err := p.state.DB.DeleteByID(ctx, fr.ID, fr); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err)) - } - frChanged = true + // Batch queue accreted client api messages. + p.state.Workers.EnqueueClientAPI(ctx, msgs...) + + return p.RelationshipGet(ctx, requestingAccount, targetAccountID) +} + +/* + Utility functions. +*/ + +// getFollowTarget is a convenience function which: +// - Checks if account is trying to follow/unfollow itself. +// - Returns not found if there's a block in place between accounts. +// - Returns target account according to its id. +func (p *Processor) getFollowTarget(ctx context.Context, requestingAccountID string, targetAccountID string) (*gtsmodel.Account, gtserror.WithCode) { + // Account can't follow or unfollow itself. + if requestingAccountID == targetAccountID { + err := errors.New("account can't follow or unfollow itself") + return nil, gtserror.NewErrorNotAcceptable(err) } - // now do the same thing for any existing follow - var fChanged bool - var fURI string - f := >smodel.Follow{} - if err := p.state.DB.GetWhere(ctx, []db.Where{ - {Key: "account_id", Value: requestingAccount.ID}, - {Key: "target_account_id", Value: targetAccountID}, - }, f); err == nil { - fURI = f.URI - if err := p.state.DB.DeleteByID(ctx, f.ID, f); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err)) + // Do nothing if a block exists in either direction between accounts. + if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccountID, targetAccountID, true); err != nil { + err = fmt.Errorf("db error checking block between accounts: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } else if blocked { + err = errors.New("block exists between accounts") + return nil, gtserror.NewErrorNotFound(err) + } + + // Ensure target account retrievable. + targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID) + if err != nil { + if !errors.Is(err, db.ErrNoEntries) { + // Real db error. + err = fmt.Errorf("db error looking for target account %s: %w", targetAccountID, err) + return nil, gtserror.NewErrorInternalError(err) } - fChanged = true + // Account not found. + err = fmt.Errorf("target account %s not found in the db", targetAccountID) + return nil, gtserror.NewErrorNotFound(err, err.Error()) } - // follow request status changed so send the UNDO activity to the channel for async processing - if frChanged { - p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ + return targetAccount, nil +} + +// unfollow is a convenience function for having requesting account +// unfollow (and un follow request) target account, if follows and/or +// follow requests exist. +// +// If a follow and/or follow request was removed this way, one or two +// messages will be returned which should then be processed by a client +// api worker. +func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) ([]messages.FromClientAPI, error) { + msgs := []messages.FromClientAPI{} + + if fURI, err := p.state.DB.Unfollow(ctx, requestingAccount.ID, targetAccount.ID); err != nil { + err = fmt.Errorf("unfollow: error deleting follow from %s targeting %s: %w", requestingAccount.ID, targetAccount.ID, err) + return nil, err + } else if fURI != "" { + // Follow status changed, process side effects. + msgs = append(msgs, messages.FromClientAPI{ APObjectType: ap.ActivityFollow, APActivityType: ap.ActivityUndo, GTSModel: >smodel.Follow{ AccountID: requestingAccount.ID, - TargetAccountID: targetAccountID, - URI: frURI, + TargetAccountID: targetAccount.ID, + URI: fURI, }, OriginAccount: requestingAccount, - TargetAccount: targetAcct, + TargetAccount: targetAccount, }) } - // follow status changed so send the UNDO activity to the channel for async processing - if fChanged { - p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ + if frURI, err := p.state.DB.UnfollowRequest(ctx, requestingAccount.ID, targetAccount.ID); err != nil { + err = fmt.Errorf("unfollow: error deleting follow request from %s targeting %s: %w", requestingAccount.ID, targetAccount.ID, err) + return nil, err + } else if frURI != "" { + // Follow request status changed, process side effects. + msgs = append(msgs, messages.FromClientAPI{ APObjectType: ap.ActivityFollow, APActivityType: ap.ActivityUndo, GTSModel: >smodel.Follow{ AccountID: requestingAccount.ID, - TargetAccountID: targetAccountID, - URI: fURI, + TargetAccountID: targetAccount.ID, + URI: frURI, }, OriginAccount: requestingAccount, - TargetAccount: targetAcct, + TargetAccount: targetAccount, }) } - // return whatever relationship results from all this - return p.RelationshipGet(ctx, requestingAccount, targetAccountID) + return msgs, nil } |