mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 12:22:25 -05:00
[bugfix] process account delete synchronously to prevent OOM (#4260)
# Description - updates account delete processing to handle side-effects synchronously to prevent OOM - updates account delete processing to check more often if account.IsLocal() for certain deletes / side-effects - ensures that mutes get removed from database on delete ## Checklist - [x] I/we have read the [GoToSocial contribution guidelines](https://codeberg.org/superseriousbusiness/gotosocial/src/branch/main/CONTRIBUTING.md). - [x] I/we have discussed the proposed changes already, either in an issue on the repository, or in the Matrix chat. - [x] I/we have not leveraged AI to create the proposed changes. - [x] I/we have performed a self-review of added code. - [x] I/we have written code that is legible and maintainable by others. - [x] I/we have commented the added code, particularly in hard-to-understand areas. - [ ] I/we have made any necessary changes to documentation. - [ ] I/we have added tests that cover new code. - [ ] I/we have run tests and they pass locally with the changes. - [x] I/we have run `go fmt ./...` and `golangci-lint run`. Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4260 Co-authored-by: kim <grufwub@gmail.com> Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
parent
d7f967cbb5
commit
e87681d433
11 changed files with 433 additions and 377 deletions
|
|
@ -20,6 +20,7 @@ package bundb
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.superseriousbusiness.org/gotosocial/internal/db"
|
"code.superseriousbusiness.org/gotosocial/internal/db"
|
||||||
|
|
@ -170,16 +171,24 @@ func (r *relationshipDB) GetAccountFollowRequesting(ctx context.Context, account
|
||||||
return r.GetFollowRequestsByIDs(ctx, followReqIDs)
|
return r.GetFollowRequestsByIDs(ctx, followReqIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *relationshipDB) GetAccountBlocks(ctx context.Context, accountID string, page *paging.Page) ([]*gtsmodel.Block, error) {
|
func (r *relationshipDB) GetAccountBlocking(ctx context.Context, accountID string, page *paging.Page) ([]*gtsmodel.Block, error) {
|
||||||
blockIDs, err := r.GetAccountBlockIDs(ctx, accountID, page)
|
blockIDs, err := r.GetAccountBlockingIDs(ctx, accountID, page)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return r.GetBlocksByIDs(ctx, blockIDs)
|
return r.GetBlocksByIDs(ctx, blockIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *relationshipDB) CountAccountBlocks(ctx context.Context, accountID string) (int, error) {
|
func (r *relationshipDB) GetAccountBlockedBy(ctx context.Context, accountID string, page *paging.Page) ([]*gtsmodel.Block, error) {
|
||||||
blockIDs, err := r.GetAccountBlockIDs(ctx, accountID, nil)
|
blockIDs, err := r.GetAccountBlockedByIDs(ctx, accountID, page)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.GetBlocksByIDs(ctx, blockIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *relationshipDB) CountAccountBlocking(ctx context.Context, accountID string) (int, error) {
|
||||||
|
blockIDs, err := r.GetAccountBlockingIDs(ctx, accountID, nil)
|
||||||
return len(blockIDs), err
|
return len(blockIDs), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,12 +282,12 @@ func (r *relationshipDB) GetAccountFollowRequestingIDs(ctx context.Context, acco
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *relationshipDB) GetAccountBlockIDs(ctx context.Context, accountID string, page *paging.Page) ([]string, error) {
|
func (r *relationshipDB) GetAccountBlockingIDs(ctx context.Context, accountID string, page *paging.Page) ([]string, error) {
|
||||||
return loadPagedIDs(&r.state.Caches.DB.BlockIDs, accountID, page, func() ([]string, error) {
|
return loadPagedIDs(&r.state.Caches.DB.BlockIDs, accountID, page, func() ([]string, error) {
|
||||||
var blockIDs []string
|
var blockIDs []string
|
||||||
|
|
||||||
// Block IDs not in cache, perform DB query!
|
// Block IDs not in cache, perform DB query!
|
||||||
q := newSelectBlocks(r.db, accountID)
|
q := newSelectBlocking(r.db, accountID)
|
||||||
if _, err := q.Exec(ctx, &blockIDs); // nocollapse
|
if _, err := q.Exec(ctx, &blockIDs); // nocollapse
|
||||||
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -288,6 +297,33 @@ func (r *relationshipDB) GetAccountBlockIDs(ctx context.Context, accountID strin
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *relationshipDB) GetAccountBlockedByIDs(ctx context.Context, accountID string, page *paging.Page) ([]string, error) {
|
||||||
|
var blockIDs []string
|
||||||
|
|
||||||
|
// NOTE that we are specifically not using
|
||||||
|
// any caching here, as this is only called
|
||||||
|
// when deleting an account, i.e. pointless!
|
||||||
|
|
||||||
|
// Block IDs not in cache, perform DB query!
|
||||||
|
q := newSelectBlockedBy(r.db, accountID)
|
||||||
|
if _, err := q.Exec(ctx, &blockIDs); // nocollapse
|
||||||
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our selected IDs are ALWAYS fetched
|
||||||
|
// from `loadDESC` in descending order.
|
||||||
|
// Depending on the paging requested
|
||||||
|
// this may be an unexpected order.
|
||||||
|
if page.GetOrder().Ascending() {
|
||||||
|
slices.Reverse(blockIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page the resulting block IDs.
|
||||||
|
blockIDs = page.Page(blockIDs)
|
||||||
|
return blockIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// newSelectFollowRequests returns a new select query for all rows in the follow_requests table with target_account_id = accountID.
|
// newSelectFollowRequests returns a new select query for all rows in the follow_requests table with target_account_id = accountID.
|
||||||
func newSelectFollowRequests(db *bun.DB, accountID string) *bun.SelectQuery {
|
func newSelectFollowRequests(db *bun.DB, accountID string) *bun.SelectQuery {
|
||||||
return db.NewSelect().
|
return db.NewSelect().
|
||||||
|
|
@ -360,11 +396,20 @@ func newSelectLocalFollowers(db *bun.DB, accountID string) *bun.SelectQuery {
|
||||||
OrderExpr("? DESC", bun.Ident("created_at"))
|
OrderExpr("? DESC", bun.Ident("created_at"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSelectBlocks returns a new select query for all rows in the blocks table with account_id = accountID.
|
// newSelectBlocking returns a new select query for all rows in the blocks table with account_id = accountID.
|
||||||
func newSelectBlocks(db *bun.DB, accountID string) *bun.SelectQuery {
|
func newSelectBlocking(db *bun.DB, accountID string) *bun.SelectQuery {
|
||||||
return db.NewSelect().
|
return db.NewSelect().
|
||||||
TableExpr("?", bun.Ident("blocks")).
|
TableExpr("?", bun.Ident("blocks")).
|
||||||
ColumnExpr("?", bun.Ident("id")).
|
ColumnExpr("?", bun.Ident("id")).
|
||||||
Where("? = ?", bun.Ident("account_id"), accountID).
|
Where("? = ?", bun.Ident("account_id"), accountID).
|
||||||
OrderExpr("? DESC", bun.Ident("id"))
|
OrderExpr("? DESC", bun.Ident("id"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newSelectBlocking returns a new select query for all rows in the blocks table with target_account_id = accountID.
|
||||||
|
func newSelectBlockedBy(db *bun.DB, accountID string) *bun.SelectQuery {
|
||||||
|
return db.NewSelect().
|
||||||
|
TableExpr("?", bun.Ident("blocks")).
|
||||||
|
ColumnExpr("?", bun.Ident("id")).
|
||||||
|
Where("? = ?", bun.Ident("target_account_id"), accountID).
|
||||||
|
OrderExpr("? DESC", bun.Ident("id"))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -176,14 +176,20 @@ type Relationship interface {
|
||||||
// GetAccountFollowRequestingIDs is like GetAccountFollowRequesting, but returns just IDs.
|
// GetAccountFollowRequestingIDs is like GetAccountFollowRequesting, but returns just IDs.
|
||||||
GetAccountFollowRequestingIDs(ctx context.Context, accountID string, page *paging.Page) ([]string, error)
|
GetAccountFollowRequestingIDs(ctx context.Context, accountID string, page *paging.Page) ([]string, error)
|
||||||
|
|
||||||
// GetAccountBlocks returns all blocks originating from the given account, with given optional paging parameters.
|
// GetAccountBlocking returns all blocks originating from the given account, with given optional paging parameters.
|
||||||
GetAccountBlocks(ctx context.Context, accountID string, paging *paging.Page) ([]*gtsmodel.Block, error)
|
GetAccountBlocking(ctx context.Context, accountID string, paging *paging.Page) ([]*gtsmodel.Block, error)
|
||||||
|
|
||||||
// GetAccountBlockIDs is like GetAccountBlocks, but returns just IDs.
|
// GetAccountBlockingIDs is like GetAccountBlocking, but returns just IDs.
|
||||||
GetAccountBlockIDs(ctx context.Context, accountID string, page *paging.Page) ([]string, error)
|
GetAccountBlockingIDs(ctx context.Context, accountID string, page *paging.Page) ([]string, error)
|
||||||
|
|
||||||
// CountAccountBlocks counts the number of blocks owned by the given account.
|
// GetAccountBlockedBy returns all blocks targeting the given account, with optional paging parameters.
|
||||||
CountAccountBlocks(ctx context.Context, accountID string) (int, error)
|
GetAccountBlockedBy(ctx context.Context, accountID string, page *paging.Page) ([]*gtsmodel.Block, error)
|
||||||
|
|
||||||
|
// GetAccountBlockedByIDs is like GetAccountBlockedBy, but returns just IDs.
|
||||||
|
GetAccountBlockedByIDs(ctx context.Context, accountID string, page *paging.Page) ([]string, error)
|
||||||
|
|
||||||
|
// CountAccountBlocking counts the number of blocks owned by the given account.
|
||||||
|
CountAccountBlocking(ctx context.Context, accountID string) (int, error)
|
||||||
|
|
||||||
// GetNote gets a private note from a source account on a target account, if it exists.
|
// GetNote gets a private note from a source account on a target account, if it exists.
|
||||||
GetNote(ctx context.Context, sourceAccountID string, targetAccountID string) (*gtsmodel.AccountNote, error)
|
GetNote(ctx context.Context, sourceAccountID string, targetAccountID string) (*gtsmodel.AccountNote, error)
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,14 @@ package middleware
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
|
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
|
||||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||||
"code.superseriousbusiness.org/gotosocial/internal/log"
|
"code.superseriousbusiness.org/gotosocial/internal/log"
|
||||||
|
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||||
"codeberg.org/gruf/go-bytesize"
|
"codeberg.org/gruf/go-bytesize"
|
||||||
"codeberg.org/gruf/go-errors/v2"
|
|
||||||
"codeberg.org/gruf/go-kv"
|
"codeberg.org/gruf/go-kv"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
@ -49,23 +48,18 @@ func Logger(logClientIP bool) gin.HandlerFunc {
|
||||||
// Get request context.
|
// Get request context.
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
if r := recover(); r != nil {
|
// Recover from any panics
|
||||||
|
// and dump stack to stderr.
|
||||||
|
if r := util.Recover(); r != nil {
|
||||||
|
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
// No response was written, send a generic Internal Error
|
// No response was written, send a generic Internal Error
|
||||||
c.Writer.WriteHeader(http.StatusInternalServerError)
|
c.Writer.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append panic information to the request ctx
|
// Append panic information to the request.
|
||||||
err := fmt.Errorf("recovered panic: %v", r)
|
err := fmt.Errorf("recovered panic: %v", r)
|
||||||
_ = c.Error(err)
|
_ = c.Error(err)
|
||||||
|
|
||||||
// Dump a stacktrace to error log
|
|
||||||
pcs := make([]uintptr, 10)
|
|
||||||
n := runtime.Callers(3, pcs)
|
|
||||||
iter := runtime.CallersFrames(pcs[:n])
|
|
||||||
callers := errors.Callers(gatherFrames(iter, n))
|
|
||||||
log.WithContext(c.Request.Context()).
|
|
||||||
WithField("stacktrace", callers).Error(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the logging fields
|
// Initialize the logging fields
|
||||||
|
|
@ -134,7 +128,7 @@ func Logger(logClientIP bool) gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a nicer looking bytecount
|
// Generate a nicer looking bytecount.
|
||||||
size := bytesize.Size(c.Writer.Size()) // #nosec G115 -- Just logging
|
size := bytesize.Size(c.Writer.Size()) // #nosec G115 -- Just logging
|
||||||
|
|
||||||
// Write log entry with status text + body size.
|
// Write log entry with status text + body size.
|
||||||
|
|
@ -152,19 +146,3 @@ func Logger(logClientIP bool) gin.HandlerFunc {
|
||||||
func sensitiveQuery(query string) bool {
|
func sensitiveQuery(query string) bool {
|
||||||
return strings.Contains(query, "token")
|
return strings.Contains(query, "token")
|
||||||
}
|
}
|
||||||
|
|
||||||
// gatherFrames gathers runtime frames from a frame iterator.
|
|
||||||
func gatherFrames(iter *runtime.Frames, n int) []runtime.Frame {
|
|
||||||
if iter == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
frames := make([]runtime.Frame, 0, n)
|
|
||||||
for {
|
|
||||||
f, ok := iter.Next()
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
frames = append(frames, f)
|
|
||||||
}
|
|
||||||
return frames
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ func (p *Processor) BlocksGet(
|
||||||
requestingAccount *gtsmodel.Account,
|
requestingAccount *gtsmodel.Account,
|
||||||
page *paging.Page,
|
page *paging.Page,
|
||||||
) (*apimodel.PageableResponse, gtserror.WithCode) {
|
) (*apimodel.PageableResponse, gtserror.WithCode) {
|
||||||
blocks, err := p.state.DB.GetAccountBlocks(ctx,
|
blocks, err := p.state.DB.GetAccountBlocking(ctx,
|
||||||
requestingAccount.ID,
|
requestingAccount.ID,
|
||||||
page,
|
page,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -36,309 +36,300 @@ import (
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const deleteSelectLimit = 50
|
const deleteSelectLimit = 100
|
||||||
|
|
||||||
// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc.
|
// 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.
|
// 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,
|
// This delete function handles the case of both local and remote accounts, and processes side
|
||||||
account *gtsmodel.Account,
|
// effects synchronously to not clog worker queues with potentially tens-of-thousands of requests.
|
||||||
origin string,
|
func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) error {
|
||||||
) gtserror.WithCode {
|
|
||||||
l := log.WithContext(ctx).WithFields(kv.Fields{
|
// Prepare a new log entry for account delete.
|
||||||
{"username", account.Username},
|
log := log.WithContext(ctx).WithFields(kv.Fields{
|
||||||
{"domain", account.Domain},
|
{"uri", account.URI},
|
||||||
|
{"origin", origin},
|
||||||
}...)
|
}...)
|
||||||
l.Trace("beginning account delete process")
|
|
||||||
|
|
||||||
// Delete statuses *before* follows to ensure correct addressing
|
var err error
|
||||||
// of any outgoing fedi messages generated by deleting statuses.
|
|
||||||
if err := p.deleteAccountStatuses(ctx, account); err != nil {
|
|
||||||
l.Errorf("continuing after error during account delete: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.deleteAccountFollows(ctx, account); err != nil {
|
// Log operation start / stop.
|
||||||
l.Errorf("continuing after error during account delete: %v", err)
|
log.Info("start account delete")
|
||||||
}
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("fatal error during account delete: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("finished account delete")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err := p.deleteAccountBlocks(ctx, account); err != nil {
|
// Delete statuses *before* anything else as for local
|
||||||
l.Errorf("continuing after error during account delete: %v", err)
|
// accounts we need to federate out deletes, which relies
|
||||||
}
|
// on follows for addressing the appropriate accounts.
|
||||||
|
p.deleteAccountStatuses(ctx, &log, account)
|
||||||
|
|
||||||
if err := p.deleteAccountNotifications(ctx, account); err != nil {
|
// Now delete relationships to / from account.
|
||||||
l.Errorf("continuing after error during account delete: %v", err)
|
p.deleteAccountRelations(ctx, &log, account)
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.deleteAccountPeripheral(ctx, account); err != nil {
|
// Now delete any notifications to / from account.
|
||||||
l.Errorf("continuing after error during account delete: %v", err)
|
p.deleteAccountNotifications(ctx, &log, account)
|
||||||
}
|
|
||||||
|
// Delete other peripheral objects ownable /
|
||||||
|
// manageable by any local / remote account.
|
||||||
|
p.deleteAccountPeripheral(ctx, &log, account)
|
||||||
|
|
||||||
if account.IsLocal() {
|
if account.IsLocal() {
|
||||||
// We delete tokens, applications and clients for
|
// We delete tokens, applications and clients for
|
||||||
// account as one of the last stages during deletion,
|
// account as one of the last stages during deletion,
|
||||||
// as other database models rely on these.
|
// as other database models rely on these.
|
||||||
if err := p.deleteUserAndTokensForAccount(ctx, account); err != nil {
|
if err = p.deleteUserAndTokensForAccount(ctx, &log, account); err != nil {
|
||||||
l.Errorf("continuing after error during account delete: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// To prevent the account being created again,
|
// To prevent the account being created again,
|
||||||
// stubbify it and update it in the db.
|
// (which would cause horrible federation shenanigans),
|
||||||
// The account will not be deleted, but it
|
// the account will be stubbed out to an unusable state
|
||||||
// will become completely unusable.
|
// with no identifying info remaining, but NOT deleted.
|
||||||
columns := stubbifyAccount(account, origin)
|
columns := stubbifyAccount(account, origin)
|
||||||
if err := p.state.DB.UpdateAccount(ctx, account, columns...); err != nil {
|
if err = p.state.DB.UpdateAccount(ctx, account, columns...); err != nil {
|
||||||
return gtserror.NewErrorInternalError(err)
|
return gtserror.Newf("error stubbing out account: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Info("account delete process complete")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteUserAndTokensForAccount deletes the gtsmodel.User and
|
func (p *Processor) deleteUserAndTokensForAccount(
|
||||||
// any OAuth tokens, applications, and Web Push subscriptions for the given account.
|
ctx context.Context,
|
||||||
//
|
log *log.Entry,
|
||||||
// Callers to this function should already have checked that
|
account *gtsmodel.Account,
|
||||||
// this is a local account, or else it won't have a user associated
|
) error {
|
||||||
// with it, and this will fail.
|
|
||||||
func (p *Processor) deleteUserAndTokensForAccount(ctx context.Context, account *gtsmodel.Account) error {
|
// Fetch the associated user for account, on fail return
|
||||||
|
// early as all other parts of this func rely on this user.
|
||||||
user, err := p.state.DB.GetUserByAccountID(ctx, account.ID)
|
user, err := p.state.DB.GetUserByAccountID(ctx, account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("db error getting user: %w", err)
|
return gtserror.Newf("error getting account user: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all applications owned by user.
|
// Get list of applications managed by deleting user.
|
||||||
apps, err := p.state.DB.GetApplicationsManagedByUserID(ctx, user.ID, nil)
|
apps, err := p.state.DB.GetApplicationsManagedByUserID(ctx,
|
||||||
|
user.ID,
|
||||||
|
nil, // i.e. all
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("db error getting apps: %w", err)
|
log.Errorf("error getting user applications: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete each app and any tokens it had created
|
// Delete each app and any tokens it had created
|
||||||
// (not necessarily owned by deleted account).
|
// (not necessarily owned by deleted account).
|
||||||
for _, a := range apps {
|
for _, app := range apps {
|
||||||
if err := p.state.DB.DeleteApplicationByID(ctx, a.ID); err != nil {
|
if err := p.state.DB.DeleteTokensByClientID(ctx, app.ClientID); err != nil {
|
||||||
return gtserror.Newf("db error deleting app: %w", err)
|
log.Errorf("error deleting application token: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := p.state.DB.DeleteApplicationByID(ctx, app.ID); err != nil {
|
||||||
if err := p.state.DB.DeleteTokensByClientID(ctx, a.ClientID); err != nil {
|
log.Errorf("error deleting user application: %v", err)
|
||||||
return gtserror.Newf("db error deleting tokens for app: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get any remaining access tokens owned by user.
|
// Get any remaining access tokens owned by user.
|
||||||
tokens, err := p.state.DB.GetAccessTokens(ctx, user.ID, nil)
|
tokens, err := p.state.DB.GetAccessTokens(ctx,
|
||||||
|
user.ID,
|
||||||
|
nil, // i.e. all
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("db error getting tokens: %w", err)
|
log.Errorf("error getting user access tokens: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete each token.
|
// Delete user access tokens.
|
||||||
for _, t := range tokens {
|
for _, token := range tokens {
|
||||||
if err := p.state.DB.DeleteTokenByID(ctx, t.ID); err != nil {
|
if err := p.state.DB.DeleteTokenByID(ctx, token.ID); err != nil {
|
||||||
return gtserror.Newf("db error deleting token: %w", err)
|
log.Errorf("error deleting user access token: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete any web push subscriptions created by this local user account.
|
||||||
if err := p.state.DB.DeleteWebPushSubscriptionsByAccountID(ctx, account.ID); err != nil {
|
if err := p.state.DB.DeleteWebPushSubscriptionsByAccountID(ctx, account.ID); err != nil {
|
||||||
return gtserror.Newf("db error deleting Web Push subscriptions: %w", err)
|
log.Errorf("error deleting account web push subscriptions: %v", err)
|
||||||
}
|
|
||||||
|
|
||||||
columns, err := stubbifyUser(user)
|
|
||||||
if err != nil {
|
|
||||||
return gtserror.Newf("error stubbifying user: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To prevent the user being created again,
|
||||||
|
// the user will be stubbed out to an unusable state
|
||||||
|
// with no identifying info remaining, but NOT deleted.
|
||||||
|
columns := stubbifyUser(user)
|
||||||
if err := p.state.DB.UpdateUser(ctx, user, columns...); err != nil {
|
if err := p.state.DB.UpdateUser(ctx, user, columns...); err != nil {
|
||||||
return gtserror.Newf("db error updating user: %w", err)
|
return gtserror.Newf("error stubbing out user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteAccountFollows deletes:
|
func (p *Processor) deleteAccountRelations(
|
||||||
// - Follows targeting account.
|
ctx context.Context,
|
||||||
// - Follow requests targeting account.
|
log *log.Entry,
|
||||||
// - Follows created by account.
|
account *gtsmodel.Account,
|
||||||
// - Follow requests created by account.
|
) {
|
||||||
func (p *Processor) deleteAccountFollows(ctx context.Context, account *gtsmodel.Account) error {
|
// Get a list of the follows targeting this account.
|
||||||
// Delete follows targeting this account.
|
followedBy, err := p.state.DB.GetAccountFollowers(ctx,
|
||||||
followedBy, err := p.state.DB.GetAccountFollowers(ctx, account.ID, nil)
|
account.ID,
|
||||||
|
nil, // i.e. all
|
||||||
|
)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return gtserror.Newf("db error getting follows targeting account %s: %w", account.ID, err)
|
log.Errorf("error getting account followed-bys: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete these follows from database.
|
||||||
for _, follow := range followedBy {
|
for _, follow := range followedBy {
|
||||||
if err := p.state.DB.DeleteFollowByID(ctx, follow.ID); err != nil {
|
if err := p.state.DB.DeleteFollowByID(ctx, follow.ID); err != nil {
|
||||||
return gtserror.Newf("db error unfollowing account followedBy: %w", err)
|
log.Errorf("error deleting account followed-by %s: %v", follow.URI, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete follow requests targeting this account.
|
// Get a list of the follow requests targeting this account.
|
||||||
followRequestedBy, err := p.state.DB.GetAccountFollowRequests(ctx, account.ID, nil)
|
followRequestedBy, err := p.state.DB.GetAccountFollowRequests(ctx,
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
account.ID,
|
||||||
return gtserror.Newf("db error getting follow requests targeting account %s: %w", account.ID, err)
|
nil, // i.e. all
|
||||||
}
|
|
||||||
|
|
||||||
for _, followRequest := range followRequestedBy {
|
|
||||||
if err := p.state.DB.DeleteFollowRequestByID(ctx, followRequest.ID); err != nil {
|
|
||||||
return gtserror.Newf("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.IsLocal())
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delete follows originating from this account.
|
|
||||||
following, err := p.state.DB.GetAccountFollows(ctx, account.ID, nil)
|
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return gtserror.Newf("db error getting follows owned by account %s: %w", account.ID, err)
|
log.Errorf("error getting account follow-requested-bys: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each follow owned by this account, unfollow
|
// Delete these follow requests from database.
|
||||||
// and process side effects (noop if remote account).
|
for _, followReq := range followRequestedBy {
|
||||||
|
if err := p.state.DB.DeleteFollowRequestByID(ctx, followReq.ID); err != nil {
|
||||||
|
log.Errorf("error deleting account follow-requested-by %s: %v", followReq.URI, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a list of the blocks targeting this account.
|
||||||
|
blockedBy, err := p.state.DB.GetAccountBlockedBy(ctx,
|
||||||
|
account.ID,
|
||||||
|
nil, // i.e. all
|
||||||
|
)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
log.Errorf("error getting account blocked-bys: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete these blocks from database.
|
||||||
|
for _, block := range blockedBy {
|
||||||
|
if err := p.state.DB.DeleteBlockByID(ctx, block.ID); err != nil {
|
||||||
|
log.Errorf("error deleting account blocked-by %s: %v", block.URI, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the follows originating from this account.
|
||||||
|
following, err := p.state.DB.GetAccountFollows(ctx,
|
||||||
|
account.ID,
|
||||||
|
nil, // i.e. all
|
||||||
|
)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
log.Errorf("error getting account follows: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete these follows from database.
|
||||||
for _, follow := range following {
|
for _, follow := range following {
|
||||||
if err := p.state.DB.DeleteFollowByID(ctx, follow.ID); err != nil {
|
if err := p.state.DB.DeleteFollowByID(ctx, follow.ID); err != nil {
|
||||||
return gtserror.Newf("db error unfollowing account: %w", err)
|
log.Errorf("error deleting account followed %s: %v", follow.URI, err)
|
||||||
}
|
|
||||||
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.
|
// Get a list of the follow requests originating from this account.
|
||||||
followRequesting, err := p.state.DB.GetAccountFollowRequesting(ctx, account.ID, nil)
|
followRequesting, err := p.state.DB.GetAccountFollowRequesting(ctx,
|
||||||
|
account.ID,
|
||||||
|
nil, // i.e. all
|
||||||
|
)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return gtserror.Newf("db error getting follow requests owned by account %s: %w", account.ID, err)
|
log.Errorf("error getting account follow-requests: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each follow owned by this account, unfollow
|
// Delete these follow requests from database.
|
||||||
// and process side effects (noop if remote account).
|
for _, followReq := range followRequesting {
|
||||||
for _, followRequest := range followRequesting {
|
if err := p.state.DB.DeleteFollowRequestByID(ctx, followReq.ID); err != nil {
|
||||||
if err := p.state.DB.DeleteFollowRequestByID(ctx, followRequest.ID); err != nil {
|
log.Errorf("error deleting account follow-request %s: %v", followReq.URI, err)
|
||||||
return gtserror.Newf("db error unfollowingRequesting account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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: followRequest.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 in serial.
|
// Get the blocks originating from this account.
|
||||||
for _, msg := range msgs {
|
blocking, err := p.state.DB.GetAccountBlocking(ctx,
|
||||||
if err := p.state.Workers.Client.Process(ctx, msg); err != nil {
|
account.ID,
|
||||||
log.Errorf(
|
nil, // i.e. all
|
||||||
ctx,
|
)
|
||||||
"error processing %s of %s during Delete of account %s: %v",
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
msg.APActivityType, msg.APObjectType, account.ID, err,
|
log.Errorf("error getting account blocks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete these blocks from database.
|
||||||
|
for _, block := range blocking {
|
||||||
|
if err := p.state.DB.DeleteBlockByID(ctx, block.ID); err != nil {
|
||||||
|
log.Errorf("error deleting account block %s: %v", block.URI, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all mutes targetting / originating from account.
|
||||||
|
if err := p.state.DB.DeleteAccountMutes(ctx, account.ID); // nocollapse
|
||||||
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
log.Errorf("error deleting mutes to / from account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.IsLocal() {
|
||||||
|
// Process side-effects for deleting
|
||||||
|
// of account follows from local user.
|
||||||
|
for _, follow := range following {
|
||||||
|
p.processSideEffect(ctx, log,
|
||||||
|
ap.ActivityUndo,
|
||||||
|
ap.ActivityFollow,
|
||||||
|
follow,
|
||||||
|
account,
|
||||||
|
follow.TargetAccount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process side-effects for deleting of account follow requests
|
||||||
|
// from local user. Though handled as though UNDO of a follow.
|
||||||
|
for _, followReq := range followRequesting {
|
||||||
|
p.processSideEffect(ctx, log,
|
||||||
|
ap.ActivityUndo,
|
||||||
|
ap.ActivityFollow,
|
||||||
|
>smodel.Follow{
|
||||||
|
ID: followReq.ID,
|
||||||
|
URI: followReq.URI,
|
||||||
|
AccountID: followReq.AccountID,
|
||||||
|
Account: followReq.Account,
|
||||||
|
TargetAccountID: followReq.TargetAccountID,
|
||||||
|
TargetAccount: followReq.TargetAccount,
|
||||||
|
ShowReblogs: new(bool),
|
||||||
|
Notify: new(bool),
|
||||||
|
},
|
||||||
|
account,
|
||||||
|
followReq.TargetAccount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process side-effects for deleting
|
||||||
|
// of account blocks from local user.
|
||||||
|
for _, block := range blocking {
|
||||||
|
p.processSideEffect(ctx, log,
|
||||||
|
ap.ActivityUndo,
|
||||||
|
ap.ActivityBlock,
|
||||||
|
block,
|
||||||
|
account,
|
||||||
|
block.TargetAccount,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) unfollowSideEffectsFunc(local bool) func(
|
|
||||||
ctx context.Context,
|
|
||||||
account *gtsmodel.Account,
|
|
||||||
follow *gtsmodel.Follow,
|
|
||||||
) *messages.FromClientAPI {
|
|
||||||
if !local {
|
|
||||||
// Don't try to process side effects
|
|
||||||
// for accounts that aren't local.
|
|
||||||
return func(
|
|
||||||
_ context.Context,
|
|
||||||
_ *gtsmodel.Account,
|
|
||||||
_ *gtsmodel.Follow,
|
|
||||||
) *messages.FromClientAPI {
|
|
||||||
// noop
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
Origin: account,
|
|
||||||
Target: follow.TargetAccount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Processor) deleteAccountBlocks(ctx context.Context, account *gtsmodel.Account) error {
|
|
||||||
if err := p.state.DB.DeleteAccountBlocks(ctx, account.ID); err != nil {
|
|
||||||
return gtserror.Newf("db error deleting account blocks for %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 processing.
|
|
||||||
func (p *Processor) deleteAccountStatuses(
|
func (p *Processor) deleteAccountStatuses(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
log *log.Entry,
|
||||||
account *gtsmodel.Account,
|
account *gtsmodel.Account,
|
||||||
) error {
|
) {
|
||||||
// We'll select statuses 50 at a time so we don't wreck the db,
|
var maxID string
|
||||||
// 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 {
|
for {
|
||||||
// Page through account's statuses.
|
// Page through deleting account's statuses.
|
||||||
statuses, err = p.state.DB.GetAccountStatuses(
|
statuses, err := p.state.DB.GetAccountStatuses(
|
||||||
ctx,
|
gtscontext.SetBarebones(ctx),
|
||||||
account.ID,
|
account.ID,
|
||||||
deleteSelectLimit,
|
deleteSelectLimit,
|
||||||
false,
|
false,
|
||||||
|
|
@ -349,12 +340,14 @@ statusLoop:
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
// Make sure we don't have a real error.
|
log.Errorf("error getting account statuses: %v", err)
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(statuses) == 0 {
|
if len(statuses) == 0 {
|
||||||
break statusLoop
|
// reached
|
||||||
|
// the end.
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update next maxID from last status.
|
// Update next maxID from last status.
|
||||||
|
|
@ -365,140 +358,162 @@ statusLoop:
|
||||||
status.Account = account
|
status.Account = account
|
||||||
|
|
||||||
// Look for any boosts of this status in DB.
|
// Look for any boosts of this status in DB.
|
||||||
//
|
|
||||||
// We put these in the msgs slice first so
|
|
||||||
// that they're handled first, before the
|
|
||||||
// parent status that's being boosted.
|
|
||||||
//
|
|
||||||
// Use a barebones context and just select the
|
|
||||||
// origin account separately. The rest will be
|
|
||||||
// populated later anyway, and we don't want to
|
|
||||||
// stop now because we couldn't get something.
|
|
||||||
boosts, err := p.state.DB.GetStatusBoosts(
|
boosts, err := p.state.DB.GetStatusBoosts(
|
||||||
gtscontext.SetBarebones(ctx),
|
gtscontext.SetBarebones(ctx),
|
||||||
status.ID,
|
status.ID,
|
||||||
)
|
)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return gtserror.Newf("error fetching status boosts for %s: %w", status.ID, err)
|
log.Errorf("error getting status boosts for %s: %v", status.URI, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare to Undo each boost.
|
// Prepare to Undo each boost.
|
||||||
for _, boost := range boosts {
|
for _, boost := range boosts {
|
||||||
|
|
||||||
|
// Fetch the owning account of this boost.
|
||||||
boost.Account, err = p.state.DB.GetAccountByID(
|
boost.Account, err = p.state.DB.GetAccountByID(
|
||||||
gtscontext.SetBarebones(ctx),
|
gtscontext.SetBarebones(ctx),
|
||||||
boost.AccountID,
|
boost.AccountID,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf(
|
log.Errorf("error getting owner %s of status boost %s: %v",
|
||||||
ctx,
|
boost.AccountURI, boost.URI, err)
|
||||||
"db error getting owner %s of status boost %s: %v",
|
|
||||||
boost.AccountID, boost.ID, err,
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
msgs = append(msgs, &messages.FromClientAPI{
|
// Process boost undo event.
|
||||||
APObjectType: ap.ActivityAnnounce,
|
p.processSideEffect(ctx, log,
|
||||||
APActivityType: ap.ActivityUndo,
|
ap.ActivityUndo,
|
||||||
GTSModel: status,
|
ap.ActivityAnnounce,
|
||||||
Origin: boost.Account,
|
boost,
|
||||||
Target: account,
|
account,
|
||||||
})
|
account,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now prepare to Delete status.
|
// Process status delete event.
|
||||||
msgs = append(msgs, &messages.FromClientAPI{
|
p.processSideEffect(ctx, log,
|
||||||
APObjectType: ap.ObjectNote,
|
ap.ActivityDelete,
|
||||||
APActivityType: ap.ActivityDelete,
|
ap.ObjectNote,
|
||||||
GTSModel: status,
|
status,
|
||||||
Origin: account,
|
account,
|
||||||
Target: account,
|
account,
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process accreted messages in serial.
|
|
||||||
for _, msg := range msgs {
|
|
||||||
if err := p.state.Workers.Client.Process(ctx, msg); err != nil {
|
|
||||||
log.Errorf(
|
|
||||||
ctx,
|
|
||||||
"error processing %s of %s during Delete of account %s: %v",
|
|
||||||
msg.APActivityType, msg.APObjectType, account.ID, err,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) deleteAccountNotifications(ctx context.Context, account *gtsmodel.Account) error {
|
func (p *Processor) deleteAccountNotifications(
|
||||||
// Delete all notifications of all types targeting given account.
|
ctx context.Context,
|
||||||
if err := p.state.DB.DeleteNotifications(ctx, nil, account.ID, ""); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
log *log.Entry,
|
||||||
return gtserror.Newf("error deleting notifications targeting account: %w", err)
|
account *gtsmodel.Account,
|
||||||
|
) {
|
||||||
|
if account.IsLocal() {
|
||||||
|
// Delete all types of notifications targeting this local account.
|
||||||
|
if err := p.state.DB.DeleteNotifications(ctx, nil, account.ID, ""); // nocollapse
|
||||||
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
log.Errorf("error deleting notifications targeting account: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all notifications of all types originating from given account.
|
// Delete all types of notifications originating from this account.
|
||||||
if err := p.state.DB.DeleteNotifications(ctx, nil, "", account.ID); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err := p.state.DB.DeleteNotifications(ctx, nil, "", account.ID); // nocollapse
|
||||||
return gtserror.Newf("error deleting notifications by account: %w", 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) {
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return gtserror.Newf("error deleting bookmarks by account: %w", err)
|
log.Errorf("error deleting notifications originating from account: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Processor) deleteAccountPeripheral(
|
||||||
|
ctx context.Context,
|
||||||
|
log *log.Entry,
|
||||||
|
account *gtsmodel.Account,
|
||||||
|
) {
|
||||||
|
if account.IsLocal() {
|
||||||
|
// Delete all bookmarks owned by given account, only for local.
|
||||||
|
if err := p.state.DB.DeleteStatusBookmarks(ctx, account.ID, ""); // nocollapse
|
||||||
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
log.Errorf("error deleting bookmarks by account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all faves owned by given account, only for local.
|
||||||
|
if err := p.state.DB.DeleteStatusFaves(ctx, account.ID, ""); // nocollapse
|
||||||
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
log.Errorf("error deleting faves by account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all conversations owned by given account, only for local.
|
||||||
|
//
|
||||||
|
// *Participated* conversations will be retained, leaving up to *their* owners.
|
||||||
|
if err := p.state.DB.DeleteConversationsByOwnerAccountID(ctx, account.ID); // nocollapse
|
||||||
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
log.Errorf("error deleting conversations by account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all followed tags owned by given account, only for local.
|
||||||
|
if err := p.state.DB.DeleteFollowedTagsByAccountID(ctx, account.ID); // nocollapse
|
||||||
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
log.Errorf("error deleting followed tags by account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete stats model stored for given account, only for local.
|
||||||
|
if err := p.state.DB.DeleteAccountStats(ctx, account.ID); err != nil {
|
||||||
|
log.Errorf("error deleting stats for account: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all bookmarks targeting given account.
|
// Delete all bookmarks targeting given account, local and remote.
|
||||||
if err := p.state.DB.DeleteStatusBookmarks(ctx, "", account.ID); // nocollapse
|
if err := p.state.DB.DeleteStatusBookmarks(ctx, "", account.ID); // nocollapse
|
||||||
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return gtserror.Newf("error deleting bookmarks targeting account: %w", err)
|
log.Errorf("error deleting bookmarks targeting account: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all faves owned by given account.
|
// Delete all faves targeting given account, local and remote.
|
||||||
if err := p.state.DB.DeleteStatusFaves(ctx, account.ID, ""); // nocollapse
|
|
||||||
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
|
||||||
return gtserror.Newf("error deleting faves by account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all faves targeting given account.
|
|
||||||
if err := p.state.DB.DeleteStatusFaves(ctx, "", account.ID); // nocollapse
|
if err := p.state.DB.DeleteStatusFaves(ctx, "", account.ID); // nocollapse
|
||||||
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return gtserror.Newf("error deleting faves targeting account: %w", err)
|
log.Errorf("error deleting faves targeting account: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add status mutes here when they're implemented.
|
// Delete all poll votes owned by given account, local and remote.
|
||||||
|
|
||||||
// Delete all conversations owned by given account.
|
|
||||||
// Conversations in which it has only participated will be retained;
|
|
||||||
// they can always be deleted by their owners.
|
|
||||||
if err := p.state.DB.DeleteConversationsByOwnerAccountID(ctx, account.ID); // nocollapse
|
|
||||||
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
|
||||||
return gtserror.Newf("error deleting conversations owned by account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete all poll votes owned by given account.
|
|
||||||
if err := p.state.DB.DeletePollVotesByAccountID(ctx, account.ID); // nocollapse
|
if err := p.state.DB.DeletePollVotesByAccountID(ctx, account.ID); // nocollapse
|
||||||
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return gtserror.Newf("error deleting poll votes by account: %w", err)
|
log.Errorf("error deleting poll votes by account: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Delete all followed tags owned by given account.
|
// processSideEffect will process the given side effect details,
|
||||||
if err := p.state.DB.DeleteFollowedTagsByAccountID(ctx, account.ID); // nocollapse
|
// with appropriate worker depending on if origin is local / remote.
|
||||||
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
func (p *Processor) processSideEffect(
|
||||||
return gtserror.Newf("error deleting followed tags by account: %w", err)
|
ctx context.Context,
|
||||||
|
log *log.Entry,
|
||||||
|
activityType string,
|
||||||
|
objectType string,
|
||||||
|
gtsModel any,
|
||||||
|
origin *gtsmodel.Account,
|
||||||
|
target *gtsmodel.Account,
|
||||||
|
) {
|
||||||
|
if origin.IsLocal() {
|
||||||
|
// Process side-effect through our client API as this is a local account.
|
||||||
|
if err := p.state.Workers.Client.Process(ctx, &messages.FromClientAPI{
|
||||||
|
APActivityType: activityType,
|
||||||
|
APObjectType: objectType,
|
||||||
|
GTSModel: gtsModel,
|
||||||
|
Origin: origin,
|
||||||
|
Target: target,
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorf("error processing %s of %s during local account %s delete: %v", activityType, objectType, origin.ID, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Process side-effect through our fedi API as this is a remote account.
|
||||||
|
if err := p.state.Workers.Federator.Process(ctx, &messages.FromFediAPI{
|
||||||
|
APActivityType: activityType,
|
||||||
|
APObjectType: objectType,
|
||||||
|
GTSModel: gtsModel,
|
||||||
|
Requesting: origin,
|
||||||
|
Receiving: target,
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorf("error processing %s of %s during local account %s delete: %v", activityType, objectType, origin.ID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete account stats model.
|
|
||||||
if err := p.state.DB.DeleteAccountStats(ctx, account.ID); err != nil {
|
|
||||||
return gtserror.Newf("error deleting stats for account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// stubbifyAccount renders the given account as a stub,
|
// stubbifyAccount renders the given account as a stub,
|
||||||
|
|
@ -567,15 +582,21 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
|
||||||
//
|
//
|
||||||
// For caller's convenience, this function returns the db
|
// For caller's convenience, this function returns the db
|
||||||
// names of all columns that are updated by it.
|
// names of all columns that are updated by it.
|
||||||
func stubbifyUser(user *gtsmodel.User) ([]string, error) {
|
func stubbifyUser(user *gtsmodel.User) []string {
|
||||||
uuid, err := uuid.New().MarshalBinary()
|
uuid, err := uuid.New().MarshalBinary()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// this should never happen,
|
||||||
|
// it indicates /dev/random
|
||||||
|
// is misbehaving.
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dummyPassword, err := bcrypt.GenerateFromPassword(uuid, bcrypt.DefaultCost)
|
dummyPassword, err := bcrypt.GenerateFromPassword(uuid, bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
// this should never happen,
|
||||||
|
// it indicates /dev/random
|
||||||
|
// is misbehaving.
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
never := time.Time{}
|
never := time.Time{}
|
||||||
|
|
@ -600,5 +621,5 @@ func stubbifyUser(user *gtsmodel.User) ([]string, error) {
|
||||||
"confirmation_sent_at",
|
"confirmation_sent_at",
|
||||||
"reset_password_token",
|
"reset_password_token",
|
||||||
"reset_password_sent_at",
|
"reset_password_sent_at",
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ func (p *Processor) ExportBlocks(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
requester *gtsmodel.Account,
|
requester *gtsmodel.Account,
|
||||||
) ([][]string, gtserror.WithCode) {
|
) ([][]string, gtserror.WithCode) {
|
||||||
blocks, err := p.state.DB.GetAccountBlocks(ctx, requester.ID, nil)
|
blocks, err := p.state.DB.GetAccountBlocking(ctx, requester.ID, nil)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
err = gtserror.Newf("db error getting blocks: %w", err)
|
err = gtserror.Newf("db error getting blocks: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ func importBlocksAsyncF(
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
prevBlocks, err = p.state.DB.GetAccountBlocks(ctx, requester.ID, nil)
|
prevBlocks, err = p.state.DB.GetAccountBlocking(ctx, requester.ID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "db error getting blocks: %v", err)
|
log.Errorf(ctx, "db error getting blocks: %v", err)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1093,11 +1093,13 @@ func (p *clientAPI) DeleteAccountOrUser(ctx context.Context, cMsg *messages.From
|
||||||
p.state.Caches.Timelines.List.Delete(listID)
|
p.state.Caches.Timelines.List.Delete(listID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Federate out a delete activity targeting account to remote servers.
|
||||||
if err := p.federate.DeleteAccount(ctx, cMsg.Target); err != nil {
|
if err := p.federate.DeleteAccount(ctx, cMsg.Target); err != nil {
|
||||||
log.Errorf(ctx, "error federating account delete: %v", err)
|
log.Errorf(ctx, "error federating account delete: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.account.Delete(ctx, cMsg.Target, originID); err != nil {
|
// And finally, perform the actual account deletion synchronously.
|
||||||
|
if err := p.account.Delete(ctx, account, originID); err != nil {
|
||||||
log.Errorf(ctx, "error deleting account: %v", err)
|
log.Errorf(ctx, "error deleting account: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1120,7 +1120,7 @@ func (p *fediAPI) DeleteAccount(ctx context.Context, fMsg *messages.FromFediAPI)
|
||||||
// Remove any entries authored by account from timelines.
|
// Remove any entries authored by account from timelines.
|
||||||
p.surface.removeTimelineEntriesByAccount(account.ID)
|
p.surface.removeTimelineEntriesByAccount(account.ID)
|
||||||
|
|
||||||
// First perform the actual account deletion.
|
// And finally, perform the actual account deletion synchronously.
|
||||||
if err := p.account.Delete(ctx, account, account.ID); err != nil {
|
if err := p.account.Delete(ctx, account, account.ID); err != nil {
|
||||||
log.Errorf(ctx, "error deleting account: %v", err)
|
log.Errorf(ctx, "error deleting account: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ func (c *Converter) AccountToExportStats(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
blockingCount, err := c.state.DB.CountAccountBlocks(ctx, a.ID)
|
blockingCount, err := c.state.DB.CountAccountBlocking(ctx, a.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf(
|
return nil, gtserror.Newf(
|
||||||
"error counting lists for account %s: %w",
|
"error counting lists for account %s: %w",
|
||||||
|
|
|
||||||
|
|
@ -32,18 +32,7 @@ func Must(fn func()) {
|
||||||
panic("nil func")
|
panic("nil func")
|
||||||
}
|
}
|
||||||
for !func() (done bool) {
|
for !func() (done bool) {
|
||||||
defer func() {
|
defer Recover()
|
||||||
if r := recover(); r != nil {
|
|
||||||
// Gather calling func frames.
|
|
||||||
pcs := make([]uintptr, 10)
|
|
||||||
n := runtime.Callers(3, pcs)
|
|
||||||
i := runtime.CallersFrames(pcs[:n])
|
|
||||||
c := gatherFrames(i, n)
|
|
||||||
|
|
||||||
const msg = "recovered panic: %v\n\n%s\n"
|
|
||||||
fmt.Fprintf(os.Stderr, msg, r, c.String())
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
fn()
|
fn()
|
||||||
done = true
|
done = true
|
||||||
return
|
return
|
||||||
|
|
@ -51,6 +40,21 @@ func Must(fn func()) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recover wraps runtime.recover() to dump the current
|
||||||
|
// stack to stderr on panic and return the panic value.
|
||||||
|
func Recover() any {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
// Gather calling func frames.
|
||||||
|
pcs := make([]uintptr, 10)
|
||||||
|
n := runtime.Callers(3, pcs)
|
||||||
|
i := runtime.CallersFrames(pcs[:n])
|
||||||
|
c := gatherFrames(i, n)
|
||||||
|
fmt.Fprintf(os.Stderr, "recovered panic: %v\n\n%s\n", r, c.String())
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// gatherFrames collates runtime frames from a frame iterator.
|
// gatherFrames collates runtime frames from a frame iterator.
|
||||||
func gatherFrames(iter *runtime.Frames, n int) errors.Callers {
|
func gatherFrames(iter *runtime.Frames, n int) errors.Callers {
|
||||||
if iter == nil {
|
if iter == nil {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue