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,