diff options
author | 2023-03-20 19:10:08 +0100 | |
---|---|---|
committer | 2023-03-20 18:10:08 +0000 | |
commit | e8595f0c64f527af0913d1a426b697e67ff74ac9 (patch) | |
tree | a5d45b1ad8b96318944408a23fda91f008643900 /internal/processing/account | |
parent | [chore]: Bump github.com/miekg/dns from 1.1.51 to 1.1.52 (#1636) (diff) | |
download | gotosocial-e8595f0c64f527af0913d1a426b697e67ff74ac9.tar.xz |
[chore] Refactor account deleting/block logic, tidy up some other processing things (#1599)
* start refactoring account deletion
* update to use state.DB
* further messing about
* some more tidying up
* more tidying, cleaning, nice-making
* further adventures in refactoring and the woes of technical debt
* update fr accept/reject
* poking + prodding
* fix up deleting
* create fave uri
* don't log using requestingAccount.ID because it might be nil
* move getBookmarks function
* use exists query to check for status bookmark
* use deletenotifications func
* fiddle
* delete follow request notif
* split up some db functions
* Fix possible nil pointer panic
* fix more possible nil pointers
* fix license headers
* warn when follow missing (target) account
* return wrapped err when bookmark/fave models can't be retrieved
* simplify self account delete
* warn log likely race condition
* de-sillify status delete loop
* move error check due north
* warn when unfollowSideEffects has no target account
* warn when no boost account is found
* warn + dump follow when no account
* more warnings
* warn on fave account not set
* move for loop inside anonymous function
* fix funky logic
* don't remove mutual account items on block;
do make sure unfollow occurs in both directions!
Diffstat (limited to 'internal/processing/account')
-rw-r--r-- | internal/processing/account/account_test.go | 6 | ||||
-rw-r--r-- | internal/processing/account/block.go | 204 | ||||
-rw-r--r-- | internal/processing/account/bookmarks.go | 2 | ||||
-rw-r--r-- | internal/processing/account/delete.go | 573 | ||||
-rw-r--r-- | internal/processing/account/follow.go | 227 | ||||
-rw-r--r-- | internal/processing/account/relationships.go | 112 |
6 files changed, 633 insertions, 491 deletions
diff --git a/internal/processing/account/account_test.go b/internal/processing/account/account_test.go index 667ad793f..0898e707b 100644 --- a/internal/processing/account/account_test.go +++ b/internal/processing/account/account_test.go @@ -91,8 +91,10 @@ func (suite *AccountStandardTestSuite) SetupTest() { suite.oauthServer = testrig.NewTestOauthServer(suite.db) suite.fromClientAPIChan = make(chan messages.FromClientAPI, 100) - suite.state.Workers.EnqueueClientAPI = func(ctx context.Context, msg messages.FromClientAPI) { - suite.fromClientAPIChan <- msg + suite.state.Workers.EnqueueClientAPI = func(ctx context.Context, msgs ...messages.FromClientAPI) { + for _, msg := range msgs { + suite.fromClientAPIChan <- msg + } } suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media")) diff --git a/internal/processing/account/block.go b/internal/processing/account/block.go index 5564bb8c9..1ec31a753 100644 --- a/internal/processing/account/block.go +++ b/internal/processing/account/block.go @@ -34,121 +34,93 @@ import ( // BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local. func (p *Processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - // make sure the target account actually exists in our db - targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err)) + targetAccount, existingBlock, errWithCode := p.getBlockTarget(ctx, requestingAccount, targetAccountID) + if errWithCode != nil { + return nil, errWithCode } - // if requestingAccount already blocks target account, we don't need to do anything - if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, false); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error checking existence of block: %s", err)) - } else if blocked { + if existingBlock != nil { + // Block already exists, nothing to do. return p.RelationshipGet(ctx, requestingAccount, targetAccountID) } - // don't block yourself, silly - if requestingAccount.ID == targetAccountID { - return nil, gtserror.NewErrorNotAcceptable(fmt.Errorf("BlockCreate: account %s cannot block itself", requestingAccount.ID)) + // Create and store a new block. + blockID := id.NewULID() + blockURI := uris.GenerateURIForBlock(requestingAccount.Username, blockID) + block := >smodel.Block{ + ID: blockID, + URI: blockURI, + AccountID: requestingAccount.ID, + Account: requestingAccount, + TargetAccountID: targetAccountID, + TargetAccount: targetAccount, } - // make the block - block := >smodel.Block{} - newBlockID := id.NewULID() - block.ID = newBlockID - block.AccountID = requestingAccount.ID - block.Account = requestingAccount - block.TargetAccountID = targetAccountID - block.TargetAccount = targetAccount - block.URI = uris.GenerateURIForBlock(requestingAccount.Username, newBlockID) - - // whack it in the database if err := p.state.DB.PutBlock(ctx, block); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error creating block in db: %s", err)) + err = fmt.Errorf("BlockCreate: error creating block in db: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - // clear any follows or follow requests from the blocked account to the target account -- this is a simple delete - if err := p.state.DB.DeleteWhere(ctx, []db.Where{ - {Key: "account_id", Value: targetAccountID}, - {Key: "target_account_id", Value: requestingAccount.ID}, - }, >smodel.Follow{}); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err)) - } - if err := p.state.DB.DeleteWhere(ctx, []db.Where{ - {Key: "account_id", Value: targetAccountID}, - {Key: "target_account_id", Value: requestingAccount.ID}, - }, >smodel.FollowRequest{}); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err)) + // Ensure each account unfollows the other. + // We only care about processing unfollow side + // effects from requesting account -> target + // account, since requesting account is ours, + // and target account might not be. + msgs, err := p.unfollow(ctx, requestingAccount, targetAccount) + if err != nil { + err = fmt.Errorf("BlockCreate: error unfollowing: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - // clear any follows or follow requests from the requesting account to the target account -- - // this might require federation so we need to pass some messages around - - // check if a follow request exists from the requesting account to the target account, 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("BlockCreate: error removing follow request from db: %s", err)) - } - frChanged = true + // Ensure unfollowed in other direction; + // ignore/don't process returned messages. + if _, err := p.unfollow(ctx, targetAccount, requestingAccount); err != nil { + err = fmt.Errorf("BlockCreate: error unfollowing: %w", err) + return nil, gtserror.NewErrorInternalError(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("BlockCreate: error removing follow from db: %s", err)) - } - fChanged = true + // Process block side effects (federation etc). + msgs = append(msgs, messages.FromClientAPI{ + APObjectType: ap.ActivityBlock, + APActivityType: ap.ActivityCreate, + GTSModel: block, + OriginAccount: requestingAccount, + TargetAccount: targetAccount, + }) + + // Batch queue accreted client api messages. + p.state.Workers.EnqueueClientAPI(ctx, msgs...) + + return p.RelationshipGet(ctx, requestingAccount, targetAccountID) +} + +// BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local. +func (p *Processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { + targetAccount, existingBlock, errWithCode := p.getBlockTarget(ctx, requestingAccount, targetAccountID) + if errWithCode != nil { + return nil, errWithCode } - // follow request status changed so send the UNDO activity to the channel for async processing - if frChanged { - p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ - APObjectType: ap.ActivityFollow, - APActivityType: ap.ActivityUndo, - GTSModel: >smodel.Follow{ - AccountID: requestingAccount.ID, - TargetAccountID: targetAccountID, - URI: frURI, - }, - OriginAccount: requestingAccount, - TargetAccount: targetAccount, - }) + if existingBlock == nil { + // Already not blocked, nothing to do. + return p.RelationshipGet(ctx, requestingAccount, targetAccountID) } - // follow status changed so send the UNDO activity to the channel for async processing - if fChanged { - p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ - APObjectType: ap.ActivityFollow, - APActivityType: ap.ActivityUndo, - GTSModel: >smodel.Follow{ - AccountID: requestingAccount.ID, - TargetAccountID: targetAccountID, - URI: fURI, - }, - OriginAccount: requestingAccount, - TargetAccount: targetAccount, - }) + // We got a block, remove it from the db. + if err := p.state.DB.DeleteBlockByID(ctx, existingBlock.ID); err != nil { + err := fmt.Errorf("BlockRemove: error removing block from db: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - // handle the rest of the block process asynchronously + // Populate account fields for convenience. + existingBlock.Account = requestingAccount + existingBlock.TargetAccount = targetAccount + + // Process block removal side effects (federation etc). p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ APObjectType: ap.ActivityBlock, - APActivityType: ap.ActivityCreate, - GTSModel: block, + APActivityType: ap.ActivityUndo, + GTSModel: existingBlock, OriginAccount: requestingAccount, TargetAccount: targetAccount, }) @@ -156,36 +128,32 @@ func (p *Processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel return p.RelationshipGet(ctx, requestingAccount, targetAccountID) } -// BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local. -func (p *Processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - // make sure the target account actually exists in our db +func (p *Processor) getBlockTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*gtsmodel.Account, *gtsmodel.Block, gtserror.WithCode) { + // Account should not block or unblock itself. + if requestingAccount.ID == targetAccountID { + err := fmt.Errorf("getBlockTarget: account %s cannot block or unblock itself", requestingAccount.ID) + return nil, nil, gtserror.NewErrorNotAcceptable(err, err.Error()) + } + + // Ensure target account retrievable. targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID) if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err)) + if !errors.Is(err, db.ErrNoEntries) { + // Real db error. + err = fmt.Errorf("getBlockTarget: db error looking for target account %s: %w", targetAccountID, err) + return nil, nil, gtserror.NewErrorInternalError(err) + } + // Account not found. + err = fmt.Errorf("getBlockTarget: target account %s not found in the db", targetAccountID) + return nil, nil, gtserror.NewErrorNotFound(err, err.Error()) } - // check if a block exists, and remove it if it does + // Check if currently blocked. block, err := p.state.DB.GetBlock(ctx, requestingAccount.ID, targetAccountID) - if err == nil { - // we got a block, remove it - block.Account = requestingAccount - block.TargetAccount = targetAccount - if err := p.state.DB.DeleteBlockByID(ctx, block.ID); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error removing block from db: %s", err)) - } - - // send the UNDO activity to the client worker for async processing - p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ - APObjectType: ap.ActivityBlock, - APActivityType: ap.ActivityUndo, - GTSModel: block, - OriginAccount: requestingAccount, - TargetAccount: targetAccount, - }) - } else if !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error getting possible block from db: %s", err)) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("getBlockTarget: db error checking existing block: %w", err) + return nil, nil, gtserror.NewErrorInternalError(err) } - // return whatever relationship results from all this - return p.RelationshipGet(ctx, requestingAccount, targetAccountID) + return targetAccount, block, nil } diff --git a/internal/processing/account/bookmarks.go b/internal/processing/account/bookmarks.go index add3d8d8d..56f0fc9e0 100644 --- a/internal/processing/account/bookmarks.go +++ b/internal/processing/account/bookmarks.go @@ -33,7 +33,7 @@ import ( // BookmarksGet returns a pageable response of statuses that are bookmarked by requestingAccount. // Paging for this response is done based on bookmark ID rather than status ID. func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode) { - bookmarks, err := p.state.DB.GetBookmarks(ctx, requestingAccount.ID, limit, maxID, minID) + bookmarks, err := p.state.DB.GetStatusBookmarks(ctx, requestingAccount.ID, limit, maxID, minID) if err != nil { return nil, gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index 6419033ec..9c59e1b99 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -20,135 +20,322 @@ package account import ( "context" "errors" + "fmt" "time" "codeberg.org/gruf/go-kv" "github.com/superseriousbusiness/gotosocial/internal/ap" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/messages" - "golang.org/x/crypto/bcrypt" ) +const deleteSelectLimit = 50 + // Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc. // The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block. func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode { - fields := kv.Fields{{"username", account.Username}} + l := log.WithContext(ctx).WithFields(kv.Fields{ + {"username", account.Username}, + {"domain", account.Domain}, + }...) + l.Trace("beginning account delete process") - if account.Domain != "" { - fields = append(fields, kv.Field{ - "domain", account.Domain, - }) + if account.IsLocal() { + if err := p.deleteUserAndTokensForAccount(ctx, account); err != nil { + return gtserror.NewErrorInternalError(err) + } } - l := log.WithContext(ctx).WithFields(fields...) - l.Trace("beginning account delete process") + if err := p.deleteAccountFollows(ctx, account); err != nil { + return gtserror.NewErrorInternalError(err) + } - // 1. Delete account's application(s), clients, and oauth tokens - // we only need to do this step for local account since remote ones won't have any tokens or applications on our server - var user *gtsmodel.User - if account.Domain == "" { - // see if we can get a user for this account - var err error - if user, err = p.state.DB.GetUserByAccountID(ctx, account.ID); err == nil { - // we got one! select all tokens with the user's ID - tokens := []*gtsmodel.Token{} - if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "user_id", Value: user.ID}}, &tokens); err == nil { - // we have some tokens to delete - for _, t := range tokens { - // delete client(s) associated with this token - if err := p.state.DB.DeleteByID(ctx, t.ClientID, >smodel.Client{}); err != nil { - l.Errorf("error deleting oauth client: %s", err) - } - // delete application(s) associated with this token - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "client_id", Value: t.ClientID}}, >smodel.Application{}); err != nil { - l.Errorf("error deleting application: %s", err) - } - // delete the token itself - if err := p.state.DB.DeleteByID(ctx, t.ID, t); err != nil { - l.Errorf("error deleting oauth token: %s", err) - } - } - } + if err := p.deleteAccountBlocks(ctx, account); err != nil { + return gtserror.NewErrorInternalError(err) + } + + if err := p.deleteAccountStatuses(ctx, account); err != nil { + return gtserror.NewErrorInternalError(err) + } + + if err := p.deleteAccountNotifications(ctx, account); err != nil { + return gtserror.NewErrorInternalError(err) + } + + if err := p.deleteAccountPeripheral(ctx, account); err != nil { + return gtserror.NewErrorInternalError(err) + } + + // To prevent the account being created again, + // stubbify it and update it in the db. + // The account will not be deleted, but it + // will become completely unusable. + columns := stubbifyAccount(account, origin) + if err := p.state.DB.UpdateAccount(ctx, account, columns...); err != nil { + return gtserror.NewErrorInternalError(err) + } + + l.Info("account deleted") + return nil +} + +// DeleteSelf is like Delete, but specifically for local accounts deleting themselves. +// +// Calling DeleteSelf results in a delete message being enqueued in the processor, +// which causes side effects to occur: delete will be federated out to other instances, +// and the above Delete function will be called afterwards from the processor, to clear +// out the account's bits and bobs, and stubbify it. +func (p *Processor) DeleteSelf(ctx context.Context, account *gtsmodel.Account) gtserror.WithCode { + fromClientAPIMessage := messages.FromClientAPI{ + APObjectType: ap.ActorPerson, + APActivityType: ap.ActivityDelete, + OriginAccount: account, + TargetAccount: account, + } + + // Process the delete side effects asynchronously. + p.state.Workers.EnqueueClientAPI(ctx, fromClientAPIMessage) + + return nil +} + +// deleteUserAndTokensForAccount deletes the gtsmodel.User and +// any OAuth tokens and applications for the given account. +// +// Callers to this function should already have checked that +// this is a local account, or else it won't have a user associated +// with it, and this will fail. +func (p *Processor) deleteUserAndTokensForAccount(ctx context.Context, account *gtsmodel.Account) error { + user, err := p.state.DB.GetUserByAccountID(ctx, account.ID) + if err != nil { + return fmt.Errorf("deleteUserAndTokensForAccount: db error getting user: %w", err) + } + + tokens := []*gtsmodel.Token{} + if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "user_id", Value: user.ID}}, &tokens); err != nil { + return fmt.Errorf("deleteUserAndTokensForAccount: db error getting tokens: %w", err) + } + + for _, t := range tokens { + // Delete any OAuth clients associated with this token. + if err := p.state.DB.DeleteByID(ctx, t.ClientID, &[]*gtsmodel.Client{}); err != nil { + return fmt.Errorf("deleteUserAndTokensForAccount: db error deleting client: %w", err) + } + + // Delete any OAuth applications associated with this token. + if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "client_id", Value: t.ClientID}}, &[]*gtsmodel.Application{}); err != nil { + return fmt.Errorf("deleteUserAndTokensForAccount: db error deleting application: %w", err) + } + + // Delete the token itself. + if err := p.state.DB.DeleteByID(ctx, t.ID, t); err != nil { + return fmt.Errorf("deleteUserAndTokensForAccount: db error deleting token: %w", err) } } - // 2. Delete account's blocks - l.Trace("deleting account blocks") - // first delete any blocks that this account created - if err := p.state.DB.DeleteBlocksByOriginAccountID(ctx, account.ID); err != nil { - l.Errorf("error deleting blocks created by account: %s", err) + if err := p.state.DB.DeleteUserByID(ctx, user.ID); err != nil { + return fmt.Errorf("deleteUserAndTokensForAccount: db error deleting user: %w", err) } - // now delete any blocks that target this account - if err := p.state.DB.DeleteBlocksByTargetAccountID(ctx, account.ID); err != nil { - l.Errorf("error deleting blocks targeting account: %s", err) + return nil +} + +// deleteAccountFollows deletes: +// - Follows targeting account. +// - Follow requests targeting account. +// - Follows created by account. +// - Follow requests created by account. +func (p *Processor) deleteAccountFollows(ctx context.Context, account *gtsmodel.Account) error { + // Delete follows targeting this account. + followedBy, err := p.state.DB.GetFollows(ctx, "", account.ID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return fmt.Errorf("deleteAccountFollows: db error getting follows targeting account %s: %w", account.ID, err) + } + + for _, follow := range followedBy { + if _, err := p.state.DB.Unfollow(ctx, follow.AccountID, account.ID); err != nil { + return fmt.Errorf("deleteAccountFollows: db error unfollowing account followedBy: %w", err) + } } - // 3. Delete account's emoji - // nothing to do here + // Delete follow requests targeting this account. + followRequestedBy, err := p.state.DB.GetFollowRequests(ctx, "", account.ID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return fmt.Errorf("deleteAccountFollows: db error getting follow requests targeting account %s: %w", account.ID, err) + } - // 4. Delete account's follow requests - // TODO: federate these if necessary - l.Trace("deleting account follow requests") - // first delete any follow requests that this account created - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.FollowRequest{}); err != nil { - l.Errorf("error deleting follow requests created by account: %s", err) + for _, followRequest := range followRequestedBy { + if _, err := p.state.DB.UnfollowRequest(ctx, followRequest.AccountID, account.ID); err != nil { + return fmt.Errorf("deleteAccountFollows: db error unfollowing account followRequestedBy: %w", err) + } } - // now delete any follow requests that target this account - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.FollowRequest{}); err != nil { - l.Errorf("error deleting follow requests targeting account: %s", err) + var ( + // Use this slice to batch unfollow messages. + msgs = []messages.FromClientAPI{} + // To avoid checking if account is local over + over + // inside the subsequent loops, just generate static + // side effects function once now. + unfollowSideEffects = p.unfollowSideEffectsFunc(account) + ) + + // Delete follows originating from this account. + following, err := p.state.DB.GetFollows(ctx, account.ID, "") + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return fmt.Errorf("deleteAccountFollows: db error getting follows owned by account %s: %w", account.ID, err) } - // 5. Delete account's follows - // TODO: federate these if necessary - l.Trace("deleting account follows") - // first delete any follows that this account created - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.Follow{}); err != nil { - l.Errorf("error deleting follows created by account: %s", err) + // For each follow owned by this account, unfollow + // and process side effects (noop if remote account). + for _, follow := range following { + if uri, err := p.state.DB.Unfollow(ctx, account.ID, follow.TargetAccountID); err != nil { + return fmt.Errorf("deleteAccountFollows: db error unfollowing account: %w", err) + } else if uri == "" { + // There was no follow after all. + // Some race condition? Skip. + log.WithContext(ctx).WithField("follow", follow).Warn("Unfollow did not return uri, likely race condition") + continue + } + + if msg := unfollowSideEffects(ctx, account, follow); msg != nil { + // There was a side effect to process. + msgs = append(msgs, *msg) + } } - // now delete any follows that target this account - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.Follow{}); err != nil { - l.Errorf("error deleting follows targeting account: %s", err) + // Delete follow requests originating from this account. + followRequesting, err := p.state.DB.GetFollowRequests(ctx, account.ID, "") + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return fmt.Errorf("deleteAccountFollows: db error getting follow requests owned by account %s: %w", account.ID, err) } - var maxID string + // For each follow owned by this account, unfollow + // and process side effects (noop if remote account). + for _, followRequest := range followRequesting { + uri, err := p.state.DB.UnfollowRequest(ctx, account.ID, followRequest.TargetAccountID) + if err != nil { + return fmt.Errorf("deleteAccountFollows: db error unfollowRequesting account: %w", err) + } - // 6. Delete account's statuses - l.Trace("deleting account statuses") + if uri == "" { + // There was no follow request after all. + // Some race condition? Skip. + log.WithContext(ctx).WithField("followRequest", followRequest).Warn("UnfollowRequest did not return uri, likely race condition") + continue + } - // we'll select statuses 20 at a time so we don't wreck the db, and pass them through to the client api channel - // Deleting the statuses in this way also handles 7. Delete account's media attachments, 8. Delete account's mentions, and 9. Delete account's polls, - // since these are all attached to statuses. + // Dummy out a follow so our side effects func + // has something to work with. This follow will + // never enter the db, it's just for convenience. + follow := >smodel.Follow{ + URI: uri, + AccountID: followRequest.AccountID, + Account: followRequest.Account, + TargetAccountID: followRequest.TargetAccountID, + TargetAccount: followRequest.TargetAccount, + } + + if msg := unfollowSideEffects(ctx, account, follow); msg != nil { + // There was a side effect to process. + msgs = append(msgs, *msg) + } + } + + // Process accreted messages asynchronously. + p.state.Workers.EnqueueClientAPI(ctx, msgs...) + return nil +} + +func (p *Processor) unfollowSideEffectsFunc(deletedAccount *gtsmodel.Account) func(ctx context.Context, account *gtsmodel.Account, follow *gtsmodel.Follow) *messages.FromClientAPI { + if !deletedAccount.IsLocal() { + // Don't try to process side effects + // for accounts that aren't local. + return func(ctx context.Context, account *gtsmodel.Account, follow *gtsmodel.Follow) *messages.FromClientAPI { + return nil // noop + } + } + + return func(ctx context.Context, account *gtsmodel.Account, follow *gtsmodel.Follow) *messages.FromClientAPI { + if follow.TargetAccount == nil { + // TargetAccount seems to have gone; + // race condition? db corruption? + log.WithContext(ctx).WithField("follow", follow).Warn("follow had no TargetAccount, likely race condition") + return nil + } + + if follow.TargetAccount.IsLocal() { + // No side effects for local unfollows. + return nil + } + + // There was a follow, process side effects. + return &messages.FromClientAPI{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityUndo, + GTSModel: follow, + OriginAccount: account, + TargetAccount: follow.TargetAccount, + } + } +} + +func (p *Processor) deleteAccountBlocks(ctx context.Context, account *gtsmodel.Account) error { + // Delete blocks created by this account. + if err := p.state.DB.DeleteBlocksByOriginAccountID(ctx, account.ID); err != nil { + return fmt.Errorf("deleteAccountBlocks: db error deleting blocks created by account %s: %w", account.ID, err) + } + + // Delete blocks targeting this account. + if err := p.state.DB.DeleteBlocksByTargetAccountID(ctx, account.ID); err != nil { + return fmt.Errorf("deleteAccountBlocks: db error deleting blocks targeting account %s: %w", account.ID, err) + } + + return nil +} + +// deleteAccountStatuses iterates through all statuses owned by +// the given account, passing each discovered status (and boosts +// thereof) to the processor workers for further async processing. +func (p *Processor) deleteAccountStatuses(ctx context.Context, account *gtsmodel.Account) error { + // We'll select statuses 50 at a time so we don't wreck the db, + // and pass them through to the client api worker to handle. + // + // Deleting the statuses in this way also handles deleting the + // account's media attachments, mentions, and polls, since these + // are all attached to statuses. + + var ( + statuses []*gtsmodel.Status + err error + maxID string + msgs = []messages.FromClientAPI{} + ) + +statusLoop: for { - // Fetch next block of account statuses from database - statuses, err := p.state.DB.GetAccountStatuses(ctx, account.ID, 20, false, false, maxID, "", false, false) - if err != nil { - if !errors.Is(err, db.ErrNoEntries) { - // an actual error has occurred - l.Errorf("Delete: db error selecting statuses for account %s: %s", account.Username, err) - } - break + // Page through account's statuses. + statuses, err = p.state.DB.GetAccountStatuses(ctx, account.ID, deleteSelectLimit, false, false, maxID, "", false, false) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + // Make sure we don't have a real error. + return err } if len(statuses) == 0 { - break // reached end + break statusLoop } - for _, status := range statuses { - // Ensure account is set - status.Account = account + // Update next maxID from last status. + maxID = statuses[len(statuses)-1].ID - l.Tracef("queue client API status delete: %s", status.ID) + for _, status := range statuses { + status.Account = account // ensure account is set - // pass the status delete through the client api channel for processing - p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ + // Pass the status delete through the client api worker for processing. + msgs = append(msgs, messages.FromClientAPI{ APObjectType: ap.ObjectNote, APActivityType: ap.ActivityDelete, GTSModel: status, @@ -156,30 +343,32 @@ func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origi TargetAccount: account, }) - // Look for any boosts of this status in DB + // Look for any boosts of this status in DB. boosts, err := p.state.DB.GetStatusReblogs(ctx, status) if err != nil && !errors.Is(err, db.ErrNoEntries) { - l.Errorf("error fetching status reblogs for %q: %v", status.ID, err) - continue + return fmt.Errorf("deleteAccountStatuses: error fetching status reblogs for %s: %w", status.ID, err) } for _, boost := range boosts { if boost.Account == nil { - // Fetch the relevant account for this status boost + // Fetch the relevant account for this status boost. boostAcc, err := p.state.DB.GetAccountByID(ctx, boost.AccountID) if err != nil { - l.Errorf("error fetching boosted status account for %q: %v", boost.AccountID, err) - continue + if errors.Is(err, db.ErrNoEntries) { + // We don't have an account for this boost + // for some reason, so just skip processing. + log.WithContext(ctx).WithField("boost", boost).Warnf("no account found with id %s for boost %s", boost.AccountID, boost.ID) + continue + } + return fmt.Errorf("deleteAccountStatuses: error fetching boosted status account for %s: %w", boost.AccountID, err) } // Set account model boost.Account = boostAcc } - l.Tracef("queue client API boost delete: %s", status.ID) - - // pass the boost delete through the client api channel for processing - p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ + // Pass the boost delete through the client api worker for processing. + msgs = append(msgs, messages.FromClientAPI{ APObjectType: ap.ActivityAnnounce, APActivityType: ap.ActivityUndo, GTSModel: status, @@ -188,128 +377,120 @@ func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origi }) } } - - // Update next maxID from last status - maxID = statuses[len(statuses)-1].ID } - // 10. Delete account's notifications - l.Trace("deleting account notifications") - // first notifications created by account - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "origin_account_id", Value: account.ID}}, &[]*gtsmodel.Notification{}); err != nil { - l.Errorf("error deleting notifications created by account: %s", err) - } + // Batch process all accreted messages. + p.state.Workers.EnqueueClientAPI(ctx, msgs...) - // now notifications targeting account - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.Notification{}); err != nil { - l.Errorf("error deleting notifications targeting account: %s", err) - } + return nil +} - // 11. Delete account's bookmarks - l.Trace("deleting account bookmarks") - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusBookmark{}); err != nil { - l.Errorf("error deleting bookmarks created by account: %s", err) +func (p *Processor) deleteAccountNotifications(ctx context.Context, account *gtsmodel.Account) error { + // Delete all notifications targeting given account. + if err := p.state.DB.DeleteNotifications(ctx, account.ID, ""); err != nil && !errors.Is(err, db.ErrNoEntries) { + return err } - // 12. Delete account's faves - // TODO: federate these if necessary - l.Trace("deleting account faves") - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusFave{}); err != nil { - l.Errorf("error deleting faves created by account: %s", err) + // Delete all notifications originating from given account. + if err := p.state.DB.DeleteNotifications(ctx, "", account.ID); err != nil && !errors.Is(err, db.ErrNoEntries) { + return err } - // 13. Delete account's mutes - l.Trace("deleting account mutes") - if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusMute{}); err != nil { - l.Errorf("error deleting status mutes created by account: %s", err) + return nil +} + +func (p *Processor) deleteAccountPeripheral(ctx context.Context, account *gtsmodel.Account) error { + // Delete all bookmarks owned by given account. + if err := p.state.DB.DeleteStatusBookmarks(ctx, account.ID, ""); // nocollapse + err != nil && !errors.Is(err, db.ErrNoEntries) { + return err } - // 14. Delete account's streams - // TODO + // Delete all bookmarks targeting given account. + if err := p.state.DB.DeleteStatusBookmarks(ctx, "", account.ID); // nocollapse + err != nil && !errors.Is(err, db.ErrNoEntries) { + return err + } - // 15. Delete account's tags - // TODO + // Delete all faves owned by given account. + if err := p.state.DB.DeleteStatusFaves(ctx, account.ID, ""); // nocollapse + err != nil && !errors.Is(err, db.ErrNoEntries) { + return err + } - // 16. Delete account's user - if user != nil { - l.Trace("deleting account user") - if err := p.state.DB.DeleteUserByID(ctx, user.ID); err != nil { - return gtserror.NewErrorInternalError(err) - } + // Delete all faves targeting given account. + if err := p.state.DB.DeleteStatusFaves(ctx, "", account.ID); // nocollapse + err != nil && !errors.Is(err, db.ErrNoEntries) { + return err } - // 17. Delete account's timeline - // TODO + // TODO: add status mutes here when they're implemented. - // 18. Delete account itself - // to prevent the account being created again, set all these fields and update it in the db - // the account won't actually be *removed* from the database but it will be set to just a stub - account.Note = "" - account.DisplayName = "" + return nil +} + +// stubbifyAccount renders the given account as a stub, +// removing most information from it and marking it as +// suspended. +// +// The origin parameter refers to the origin of the +// suspension action; should be an account ID or domain +// block ID. +// +// For caller's convenience, this function returns the db +// names of all columns that are updated by it. +func stubbifyAccount(account *gtsmodel.Account, origin string) []string { + var ( + falseBool = func() *bool { b := false; return &b } + trueBool = func() *bool { b := true; return &b } + now = time.Now() + never = time.Time{} + ) + + account.FetchedAt = never account.AvatarMediaAttachmentID = "" account.AvatarRemoteURL = "" account.HeaderMediaAttachmentID = "" account.HeaderRemoteURL = "" + account.DisplayName = "" + account.EmojiIDs = nil + account.Emojis = nil + account.Fields = nil + account.Note = "" + account.NoteRaw = "" + account.Memorial = falseBool() + account.AlsoKnownAs = "" + account.MovedToAccountID = "" account.Reason = "" - account.Emojis = []*gtsmodel.Emoji{} - account.EmojiIDs = []string{} - account.Fields = []gtsmodel.Field{} - hideCollections := true - account.HideCollections = &hideCollections - discoverable := false - account.Discoverable = &discoverable - account.SuspendedAt = time.Now() + account.Discoverable = falseBool() + account.StatusContentType = "" + account.CustomCSS = "" + account.SuspendedAt = now account.SuspensionOrigin = origin - err := p.state.DB.UpdateAccount(ctx, account) - if err != nil { - return gtserror.NewErrorInternalError(err) - } - - l.Infof("deleted account with username %s from domain %s", account.Username, account.Domain) - return nil -} - -// DeleteLocal is like Delete, but specifically for deletion of local accounts rather than federated ones. -// Unlike Delete, it will propagate the deletion out across the federating API to other instances. -func (p *Processor) DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode { - fromClientAPIMessage := messages.FromClientAPI{ - APObjectType: ap.ActorPerson, - APActivityType: ap.ActivityDelete, - TargetAccount: account, + account.HideCollections = trueBool() + account.EnableRSS = falseBool() + + return []string{ + "fetched_at", + "avatar_media_attachment_id", + "avatar_remote_url", + "header_media_attachment_id", + "header_remote_url", + "display_name", + "emojis", + "fields", + "note", + "note_raw", + "memorial", + "also_known_as", + "moved_to_account_id", + "reason", + "discoverable", + "status_content_type", + "custom_css", + "suspended_at", + "suspension_origin", + "hide_collections", + "enable_rss", } - - if form.DeleteOriginID == account.ID { - // the account owner themself has requested deletion via the API, get their user from the db - user, err := p.state.DB.GetUserByAccountID(ctx, account.ID) - if err != nil { - return gtserror.NewErrorInternalError(err) - } - - // now check that the password they supplied is correct - // make sure a password is actually set and bail if not - if user.EncryptedPassword == "" { - return gtserror.NewErrorForbidden(errors.New("user password was not set")) - } - - // compare the provided password with the encrypted one from the db, bail if they don't match - if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(form.Password)); err != nil { - return gtserror.NewErrorForbidden(errors.New("invalid password")) - } - - fromClientAPIMessage.OriginAccount = account - } else { - // the delete has been requested by some other account, grab it; - // if we've reached this point we know it has permission already - requestingAccount, err := p.state.DB.GetAccountByID(ctx, form.DeleteOriginID) - if err != nil { - return gtserror.NewErrorInternalError(err) - } - - fromClientAPIMessage.OriginAccount = requestingAccount - } - - // put the delete in the processor queue to handle the rest of it asynchronously - p.state.Workers.EnqueueClientAPI(ctx, fromClientAPIMessage) - - return nil } 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 } diff --git a/internal/processing/account/relationships.go b/internal/processing/account/relationships.go index 54a0b82ab..4afe36afe 100644 --- a/internal/processing/account/relationships.go +++ b/internal/processing/account/relationships.go @@ -26,98 +26,51 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // FollowersGet fetches a list of the target account's followers. func (p *Processor) FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { + err = fmt.Errorf("FollowersGet: db error checking block: %w", err) return nil, gtserror.NewErrorInternalError(err) } else if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) + err = errors.New("FollowersGet: block exists between accounts") + return nil, gtserror.NewErrorNotFound(err) } - accounts := []apimodel.Account{} - follows, err := p.state.DB.GetAccountFollowedBy(ctx, targetAccountID, false) + follows, err := p.state.DB.GetFollows(ctx, "", targetAccountID) if err != nil { - if err == db.ErrNoEntries { - return accounts, nil - } - return nil, gtserror.NewErrorInternalError(err) - } - - for _, f := range follows { - blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if blocked { - continue - } - - if f.Account == nil { - a, err := p.state.DB.GetAccountByID(ctx, f.AccountID) - if err != nil { - if err == db.ErrNoEntries { - continue - } - return nil, gtserror.NewErrorInternalError(err) - } - f.Account = a - } - - account, err := p.tc.AccountToAPIAccountPublic(ctx, f.Account) - if err != nil { + if !errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("FollowersGet: db error getting followers: %w", err) return nil, gtserror.NewErrorInternalError(err) } - accounts = append(accounts, *account) + return []apimodel.Account{}, nil } - return accounts, nil + + return p.accountsFromFollows(ctx, follows, requestingAccount.ID) } // FollowingGet fetches a list of the accounts that target account is following. func (p *Processor) FollowingGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { + err = fmt.Errorf("FollowingGet: db error checking block: %w", err) return nil, gtserror.NewErrorInternalError(err) } else if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) + err = errors.New("FollowingGet: block exists between accounts") + return nil, gtserror.NewErrorNotFound(err) } - accounts := []apimodel.Account{} - follows, err := p.state.DB.GetAccountFollows(ctx, targetAccountID) + follows, err := p.state.DB.GetFollows(ctx, targetAccountID, "") if err != nil { - if err == db.ErrNoEntries { - return accounts, nil - } - return nil, gtserror.NewErrorInternalError(err) - } - - for _, f := range follows { - blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if blocked { - continue - } - - if f.TargetAccount == nil { - a, err := p.state.DB.GetAccountByID(ctx, f.TargetAccountID) - if err != nil { - if err == db.ErrNoEntries { - continue - } - return nil, gtserror.NewErrorInternalError(err) - } - f.TargetAccount = a - } - - account, err := p.tc.AccountToAPIAccountPublic(ctx, f.TargetAccount) - if err != nil { + if !errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("FollowingGet: db error getting followers: %w", err) return nil, gtserror.NewErrorInternalError(err) } - accounts = append(accounts, *account) + return []apimodel.Account{}, nil } - return accounts, nil + + return p.accountsFromFollows(ctx, follows, requestingAccount.ID) } // RelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account. @@ -138,3 +91,30 @@ func (p *Processor) RelationshipGet(ctx context.Context, requestingAccount *gtsm return r, nil } + +func (p *Processor) accountsFromFollows(ctx context.Context, follows []*gtsmodel.Follow, requestingAccountID string) ([]apimodel.Account, gtserror.WithCode) { + accounts := make([]apimodel.Account, 0, len(follows)) + for _, follow := range follows { + if follow.Account == nil { + // No account set for some reason; just skip. + log.WithContext(ctx).WithField("follow", follow).Warn("follow had no associated account") + continue + } + + if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccountID, follow.AccountID, true); err != nil { + err = fmt.Errorf("accountsFromFollows: db error checking block: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } else if blocked { + continue + } + + account, err := p.tc.AccountToAPIAccountPublic(ctx, follow.Account) + if err != nil { + err = fmt.Errorf("accountsFromFollows: error converting account to api account: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + accounts = append(accounts, *account) + } + + return accounts, nil +} |