summaryrefslogtreecommitdiff
path: root/internal/processing/account
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/account')
-rw-r--r--internal/processing/account/account_test.go6
-rw-r--r--internal/processing/account/block.go204
-rw-r--r--internal/processing/account/bookmarks.go2
-rw-r--r--internal/processing/account/delete.go573
-rw-r--r--internal/processing/account/follow.go227
-rw-r--r--internal/processing/account/relationships.go112
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 := &gtsmodel.Block{
+ ID: blockID,
+ URI: blockURI,
+ AccountID: requestingAccount.ID,
+ Account: requestingAccount,
+ TargetAccountID: targetAccountID,
+ TargetAccount: targetAccount,
}
- // make the block
- block := &gtsmodel.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},
- }, &gtsmodel.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},
- }, &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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: &gtsmodel.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: &gtsmodel.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, &gtsmodel.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}}, &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.FollowRequest{
- ID: newFollowID,
+ ID: followID,
+ URI: followURI,
AccountID: requestingAccount.ID,
+ Account: requestingAccount,
TargetAccountID: form.ID,
- ShowReblogs: &showReblogs,
- URI: uris.GenerateURIForFollow(requestingAccount.Username, newFollowID),
- Notify: &notify,
- }
- 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 := &gtsmodel.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 := &gtsmodel.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: &gtsmodel.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: &gtsmodel.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
+}