optimistic updates for account state

This commit is contained in:
tobi 2024-04-13 13:18:02 +02:00
parent c10248be6d
commit 36d9b2885d
5 changed files with 84 additions and 43 deletions

View file

@ -28,7 +28,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *Processor) AccountApprove(
@ -47,14 +46,13 @@ func (p *Processor) AccountApprove(
return nil, gtserror.NewErrorNotFound(err, err.Error())
}
if !*user.Approved {
// Mark user as approved.
user.Approved = util.Ptr(true)
if err := p.state.DB.UpdateUser(ctx, user, "approved"); err != nil {
err := gtserror.Newf("db error updating user %s: %w", user.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Get a lock on the account URI,
// to ensure it's not also being
// rejected at the same time!
unlock := p.state.ClientLocks.Lock(user.Account.URI)
defer unlock()
if !*user.Approved {
// Process approval side effects asynschronously.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActorPerson,
@ -71,5 +69,11 @@ func (p *Processor) AccountApprove(
return nil, gtserror.NewErrorInternalError(err)
}
// Optimistically set approved to true and
// clear sign-up IP to reflect state that
// will be produced by side effects.
apiAccount.Approved = true
apiAccount.IP = nil
return apiAccount, nil
}

View file

@ -23,6 +23,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type AdminApproveTestSuite struct {
@ -53,13 +54,20 @@ func (suite *AdminApproveTestSuite) TestApprove() {
// Account should be approved.
suite.NotNil(acct)
suite.True(acct.Approved)
suite.Nil(acct.IP)
// Check DB entry too.
dbUser, err := suite.state.DB.GetUserByID(ctx, targetUser.ID)
if err != nil {
suite.FailNow(err.Error())
// Wait for processor to
// handle side effects.
var (
dbUser *gtsmodel.User
err error
)
if !testrig.WaitFor(func() bool {
dbUser, err = suite.state.DB.GetUserByID(ctx, targetUser.ID)
return err == nil && dbUser != nil && *dbUser.Approved
}) {
suite.FailNow("waiting for approved user")
}
suite.True(*dbUser.Approved)
}
func TestAdminApproveTestSuite(t *testing.T) {

View file

@ -71,18 +71,12 @@ func (p *Processor) AccountReject(
return nil, gtserror.NewErrorInternalError(err)
}
// Remove the account.
if err := p.state.DB.DeleteAccount(ctx, accountID); err != nil {
err := gtserror.Newf("db error deleting account %s: %w", accountID, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Remove the user.
if err := p.state.DB.DeleteUserByID(ctx, user.ID); err != nil {
err := gtserror.Newf("db error deleting user %s: %w", user.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Set approved to false on the API model, to
// reflect the changes that will occur
// asynchronously in the processor.
apiAccount.Approved = false
// Ensure we an email address.
var email string
if user.Email != "" {
email = user.Email
@ -90,7 +84,8 @@ func (p *Processor) AccountReject(
email = user.UnconfirmedEmail
}
// Create and store a denied user entry.
// Create a denied user entry for
// the worker to process + store.
deniedUser := &gtsmodel.DeniedUser{
ID: user.ID,
Email: email,
@ -105,11 +100,6 @@ func (p *Processor) AccountReject(
Message: message,
}
if err := p.state.DB.PutDeniedUser(ctx, deniedUser); err != nil {
err := gtserror.Newf("db error putting denied user %s: %w", deniedUser.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Process rejection side effects asynschronously.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ActorPerson,

View file

@ -23,6 +23,8 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type AdminRejectTestSuite struct {
@ -51,13 +53,20 @@ func (suite *AdminRejectTestSuite) TestReject() {
if errWithCode != nil {
suite.FailNow(errWithCode.Error())
}
suite.NotNil(acct)
suite.False(acct.Approved)
// Should be a denied user entry now.
deniedUser, err := suite.state.DB.GetDeniedUserByID(ctx, targetUser.ID)
if err != nil {
suite.FailNow(err.Error())
// Wait for processor to
// handle side effects.
var (
deniedUser *gtsmodel.DeniedUser
err error
)
if !testrig.WaitFor(func() bool {
deniedUser, err = suite.state.DB.GetDeniedUserByID(ctx, targetUser.ID)
return deniedUser != nil && err == nil
}) {
suite.FailNow("waiting for denied user")
}
// Ensure fields as expected.

View file

@ -33,6 +33,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// clientAPI wraps processing functions
@ -700,6 +701,15 @@ func (p *clientAPI) AcceptAccount(ctx context.Context, cMsg messages.FromClientA
return gtserror.Newf("%T not parseable as *gtsmodel.User", cMsg.GTSModel)
}
// Mark user as approved + clear sign-up IP.
newUser.Approved = util.Ptr(true)
newUser.SignUpIP = nil
if err := p.state.DB.UpdateUser(ctx, newUser, "approved", "sign_up_ip"); err != nil {
// Error now means we should return without
// sending email + let admin try to approve again.
return gtserror.Newf("db error updating user %s: %w", newUser.ID, err)
}
// Send "your sign-up has been approved" email to the new user.
if err := p.surface.emailUserSignupApproved(ctx, newUser); err != nil {
log.Errorf(ctx, "error emailing: %v", err)
@ -709,20 +719,40 @@ func (p *clientAPI) AcceptAccount(ctx context.Context, cMsg messages.FromClientA
}
func (p *clientAPI) RejectAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
denied, ok := cMsg.GTSModel.(*gtsmodel.DeniedUser)
deniedUser, ok := cMsg.GTSModel.(*gtsmodel.DeniedUser)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.DeniedUser", cMsg.GTSModel)
}
if !*denied.SendEmail {
// No need to send an
// email. Nothing to do.
return nil
// Remove the account.
if err := p.state.DB.DeleteAccount(ctx, cMsg.TargetAccount.ID); err != nil {
log.Errorf(ctx,
"db error deleting account %s: %v",
cMsg.TargetAccount.ID, err,
)
}
// Send "your sign-up has been rejected" email to the denied user.
if err := p.surface.emailUserSignupRejected(ctx, denied); err != nil {
log.Errorf(ctx, "error emailing: %v", err)
// Remove the user.
if err := p.state.DB.DeleteUserByID(ctx, deniedUser.ID); err != nil {
log.Errorf(ctx,
"db error deleting user %s: %v",
deniedUser.ID, err,
)
}
// Store the deniedUser entry.
if err := p.state.DB.PutDeniedUser(ctx, deniedUser); err != nil {
log.Errorf(ctx,
"db error putting denied user %s: %v",
deniedUser.ID, err,
)
}
if *deniedUser.SendEmail {
// Send "your sign-up has been rejected" email to the denied user.
if err := p.surface.emailUserSignupRejected(ctx, deniedUser); err != nil {
log.Errorf(ctx, "error emailing: %v", err)
}
}
return nil