From b22e213e15a7bc64773e626d76305bd860e6301c Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Wed, 6 Mar 2024 11:18:57 +0100
Subject: [PATCH] [feature/chore] Add Move database functions + cache (#2647)
* [feature/chore] Add Move database functions + cache
* add move mem ratio to envparsing.sh
* update comment
---
internal/cache/cache.go | 2 +
internal/cache/db.go | 32 +++
internal/cache/invalidate.go | 4 +
internal/cache/size.go | 13 +
internal/config/config.go | 1 +
internal/config/defaults.go | 1 +
internal/config/helpers.gen.go | 25 ++
internal/db/bundb/account.go | 11 +
internal/db/bundb/bundb.go | 5 +
.../20240129170725_moved_to_also_known_as.go | 61 +++++
internal/db/bundb/move.go | 236 ++++++++++++++++++
internal/db/bundb/move_test.go | 168 +++++++++++++
internal/db/db.go | 1 +
internal/db/move.go | 56 +++++
internal/gtsmodel/account.go | 17 +-
internal/gtsmodel/move.go | 38 +++
test/envparsing.sh | 1 +
17 files changed, 671 insertions(+), 1 deletion(-)
create mode 100644 internal/db/bundb/migrations/20240129170725_moved_to_also_known_as.go
create mode 100644 internal/db/bundb/move.go
create mode 100644 internal/db/bundb/move_test.go
create mode 100644 internal/db/move.go
create mode 100644 internal/gtsmodel/move.go
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index 9b70a565c..e2fe43a1f 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -75,6 +75,7 @@ func (c *Caches) Init() {
c.initMarker()
c.initMedia()
c.initMention()
+ c.initMove()
c.initNotification()
c.initPoll()
c.initPollVote()
@@ -135,6 +136,7 @@ func (c *Caches) Sweep(threshold float64) {
c.GTS.Marker.Trim(threshold)
c.GTS.Media.Trim(threshold)
c.GTS.Mention.Trim(threshold)
+ c.GTS.Move.Trim(threshold)
c.GTS.Notification.Trim(threshold)
c.GTS.Poll.Trim(threshold)
c.GTS.Report.Trim(threshold)
diff --git a/internal/cache/db.go b/internal/cache/db.go
index dc9e385cd..00dfe204a 100644
--- a/internal/cache/db.go
+++ b/internal/cache/db.go
@@ -117,6 +117,9 @@ type GTSCaches struct {
// Mention provides access to the gtsmodel Mention database cache.
Mention structr.Cache[*gtsmodel.Mention]
+ // Move provides access to the gtsmodel Move database cache.
+ Move structr.Cache[*gtsmodel.Move]
+
// Notification provides access to the gtsmodel Notification database cache.
Notification structr.Cache[*gtsmodel.Notification]
@@ -185,6 +188,8 @@ func (c *Caches) initAccount() {
a2.AvatarMediaAttachment = nil
a2.HeaderMediaAttachment = nil
a2.Emojis = nil
+ a2.AlsoKnownAs = nil
+ a2.Move = nil
return a2
}
@@ -816,6 +821,33 @@ func (c *Caches) initMention() {
})
}
+func (c *Caches) initMove() {
+ // Calculate maximum cache size.
+ cap := calculateResultCacheMax(
+ sizeofMove(), // model in-mem size.
+ config.GetCacheMoveMemRatio(),
+ )
+
+ log.Infof(nil, "cache size = %d", cap)
+
+ c.GTS.Move.Init(structr.Config[*gtsmodel.Move]{
+ Indices: []structr.IndexConfig{
+ {Fields: "ID"},
+ {Fields: "URI"},
+ {Fields: "OriginURI,TargetURI"},
+ {Fields: "OriginURI", Multiple: true},
+ {Fields: "TargetURI", Multiple: true},
+ },
+ MaxSize: cap,
+ IgnoreErr: ignoreErrors,
+ CopyValue: func(m1 *gtsmodel.Move) *gtsmodel.Move {
+ m2 := new(gtsmodel.Move)
+ *m2 = *m1
+ return m2
+ },
+ })
+}
+
func (c *Caches) initNotification() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(
diff --git a/internal/cache/invalidate.go b/internal/cache/invalidate.go
index e7dfa9e8a..a7c4a1552 100644
--- a/internal/cache/invalidate.go
+++ b/internal/cache/invalidate.go
@@ -54,6 +54,10 @@ func (c *Caches) OnInvalidateAccount(account *gtsmodel.Account) {
// Invalidate this account's block lists.
c.GTS.BlockIDs.Invalidate(account.ID)
+
+ // Invalidate this account's Move(s).
+ c.GTS.Move.Invalidate("OriginURI", account.URI)
+ c.GTS.Move.Invalidate("TargetURI", account.URI)
}
func (c *Caches) OnInvalidateBlock(block *gtsmodel.Block) {
diff --git a/internal/cache/size.go b/internal/cache/size.go
index f9d88491d..b1c431c55 100644
--- a/internal/cache/size.go
+++ b/internal/cache/size.go
@@ -460,6 +460,19 @@ func sizeofMention() uintptr {
}))
}
+func sizeofMove() uintptr {
+ return uintptr(size.Of(>smodel.Move{
+ ID: exampleID,
+ CreatedAt: exampleTime,
+ UpdatedAt: exampleTime,
+ AttemptedAt: exampleTime,
+ SucceededAt: exampleTime,
+ OriginURI: exampleURI,
+ TargetURI: exampleURI,
+ URI: exampleURI,
+ }))
+}
+
func sizeofNotification() uintptr {
return uintptr(size.Of(>smodel.Notification{
ID: exampleID,
diff --git a/internal/config/config.go b/internal/config/config.go
index ea84a4af7..f4ea64f93 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -215,6 +215,7 @@ type CacheConfiguration struct {
MarkerMemRatio float64 `name:"marker-mem-ratio"`
MediaMemRatio float64 `name:"media-mem-ratio"`
MentionMemRatio float64 `name:"mention-mem-ratio"`
+ MoveMemRatio float64 `name:"move-mem-ratio"`
NotificationMemRatio float64 `name:"notification-mem-ratio"`
PollMemRatio float64 `name:"poll-mem-ratio"`
PollVoteMemRatio float64 `name:"poll-vote-mem-ratio"`
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index c98b54b0b..6ca508d5a 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -179,6 +179,7 @@ var Defaults = Configuration{
MarkerMemRatio: 0.5,
MediaMemRatio: 4,
MentionMemRatio: 2,
+ MoveMemRatio: 0.1,
NotificationMemRatio: 2,
PollMemRatio: 1,
PollVoteMemRatio: 2,
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index c5d4c992b..5f65a6e28 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -3325,6 +3325,31 @@ func GetCacheMentionMemRatio() float64 { return global.GetCacheMentionMemRatio()
// SetCacheMentionMemRatio safely sets the value for global configuration 'Cache.MentionMemRatio' field
func SetCacheMentionMemRatio(v float64) { global.SetCacheMentionMemRatio(v) }
+// GetCacheMoveMemRatio safely fetches the Configuration value for state's 'Cache.MoveMemRatio' field
+func (st *ConfigState) GetCacheMoveMemRatio() (v float64) {
+ st.mutex.RLock()
+ v = st.config.Cache.MoveMemRatio
+ st.mutex.RUnlock()
+ return
+}
+
+// SetCacheMoveMemRatio safely sets the Configuration value for state's 'Cache.MoveMemRatio' field
+func (st *ConfigState) SetCacheMoveMemRatio(v float64) {
+ st.mutex.Lock()
+ defer st.mutex.Unlock()
+ st.config.Cache.MoveMemRatio = v
+ st.reloadToViper()
+}
+
+// CacheMoveMemRatioFlag returns the flag name for the 'Cache.MoveMemRatio' field
+func CacheMoveMemRatioFlag() string { return "cache-move-mem-ratio" }
+
+// GetCacheMoveMemRatio safely fetches the value for global configuration 'Cache.MoveMemRatio' field
+func GetCacheMoveMemRatio() float64 { return global.GetCacheMoveMemRatio() }
+
+// SetCacheMoveMemRatio safely sets the value for global configuration 'Cache.MoveMemRatio' field
+func SetCacheMoveMemRatio(v float64) { global.SetCacheMoveMemRatio(v) }
+
// GetCacheNotificationMemRatio safely fetches the Configuration value for state's 'Cache.NotificationMemRatio' field
func (st *ConfigState) GetCacheNotificationMemRatio() (v float64) {
st.mutex.RLock()
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 .
+
+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 .
+
+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 .
+
+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))
+}
diff --git a/internal/db/db.go b/internal/db/db.go
index f23324777..330766306 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -37,6 +37,7 @@ type DB interface {
Marker
Media
Mention
+ Move
Notification
Poll
Relationship
diff --git a/internal/db/move.go b/internal/db/move.go
new file mode 100644
index 000000000..5bce781a3
--- /dev/null
+++ b/internal/db/move.go
@@ -0,0 +1,56 @@
+// 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 .
+
+package db
+
+import (
+ "context"
+ "time"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+type Move interface {
+ // GetMoveByID gets one Move with the given internal ID.
+ GetMoveByID(ctx context.Context, id string) (*gtsmodel.Move, error)
+
+ // GetMoveByURI gets one Move with the given AP URI.
+ GetMoveByURI(ctx context.Context, uri string) (*gtsmodel.Move, error)
+
+ // GetMoveByOriginTarget gets one move with the given originURI and targetURI.
+ GetMoveByOriginTarget(ctx context.Context, originURI string, targetURI string) (*gtsmodel.Move, error)
+
+ // GetLatestMoveSuccessInvolvingURIs gets the time of
+ // the latest successfully-processed Move that includes
+ // either uri1 or uri2 in target or origin positions.
+ GetLatestMoveSuccessInvolvingURIs(ctx context.Context, uri1 string, uri2 string) (time.Time, error)
+
+ // GetLatestMoveAttemptInvolvingURIs gets the time
+ // of the latest Move attempt that includes either
+ // uri1 or uri2 in target or origin positions.
+ GetLatestMoveAttemptInvolvingURIs(ctx context.Context, uri1 string, uri2 string) (time.Time, error)
+
+ // PutMove puts the given Move in the database.
+ PutMove(ctx context.Context, move *gtsmodel.Move) error
+
+ // UpdateMove updates the given Move by primary key.
+ // Updates specific columns if provided, all columns if not.
+ UpdateMove(ctx context.Context, move *gtsmodel.Move, columns ...string) error
+
+ // DeleteMoveByID deletes a move with the given internal ID.
+ DeleteMoveByID(ctx context.Context, id string) error
+}
diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go
index 5421c41bb..8b1412255 100644
--- a/internal/gtsmodel/account.go
+++ b/internal/gtsmodel/account.go
@@ -23,6 +23,7 @@ package gtsmodel
import (
"crypto/rsa"
+ "slices"
"strings"
"time"
@@ -54,8 +55,10 @@ type Account struct {
Memorial *bool `bun:",default:false"` // Is this a memorial account, ie., has the user passed away?
AlsoKnownAsURIs []string `bun:"also_known_as_uris,array"` // This account is associated with these account URIs.
AlsoKnownAs []*Account `bun:"-"` // This account is associated with these accounts (field not stored in the db).
- MovedToURI string `bun:",nullzero"` // This account has moved to this account URI.
+ MovedToURI string `bun:",nullzero"` // This account has (or claims to have) moved to this account URI. Even if this field is set the move may not yet have been processed. Check `move` for this.
MovedTo *Account `bun:"-"` // This account has moved to this account (field not stored in the db).
+ MoveID string `bun:""` // ID of a Move in the database for this account. Only set if we received or created a Move activity for which this account URI was the origin.
+ Move *Move `bun:"-"` // Move corresponding to MoveID, if set.
Bot *bool `bun:",default:false"` // Does this account identify itself as a bot?
Reason string `bun:""` // What reason was given for signing up when this account was created?
Locked *bool `bun:",default:true"` // Does this account need an approval for new followers?
@@ -172,6 +175,18 @@ func (a *Account) PubKeyExpired() bool {
a.PublicKeyExpiresAt.Before(time.Now())
}
+// IsAliasedTo returns true if account
+// is aliased to the given account URI.
+func (a *Account) IsAliasedTo(uri string) bool {
+ return slices.Contains(a.AlsoKnownAsURIs, uri)
+}
+
+// IsSuspended returns true if account
+// has been suspended from this instance.
+func (a *Account) IsSuspended() bool {
+ return !a.SuspendedAt.IsZero()
+}
+
// AccountToEmoji is an intermediate struct to facilitate the many2many relationship between an account and one or more emojis.
type AccountToEmoji struct {
AccountID string `bun:"type:CHAR(26),unique:accountemoji,nullzero,notnull"`
diff --git a/internal/gtsmodel/move.go b/internal/gtsmodel/move.go
new file mode 100644
index 000000000..c8b803822
--- /dev/null
+++ b/internal/gtsmodel/move.go
@@ -0,0 +1,38 @@
+// 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 .
+
+package gtsmodel
+
+import (
+ "net/url"
+ "time"
+)
+
+// Move represents an ActivityPub "Move" activity
+// received (or created) by this instance.
+type Move struct {
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
+ CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was item created.
+ UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // When was item last updated.
+ AttemptedAt time.Time `bun:"type:timestamptz,nullzero"` // When was processing of the Move to TargetURI last attempted by our instance (zero if not yet attempted).
+ SucceededAt time.Time `bun:"type:timestamptz,nullzero"` // When did the processing of the Move to TargetURI succeed according to our criteria (zero if not yet complete).
+ OriginURI string `bun:",nullzero,notnull,unique:moveorigintarget"` // OriginURI of the Move. Ie., the Move Object.
+ Origin *url.URL `bun:"-"` // URL corresponding to OriginURI. Not stored in the database.
+ TargetURI string `bun:",nullzero,notnull,unique:moveorigintarget"` // TargetURI of the Move. Ie., the Move Target.
+ Target *url.URL `bun:"-"` // URL corresponding to TargetURI. Not stored in the database.
+ URI string `bun:",nullzero,notnull,unique"` // ActivityPub ID/URI of the Move Activity itself.
+}
diff --git a/test/envparsing.sh b/test/envparsing.sh
index 617bfc63f..72b7caa1d 100755
--- a/test/envparsing.sh
+++ b/test/envparsing.sh
@@ -46,6 +46,7 @@ EXPECT=$(cat << "EOF"
"media-mem-ratio": 4,
"memory-target": 104857600,
"mention-mem-ratio": 2,
+ "move-mem-ratio": 0.1,
"notification-mem-ratio": 2,
"poll-mem-ratio": 1,
"poll-vote-ids-mem-ratio": 2,