[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:
tobi
2023-03-20 19:10:08 +01:00
committed by GitHub
parent 276d773438
commit e8595f0c64
53 changed files with 2472 additions and 1321 deletions

View File

@@ -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"))

View File

@@ -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 := &gtsmodel.Block{
ID: blockID,
URI: blockURI,
AccountID: requestingAccount.ID,
Account: requestingAccount,
TargetAccountID: targetAccountID,
TargetAccount: targetAccount,
}
// make the block
block := &gtsmodel.Block{}
newBlockID := id.NewULID()
block.ID = newBlockID
block.AccountID = requestingAccount.ID
block.Account = requestingAccount
block.TargetAccountID = targetAccountID
block.TargetAccount = targetAccount
block.URI = uris.GenerateURIForBlock(requestingAccount.Username, newBlockID)
// whack it in the database
if err := p.state.DB.PutBlock(ctx, block); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error creating block in db: %s", err))
err = fmt.Errorf("BlockCreate: error creating block in db: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// clear any follows or follow requests from the blocked account to the target account -- this is a simple delete
if err := p.state.DB.DeleteWhere(ctx, []db.Where{
{Key: "account_id", Value: targetAccountID},
{Key: "target_account_id", Value: requestingAccount.ID},
}, &gtsmodel.Follow{}); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err))
}
if err := p.state.DB.DeleteWhere(ctx, []db.Where{
{Key: "account_id", Value: targetAccountID},
{Key: "target_account_id", Value: requestingAccount.ID},
}, &gtsmodel.FollowRequest{}); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err))
// Ensure each account unfollows the other.
// We only care about processing unfollow side
// effects from requesting account -> target
// account, since requesting account is ours,
// and target account might not be.
msgs, err := p.unfollow(ctx, requestingAccount, targetAccount)
if err != nil {
err = fmt.Errorf("BlockCreate: error unfollowing: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// clear any follows or follow requests from the requesting account to the target account --
// this might require federation so we need to pass some messages around
// check if a follow request exists from the requesting account to the target account, and remove it if it does (storing the URI for later)
var frChanged bool
var frURI string
fr := &gtsmodel.FollowRequest{}
if err := p.state.DB.GetWhere(ctx, []db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, fr); err == nil {
frURI = fr.URI
if err := p.state.DB.DeleteByID(ctx, fr.ID, fr); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow request from db: %s", err))
}
frChanged = true
// Ensure unfollowed in other direction;
// ignore/don't process returned messages.
if _, err := p.unfollow(ctx, targetAccount, requestingAccount); err != nil {
err = fmt.Errorf("BlockCreate: error unfollowing: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// now do the same thing for any existing follow
var fChanged bool
var fURI string
f := &gtsmodel.Follow{}
if err := p.state.DB.GetWhere(ctx, []db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, f); err == nil {
fURI = f.URI
if err := p.state.DB.DeleteByID(ctx, f.ID, f); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow from db: %s", err))
}
fChanged = true
}
// follow request status changed so send the UNDO activity to the channel for async processing
if frChanged {
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
GTSModel: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
TargetAccountID: targetAccountID,
URI: frURI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
})
}
// follow status changed so send the UNDO activity to the channel for async processing
if fChanged {
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
GTSModel: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
TargetAccountID: targetAccountID,
URI: fURI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
})
}
// 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
}

View File

@@ -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)
}

View File

@@ -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, &gtsmodel.Client{}); err != nil {
l.Errorf("error deleting oauth client: %s", err)
}
// delete application(s) associated with this token
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "client_id", Value: t.ClientID}}, &gtsmodel.Application{}); err != nil {
l.Errorf("error deleting application: %s", err)
}
// delete the token itself
if err := p.state.DB.DeleteByID(ctx, t.ID, t); err != nil {
l.Errorf("error deleting oauth token: %s", err)
}
}
}
if 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 := &gtsmodel.Follow{
URI: uri,
AccountID: followRequest.AccountID,
Account: followRequest.Account,
TargetAccountID: followRequest.TargetAccountID,
TargetAccount: followRequest.TargetAccount,
}
if msg := unfollowSideEffects(ctx, account, follow); msg != nil {
// There was a side effect to process.
msgs = append(msgs, *msg)
}
}
// Process accreted messages asynchronously.
p.state.Workers.EnqueueClientAPI(ctx, msgs...)
return nil
}
func (p *Processor) unfollowSideEffectsFunc(deletedAccount *gtsmodel.Account) func(ctx context.Context, account *gtsmodel.Account, follow *gtsmodel.Follow) *messages.FromClientAPI {
if !deletedAccount.IsLocal() {
// Don't try to process side effects
// for accounts that aren't local.
return func(ctx context.Context, account *gtsmodel.Account, follow *gtsmodel.Follow) *messages.FromClientAPI {
return nil // noop
}
}
return func(ctx context.Context, account *gtsmodel.Account, follow *gtsmodel.Follow) *messages.FromClientAPI {
if follow.TargetAccount == nil {
// TargetAccount seems to have gone;
// race condition? db corruption?
log.WithContext(ctx).WithField("follow", follow).Warn("follow had no TargetAccount, likely race condition")
return nil
}
if follow.TargetAccount.IsLocal() {
// No side effects for local unfollows.
return nil
}
// There was a follow, process side effects.
return &messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
GTSModel: follow,
OriginAccount: account,
TargetAccount: follow.TargetAccount,
}
}
}
func (p *Processor) deleteAccountBlocks(ctx context.Context, account *gtsmodel.Account) error {
// Delete blocks created by this account.
if err := p.state.DB.DeleteBlocksByOriginAccountID(ctx, account.ID); err != nil {
return fmt.Errorf("deleteAccountBlocks: db error deleting blocks created by account %s: %w", account.ID, err)
}
// Delete blocks targeting this account.
if err := p.state.DB.DeleteBlocksByTargetAccountID(ctx, account.ID); err != nil {
return fmt.Errorf("deleteAccountBlocks: db error deleting blocks targeting account %s: %w", account.ID, err)
}
return nil
}
// deleteAccountStatuses iterates through all statuses owned by
// the given account, passing each discovered status (and boosts
// thereof) to the processor workers for further async processing.
func (p *Processor) deleteAccountStatuses(ctx context.Context, account *gtsmodel.Account) error {
// We'll select statuses 50 at a time so we don't wreck the db,
// and pass them through to the client api worker to handle.
//
// Deleting the statuses in this way also handles deleting the
// account's media attachments, mentions, and polls, since these
// are all attached to statuses.
var (
statuses []*gtsmodel.Status
err error
maxID string
msgs = []messages.FromClientAPI{}
)
statusLoop:
for {
// 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",
}
}

View File

@@ -19,6 +19,7 @@ package account
import (
"context"
"errors"
"fmt"
"github.com/superseriousbusiness/gotosocial/internal/ap"
@@ -33,172 +34,182 @@ import (
// FollowCreate handles a follow request to an account, either remote or local.
func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
// if there's a block between the accounts we shouldn't create the request ofc
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, form.ID, true); err != nil {
return nil, gtserror.NewErrorInternalError(err)
} else if blocked {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
targetAccount, errWithCode := p.getFollowTarget(ctx, requestingAccount.ID, form.ID)
if errWithCode != nil {
return nil, errWithCode
}
// make sure the target account actually exists in our db
targetAcct, err := p.state.DB.GetAccountByID(ctx, form.ID)
if err != nil {
if err == db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.ID, err))
}
// Check if a follow exists already.
if follows, err := p.state.DB.IsFollowing(ctx, requestingAccount, targetAccount); err != nil {
err = fmt.Errorf("FollowCreate: db error checking follow: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// check if a follow exists already
if follows, err := p.state.DB.IsFollowing(ctx, requestingAccount, targetAcct); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
} else if follows {
// already follows so just return the relationship
// Already follows, just return current relationship.
return p.RelationshipGet(ctx, requestingAccount, form.ID)
}
// check if a follow request exists already
if followRequested, err := p.state.DB.IsFollowRequested(ctx, requestingAccount, targetAcct); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
// Check if a follow request exists already.
if followRequested, err := p.state.DB.IsFollowRequested(ctx, requestingAccount, targetAccount); err != nil {
err = fmt.Errorf("FollowCreate: db error checking follow request: %w", err)
return nil, gtserror.NewErrorInternalError(err)
} else if followRequested {
// already follow requested so just return the relationship
// Already follow requested, just return current relationship.
return p.RelationshipGet(ctx, requestingAccount, form.ID)
}
// check for attempt to follow self
if requestingAccount.ID == targetAcct.ID {
return nil, gtserror.NewErrorNotAcceptable(fmt.Errorf("accountfollowcreate: account %s cannot follow itself", requestingAccount.ID))
}
// make the follow request
newFollowID, err := id.NewRandomULID()
// Create and store a new follow request.
followID, err := id.NewRandomULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
followURI := uris.GenerateURIForFollow(requestingAccount.Username, followID)
showReblogs := true
notify := false
fr := &gtsmodel.FollowRequest{
ID: newFollowID,
ID: followID,
URI: followURI,
AccountID: requestingAccount.ID,
Account: requestingAccount,
TargetAccountID: form.ID,
ShowReblogs: &showReblogs,
URI: uris.GenerateURIForFollow(requestingAccount.Username, newFollowID),
Notify: &notify,
}
if form.Reblogs != nil {
fr.ShowReblogs = form.Reblogs
}
if form.Notify != nil {
fr.Notify = form.Notify
TargetAccount: targetAccount,
ShowReblogs: form.Reblogs,
Notify: form.Notify,
}
// whack it in the database
if err := p.state.DB.Put(ctx, fr); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
err = fmt.Errorf("FollowCreate: error creating follow request in db: %s", err)
return nil, gtserror.NewErrorInternalError(err)
}
// if it's a local account that's not locked we can just straight up accept the follow request
if !*targetAcct.Locked && targetAcct.Domain == "" {
if targetAccount.IsLocal() && !*targetAccount.Locked {
// If the target account is local and not locked,
// we can already accept the follow request and
// skip any further processing.
//
// Because we know the requestingAccount is also
// local, we don't need to federate the accept out.
if _, err := p.state.DB.AcceptFollowRequest(ctx, requestingAccount.ID, form.ID); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
err = fmt.Errorf("FollowCreate: error accepting follow request for local unlocked account: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// return the new relationship
return p.RelationshipGet(ctx, requestingAccount, form.ID)
} else if targetAccount.IsRemote() {
// Otherwise we leave the follow request as it is,
// and we handle the rest of the process async.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: fr,
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
})
}
// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: fr,
OriginAccount: requestingAccount,
TargetAccount: targetAcct,
})
// return whatever relationship results from this
return p.RelationshipGet(ctx, requestingAccount, form.ID)
}
// FollowRemove handles the removal of a follow/follow request to an account, either remote or local.
func (p *Processor) FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
// if there's a block between the accounts we shouldn't do anything
blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true)
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 := &gtsmodel.FollowRequest{}
if err := p.state.DB.GetWhere(ctx, []db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, fr); err == nil {
frURI = fr.URI
if err := p.state.DB.DeleteByID(ctx, fr.ID, fr); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
}
frChanged = true
}
return targetAccount, nil
}
// now do the same thing for any existing follow
var fChanged bool
var fURI string
f := &gtsmodel.Follow{}
if err := p.state.DB.GetWhere(ctx, []db.Where{
{Key: "account_id", Value: requestingAccount.ID},
{Key: "target_account_id", Value: targetAccountID},
}, f); err == nil {
fURI = f.URI
if err := p.state.DB.DeleteByID(ctx, f.ID, f); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
}
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: &gtsmodel.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: &gtsmodel.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: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
TargetAccountID: targetAccount.ID,
URI: frURI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
})
}
return msgs, nil
}

View File

@@ -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
}

View File

@@ -26,7 +26,6 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/activity/pub"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -52,10 +51,7 @@ func (suite *AccountTestSuite) TestAccountDeleteLocal() {
err := suite.db.Put(ctx, follow)
suite.NoError(err)
errWithCode := suite.processor.Account().DeleteLocal(ctx, suite.testAccounts["local_account_1"], &apimodel.AccountDeleteRequest{
Password: "password",
DeleteOriginID: deletingAccount.ID,
})
errWithCode := suite.processor.Account().DeleteSelf(ctx, suite.testAccounts["local_account_1"])
suite.NoError(errWithCode)
// the delete should be federated outwards to the following account's inbox

View File

@@ -19,34 +19,33 @@ package processing
import (
"context"
"errors"
"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/log"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *Processor) FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) {
frs, err := p.state.DB.GetAccountFollowRequests(ctx, auth.Account.ID)
if err != nil {
if err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(err)
}
followRequests, err := p.state.DB.GetFollowRequests(ctx, "", auth.Account.ID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.NewErrorInternalError(err)
}
accts := []apimodel.Account{}
for _, fr := range frs {
if fr.Account == nil {
frAcct, err := p.state.DB.GetAccountByID(ctx, fr.AccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
fr.Account = frAcct
accts := make([]apimodel.Account, 0, len(followRequests))
for _, followRequest := range followRequests {
if followRequest.Account == nil {
// The creator of the follow doesn't exist,
// just skip this one.
log.WithContext(ctx).WithField("followRequest", followRequest).Warn("follow request had no associated account")
continue
}
apiAcct, err := p.tc.AccountToAPIAccountPublic(ctx, fr.Account)
apiAcct, err := p.tc.AccountToAPIAccountPublic(ctx, followRequest.Account)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -62,19 +61,10 @@ func (p *Processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, a
}
if follow.Account == nil {
followAccount, err := p.state.DB.GetAccountByID(ctx, follow.AccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
follow.Account = followAccount
}
if follow.TargetAccount == nil {
followTargetAccount, err := p.state.DB.GetAccountByID(ctx, follow.TargetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
follow.TargetAccount = followTargetAccount
// The creator of the follow doesn't exist,
// so we can't do further processing.
log.WithContext(ctx).WithField("follow", follow).Warn("follow had no associated account")
return p.relationship(ctx, auth.Account.ID, accountID)
}
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
@@ -85,17 +75,7 @@ func (p *Processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, a
TargetAccount: follow.TargetAccount,
})
gtsR, err := p.state.DB.GetRelationship(ctx, auth.Account.ID, accountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
r, err := p.tc.RelationshipToAPIRelationship(ctx, gtsR)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return r, nil
return p.relationship(ctx, auth.Account.ID, accountID)
}
func (p *Processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) {
@@ -105,19 +85,9 @@ func (p *Processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, a
}
if followRequest.Account == nil {
a, err := p.state.DB.GetAccountByID(ctx, followRequest.AccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
followRequest.Account = a
}
if followRequest.TargetAccount == nil {
a, err := p.state.DB.GetAccountByID(ctx, followRequest.TargetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
followRequest.TargetAccount = a
// The creator of the request doesn't exist,
// so we can't do further processing.
return p.relationship(ctx, auth.Account.ID, accountID)
}
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
@@ -128,15 +98,19 @@ func (p *Processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, a
TargetAccount: followRequest.TargetAccount,
})
gtsR, err := p.state.DB.GetRelationship(ctx, auth.Account.ID, accountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
r, err := p.tc.RelationshipToAPIRelationship(ctx, gtsR)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return r, nil
return p.relationship(ctx, auth.Account.ID, accountID)
}
func (p *Processor) relationship(ctx context.Context, accountID string, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
relationship, err := p.state.DB.GetRelationship(ctx, accountID, targetAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
apiRelationship, err := p.tc.RelationshipToAPIRelationship(ctx, relationship)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
return apiRelationship, nil
}

View File

@@ -139,8 +139,8 @@ func (p *Processor) processCreateAccountFromClientAPI(ctx context.Context, clien
return errors.New("account was not parseable as *gtsmodel.Account")
}
// return if the account isn't from this domain
if account.Domain != "" {
// Do nothing if this isn't our activity.
if !account.IsLocal() {
return nil
}
@@ -383,8 +383,8 @@ func (p *Processor) processReportAccountFromClientAPI(ctx context.Context, clien
// TODO: move all the below functions into federation.Federator
func (p *Processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error {
// do nothing if this isn't our account
if account.Domain != "" {
// Do nothing if this isn't our activity.
if !account.IsLocal() {
return nil
}
@@ -449,8 +449,8 @@ func (p *Processor) federateStatus(ctx context.Context, status *gtsmodel.Status)
status.Account = statusAccount
}
// do nothing if this isn't our status
if status.Account.Domain != "" {
// Do nothing if this isn't our activity.
if !status.Account.IsLocal() {
return nil
}
@@ -482,8 +482,8 @@ func (p *Processor) federateStatusDelete(ctx context.Context, status *gtsmodel.S
status.Account = statusAccount
}
// do nothing if this isn't our status
if status.Account.Domain != "" {
// Do nothing if this isn't our activity.
if !status.Account.IsLocal() {
return nil
}
@@ -502,8 +502,8 @@ func (p *Processor) federateStatusDelete(ctx context.Context, status *gtsmodel.S
}
func (p *Processor) federateFollow(ctx context.Context, followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
// Do nothing if both accounts are local.
if originAccount.IsLocal() && targetAccount.IsLocal() {
return nil
}
@@ -524,8 +524,8 @@ func (p *Processor) federateFollow(ctx context.Context, followRequest *gtsmodel.
}
func (p *Processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
// Do nothing if both accounts are local.
if originAccount.IsLocal() && targetAccount.IsLocal() {
return nil
}
@@ -565,8 +565,8 @@ func (p *Processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follo
}
func (p *Processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
// Do nothing if both accounts are local.
if originAccount.IsLocal() && targetAccount.IsLocal() {
return nil
}
@@ -604,8 +604,8 @@ func (p *Processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFav
}
func (p *Processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
if originAccount.Domain != "" {
// nothing to do here
// Do nothing if this isn't our activity.
if !originAccount.IsLocal() {
return nil
}
@@ -657,13 +657,9 @@ func (p *Processor) federateAcceptFollowRequest(ctx context.Context, follow *gts
}
targetAccount := follow.TargetAccount
// if target account isn't from our domain we shouldn't do anything
if targetAccount.Domain != "" {
return nil
}
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
// Do nothing if target account *isn't* local,
// or both origin + target *are* local.
if targetAccount.IsRemote() || originAccount.IsLocal() {
return nil
}
@@ -730,13 +726,9 @@ func (p *Processor) federateRejectFollowRequest(ctx context.Context, followReque
}
targetAccount := followRequest.TargetAccount
// if target account isn't from our domain we shouldn't do anything
if targetAccount.Domain != "" {
return nil
}
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
// Do nothing if target account *isn't* local,
// or both origin + target *are* local.
if targetAccount.IsRemote() || originAccount.IsLocal() {
return nil
}
@@ -786,8 +778,8 @@ func (p *Processor) federateRejectFollowRequest(ctx context.Context, followReque
}
func (p *Processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
// Do nothing if both accounts are local.
if originAccount.IsLocal() && targetAccount.IsLocal() {
return nil
}
@@ -857,8 +849,8 @@ func (p *Processor) federateBlock(ctx context.Context, block *gtsmodel.Block) er
block.TargetAccount = blockTargetAccount
}
// if both accounts are local there's nothing to do here
if block.Account.Domain == "" && block.TargetAccount.Domain == "" {
// Do nothing if both accounts are local.
if block.Account.IsLocal() && block.TargetAccount.IsLocal() {
return nil
}
@@ -893,8 +885,8 @@ func (p *Processor) federateUnblock(ctx context.Context, block *gtsmodel.Block)
block.TargetAccount = blockTargetAccount
}
// if both accounts are local there's nothing to do here
if block.Account.Domain == "" && block.TargetAccount.Domain == "" {
// Do nothing if both accounts are local.
if block.Account.IsLocal() && block.TargetAccount.IsLocal() {
return nil
}

View File

@@ -21,12 +21,11 @@ import (
"context"
"errors"
"fmt"
"strings"
"sync"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/stream"
@@ -413,45 +412,30 @@ func (p *Processor) timelineStatus(ctx context.Context, status *gtsmodel.Status)
status.Account = a
}
// get local followers of the account that posted the status
follows, err := p.state.DB.GetAccountFollowedBy(ctx, status.AccountID, true)
// Get LOCAL followers of the account that posted the status;
// we know that remote accounts don't have timelines on this
// instance, so there's no point selecting them too.
accountIDs, err := p.state.DB.GetLocalFollowersIDs(ctx, status.AccountID)
if err != nil {
return fmt.Errorf("timelineStatus: error getting followers for account id %s: %s", status.AccountID, err)
}
// if the poster is local, add a fake entry for them to the followers list so they can see their own status in their timeline
if status.Account.Domain == "" {
follows = append(follows, &gtsmodel.Follow{
AccountID: status.AccountID,
Account: status.Account,
})
// If the poster is also local, add a fake entry for them
// so they can see their own status in their timeline.
if status.Account.IsLocal() {
accountIDs = append(accountIDs, status.AccountID)
}
wg := sync.WaitGroup{}
wg.Add(len(follows))
errors := make(chan error, len(follows))
for _, f := range follows {
go p.timelineStatusForAccount(ctx, status, f.AccountID, errors, &wg)
}
// read any errors that come in from the async functions
errs := []string{}
go func(errs []string) {
for range errors {
if e := <-errors; e != nil {
errs = append(errs, e.Error())
}
// Timeline the status for each local following account.
errors := gtserror.MultiError{}
for _, accountID := range accountIDs {
if err := p.timelineStatusForAccount(ctx, status, accountID); err != nil {
errors.Append(err)
}
}(errs)
}
// wait til all functions have returned and then close the error channel
wg.Wait()
close(errors)
if len(errs) != 0 {
// we have at least one error
return fmt.Errorf("timelineStatus: one or more errors timelining statuses: %s", strings.Join(errs, ";"))
if len(errors) != 0 {
return fmt.Errorf("timelineStatus: one or more errors timelining statuses: %w", errors.Combine())
}
return nil
@@ -462,46 +446,38 @@ func (p *Processor) timelineStatus(ctx context.Context, status *gtsmodel.Status)
//
// If the status was inserted into the home timeline of the given account,
// it will also be streamed via websockets to the user.
func (p *Processor) timelineStatusForAccount(ctx context.Context, status *gtsmodel.Status, accountID string, errors chan error, wg *sync.WaitGroup) {
defer wg.Done()
func (p *Processor) timelineStatusForAccount(ctx context.Context, status *gtsmodel.Status, accountID string) error {
// get the timeline owner account
timelineAccount, err := p.state.DB.GetAccountByID(ctx, accountID)
if err != nil {
errors <- fmt.Errorf("timelineStatusForAccount: error getting account for timeline with id %s: %s", accountID, err)
return
return fmt.Errorf("timelineStatusForAccount: error getting account for timeline with id %s: %w", accountID, err)
}
// make sure the status is timelineable
timelineable, err := p.filter.StatusHometimelineable(ctx, status, timelineAccount)
if err != nil {
errors <- fmt.Errorf("timelineStatusForAccount: error getting timelineability for status for timeline with id %s: %s", accountID, err)
return
}
if !timelineable {
return
if timelineable, err := p.filter.StatusHometimelineable(ctx, status, timelineAccount); err != nil {
return fmt.Errorf("timelineStatusForAccount: error getting timelineability for status for timeline with id %s: %w", accountID, err)
} else if !timelineable {
return nil
}
// stick the status in the timeline for the account and then immediately prepare it so they can see it right away
inserted, err := p.statusTimelines.IngestAndPrepare(ctx, status, timelineAccount.ID)
if err != nil {
errors <- fmt.Errorf("timelineStatusForAccount: error ingesting status %s: %s", status.ID, err)
return
if inserted, err := p.statusTimelines.IngestAndPrepare(ctx, status, timelineAccount.ID); err != nil {
return fmt.Errorf("timelineStatusForAccount: error ingesting status %s: %w", status.ID, err)
} else if !inserted {
return nil
}
// the status was inserted so stream it to the user
if inserted {
apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, timelineAccount)
if err != nil {
errors <- fmt.Errorf("timelineStatusForAccount: error converting status %s to frontend representation: %s", status.ID, err)
return
}
if err := p.stream.Update(apiStatus, timelineAccount, stream.TimelineHome); err != nil {
errors <- fmt.Errorf("timelineStatusForAccount: error streaming status %s: %s", status.ID, err)
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, timelineAccount)
if err != nil {
return fmt.Errorf("timelineStatusForAccount: error converting status %s to frontend representation: %w", status.ID, err)
}
if err := p.stream.Update(apiStatus, timelineAccount, stream.TimelineHome); err != nil {
return fmt.Errorf("timelineStatusForAccount: error streaming update for status %s: %w", status.ID, err)
}
return nil
}
// deleteStatusFromTimelines completely removes the given status from all timelines.
@@ -544,12 +520,17 @@ func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta
}
// delete all notification entries generated by this status
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil {
if err := p.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
return err
}
// delete all bookmarks that point to this status
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.StatusBookmark{}); err != nil {
if err := p.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
return err
}
// delete all faves of this status
if err := p.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
return err
}

View File

@@ -19,8 +19,10 @@ package processing
import (
"context"
"errors"
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/log"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
@@ -39,7 +41,7 @@ func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ex
return util.EmptyPageableResponse(), nil
}
items := []interface{}{}
items := make([]interface{}, 0, count)
nextMaxIDValue := ""
prevMinIDValue := ""
for i, n := range notifs {
@@ -71,8 +73,8 @@ func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ex
}
func (p *Processor) NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode {
err := p.state.DB.ClearNotifications(ctx, authed.Account.ID)
if err != nil {
// Delete all notifications that target the authorized account.
if err := p.state.DB.DeleteNotifications(ctx, authed.Account.ID, ""); err != nil && !errors.Is(err, db.ErrNoEntries) {
return gtserror.NewErrorInternalError(err)
}

View File

@@ -138,20 +138,24 @@ func NewProcessor(
return processor
}
func (p *Processor) EnqueueClientAPI(ctx context.Context, msg messages.FromClientAPI) {
log.WithContext(ctx).WithField("msg", msg).Trace("enqueuing client API")
func (p *Processor) EnqueueClientAPI(ctx context.Context, msgs ...messages.FromClientAPI) {
log.Trace(ctx, "enqueuing client API")
_ = p.state.Workers.ClientAPI.MustEnqueueCtx(ctx, func(ctx context.Context) {
if err := p.ProcessFromClientAPI(ctx, msg); err != nil {
log.Errorf(ctx, "error processing client API message: %v", err)
for _, msg := range msgs {
if err := p.ProcessFromClientAPI(ctx, msg); err != nil {
log.WithContext(ctx).WithField("msg", msg).Errorf("error processing client API message: %v", err)
}
}
})
}
func (p *Processor) EnqueueFederator(ctx context.Context, msg messages.FromFederator) {
log.WithContext(ctx).WithField("msg", msg).Trace("enqueuing federator")
func (p *Processor) EnqueueFederator(ctx context.Context, msgs ...messages.FromFederator) {
log.Trace(ctx, "enqueuing federator")
_ = p.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
if err := p.ProcessFromFederator(ctx, msg); err != nil {
log.Errorf(ctx, "error processing federator message: %v", err)
for _, msg := range msgs {
if err := p.ProcessFromFederator(ctx, msg); err != nil {
log.WithContext(ctx).WithField("msg", msg).Errorf("error processing federator message: %v", err)
}
}
})
}

View File

@@ -31,91 +31,67 @@ import (
// BookmarkCreate adds a bookmark for the requestingAccount, targeting the given status (no-op if bookmark already exists).
func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
targetStatus, existingBookmarkID, errWithCode := p.getBookmarkTarget(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
// first check if the status is already bookmarked, if so we don't need to do anything
newBookmark := true
gtsBookmark := &gtsmodel.StatusBookmark{}
if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil {
// we already have a bookmark for this status
newBookmark = false
if existingBookmarkID != "" {
// Status is already bookmarked.
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
if newBookmark {
// we need to create a new bookmark in the database
gtsBookmark := &gtsmodel.StatusBookmark{
ID: id.NewULID(),
AccountID: requestingAccount.ID,
Account: requestingAccount,
TargetAccountID: targetStatus.AccountID,
TargetAccount: targetStatus.Account,
StatusID: targetStatus.ID,
Status: targetStatus,
}
if err := p.state.DB.Put(ctx, gtsBookmark); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting bookmark in database: %s", err))
}
// Create and store a new bookmark.
gtsBookmark := &gtsmodel.StatusBookmark{
ID: id.NewULID(),
AccountID: requestingAccount.ID,
Account: requestingAccount,
TargetAccountID: targetStatus.AccountID,
TargetAccount: targetStatus.Account,
StatusID: targetStatus.ID,
Status: targetStatus,
}
// return the apidon representation of the target status
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
if err := p.state.DB.PutStatusBookmark(ctx, gtsBookmark); err != nil {
err = fmt.Errorf("BookmarkCreate: error putting bookmark in database: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return apiStatus, nil
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
// BookmarkRemove removes a bookmark for the requesting account, targeting the given status (no-op if bookmark doesn't exist).
func (p *Processor) BookmarkRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
targetStatus, existingBookmarkID, errWithCode := p.getBookmarkTarget(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
// first check if the status is actually bookmarked
toUnbookmark := false
gtsBookmark := &gtsmodel.StatusBookmark{}
if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil {
// we have a bookmark for this status
toUnbookmark = true
if existingBookmarkID == "" {
// Status isn't bookmarked.
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
if toUnbookmark {
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
}
// We have a bookmark to remove.
if err := p.state.DB.DeleteStatusBookmark(ctx, existingBookmarkID); err != nil {
err = fmt.Errorf("BookmarkRemove: error removing status bookmark: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// return the api representation of the target status
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
func (p *Processor) getBookmarkTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, string, gtserror.WithCode) {
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, "", errWithCode
}
bookmarkID, err := p.state.DB.GetStatusBookmarkID(ctx, requestingAccount.ID, targetStatus.ID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("getBookmarkTarget: error checking existing bookmark: %w", err)
return nil, "", gtserror.NewErrorInternalError(err)
}
return targetStatus, bookmarkID, nil
}

View File

@@ -86,13 +86,7 @@ func (p *Processor) BoostCreate(ctx context.Context, requestingAccount *gtsmodel
TargetAccount: targetStatus.Account,
})
// return the frontend representation of the new status to the submitter
apiStatus, err := p.tc.StatusToAPIStatus(ctx, boostWrapperStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
return p.apiStatus(ctx, boostWrapperStatus, requestingAccount)
}
// BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well.
@@ -159,12 +153,7 @@ func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel
})
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.

View File

@@ -0,0 +1,63 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package status
import (
"context"
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
func (p *Processor) apiStatus(ctx context.Context, targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, gtserror.WithCode) {
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
err = fmt.Errorf("error converting status %s to frontend representation: %w", targetStatus.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
return apiStatus, nil
}
func (p *Processor) getVisibleStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, gtserror.WithCode) {
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
if err != nil {
err = fmt.Errorf("getVisibleStatus: db error fetching status %s: %w", targetStatusID, err)
return nil, gtserror.NewErrorNotFound(err)
}
if targetStatus.Account == nil {
err = fmt.Errorf("getVisibleStatus: no status owner for status %s", targetStatusID)
return nil, gtserror.NewErrorNotFound(err)
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
err = fmt.Errorf("getVisibleStatus: error seeing if status %s is visible: %w", targetStatus.ID, err)
return nil, gtserror.NewErrorNotFound(err)
}
if !visible {
err = fmt.Errorf("getVisibleStatus: status %s is not visible to requesting account", targetStatusID)
return nil, gtserror.NewErrorNotFound(err)
}
return targetStatus, nil
}

View File

@@ -93,13 +93,7 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, appli
OriginAccount: account,
})
// return the frontend representation of the new status to the submitter
apiStatus, err := p.tc.StatusToAPIStatus(ctx, newStatus, account)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", newStatus.ID, err))
}
return apiStatus, nil
return p.apiStatus(ctx, newStatus, account)
}
func processReplyToID(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {

View File

@@ -35,6 +35,7 @@ func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Acco
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
@@ -43,12 +44,13 @@ func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Acco
return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account"))
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
// Parse the status to API model BEFORE deleting it.
apiStatus, errWithCode := p.apiStatus(ctx, targetStatus, requestingAccount)
if errWithCode != nil {
return nil, errWithCode
}
// send the status back to the processor for async processing
// Process delete side effects.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityDelete,

View File

@@ -28,181 +28,140 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
// FaveCreate processes the faving of a given status, returning the updated status if the fave goes through.
// FaveCreate adds a fave for the requestingAccount, targeting the given status (no-op if fave already exists).
func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
targetStatus, existingFave, errWithCode := p.getFaveTarget(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
if !*targetStatus.Likeable {
return nil, gtserror.NewErrorForbidden(errors.New("status is not faveable"))
if existingFave != nil {
// Status is already faveed.
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
// first check if the status is already faved, if so we don't need to do anything
newFave := true
gtsFave := &gtsmodel.StatusFave{}
if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err == nil {
// we already have a fave for this status
newFave = false
// Create and store a new fave
faveID := id.NewULID()
gtsFave := &gtsmodel.StatusFave{
ID: faveID,
AccountID: requestingAccount.ID,
Account: requestingAccount,
TargetAccountID: targetStatus.AccountID,
TargetAccount: targetStatus.Account,
StatusID: targetStatus.ID,
Status: targetStatus,
URI: uris.GenerateURIForLike(requestingAccount.Username, faveID),
}
if newFave {
thisFaveID := id.NewULID()
// we need to create a new fave in the database
gtsFave := &gtsmodel.StatusFave{
ID: thisFaveID,
AccountID: requestingAccount.ID,
Account: requestingAccount,
TargetAccountID: targetStatus.AccountID,
TargetAccount: targetStatus.Account,
StatusID: targetStatus.ID,
Status: targetStatus,
URI: uris.GenerateURIForLike(requestingAccount.Username, thisFaveID),
}
if err := p.state.DB.Put(ctx, gtsFave); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting fave in database: %s", err))
}
// send it back to the processor for async processing
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: gtsFave,
OriginAccount: requestingAccount,
TargetAccount: targetStatus.Account,
})
if err := p.state.DB.PutStatusFave(ctx, gtsFave); err != nil {
err = fmt.Errorf("FaveCreate: error putting fave in database: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// return the apidon representation of the target status
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
// Process new status fave side effects.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: gtsFave,
OriginAccount: requestingAccount,
TargetAccount: targetStatus.Account,
})
return apiStatus, nil
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
// FaveRemove processes the unfaving of a given status, returning the updated status if the fave goes through.
// FaveRemove removes a fave for the requesting account, targeting the given status (no-op if fave doesn't exist).
func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
targetStatus, existingFave, errWithCode := p.getFaveTarget(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
if existingFave == nil {
// Status isn't faveed.
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
// check if we actually have a fave for this status
var toUnfave bool
gtsFave := &gtsmodel.StatusFave{}
err = p.state.DB.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave)
if err == nil {
// we have a fave
toUnfave = true
}
if err != nil {
// something went wrong in the db finding the fave
if err != db.ErrNoEntries {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err))
}
// we just don't have a fave
toUnfave = false
// We have a fave to remove.
if err := p.state.DB.DeleteStatusFave(ctx, existingFave.ID); err != nil {
err = fmt.Errorf("FaveRemove: error removing status fave: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
if toUnfave {
// we had a fave, so take some action to get rid of it
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
}
// Process remove status fave side effects.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityUndo,
GTSModel: existingFave,
OriginAccount: requestingAccount,
TargetAccount: targetStatus.Account,
})
// send it back to the processor for async processing
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityUndo,
GTSModel: gtsFave,
OriginAccount: requestingAccount,
TargetAccount: targetStatus.Account,
})
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
statusFaves, err := p.state.DB.GetStatusFaves(ctx, targetStatus.ID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
return nil, gtserror.NewErrorNotFound(fmt.Errorf("FavedBy: error seeing who faved status: %s", err))
}
statusFaves, err := p.state.DB.GetStatusFaves(ctx, targetStatus)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing who faved status: %s", err))
}
// filter the list so the user doesn't see accounts they blocked or which blocked them
filteredAccounts := []*gtsmodel.Account{}
// For each fave, ensure that we're only showing
// the requester accounts that they don't block,
// and which don't block them.
apiAccounts := make([]*apimodel.Account, 0, len(statusFaves))
for _, fave := range statusFaves {
blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, fave.AccountID, true)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err))
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, fave.AccountID, true); err != nil {
err = fmt.Errorf("FavedBy: error checking blocks: %w", err)
return nil, gtserror.NewErrorInternalError(err)
} else if blocked {
continue
}
if !blocked {
filteredAccounts = append(filteredAccounts, fave.Account)
}
}
// now we can return the api representation of those accounts
apiAccounts := []*apimodel.Account{}
for _, acc := range filteredAccounts {
apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, acc)
if fave.Account == nil {
// Account isn't set for some reason, just skip.
log.WithContext(ctx).WithField("fave", fave).Warn("fave had no associated account")
continue
}
apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, fave.Account)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
err = fmt.Errorf("FavedBy: error converting account %s to frontend representation: %w", fave.AccountID, err)
return nil, gtserror.NewErrorInternalError(err)
}
apiAccounts = append(apiAccounts, apiAccount)
}
return apiAccounts, nil
}
func (p *Processor) getFaveTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, *gtsmodel.StatusFave, gtserror.WithCode) {
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, nil, errWithCode
}
if !*targetStatus.Likeable {
err := errors.New("status is not faveable")
return nil, nil, gtserror.NewErrorForbidden(err, err.Error())
}
fave, err := p.state.DB.GetStatusFaveByAccountID(ctx, requestingAccount.ID, targetStatusID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err)
return nil, nil, gtserror.NewErrorInternalError(err)
}
return targetStatus, fave, nil
}

View File

@@ -19,8 +19,6 @@ package status
import (
"context"
"errors"
"fmt"
"sort"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
@@ -30,46 +28,19 @@ import (
// Get gets the given status, taking account of privacy settings and blocks etc.
func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
}
return apiStatus, nil
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
// ContextGet returns the context (previous and following posts) from the given status ID.
func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
}
if targetStatus.Account == nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
}
if !visible {
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
context := &apimodel.Context{

View File

@@ -38,40 +38,24 @@ const allowedPinnedCount = 10
// - Status belongs to requesting account.
// - Status is public, unlisted, or followers-only.
// - Status is not a boost.
func (p *Processor) getPinnableStatus(ctx context.Context, targetStatusID string, requestingAccountID string) (*gtsmodel.Status, gtserror.WithCode) {
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
if err != nil {
err = fmt.Errorf("error fetching status %s: %w", targetStatusID, err)
return nil, gtserror.NewErrorNotFound(err)
func (p *Processor) getPinnableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, gtserror.WithCode) {
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
requestingAccount, err := p.state.DB.GetAccountByID(ctx, requestingAccountID)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
if !visible {
err = fmt.Errorf("status %s not visible to account %s", targetStatusID, requestingAccountID)
return nil, gtserror.NewErrorNotFound(err)
}
if targetStatus.AccountID != requestingAccountID {
err = fmt.Errorf("status %s does not belong to account %s", targetStatusID, requestingAccountID)
if targetStatus.AccountID != requestingAccount.ID {
err := fmt.Errorf("status %s does not belong to account %s", targetStatusID, requestingAccount.ID)
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
}
if targetStatus.Visibility == gtsmodel.VisibilityDirect {
err = errors.New("cannot pin direct messages")
err := errors.New("cannot pin direct messages")
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
}
if targetStatus.BoostOfID != "" {
err = errors.New("cannot pin boosts")
err := errors.New("cannot pin boosts")
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
}
@@ -89,7 +73,7 @@ func (p *Processor) getPinnableStatus(ctx context.Context, targetStatusID string
//
// If the conditions can't be met, then code 422 Unprocessable Entity will be returned.
func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, errWithCode := p.getPinnableStatus(ctx, targetStatusID, requestingAccount.ID)
targetStatus, errWithCode := p.getPinnableStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
@@ -114,12 +98,7 @@ func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.A
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error pinning status: %w", err))
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %w", targetStatus.ID, err))
}
return apiStatus, nil
return p.apiStatus(ctx, targetStatus, requestingAccount)
}
// PinRemove unpins the target status from the top of requestingAccount's profile, if possible.
@@ -134,7 +113,7 @@ func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.A
// Unlike with PinCreate, statuses that are already unpinned will not return 422, but just do
// nothing and return the api model representation of the status, to conform to the masto API.
func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, errWithCode := p.getPinnableStatus(ctx, targetStatusID, requestingAccount.ID)
targetStatus, errWithCode := p.getPinnableStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
@@ -146,10 +125,5 @@ func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.A
}
}
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %w", targetStatus.ID, err))
}
return apiStatus, nil
return p.apiStatus(ctx, targetStatus, requestingAccount)
}