mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2026-01-06 20:13:17 -06:00
[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!
This commit is contained in:
parent
276d773438
commit
e8595f0c64
53 changed files with 2472 additions and 1321 deletions
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -34,118 +34,53 @@ 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
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
||||
// handle the rest of the block process asynchronously
|
||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||
// Process block side effects (federation etc).
|
||||
msgs = append(msgs, messages.FromClientAPI{
|
||||
APObjectType: ap.ActivityBlock,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: block,
|
||||
|
|
@ -153,39 +88,72 @@ func (p *Processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel
|
|||
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) {
|
||||
// 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
|
||||
}
|
||||
|
||||
// check if a block exists, and remove it if it does
|
||||
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 existingBlock == nil {
|
||||
// Already not blocked, nothing to do.
|
||||
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
||||
}
|
||||
|
||||
// return whatever relationship results from all this
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.ActivityUndo,
|
||||
GTSModel: existingBlock,
|
||||
OriginAccount: requestingAccount,
|
||||
TargetAccount: targetAccount,
|
||||
})
|
||||
|
||||
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 currently blocked.
|
||||
block, err := p.state.DB.GetBlock(ctx, requestingAccount.ID, targetAccountID)
|
||||
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 targetAccount, block, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
||||
if account.Domain != "" {
|
||||
fields = append(fields, kv.Field{
|
||||
"domain", account.Domain,
|
||||
})
|
||||
}
|
||||
|
||||
l := log.WithContext(ctx).WithFields(fields...)
|
||||
l := log.WithContext(ctx).WithFields(kv.Fields{
|
||||
{"username", account.Username},
|
||||
{"domain", account.Domain},
|
||||
}...)
|
||||
l.Trace("beginning account delete process")
|
||||
|
||||
// 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 account.IsLocal() {
|
||||
if err := p.deleteUserAndTokensForAccount(ctx, account); err != nil {
|
||||
return gtserror.NewErrorInternalError(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.deleteAccountFollows(ctx, account); err != nil {
|
||||
return gtserror.NewErrorInternalError(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)
|
||||
if err := p.deleteAccountBlocks(ctx, account); err != nil {
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// 3. Delete account's emoji
|
||||
// nothing to do here
|
||||
|
||||
// 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)
|
||||
if err := p.deleteAccountStatuses(ctx, account); err != nil {
|
||||
return gtserror.NewErrorInternalError(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)
|
||||
if err := p.deleteAccountNotifications(ctx, account); err != nil {
|
||||
return gtserror.NewErrorInternalError(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)
|
||||
if err := p.deleteAccountPeripheral(ctx, account); err != nil {
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
}
|
||||
|
||||
var maxID string
|
||||
l.Info("account deleted")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 6. Delete account's statuses
|
||||
l.Trace("deleting account statuses")
|
||||
// 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,
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Process the delete side effects asynchronously.
|
||||
p.state.Workers.EnqueueClientAPI(ctx, fromClientAPIMessage)
|
||||
|
||||
for {
|
||||
// Fetch next block of account statuses from database
|
||||
statuses, err := p.state.DB.GetAccountStatuses(ctx, account.ID, 20, false, false, maxID, "", false, false)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.state.DB.DeleteUserByID(ctx, user.ID); err != nil {
|
||||
return fmt.Errorf("deleteUserAndTokensForAccount: db error deleting user: %w", 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
return fmt.Errorf("deleteAccountFollows: db error unfollowRequesting account: %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
}
|
||||
|
||||
// Update next maxID from last status.
|
||||
maxID = statuses[len(statuses)-1].ID
|
||||
|
||||
for _, status := range statuses {
|
||||
// Ensure account is set
|
||||
status.Account = account
|
||||
status.Account = account // ensure account is set
|
||||
|
||||
l.Tracef("queue client API status delete: %s", status.ID)
|
||||
|
||||
// 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...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
|
||||
// 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 bookmarks targeting given account.
|
||||
if err := p.state.DB.DeleteStatusBookmarks(ctx, "", account.ID); // nocollapse
|
||||
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)
|
||||
// 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
|
||||
}
|
||||
|
||||
// 14. Delete account's streams
|
||||
// TODO
|
||||
|
||||
// 15. Delete account's tags
|
||||
// TODO
|
||||
|
||||
// 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)
|
||||
}
|
||||
account.HideCollections = trueBool()
|
||||
account.EnableRSS = falseBool()
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
targetAccount, errWithCode := p.getFollowTarget(ctx, requestingAccount.ID, targetAccountID)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
// Unfollow and deal with side effects.
|
||||
msgs, err := p.unfollow(ctx, requestingAccount, targetAccount)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("FollowRemove: account %s not found in the db: %s", targetAccountID, err))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
if blocked {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
|
||||
} else if blocked {
|
||||
err = errors.New("block exists between accounts")
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
// make sure the target account actually exists in our db
|
||||
targetAcct, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
||||
// Ensure target account retrievable.
|
||||
targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
||||
if err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
|
||||
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)
|
||||
}
|
||||
// Account not found.
|
||||
err = fmt.Errorf("target account %s not found in the db", targetAccountID)
|
||||
return nil, gtserror.NewErrorNotFound(err, err.Error())
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return targetAccount, nil
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
fChanged = true
|
||||
}
|
||||
// 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{}
|
||||
|
||||
// follow request status changed so send the UNDO activity to the channel for async processing
|
||||
if frChanged {
|
||||
p.state.Workers.EnqueueClientAPI(ctx, 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,
|
||||
},
|
||||
OriginAccount: requestingAccount,
|
||||
TargetAccount: targetAcct,
|
||||
})
|
||||
}
|
||||
|
||||
// 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,
|
||||
TargetAccountID: targetAccount.ID,
|
||||
URI: fURI,
|
||||
},
|
||||
OriginAccount: requestingAccount,
|
||||
TargetAccount: targetAcct,
|
||||
TargetAccount: targetAccount,
|
||||
})
|
||||
}
|
||||
|
||||
// return whatever relationship results from all this
|
||||
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
||||
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: targetAccount.ID,
|
||||
URI: frURI,
|
||||
},
|
||||
OriginAccount: requestingAccount,
|
||||
TargetAccount: targetAccount,
|
||||
})
|
||||
}
|
||||
|
||||
return msgs, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
if !errors.Is(err, db.ErrNoEntries) {
|
||||
err = fmt.Errorf("FollowersGet: db error getting followers: %w", err)
|
||||
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 {
|
||||
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 {
|
||||
if !errors.Is(err, db.ErrNoEntries) {
|
||||
err = fmt.Errorf("FollowingGet: db error getting followers: %w", err)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue