mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[feature] Process outgoing Move from clientAPI (#2750)
* prevent moved accounts from taking create-type actions * update move logic * federate move out * indicate on web profile when an account has moved * [docs] Add migration docs section * lock while checking + setting move state * use redirectFollowers func for clientAPI as well * comment typo * linter? i barely know 'er! * Update internal/uris/uri.go Co-authored-by: Daenney <daenney@users.noreply.github.com> * add a couple tests for move * fix little mistake exposed by tests (thanks tests) * ensure Move marked as successful * attach shared util funcs to struct * lock whole account when doing move * move moving check to after error check * replace repeated text with error func * linterrrrrr!!!! * catch self follow case --------- Co-authored-by: Daenney <daenney@users.noreply.github.com>
This commit is contained in:
@@ -23,14 +23,17 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -45,13 +48,14 @@ func (p *Processor) MoveSelf(
|
||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
movedToURI, err := url.Parse(form.MovedToURI)
|
||||
targetAcctURIStr := form.MovedToURI
|
||||
targetAcctURI, err := url.Parse(form.MovedToURI)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid moved_to_uri provided in account Move request: %w", err)
|
||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
if movedToURI.Scheme != "https" && movedToURI.Scheme != "http" {
|
||||
if targetAcctURI.Scheme != "https" && targetAcctURI.Scheme != "http" {
|
||||
err := errors.New("invalid moved_to_uri provided in account Move request: uri scheme must be http or https")
|
||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
@@ -70,83 +74,244 @@ func (p *Processor) MoveSelf(
|
||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
var (
|
||||
// Current account from which
|
||||
// the move is taking place.
|
||||
account = authed.Account
|
||||
|
||||
// Target account to which
|
||||
// the move is taking place.
|
||||
targetAccount *gtsmodel.Account
|
||||
)
|
||||
|
||||
switch {
|
||||
case account.MovedToURI == "":
|
||||
// No problemo.
|
||||
|
||||
case account.MovedToURI == form.MovedToURI:
|
||||
// Trying to move again to the same
|
||||
// destination, perhaps to reprocess
|
||||
// side effects. This is OK.
|
||||
log.Info(ctx,
|
||||
"reprocessing Move side effects from %s to %s",
|
||||
account.URI, form.MovedToURI,
|
||||
)
|
||||
|
||||
default:
|
||||
// Account already moved, and now
|
||||
// trying to move somewhere else.
|
||||
// We can't/won't validate Move activities
|
||||
// to domains we have blocked, so check this.
|
||||
targetDomainBlocked, err := p.state.DB.IsDomainBlocked(ctx, targetAcctURI.Host)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"account %s is already Moved to %s, cannot also Move to %s",
|
||||
account.URI, account.MovedToURI, form.MovedToURI,
|
||||
"db error checking if target domain %s blocked: %w",
|
||||
targetAcctURI.Host, err,
|
||||
)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if targetDomainBlocked {
|
||||
err := fmt.Errorf(
|
||||
"domain of %s is blocked from this instance; "+
|
||||
"you will not be able to Move to that account",
|
||||
targetAcctURIStr,
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
var (
|
||||
// Current account from which
|
||||
// the move is taking place.
|
||||
originAcct = authed.Account
|
||||
|
||||
// Target account to which
|
||||
// the move is taking place.
|
||||
targetAcct *gtsmodel.Account
|
||||
|
||||
// AP representation of target.
|
||||
targetAcctable ap.Accountable
|
||||
)
|
||||
|
||||
// Next steps involve checking + setting
|
||||
// state that might get messed up if a
|
||||
// client triggers this function twice
|
||||
// in quick succession, so get a lock on
|
||||
// this account.
|
||||
lockKey := originAcct.URI
|
||||
unlock := p.state.ClientLocks.Lock(lockKey)
|
||||
defer unlock()
|
||||
|
||||
// Ensure we have a valid, up-to-date representation of the target account.
|
||||
targetAccount, _, err = p.federator.GetAccountByURI(ctx, account.Username, movedToURI)
|
||||
targetAcct, targetAcctable, err = p.federator.GetAccountByURI(
|
||||
ctx,
|
||||
originAcct.Username,
|
||||
targetAcctURI,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error dereferencing moved_to_uri account: %w", err)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
if !targetAccount.SuspendedAt.IsZero() {
|
||||
if !targetAcct.SuspendedAt.IsZero() {
|
||||
err := fmt.Errorf(
|
||||
"target account %s is suspended from this instance; "+
|
||||
"you will not be able to Move to that account",
|
||||
targetAccount.URI,
|
||||
targetAcct.URI,
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
if targetAcct.IsRemote() {
|
||||
// Force refresh Move target account
|
||||
// to ensure we have up-to-date version.
|
||||
targetAcct, _, err = p.federator.RefreshAccount(ctx,
|
||||
originAcct.Username,
|
||||
targetAcct,
|
||||
targetAcctable,
|
||||
dereferencing.Freshest,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"error refreshing target account %s: %w",
|
||||
targetAcctURIStr, err,
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Target account MUST be aliased to this
|
||||
// account for this to be a valid Move.
|
||||
if !slices.Contains(targetAccount.AlsoKnownAsURIs, account.URI) {
|
||||
if !slices.Contains(targetAcct.AlsoKnownAsURIs, originAcct.URI) {
|
||||
err := fmt.Errorf(
|
||||
"target account %s is not aliased to this account via alsoKnownAs; "+
|
||||
"if you just changed it, wait five minutes and try the Move again",
|
||||
targetAccount.URI,
|
||||
"if you just changed it, please wait a few minutes and try the Move again",
|
||||
targetAcct.URI,
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
// Target account cannot itself have
|
||||
// already Moved somewhere else.
|
||||
if targetAccount.MovedToURI != "" {
|
||||
if targetAcct.MovedToURI != "" {
|
||||
err := fmt.Errorf(
|
||||
"target account %s has already Moved somewhere else (%s); "+
|
||||
"you will not be able to Move to that account",
|
||||
targetAccount.URI, targetAccount.MovedToURI,
|
||||
targetAcct.URI, targetAcct.MovedToURI,
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
// Everything seems OK, so process the Move.
|
||||
// If a Move has been *attempted* within last 5m,
|
||||
// that involved the origin and target in any way,
|
||||
// then we shouldn't try to reprocess immediately.
|
||||
latestMoveAttempt, err := p.state.DB.GetLatestMoveAttemptInvolvingURIs(
|
||||
ctx, originAcct.URI, targetAcct.URI,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"error checking latest Move attempt involving origin %s and target %s: %w",
|
||||
originAcct.URI, targetAcct.URI, err,
|
||||
)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if !latestMoveAttempt.IsZero() &&
|
||||
time.Since(latestMoveAttempt) < 5*time.Minute {
|
||||
err := fmt.Errorf(
|
||||
"your account or target account have been involved in a Move attempt within "+
|
||||
"the last 5 minutes, will not process Move; please try again after %s",
|
||||
latestMoveAttempt.Add(5*time.Minute),
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
// If a Move has *succeeded* within the last week
|
||||
// that involved the origin and target in any way,
|
||||
// then we shouldn't process again for a while.
|
||||
latestMoveSuccess, err := p.state.DB.GetLatestMoveSuccessInvolvingURIs(
|
||||
ctx, originAcct.URI, targetAcct.URI,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"error checking latest Move success involving origin %s and target %s: %w",
|
||||
originAcct.URI, targetAcct.URI, err,
|
||||
)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if !latestMoveSuccess.IsZero() &&
|
||||
time.Since(latestMoveSuccess) < 168*time.Hour {
|
||||
err := fmt.Errorf(
|
||||
"your account or target account have been involved in a successful Move within "+
|
||||
"the last 7 days, will not process Move; please try again after %s",
|
||||
latestMoveSuccess.Add(168*time.Hour),
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
// See if we have a Move stored already
|
||||
// or if we need to create a new one.
|
||||
var move *gtsmodel.Move
|
||||
|
||||
if originAcct.MoveID != "" {
|
||||
// Move already stored, ensure it's
|
||||
// to the target and nothing weird is
|
||||
// happening with race conditions etc.
|
||||
move = originAcct.Move
|
||||
if move == nil {
|
||||
// This shouldn't happen...
|
||||
err := fmt.Errorf("nil move for id %s", originAcct.MoveID)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if move.OriginURI != originAcct.URI ||
|
||||
move.TargetURI != targetAcct.URI {
|
||||
// This is also weird...
|
||||
err := errors.New("a Move is already stored for your account but contains invalid fields")
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
if originAcct.MovedToURI != move.TargetURI {
|
||||
// Huh... I'll be damned.
|
||||
err := errors.New("stored Move target URI does not equal your moved_to_uri value")
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
} else {
|
||||
// Move not stored yet, create it.
|
||||
moveID := id.NewULID()
|
||||
moveURIStr := uris.GenerateURIForMove(originAcct.Username, moveID)
|
||||
|
||||
// We might have selected the target
|
||||
// using the URL and not the URI.
|
||||
// Ensure we continue with the URI!
|
||||
if targetAcctURIStr != targetAcct.URI {
|
||||
targetAcctURIStr = targetAcct.URI
|
||||
targetAcctURI, err = url.Parse(targetAcctURIStr)
|
||||
if err != nil {
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse origin URI.
|
||||
originAcctURI, err := url.Parse(originAcct.URI)
|
||||
if err != nil {
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Store the Move.
|
||||
move = >smodel.Move{
|
||||
ID: moveID,
|
||||
AttemptedAt: time.Now(),
|
||||
OriginURI: originAcct.URI,
|
||||
Origin: originAcctURI,
|
||||
TargetURI: targetAcctURIStr,
|
||||
Target: targetAcctURI,
|
||||
URI: moveURIStr,
|
||||
}
|
||||
if err := p.state.DB.PutMove(ctx, move); err != nil {
|
||||
err := fmt.Errorf("db error storing move %s: %w", moveURIStr, err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Update account with the new
|
||||
// Move, and set moved_to_uri.
|
||||
originAcct.MoveID = move.ID
|
||||
originAcct.Move = move
|
||||
originAcct.MovedToURI = targetAcct.URI
|
||||
originAcct.MovedTo = targetAcct
|
||||
if err := p.state.DB.UpdateAccount(
|
||||
ctx,
|
||||
originAcct,
|
||||
"move_id",
|
||||
"moved_to_uri",
|
||||
); err != nil {
|
||||
err := fmt.Errorf("db error updating account: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Everything seems OK, process Move side effects async.
|
||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||
APObjectType: ap.ActorPerson,
|
||||
APActivityType: ap.ActivityMove,
|
||||
OriginAccount: account,
|
||||
TargetAccount: targetAccount,
|
||||
GTSModel: move,
|
||||
OriginAccount: originAcct,
|
||||
TargetAccount: targetAcct,
|
||||
})
|
||||
|
||||
return nil
|
||||
|
175
internal/processing/account/move_test.go
Normal file
175
internal/processing/account/move_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// 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 account_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
type MoveTestSuite struct {
|
||||
AccountStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *MoveTestSuite) TestMoveAccountOK() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Copy zork.
|
||||
requestingAcct := new(gtsmodel.Account)
|
||||
*requestingAcct = *suite.testAccounts["local_account_1"]
|
||||
|
||||
// Copy admin.
|
||||
targetAcct := new(gtsmodel.Account)
|
||||
*targetAcct = *suite.testAccounts["admin_account"]
|
||||
|
||||
// Update admin to alias back to zork.
|
||||
targetAcct.AlsoKnownAsURIs = []string{requestingAcct.URI}
|
||||
if err := suite.state.DB.UpdateAccount(
|
||||
ctx,
|
||||
targetAcct,
|
||||
"also_known_as_uris",
|
||||
); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Trigger move from zork to admin.
|
||||
if err := suite.accountProcessor.MoveSelf(
|
||||
ctx,
|
||||
&oauth.Auth{
|
||||
Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
|
||||
Application: suite.testApplications["local_account_1"],
|
||||
User: suite.testUsers["local_account_1"],
|
||||
Account: requestingAcct,
|
||||
},
|
||||
&apimodel.AccountMoveRequest{
|
||||
Password: "password",
|
||||
MovedToURI: targetAcct.URI,
|
||||
},
|
||||
); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// There should be a msg heading back to fromClientAPI.
|
||||
select {
|
||||
case msg := <-suite.fromClientAPIChan:
|
||||
move, ok := msg.GTSModel.(*gtsmodel.Move)
|
||||
if !ok {
|
||||
suite.FailNow("", "could not cast %T to *gtsmodel.Move", move)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
suite.WithinDuration(now, move.CreatedAt, 5*time.Second)
|
||||
suite.WithinDuration(now, move.UpdatedAt, 5*time.Second)
|
||||
suite.WithinDuration(now, move.AttemptedAt, 5*time.Second)
|
||||
suite.Zero(move.SucceededAt)
|
||||
suite.NotZero(move.ID)
|
||||
suite.Equal(requestingAcct.URI, move.OriginURI)
|
||||
suite.NotNil(move.Origin)
|
||||
suite.Equal(targetAcct.URI, move.TargetURI)
|
||||
suite.NotNil(move.Target)
|
||||
suite.NotZero(move.URI)
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
suite.FailNow("time out waiting for message")
|
||||
}
|
||||
|
||||
// Move should be in the database now.
|
||||
move, err := suite.state.DB.GetMoveByOriginTarget(
|
||||
ctx,
|
||||
requestingAcct.URI,
|
||||
targetAcct.URI,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.NotNil(move)
|
||||
|
||||
// Origin account should have move ID and move to URI set.
|
||||
suite.Equal(move.ID, requestingAcct.MoveID)
|
||||
suite.Equal(targetAcct.URI, requestingAcct.MovedToURI)
|
||||
}
|
||||
|
||||
func (suite *MoveTestSuite) TestMoveAccountNotAliased() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Copy zork.
|
||||
requestingAcct := new(gtsmodel.Account)
|
||||
*requestingAcct = *suite.testAccounts["local_account_1"]
|
||||
|
||||
// Don't copy admin.
|
||||
targetAcct := suite.testAccounts["admin_account"]
|
||||
|
||||
// Trigger move from zork to admin.
|
||||
//
|
||||
// Move should fail since admin is
|
||||
// not aliased back to zork.
|
||||
err := suite.accountProcessor.MoveSelf(
|
||||
ctx,
|
||||
&oauth.Auth{
|
||||
Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
|
||||
Application: suite.testApplications["local_account_1"],
|
||||
User: suite.testUsers["local_account_1"],
|
||||
Account: requestingAcct,
|
||||
},
|
||||
&apimodel.AccountMoveRequest{
|
||||
Password: "password",
|
||||
MovedToURI: targetAcct.URI,
|
||||
},
|
||||
)
|
||||
suite.EqualError(err, "target account http://localhost:8080/users/admin is not aliased to this account via alsoKnownAs; if you just changed it, please wait a few minutes and try the Move again")
|
||||
}
|
||||
|
||||
func (suite *MoveTestSuite) TestMoveAccountBadPassword() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Copy zork.
|
||||
requestingAcct := new(gtsmodel.Account)
|
||||
*requestingAcct = *suite.testAccounts["local_account_1"]
|
||||
|
||||
// Don't copy admin.
|
||||
targetAcct := suite.testAccounts["admin_account"]
|
||||
|
||||
// Trigger move from zork to admin.
|
||||
//
|
||||
// Move should fail since admin is
|
||||
// not aliased back to zork.
|
||||
err := suite.accountProcessor.MoveSelf(
|
||||
ctx,
|
||||
&oauth.Auth{
|
||||
Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
|
||||
Application: suite.testApplications["local_account_1"],
|
||||
User: suite.testUsers["local_account_1"],
|
||||
Account: requestingAcct,
|
||||
},
|
||||
&apimodel.AccountMoveRequest{
|
||||
Password: "boobies",
|
||||
MovedToURI: targetAcct.URI,
|
||||
},
|
||||
)
|
||||
suite.EqualError(err, "invalid password provided in account Move request")
|
||||
}
|
||||
|
||||
func TestMoveTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MoveTestSuite))
|
||||
}
|
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/superseriousbusiness/activity/pub"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
@@ -954,3 +955,68 @@ func (f *federate) Flag(ctx context.Context, report *gtsmodel.Report) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) error {
|
||||
// Do nothing if it's not our
|
||||
// account that's been moved.
|
||||
if !account.IsLocal() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Actor doing the Move.
|
||||
actorIRI := account.Move.Origin
|
||||
|
||||
// Destination Actor of the Move.
|
||||
targetIRI := account.Move.Target
|
||||
|
||||
followersIRI, err := parseURI(account.FollowersURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publicIRI, err := parseURI(pub.PublicActivityPubIRI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new move.
|
||||
move := streams.NewActivityStreamsMove()
|
||||
|
||||
// Set the Move ID.
|
||||
if err := ap.SetJSONLDIdStr(move, account.Move.URI); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the Actor for the Move.
|
||||
ap.AppendActorIRIs(move, actorIRI)
|
||||
|
||||
// Set the account's IRI as the 'object' property.
|
||||
ap.AppendObjectIRIs(move, actorIRI)
|
||||
|
||||
// Set the target's IRI as the 'target' property.
|
||||
ap.AppendTargetIRIs(move, targetIRI)
|
||||
|
||||
// Address the move To followers.
|
||||
ap.AppendTo(move, followersIRI)
|
||||
|
||||
// Address the move CC public.
|
||||
ap.AppendCc(move, publicIRI)
|
||||
|
||||
// Send the Move via the Actor's outbox.
|
||||
if _, err := f.FederatingActor().Send(
|
||||
ctx, outboxIRI, move,
|
||||
); err != nil {
|
||||
return gtserror.Newf(
|
||||
"error sending activity %T via outbox %s: %w",
|
||||
move, outboxIRI, err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -39,12 +39,12 @@ import (
|
||||
// specifically for messages originating
|
||||
// from the client/REST API.
|
||||
type clientAPI struct {
|
||||
state *state.State
|
||||
converter *typeutils.Converter
|
||||
surface *surface
|
||||
federate *federate
|
||||
wipeStatus wipeStatus
|
||||
account *account.Processor
|
||||
state *state.State
|
||||
converter *typeutils.Converter
|
||||
surface *surface
|
||||
federate *federate
|
||||
account *account.Processor
|
||||
utilF *utilF
|
||||
}
|
||||
|
||||
func (p *Processor) EnqueueClientAPI(cctx context.Context, msgs ...messages.FromClientAPI) {
|
||||
@@ -194,6 +194,15 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg messages.From
|
||||
case ap.ObjectProfile:
|
||||
return p.clientAPI.ReportAccount(ctx, cMsg)
|
||||
}
|
||||
|
||||
// MOVE SOMETHING
|
||||
case ap.ActivityMove:
|
||||
switch cMsg.APObjectType { //nolint:gocritic
|
||||
|
||||
// MOVE PROFILE/ACCOUNT
|
||||
case ap.ObjectProfile, ap.ActorPerson:
|
||||
return p.clientAPI.MoveAccount(ctx, cMsg)
|
||||
}
|
||||
}
|
||||
|
||||
return gtserror.Newf("unhandled: %s %s", cMsg.APActivityType, cMsg.APObjectType)
|
||||
@@ -576,7 +585,7 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg messages.FromClientAP
|
||||
return gtserror.Newf("db error populating status: %w", err)
|
||||
}
|
||||
|
||||
if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil {
|
||||
if err := p.utilF.wipeStatus(ctx, status, deleteAttachments); err != nil {
|
||||
log.Errorf(ctx, "error wiping status: %v", err)
|
||||
}
|
||||
|
||||
@@ -641,3 +650,33 @@ func (p *clientAPI) ReportAccount(ctx context.Context, cMsg messages.FromClientA
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) MoveAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
|
||||
// Redirect each local follower of
|
||||
// OriginAccount to follow move target.
|
||||
p.utilF.redirectFollowers(ctx, cMsg.OriginAccount, cMsg.TargetAccount)
|
||||
|
||||
// At this point, we know OriginAccount has the
|
||||
// Move set on it. Just make sure it's populated.
|
||||
if err := p.state.DB.PopulateMove(ctx, cMsg.OriginAccount.Move); err != nil {
|
||||
return gtserror.Newf("error populating Move: %w", err)
|
||||
}
|
||||
|
||||
// Now send the Move message out to
|
||||
// OriginAccount's (remote) followers.
|
||||
if err := p.federate.MoveAccount(ctx, cMsg.OriginAccount); err != nil {
|
||||
return gtserror.Newf("error federating account move: %w", err)
|
||||
}
|
||||
|
||||
// Mark the move attempt as successful.
|
||||
cMsg.OriginAccount.Move.SucceededAt = cMsg.OriginAccount.Move.AttemptedAt
|
||||
if err := p.state.DB.UpdateMove(
|
||||
ctx,
|
||||
cMsg.OriginAccount.Move,
|
||||
"succeeded_at",
|
||||
); err != nil {
|
||||
return gtserror.Newf("error marking move as successful: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -39,11 +39,11 @@ import (
|
||||
// specifically for messages originating
|
||||
// from the federation/ActivityPub API.
|
||||
type fediAPI struct {
|
||||
state *state.State
|
||||
surface *surface
|
||||
federate *federate
|
||||
wipeStatus wipeStatus
|
||||
account *account.Processor
|
||||
state *state.State
|
||||
surface *surface
|
||||
federate *federate
|
||||
account *account.Processor
|
||||
utilF *utilF
|
||||
}
|
||||
|
||||
func (p *Processor) EnqueueFediAPI(cctx context.Context, msgs ...messages.FromFediAPI) {
|
||||
@@ -563,7 +563,7 @@ func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) e
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
|
||||
}
|
||||
|
||||
if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil {
|
||||
if err := p.utilF.wipeStatus(ctx, status, deleteAttachments); err != nil {
|
||||
log.Errorf(ctx, "error wiping status: %v", err)
|
||||
}
|
||||
|
||||
|
@@ -22,7 +22,6 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
@@ -380,7 +379,7 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er
|
||||
|
||||
// Transfer originAcct's followers
|
||||
// on this instance to targetAcct.
|
||||
redirectOK := p.RedirectAccountFollowers(
|
||||
redirectOK := p.utilF.redirectFollowers(
|
||||
ctx,
|
||||
originAcct,
|
||||
targetAcct,
|
||||
@@ -422,98 +421,6 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// RedirectAccountFollowers redirects all local
|
||||
// followers of originAcct to targetAcct.
|
||||
//
|
||||
// Both accounts must be fully dereferenced
|
||||
// already, and the Move must be valid.
|
||||
//
|
||||
// Callers to this function MUST have obtained
|
||||
// a lock already by calling FedLocks.Lock.
|
||||
//
|
||||
// Return bool will be true if all goes OK.
|
||||
func (p *fediAPI) RedirectAccountFollowers(
|
||||
ctx context.Context,
|
||||
originAcct *gtsmodel.Account,
|
||||
targetAcct *gtsmodel.Account,
|
||||
) bool {
|
||||
// Any local followers of originAcct should
|
||||
// send follow requests to targetAcct instead,
|
||||
// and have followers of originAcct removed.
|
||||
//
|
||||
// Select local followers with barebones, since
|
||||
// we only need follow.Account and we can get
|
||||
// that ourselves.
|
||||
followers, err := p.state.DB.GetAccountLocalFollowers(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
originAcct.ID,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
log.Errorf(ctx,
|
||||
"db error getting follows targeting originAcct: %v",
|
||||
err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
for _, follow := range followers {
|
||||
// Fetch the local account that
|
||||
// owns the follow targeting originAcct.
|
||||
if follow.Account, err = p.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
follow.AccountID,
|
||||
); err != nil {
|
||||
log.Errorf(ctx,
|
||||
"db error getting follow account %s: %v",
|
||||
follow.AccountID, err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Use the account processor FollowCreate
|
||||
// function to send off the new follow,
|
||||
// carrying over the Reblogs and Notify
|
||||
// values from the old follow to the new.
|
||||
//
|
||||
// This will also handle cases where our
|
||||
// account has already followed the target
|
||||
// account, by just updating the existing
|
||||
// follow of target account.
|
||||
if _, err := p.account.FollowCreate(
|
||||
ctx,
|
||||
follow.Account,
|
||||
&apimodel.AccountFollowRequest{
|
||||
ID: targetAcct.ID,
|
||||
Reblogs: follow.ShowReblogs,
|
||||
Notify: follow.Notify,
|
||||
},
|
||||
); err != nil {
|
||||
log.Errorf(ctx,
|
||||
"error creating new follow for account %s: %v",
|
||||
follow.AccountID, err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// New follow is in the process of
|
||||
// sending, remove the existing follow.
|
||||
// This will send out an Undo Activity for each Follow.
|
||||
if _, err := p.account.FollowRemove(
|
||||
ctx,
|
||||
follow.Account,
|
||||
follow.TargetAccountID,
|
||||
); err != nil {
|
||||
log.Errorf(ctx,
|
||||
"error removing old follow for account %s: %v",
|
||||
follow.AccountID, err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// RemoveAccountFollowing removes all
|
||||
// follows owned by the move originAcct.
|
||||
//
|
||||
|
240
internal/processing/workers/util.go
Normal file
240
internal/processing/workers/util.go
Normal file
@@ -0,0 +1,240 @@
|
||||
// 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 workers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
)
|
||||
|
||||
// utilF wraps util functions used by both
|
||||
// the fromClientAPI and fromFediAPI functions.
|
||||
type utilF struct {
|
||||
state *state.State
|
||||
media *media.Processor
|
||||
account *account.Processor
|
||||
surface *surface
|
||||
}
|
||||
|
||||
// wipeStatus encapsulates common logic
|
||||
// used to totally delete a status + all
|
||||
// its attachments, notifications, boosts,
|
||||
// and timeline entries.
|
||||
func (u *utilF) wipeStatus(
|
||||
ctx context.Context,
|
||||
statusToDelete *gtsmodel.Status,
|
||||
deleteAttachments bool,
|
||||
) error {
|
||||
var errs gtserror.MultiError
|
||||
|
||||
// Either delete all attachments for this status,
|
||||
// or simply unattach + clean them separately later.
|
||||
//
|
||||
// Reason to unattach rather than delete is that
|
||||
// the poster might want to reattach them to another
|
||||
// status immediately (in case of delete + redraft)
|
||||
if deleteAttachments {
|
||||
// todo:u.state.DB.DeleteAttachmentsForStatus
|
||||
for _, id := range statusToDelete.AttachmentIDs {
|
||||
if err := u.media.Delete(ctx, id); err != nil {
|
||||
errs.Appendf("error deleting media: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// todo:u.state.DB.UnattachAttachmentsForStatus
|
||||
for _, id := range statusToDelete.AttachmentIDs {
|
||||
if _, err := u.media.Unattach(ctx, statusToDelete.Account, id); err != nil {
|
||||
errs.Appendf("error unattaching media: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete all mention entries generated by this status
|
||||
// todo:u.state.DB.DeleteMentionsForStatus
|
||||
for _, id := range statusToDelete.MentionIDs {
|
||||
if err := u.state.DB.DeleteMentionByID(ctx, id); err != nil {
|
||||
errs.Appendf("error deleting status mention: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// delete all notification entries generated by this status
|
||||
if err := u.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status notifications: %w", err)
|
||||
}
|
||||
|
||||
// delete all bookmarks that point to this status
|
||||
if err := u.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status bookmarks: %w", err)
|
||||
}
|
||||
|
||||
// delete all faves of this status
|
||||
if err := u.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status faves: %w", err)
|
||||
}
|
||||
|
||||
if pollID := statusToDelete.PollID; pollID != "" {
|
||||
// Delete this poll by ID from the database.
|
||||
if err := u.state.DB.DeletePollByID(ctx, pollID); err != nil {
|
||||
errs.Appendf("error deleting status poll: %w", err)
|
||||
}
|
||||
|
||||
// Delete any poll votes pointing to this poll ID.
|
||||
if err := u.state.DB.DeletePollVotes(ctx, pollID); err != nil {
|
||||
errs.Appendf("error deleting status poll votes: %w", err)
|
||||
}
|
||||
|
||||
// Cancel any scheduled expiry task for poll.
|
||||
_ = u.state.Workers.Scheduler.Cancel(pollID)
|
||||
}
|
||||
|
||||
// delete all boosts for this status + remove them from timelines
|
||||
boosts, err := u.state.DB.GetStatusBoosts(
|
||||
// we MUST set a barebones context here,
|
||||
// as depending on where it came from the
|
||||
// original BoostOf may already be gone.
|
||||
gtscontext.SetBarebones(ctx),
|
||||
statusToDelete.ID)
|
||||
if err != nil {
|
||||
errs.Appendf("error fetching status boosts: %w", err)
|
||||
}
|
||||
|
||||
for _, boost := range boosts {
|
||||
if err := u.surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil {
|
||||
errs.Appendf("error deleting boost from timelines: %w", err)
|
||||
}
|
||||
if err := u.state.DB.DeleteStatusByID(ctx, boost.ID); err != nil {
|
||||
errs.Appendf("error deleting boost: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// delete this status from any and all timelines
|
||||
if err := u.surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status from timelines: %w", err)
|
||||
}
|
||||
|
||||
// finally, delete the status itself
|
||||
if err := u.state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status: %w", err)
|
||||
}
|
||||
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
// redirectFollowers redirects all local
|
||||
// followers of originAcct to targetAcct.
|
||||
//
|
||||
// Both accounts must be fully dereferenced
|
||||
// already, and the Move must be valid.
|
||||
//
|
||||
// Return bool will be true if all goes OK.
|
||||
func (u *utilF) redirectFollowers(
|
||||
ctx context.Context,
|
||||
originAcct *gtsmodel.Account,
|
||||
targetAcct *gtsmodel.Account,
|
||||
) bool {
|
||||
// Any local followers of originAcct should
|
||||
// send follow requests to targetAcct instead,
|
||||
// and have followers of originAcct removed.
|
||||
//
|
||||
// Select local followers with barebones, since
|
||||
// we only need follow.Account and we can get
|
||||
// that ourselves.
|
||||
followers, err := u.state.DB.GetAccountLocalFollowers(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
originAcct.ID,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
log.Errorf(ctx,
|
||||
"db error getting follows targeting originAcct: %v",
|
||||
err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
for _, follow := range followers {
|
||||
// Fetch the local account that
|
||||
// owns the follow targeting originAcct.
|
||||
if follow.Account, err = u.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
follow.AccountID,
|
||||
); err != nil {
|
||||
log.Errorf(ctx,
|
||||
"db error getting follow account %s: %v",
|
||||
follow.AccountID, err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Use the account processor FollowCreate
|
||||
// function to send off the new follow,
|
||||
// carrying over the Reblogs and Notify
|
||||
// values from the old follow to the new.
|
||||
//
|
||||
// This will also handle cases where our
|
||||
// account has already followed the target
|
||||
// account, by just updating the existing
|
||||
// follow of target account.
|
||||
//
|
||||
// Also, ensure new follow wouldn't be a
|
||||
// self follow, since that will error.
|
||||
if follow.AccountID != targetAcct.ID {
|
||||
if _, err := u.account.FollowCreate(
|
||||
ctx,
|
||||
follow.Account,
|
||||
&apimodel.AccountFollowRequest{
|
||||
ID: targetAcct.ID,
|
||||
Reblogs: follow.ShowReblogs,
|
||||
Notify: follow.Notify,
|
||||
},
|
||||
); err != nil {
|
||||
log.Errorf(ctx,
|
||||
"error creating new follow for account %s: %v",
|
||||
follow.AccountID, err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// New follow is in the process of
|
||||
// sending, remove the existing follow.
|
||||
// This will send out an Undo Activity for each Follow.
|
||||
if _, err := u.account.FollowRemove(
|
||||
ctx,
|
||||
follow.Account,
|
||||
follow.TargetAccountID,
|
||||
); err != nil {
|
||||
log.Errorf(ctx,
|
||||
"error removing old follow for account %s: %v",
|
||||
follow.AccountID, err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
@@ -1,135 +0,0 @@
|
||||
// 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 workers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
)
|
||||
|
||||
// wipeStatus encapsulates common logic used to totally delete a status
|
||||
// + all its attachments, notifications, boosts, and timeline entries.
|
||||
type wipeStatus func(context.Context, *gtsmodel.Status, bool) error
|
||||
|
||||
// wipeStatusF returns a wipeStatus util function.
|
||||
func wipeStatusF(state *state.State, media *media.Processor, surface *surface) wipeStatus {
|
||||
return func(
|
||||
ctx context.Context,
|
||||
statusToDelete *gtsmodel.Status,
|
||||
deleteAttachments bool,
|
||||
) error {
|
||||
var errs gtserror.MultiError
|
||||
|
||||
// Either delete all attachments for this status,
|
||||
// or simply unattach + clean them separately later.
|
||||
//
|
||||
// Reason to unattach rather than delete is that
|
||||
// the poster might want to reattach them to another
|
||||
// status immediately (in case of delete + redraft)
|
||||
if deleteAttachments {
|
||||
// todo:state.DB.DeleteAttachmentsForStatus
|
||||
for _, id := range statusToDelete.AttachmentIDs {
|
||||
if err := media.Delete(ctx, id); err != nil {
|
||||
errs.Appendf("error deleting media: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// todo:state.DB.UnattachAttachmentsForStatus
|
||||
for _, id := range statusToDelete.AttachmentIDs {
|
||||
if _, err := media.Unattach(ctx, statusToDelete.Account, id); err != nil {
|
||||
errs.Appendf("error unattaching media: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete all mention entries generated by this status
|
||||
// todo:state.DB.DeleteMentionsForStatus
|
||||
for _, id := range statusToDelete.MentionIDs {
|
||||
if err := state.DB.DeleteMentionByID(ctx, id); err != nil {
|
||||
errs.Appendf("error deleting status mention: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// delete all notification entries generated by this status
|
||||
if err := state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status notifications: %w", err)
|
||||
}
|
||||
|
||||
// delete all bookmarks that point to this status
|
||||
if err := state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status bookmarks: %w", err)
|
||||
}
|
||||
|
||||
// delete all faves of this status
|
||||
if err := state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status faves: %w", err)
|
||||
}
|
||||
|
||||
if pollID := statusToDelete.PollID; pollID != "" {
|
||||
// Delete this poll by ID from the database.
|
||||
if err := state.DB.DeletePollByID(ctx, pollID); err != nil {
|
||||
errs.Appendf("error deleting status poll: %w", err)
|
||||
}
|
||||
|
||||
// Delete any poll votes pointing to this poll ID.
|
||||
if err := state.DB.DeletePollVotes(ctx, pollID); err != nil {
|
||||
errs.Appendf("error deleting status poll votes: %w", err)
|
||||
}
|
||||
|
||||
// Cancel any scheduled expiry task for poll.
|
||||
_ = state.Workers.Scheduler.Cancel(pollID)
|
||||
}
|
||||
|
||||
// delete all boosts for this status + remove them from timelines
|
||||
boosts, err := state.DB.GetStatusBoosts(
|
||||
// we MUST set a barebones context here,
|
||||
// as depending on where it came from the
|
||||
// original BoostOf may already be gone.
|
||||
gtscontext.SetBarebones(ctx),
|
||||
statusToDelete.ID)
|
||||
if err != nil {
|
||||
errs.Appendf("error fetching status boosts: %w", err)
|
||||
}
|
||||
|
||||
for _, boost := range boosts {
|
||||
if err := surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil {
|
||||
errs.Appendf("error deleting boost from timelines: %w", err)
|
||||
}
|
||||
if err := state.DB.DeleteStatusByID(ctx, boost.ID); err != nil {
|
||||
errs.Appendf("error deleting boost: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// delete this status from any and all timelines
|
||||
if err := surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status from timelines: %w", err)
|
||||
}
|
||||
|
||||
// finally, delete the status itself
|
||||
if err := state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil {
|
||||
errs.Appendf("error deleting status: %w", err)
|
||||
}
|
||||
|
||||
return errs.Combine()
|
||||
}
|
||||
}
|
@@ -63,30 +63,30 @@ func New(
|
||||
converter: converter,
|
||||
}
|
||||
|
||||
// Init shared logic wipe
|
||||
// status util func.
|
||||
wipeStatus := wipeStatusF(
|
||||
state,
|
||||
media,
|
||||
surface,
|
||||
)
|
||||
// Init shared util funcs.
|
||||
utilF := &utilF{
|
||||
state: state,
|
||||
media: media,
|
||||
account: account,
|
||||
surface: surface,
|
||||
}
|
||||
|
||||
return Processor{
|
||||
workers: &state.Workers,
|
||||
clientAPI: &clientAPI{
|
||||
state: state,
|
||||
converter: converter,
|
||||
surface: surface,
|
||||
federate: federate,
|
||||
wipeStatus: wipeStatus,
|
||||
account: account,
|
||||
state: state,
|
||||
converter: converter,
|
||||
surface: surface,
|
||||
federate: federate,
|
||||
account: account,
|
||||
utilF: utilF,
|
||||
},
|
||||
fediAPI: &fediAPI{
|
||||
state: state,
|
||||
surface: surface,
|
||||
federate: federate,
|
||||
wipeStatus: wipeStatus,
|
||||
account: account,
|
||||
state: state,
|
||||
surface: surface,
|
||||
federate: federate,
|
||||
account: account,
|
||||
utilF: utilF,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user