summaryrefslogtreecommitdiff
path: root/internal/db
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-10-25 16:04:53 +0200
committerLibravatar GitHub <noreply@github.com>2023-10-25 15:04:53 +0100
commitc7b6cd7770cad9bfdc23decffa7c4068752dbbbd (patch)
tree0f039fd34fb0287860fce06ff1c30dedd1882136 /internal/db
parent[bugfix] allow store smaller PNG image than 261 bytes (#2263) (#2298) (diff)
downloadgotosocial-c7b6cd7770cad9bfdc23decffa7c4068752dbbbd.tar.xz
[feature] Status thread mute/unmute functionality (#2278)
* add db models + functions for keeping track of threads * give em the old linty testy * create, remove, check mutes * swagger * testerino * test mute/unmute via api * add info log about new index creation * thread + allow muting of any remote statuses that mention a local account * IsStatusThreadMutedBy -> IsThreadMutedByAccount * use common processing functions in status processor * set = NULL * favee! * get rekt darlings, darlings get rekt * testrig please, have mercy muy liege
Diffstat (limited to 'internal/db')
-rw-r--r--internal/db/bundb/basic.go2
-rw-r--r--internal/db/bundb/bundb.go6
-rw-r--r--internal/db/bundb/bundb_test.go2
-rw-r--r--internal/db/bundb/migrations/20231016113235_mute_status_thread.go148
-rw-r--r--internal/db/bundb/status.go55
-rw-r--r--internal/db/bundb/thread.go117
-rw-r--r--internal/db/bundb/thread_test.go91
-rw-r--r--internal/db/db.go1
-rw-r--r--internal/db/status.go3
-rw-r--r--internal/db/thread.go48
10 files changed, 459 insertions, 14 deletions
diff --git a/internal/db/bundb/basic.go b/internal/db/bundb/basic.go
index eee2a12ef..e68903efa 100644
--- a/internal/db/bundb/basic.go
+++ b/internal/db/bundb/basic.go
@@ -135,7 +135,7 @@ func (b *basicDB) CreateAllTables(ctx context.Context) error {
&gtsmodel.StatusToEmoji{},
&gtsmodel.StatusFave{},
&gtsmodel.StatusBookmark{},
- &gtsmodel.StatusMute{},
+ &gtsmodel.ThreadMute{},
&gtsmodel.Tag{},
&gtsmodel.User{},
&gtsmodel.Emoji{},
diff --git a/internal/db/bundb/bundb.go b/internal/db/bundb/bundb.go
index 819fba810..393f32eec 100644
--- a/internal/db/bundb/bundb.go
+++ b/internal/db/bundb/bundb.go
@@ -54,6 +54,7 @@ var registerTables = []interface{}{
&gtsmodel.AccountToEmoji{},
&gtsmodel.StatusToEmoji{},
&gtsmodel.StatusToTag{},
+ &gtsmodel.ThreadToStatus{},
}
// DBService satisfies the DB interface
@@ -79,6 +80,7 @@ type DBService struct {
db.StatusBookmark
db.StatusFave
db.Tag
+ db.Thread
db.Timeline
db.User
db.Tombstone
@@ -236,6 +238,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
conn: db,
state: state,
},
+ Thread: &threadDB{
+ db: db,
+ state: state,
+ },
Timeline: &timelineDB{
db: db,
state: state,
diff --git a/internal/db/bundb/bundb_test.go b/internal/db/bundb/bundb_test.go
index 2ab539147..8245937b9 100644
--- a/internal/db/bundb/bundb_test.go
+++ b/internal/db/bundb/bundb_test.go
@@ -53,6 +53,7 @@ type BunDBStandardTestSuite struct {
testAccountNotes map[string]*gtsmodel.AccountNote
testMarkers map[string]*gtsmodel.Marker
testRules map[string]*gtsmodel.Rule
+ testThreads map[string]*gtsmodel.Thread
}
func (suite *BunDBStandardTestSuite) SetupSuite() {
@@ -75,6 +76,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
suite.testAccountNotes = testrig.NewTestAccountNotes()
suite.testMarkers = testrig.NewTestMarkers()
suite.testRules = testrig.NewTestRules()
+ suite.testThreads = testrig.NewTestThreads()
}
func (suite *BunDBStandardTestSuite) SetupTest() {
diff --git a/internal/db/bundb/migrations/20231016113235_mute_status_thread.go b/internal/db/bundb/migrations/20231016113235_mute_status_thread.go
new file mode 100644
index 000000000..c4a4a4fe7
--- /dev/null
+++ b/internal/db/bundb/migrations/20231016113235_mute_status_thread.go
@@ -0,0 +1,148 @@
+// 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"
+
+ gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/uptrace/bun"
+ "github.com/uptrace/bun/dialect"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ // Create thread table.
+ if _, err := tx.
+ NewCreateTable().
+ Model(&gtsmodel.Thread{}).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Create thread intermediate table.
+ if _, err := tx.
+ NewCreateTable().
+ Model(&gtsmodel.ThreadToStatus{}).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Drop old pkey constraint from
+ // deprecated status mute table.
+ //
+ // This is only necessary with postgres.
+ if tx.Dialect().Name() == dialect.PG {
+ if _, err := tx.ExecContext(
+ ctx,
+ "ALTER TABLE ? DROP CONSTRAINT IF EXISTS ?",
+ bun.Ident("status_mutes"),
+ bun.Safe("status_mutes_pkey"),
+ ); err != nil {
+ return err
+ }
+ }
+
+ // Drop old index.
+ if _, err := tx.
+ NewDropIndex().
+ Index("status_mutes_account_id_target_account_id_status_id_idx").
+ IfExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Drop deprecated status mute table.
+ if _, err := tx.
+ NewDropTable().
+ Table("status_mutes").
+ IfExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Create new thread mute table.
+ if _, err := tx.
+ NewCreateTable().
+ Model(&gtsmodel.ThreadMute{}).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ log.Info(ctx, "creating a new index on the statuses table, please wait and don't interrupt it (this may take a few minutes)")
+
+ // Update statuses to add thread ID column.
+ _, err := tx.ExecContext(
+ ctx,
+ "ALTER TABLE ? ADD COLUMN ? CHAR(26)",
+ bun.Ident("statuses"),
+ bun.Ident("thread_id"),
+ )
+ if err != nil && !(strings.Contains(err.Error(), "already exists") ||
+ strings.Contains(err.Error(), "duplicate column name") ||
+ strings.Contains(err.Error(), "SQLSTATE 42701")) {
+ return err
+ }
+
+ // Index new + existing tables properly.
+ for table, indexes := range map[string]map[string][]string{
+ "threads": {
+ "threads_id_idx": {"id"},
+ },
+ "thread_mutes": {
+ "thread_mutes_id_idx": {"id"},
+ // Eg., check if target thread is muted by account.
+ "thread_mutes_thread_id_account_id_idx": {"thread_id", "account_id"},
+ },
+ "statuses": {
+ // Eg., select all statuses in a thread.
+ "statuses_thread_id_idx": {"thread_id"},
+ },
+ } {
+ for index, columns := range indexes {
+ if _, err := tx.
+ NewCreateIndex().
+ Table(table).
+ Index(index).
+ Column(columns...).
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go
index 26f0c1f38..0bd4ba1a9 100644
--- a/internal/db/bundb/status.go
+++ b/internal/db/bundb/status.go
@@ -324,6 +324,23 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) error
}
}
+ // If the status is threaded, create
+ // link between thread and status.
+ if status.ThreadID != "" {
+ if _, err := tx.
+ NewInsert().
+ Model(&gtsmodel.ThreadToStatus{
+ ThreadID: status.ThreadID,
+ StatusID: status.ID,
+ }).
+ On("CONFLICT (?, ?) DO NOTHING", bun.Ident("thread_id"), bun.Ident("status_id")).
+ Exec(ctx); err != nil {
+ if !errors.Is(err, db.ErrAlreadyExists) {
+ return err
+ }
+ }
+ }
+
// Finally, insert the status
_, err := tx.NewInsert().Model(status).Exec(ctx)
return err
@@ -390,6 +407,23 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, co
}
}
+ // If the status is threaded, create
+ // link between thread and status.
+ if status.ThreadID != "" {
+ if _, err := tx.
+ NewInsert().
+ Model(&gtsmodel.ThreadToStatus{
+ ThreadID: status.ThreadID,
+ StatusID: status.ID,
+ }).
+ On("CONFLICT (?, ?) DO NOTHING", bun.Ident("thread_id"), bun.Ident("status_id")).
+ Exec(ctx); err != nil {
+ if !errors.Is(err, db.ErrAlreadyExists) {
+ return err
+ }
+ }
+ }
+
// Finally, update the status
_, err := tx.
NewUpdate().
@@ -439,6 +473,17 @@ func (s *statusDB) DeleteStatusByID(ctx context.Context, id string) error {
return err
}
+ // Delete links between this status
+ // and any threads it was a part of.
+ _, err = tx.
+ NewDelete().
+ TableExpr("? AS ?", bun.Ident("thread_to_statuses"), bun.Ident("thread_to_status")).
+ Where("? = ?", bun.Ident("thread_to_status.status_id"), id).
+ Exec(ctx)
+ if err != nil {
+ return err
+ }
+
// delete the status itself
if _, err := tx.
NewDelete().
@@ -634,16 +679,6 @@ func (s *statusDB) getStatusBoostIDs(ctx context.Context, statusID string) ([]st
})
}
-func (s *statusDB) IsStatusMutedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error) {
- q := s.db.
- NewSelect().
- TableExpr("? AS ?", bun.Ident("status_mutes"), bun.Ident("status_mute")).
- Where("? = ?", bun.Ident("status_mute.status_id"), status.ID).
- Where("? = ?", bun.Ident("status_mute.account_id"), accountID)
-
- return s.db.Exists(ctx, q)
-}
-
func (s *statusDB) IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error) {
q := s.db.
NewSelect().
diff --git a/internal/db/bundb/thread.go b/internal/db/bundb/thread.go
new file mode 100644
index 000000000..e6d6154d4
--- /dev/null
+++ b/internal/db/bundb/thread.go
@@ -0,0 +1,117 @@
+// 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"
+
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/uptrace/bun"
+)
+
+type threadDB struct {
+ db *DB
+ state *state.State
+}
+
+func (t *threadDB) PutThread(ctx context.Context, thread *gtsmodel.Thread) error {
+ _, err := t.db.
+ NewInsert().
+ Model(thread).
+ Exec(ctx)
+
+ return err
+}
+
+func (t *threadDB) GetThreadMute(ctx context.Context, id string) (*gtsmodel.ThreadMute, error) {
+ return t.state.Caches.GTS.ThreadMute().Load("ID", func() (*gtsmodel.ThreadMute, error) {
+ var threadMute gtsmodel.ThreadMute
+
+ q := t.db.
+ NewSelect().
+ Model(&threadMute).
+ Where("? = ?", bun.Ident("thread_mute.id"), id)
+
+ if err := q.Scan(ctx); err != nil {
+ return nil, err
+ }
+
+ return &threadMute, nil
+ }, id)
+}
+
+func (t *threadDB) GetThreadMutedByAccount(
+ ctx context.Context,
+ threadID string,
+ accountID string,
+) (*gtsmodel.ThreadMute, error) {
+ return t.state.Caches.GTS.ThreadMute().Load("ThreadID.AccountID", func() (*gtsmodel.ThreadMute, error) {
+ var threadMute gtsmodel.ThreadMute
+
+ q := t.db.
+ NewSelect().
+ Model(&threadMute).
+ Where("? = ?", bun.Ident("thread_mute.thread_id"), threadID).
+ Where("? = ?", bun.Ident("thread_mute.account_id"), accountID)
+
+ if err := q.Scan(ctx); err != nil {
+ return nil, err
+ }
+
+ return &threadMute, nil
+ }, threadID, accountID)
+}
+
+func (t *threadDB) IsThreadMutedByAccount(
+ ctx context.Context,
+ threadID string,
+ accountID string,
+) (bool, error) {
+ if threadID == "" {
+ return false, nil
+ }
+
+ mute, err := t.GetThreadMutedByAccount(ctx, threadID, accountID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return false, err
+ }
+
+ return (mute != nil), nil
+}
+
+func (t *threadDB) PutThreadMute(ctx context.Context, threadMute *gtsmodel.ThreadMute) error {
+ return t.state.Caches.GTS.ThreadMute().Store(threadMute, func() error {
+ _, err := t.db.NewInsert().Model(threadMute).Exec(ctx)
+ return err
+ })
+}
+
+func (t *threadDB) DeleteThreadMute(ctx context.Context, id string) error {
+ if _, err := t.db.
+ NewDelete().
+ TableExpr("? AS ?", bun.Ident("thread_mutes"), bun.Ident("thread_mute")).
+ Where("? = ?", bun.Ident("thread_mute.id"), id).Exec(ctx); err != nil {
+ return err
+ }
+
+ t.state.Caches.GTS.ThreadMute().Invalidate("ID", id)
+ return nil
+}
diff --git a/internal/db/bundb/thread_test.go b/internal/db/bundb/thread_test.go
new file mode 100644
index 000000000..4d14f73e2
--- /dev/null
+++ b/internal/db/bundb/thread_test.go
@@ -0,0 +1,91 @@
+// 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"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+type ThreadTestSuite struct {
+ BunDBStandardTestSuite
+}
+
+func (suite *ThreadTestSuite) TestPutThread() {
+ suite.NoError(
+ suite.db.PutThread(
+ context.Background(),
+ &gtsmodel.Thread{
+ ID: "01HCWK4HVQ4VGSS1G4VQP3AXZF",
+ },
+ ),
+ )
+}
+
+func (suite *ThreadTestSuite) TestMuteUnmuteThread() {
+ var (
+ threadID = suite.testThreads["local_account_1_status_1"].ID
+ accountID = suite.testAccounts["local_account_1"].ID
+ ctx = context.Background()
+ threadMute = &gtsmodel.ThreadMute{
+ ID: "01HD3K14B62YJHH4RR0DSZ1EQ2",
+ ThreadID: threadID,
+ AccountID: accountID,
+ }
+ )
+
+ // Mute the thread and ensure it's actually muted.
+ if err := suite.db.PutThreadMute(ctx, threadMute); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ muted, err := suite.db.IsThreadMutedByAccount(ctx, threadID, accountID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if !muted {
+ suite.FailNow("", "expected thread %s to be muted by account %s", threadID, accountID)
+ }
+
+ _, err = suite.db.GetThreadMutedByAccount(ctx, threadID, accountID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Unmute the thread and ensure it's actually unmuted.
+ if err := suite.db.DeleteThreadMute(ctx, threadMute.ID); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ muted, err = suite.db.IsThreadMutedByAccount(ctx, threadID, accountID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if muted {
+ suite.FailNow("", "expected thread %s to not be muted by account %s", threadID, accountID)
+ }
+}
+
+func TestThreadTestSuite(t *testing.T) {
+ suite.Run(t, new(ThreadTestSuite))
+}
diff --git a/internal/db/db.go b/internal/db/db.go
index 056d03e23..41b253834 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -45,6 +45,7 @@ type DB interface {
StatusBookmark
StatusFave
Tag
+ Thread
Timeline
User
Tombstone
diff --git a/internal/db/status.go b/internal/db/status.go
index f4421fa2e..0be37421a 100644
--- a/internal/db/status.go
+++ b/internal/db/status.go
@@ -80,9 +80,6 @@ type Status interface {
// If onlyDirect is true, only the immediate children will be returned.
GetStatusChildren(ctx context.Context, status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, error)
- // IsStatusMutedBy checks if a given status has been muted by a given account ID
- IsStatusMutedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error)
-
// IsStatusBookmarkedBy checks if a given status has been bookmarked by a given account ID
IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error)
}
diff --git a/internal/db/thread.go b/internal/db/thread.go
new file mode 100644
index 000000000..dd494167a
--- /dev/null
+++ b/internal/db/thread.go
@@ -0,0 +1,48 @@
+// 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 db
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+// Thread contains functions for getting/creating
+// status threads and thread mutes in the database.
+type Thread interface {
+ // PutThread inserts a new thread.
+ PutThread(ctx context.Context, thread *gtsmodel.Thread) error
+
+ // GetThreadMute gets a single threadMute by its ID.
+ GetThreadMute(ctx context.Context, id string) (*gtsmodel.ThreadMute, error)
+
+ // GetThreadMutedByAccount gets a threadMute targeting the
+ // given thread, created by the given accountID, if it exists.
+ GetThreadMutedByAccount(ctx context.Context, threadID string, accountID string) (*gtsmodel.ThreadMute, error)
+
+ // IsThreadMutedByAccount returns true if threadID is muted
+ // by given account. Empty thread ID will return false early.
+ IsThreadMutedByAccount(ctx context.Context, threadID string, accountID string) (bool, error)
+
+ // PutThreadMute inserts a new threadMute.
+ PutThreadMute(ctx context.Context, threadMute *gtsmodel.ThreadMute) error
+
+ // DeleteThreadMute deletes threadMute with the given ID.
+ DeleteThreadMute(ctx context.Context, id string) error
+}