diff options
Diffstat (limited to 'internal/db/bundb')
| -rw-r--r-- | internal/db/bundb/account.go | 11 | ||||
| -rw-r--r-- | internal/db/bundb/bundb.go | 5 | ||||
| -rw-r--r-- | internal/db/bundb/migrations/20240129170725_moved_to_also_known_as.go | 61 | ||||
| -rw-r--r-- | internal/db/bundb/move.go | 236 | ||||
| -rw-r--r-- | internal/db/bundb/move_test.go | 168 | 
5 files changed, 481 insertions, 0 deletions
| diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index d2c9c2f51..4d078e68d 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -304,6 +304,17 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou  		account.AlsoKnownAs = alsoKnownAs  	} +	if account.Move == nil && account.MoveID != "" { +		// Account move is not set, fetch from database. +		account.Move, err = a.state.DB.GetMoveByID( +			ctx, +			account.MovedToURI, +		) +		if err != nil { +			errs.Appendf("error populating move: %w", err) +		} +	} +  	if account.MovedTo == nil && account.MovedToURI != "" {  		// Account movedTo is not set, fetch from database.  		account.MovedTo, err = a.state.DB.GetAccountByURI( diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go index c49da272b..a07cd6142 100644 --- a/internal/db/bundb/bundb.go +++ b/internal/db/bundb/bundb.go @@ -67,6 +67,7 @@ type DBService struct {  	db.Marker  	db.Media  	db.Mention +	db.Move  	db.Notification  	db.Poll  	db.Relationship @@ -221,6 +222,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {  			db:    db,  			state: state,  		}, +		Move: &moveDB{ +			db:    db, +			state: state, +		},  		Notification: ¬ificationDB{  			db:    db,  			state: state, diff --git a/internal/db/bundb/migrations/20240129170725_moved_to_also_known_as.go b/internal/db/bundb/migrations/20240129170725_moved_to_also_known_as.go new file mode 100644 index 000000000..9a2cabdfc --- /dev/null +++ b/internal/db/bundb/migrations/20240129170725_moved_to_also_known_as.go @@ -0,0 +1,61 @@ +// 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 migrations + +import ( +	"context" +	"strings" + +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/uptrace/bun" +) + +func init() { +	up := func(ctx context.Context, db *bun.DB) error { +		_, err := db.ExecContext(ctx, +			"ALTER TABLE ? ADD COLUMN ? CHAR(26)", +			bun.Ident("accounts"), bun.Ident("move_id"), +		) +		if err != nil { +			e := err.Error() +			if !(strings.Contains(e, "already exists") || +				strings.Contains(e, "duplicate column name") || +				strings.Contains(e, "SQLSTATE 42701")) { +				return err +			} +		} + +		// Create "moves" table. +		if _, err := db.NewCreateTable(). +			IfNotExists(). +			Model(>smodel.Move{}). +			Exec(ctx); err != nil { +			return err +		} + +		return nil +	} + +	down := func(ctx context.Context, db *bun.DB) error { +		return nil +	} + +	if err := Migrations.Register(up, down); err != nil { +		panic(err) +	} +} diff --git a/internal/db/bundb/move.go b/internal/db/bundb/move.go new file mode 100644 index 000000000..a66b9dea5 --- /dev/null +++ b/internal/db/bundb/move.go @@ -0,0 +1,236 @@ +// 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 bundb + +import ( +	"context" +	"errors" +	"fmt" +	"net/url" +	"time" + +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/state" +	"github.com/uptrace/bun" +) + +type moveDB struct { +	db    *bun.DB +	state *state.State +} + +func (m *moveDB) GetMoveByID( +	ctx context.Context, +	id string, +) (*gtsmodel.Move, error) { +	return m.getMove( +		ctx, +		"ID", +		func(move *gtsmodel.Move) error { +			return m.db. +				NewSelect(). +				Model(move). +				Where("? = ?", bun.Ident("move.id"), id). +				Scan(ctx) +		}, +		id, +	) +} + +func (m *moveDB) GetMoveByURI( +	ctx context.Context, +	uri string, +) (*gtsmodel.Move, error) { +	return m.getMove( +		ctx, +		"URI", +		func(move *gtsmodel.Move) error { +			return m.db. +				NewSelect(). +				Model(move). +				Where("? = ?", bun.Ident("move.uri"), uri). +				Scan(ctx) +		}, +		uri, +	) +} + +func (m *moveDB) GetMoveByOriginTarget( +	ctx context.Context, +	originURI string, +	targetURI string, +) (*gtsmodel.Move, error) { +	return m.getMove( +		ctx, +		"OriginURI,TargetURI", +		func(move *gtsmodel.Move) error { +			return m.db. +				NewSelect(). +				Model(move). +				Where("? = ?", bun.Ident("move.origin_uri"), originURI). +				Where("? = ?", bun.Ident("move.target_uri"), targetURI). +				Scan(ctx) +		}, +		originURI, targetURI, +	) +} + +func (m *moveDB) GetLatestMoveSuccessInvolvingURIs( +	ctx context.Context, +	uri1 string, +	uri2 string, +) (time.Time, error) { +	// Get at most 1 latest Move +	// involving the provided URIs. +	var moves []*gtsmodel.Move +	err := m.db. +		NewSelect(). +		Model(&moves). +		Column("succeeded_at"). +		Where("? = ?", bun.Ident("move.origin_uri"), uri1). +		WhereOr("? = ?", bun.Ident("move.origin_uri"), uri2). +		WhereOr("? = ?", bun.Ident("move.target_uri"), uri1). +		WhereOr("? = ?", bun.Ident("move.target_uri"), uri2). +		Order("id DESC"). +		Limit(1). +		Scan(ctx) +	if err != nil && !errors.Is(err, db.ErrNoEntries) { +		return time.Time{}, err +	} + +	if len(moves) != 1 { +		return time.Time{}, nil +	} + +	return moves[0].SucceededAt, nil +} + +func (m *moveDB) GetLatestMoveAttemptInvolvingURIs( +	ctx context.Context, +	uri1 string, +	uri2 string, +) (time.Time, error) { +	// Get at most 1 latest Move +	// involving the provided URIs. +	var moves []*gtsmodel.Move +	err := m.db. +		NewSelect(). +		Model(&moves). +		Column("attempted_at"). +		Where("? = ?", bun.Ident("move.origin_uri"), uri1). +		WhereOr("? = ?", bun.Ident("move.origin_uri"), uri2). +		WhereOr("? = ?", bun.Ident("move.target_uri"), uri1). +		WhereOr("? = ?", bun.Ident("move.target_uri"), uri2). +		Order("id DESC"). +		Limit(1). +		Scan(ctx) +	if err != nil && !errors.Is(err, db.ErrNoEntries) { +		return time.Time{}, err +	} + +	if len(moves) != 1 { +		return time.Time{}, nil +	} + +	return moves[0].AttemptedAt, nil +} + +func (m *moveDB) getMove( +	ctx context.Context, +	lookup string, +	dbQuery func(*gtsmodel.Move) error, +	keyParts ...any, +) (*gtsmodel.Move, error) { +	move, err := m.state.Caches.GTS.Move.LoadOne(lookup, func() (*gtsmodel.Move, error) { +		var move gtsmodel.Move + +		// Not cached! Perform database query. +		if err := dbQuery(&move); err != nil { +			return nil, err +		} + +		return &move, nil +	}, keyParts...) +	if err != nil { +		return nil, err +	} + +	if gtscontext.Barebones(ctx) { +		return move, nil +	} + +	// Populate the Move by parsing out the URIs. +	if move.Origin == nil { +		move.Origin, err = url.Parse(move.OriginURI) +		if err != nil { +			return nil, fmt.Errorf("error parsing Move originURI: %w", err) +		} +	} + +	if move.Target == nil { +		move.Target, err = url.Parse(move.TargetURI) +		if err != nil { +			return nil, fmt.Errorf("error parsing Move originURI: %w", err) +		} +	} + +	return move, nil +} + +func (m *moveDB) PutMove(ctx context.Context, move *gtsmodel.Move) error { +	return m.state.Caches.GTS.Move.Store(move, func() error { +		_, err := m.db. +			NewInsert(). +			Model(move). +			Exec(ctx) +		return err +	}) +} + +func (m *moveDB) UpdateMove(ctx context.Context, move *gtsmodel.Move, columns ...string) error { +	move.UpdatedAt = time.Now() +	if len(columns) > 0 { +		// If we're updating by column, +		// ensure "updated_at" is included. +		columns = append(columns, "updated_at") +	} + +	return m.state.Caches.GTS.Move.Store(move, func() error { +		_, err := m.db. +			NewUpdate(). +			Model(move). +			Column(columns...). +			Where("? = ?", bun.Ident("move.id"), move.ID). +			Exec(ctx) +		return err +	}) +} + +func (m *moveDB) DeleteMoveByID(ctx context.Context, id string) error { +	defer m.state.Caches.GTS.Move.Invalidate("ID", id) + +	_, err := m.db. +		NewDelete(). +		TableExpr("? AS ?", bun.Ident("moves"), bun.Ident("move")). +		Where("? = ?", bun.Ident("move.id"), id). +		Exec(ctx) + +	return err +} diff --git a/internal/db/bundb/move_test.go b/internal/db/bundb/move_test.go new file mode 100644 index 000000000..1e1a0613f --- /dev/null +++ b/internal/db/bundb/move_test.go @@ -0,0 +1,168 @@ +// 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 bundb_test + +import ( +	"context" +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type MoveTestSuite struct { +	BunDBStandardTestSuite +} + +func (suite *MoveTestSuite) TestMoveIntegration() { +	ctx := context.Background() +	firstMove := >smodel.Move{ +		ID:        "01HPPN38MZYEC6WBTR21J6241N", +		OriginURI: "https://example.org/users/my_old_account", +		TargetURI: "https://somewhere.else.net/users/my_new_account", +		URI:       "https://example.org/users/my_old_account/activities/Move/652e8361-0182-407d-8b01-4447e7fd10c0", +	} + +	// Put the move. +	if err := suite.state.DB.PutMove(ctx, firstMove); err != nil { +		suite.FailNow(err.Error()) +	} + +	// Test various ways of retrieving the Move. +	if _, err := suite.state.DB.GetMoveByID(ctx, firstMove.ID); err != nil { +		suite.FailNow(err.Error()) +	} + +	if _, err := suite.state.DB.GetMoveByOriginTarget(ctx, firstMove.OriginURI, firstMove.TargetURI); err != nil { +		suite.FailNow(err.Error()) +	} + +	// Keep the last one, and check fields set on it. +	dbMove, err := suite.state.DB.GetMoveByURI(ctx, firstMove.URI) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	// Created/Updated should be set when +	// it's first inserted into the db. +	suite.NotZero(dbMove.CreatedAt) +	suite.NotZero(dbMove.UpdatedAt) + +	// URIs should be parsed and set +	// on the move on population. +	suite.NotNil(dbMove.Origin) +	suite.NotNil(dbMove.Target) + +	// These should not be set as +	// they have no default values. +	suite.Zero(dbMove.AttemptedAt) +	suite.Zero(dbMove.SucceededAt) + +	// Update the Move to emulate +	// us succeeding in processing it. +	dbMove.AttemptedAt = time.Now() +	dbMove.SucceededAt = dbMove.AttemptedAt +	if err := suite.state.DB.UpdateMove( +		ctx, +		dbMove, +		"attempted_at", +		"succeeded_at", +	); err != nil { +		suite.FailNow(err.Error()) +	} + +	// Store dbMove as firstMove var. +	firstMove = dbMove + +	// Store another Move involving one +	// of the original URIs, and mark +	// this one as succeeded. Use a time +	// a few seconds into the future to +	// make sure it's differentiated +	// from the first move. +	secondMove := >smodel.Move{ +		ID:          "01HPPPNQWRMQTXRFEPKDV3A4W7", +		OriginURI:   "https://somewhere.else.net/users/my_new_account", +		TargetURI:   "http://localhost:8080/users/the_mighty_zork", +		URI:         "https://somewhere.else.net/activities/01HPPPPPC089VJGV0967P5YQS5", +		AttemptedAt: time.Now().Add(5 * time.Second), +		SucceededAt: time.Now().Add(5 * time.Second), +	} +	if err := suite.state.DB.PutMove(ctx, secondMove); err != nil { +		suite.FailNow(err.Error()) +	} + +	// Test getting succeeded using the +	// URI shared between the two Moves, +	// and some random account. +	ts, err := suite.state.DB.GetLatestMoveSuccessInvolvingURIs( +		ctx, +		secondMove.OriginURI, +		"https://a.secret.third.place/users/mystery_meat", +	) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	// Time should be equivalent to secondMove. +	suite.EqualValues(secondMove.SucceededAt.UnixMilli(), ts.UnixMilli()) + +	// Test getting succeeded using +	// both URIs from the first move. +	ts, err = suite.state.DB.GetLatestMoveSuccessInvolvingURIs( +		ctx, +		firstMove.OriginURI, +		firstMove.TargetURI, +	) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	// Time should be equivalent to secondMove. +	suite.EqualValues(secondMove.SucceededAt.UnixMilli(), ts.UnixMilli()) + +	// Test getting succeeded using +	// URI from the first Move, and +	// some random account. +	ts, err = suite.state.DB.GetLatestMoveSuccessInvolvingURIs( +		ctx, +		firstMove.OriginURI, +		"https://a.secret.third.place/users/mystery_meat", +	) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	// Time should be equivalent to firstMove. +	suite.EqualValues(firstMove.SucceededAt.UnixMilli(), ts.UnixMilli()) + +	// Delete the first Move. +	if err := suite.state.DB.DeleteMoveByID(ctx, firstMove.ID); err != nil { +		suite.FailNow(err.Error()) +	} + +	// Ensure first Move deleted. +	_, err = suite.state.DB.GetMoveByID(ctx, firstMove.ID) +	suite.ErrorIs(err, db.ErrNoEntries) +} + +func TestMoveTestSuite(t *testing.T) { +	suite.Run(t, new(MoveTestSuite)) +} | 
