summaryrefslogtreecommitdiff
path: root/internal/db
diff options
context:
space:
mode:
Diffstat (limited to 'internal/db')
-rw-r--r--internal/db/account.go18
-rw-r--r--internal/db/bundb/account.go33
-rw-r--r--internal/db/bundb/account_test.go41
-rw-r--r--internal/db/bundb/migrations/20230221150957_status_pin_client_api.go65
-rw-r--r--internal/db/bundb/timeline_test.go1
5 files changed, 146 insertions, 12 deletions
diff --git a/internal/db/account.go b/internal/db/account.go
index 42a8c62b0..7989b9838 100644
--- a/internal/db/account.go
+++ b/internal/db/account.go
@@ -62,15 +62,29 @@ type Account interface {
// GetAccountStatusesCount is a shortcut for the common action of counting statuses produced by accountID.
CountAccountStatuses(ctx context.Context, accountID string) (int, Error)
+ // CountAccountPinned returns the total number of pinned statuses owned by account with the given id.
+ CountAccountPinned(ctx context.Context, accountID string) (int, Error)
+
// GetAccountStatuses is a shortcut for getting the most recent statuses. accountID is optional, if not provided
// then all statuses will be returned. If limit is set to 0, the size of the returned slice will not be limited. This can
// be very memory intensive so you probably shouldn't do this!
- // In case of no entries, a 'no entries' error will be returned
- GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, Error)
+ //
+ // In the case of no statuses, this function will return db.ErrNoEntries.
+ GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, Error)
+
+ // GetAccountPinnedStatuses returns ONLY statuses owned by the give accountID for which a corresponding StatusPin
+ // exists in the database. Statuses which are not pinned will not be returned by this function.
+ //
+ // Statuses will be returned in the order in which they were pinned, from latest pinned to oldest pinned (descending).
+ //
+ // In the case of no statuses, this function will return db.ErrNoEntries.
+ GetAccountPinnedStatuses(ctx context.Context, accountID string) ([]*gtsmodel.Status, Error)
// GetAccountWebStatuses is similar to GetAccountStatuses, but it's specifically for returning statuses that
// should be visible via the web view of an account. So, only public, federated statuses that aren't boosts
// or replies.
+ //
+ // In the case of no statuses, this function will return db.ErrNoEntries.
GetAccountWebStatuses(ctx context.Context, accountID string, limit int, maxID string) ([]*gtsmodel.Status, Error)
GetBookmarks(ctx context.Context, accountID string, limit int, maxID string, minID string) ([]*gtsmodel.StatusBookmark, Error)
diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go
index 937f4ba23..26959001f 100644
--- a/internal/db/bundb/account.go
+++ b/internal/db/bundb/account.go
@@ -350,7 +350,16 @@ func (a *accountDB) CountAccountStatuses(ctx context.Context, accountID string)
Count(ctx)
}
-func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, db.Error) {
+func (a *accountDB) CountAccountPinned(ctx context.Context, accountID string) (int, db.Error) {
+ return a.conn.
+ NewSelect().
+ TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
+ Where("? = ?", bun.Ident("status.account_id"), accountID).
+ Where("? IS NOT NULL", bun.Ident("status.pinned_at")).
+ Count(ctx)
+}
+
+func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, db.Error) {
statusIDs := []string{}
q := a.conn.
@@ -390,10 +399,6 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li
q = q.Where("? > ?", bun.Ident("status.id"), minID)
}
- if pinnedOnly {
- q = q.Where("? = ?", bun.Ident("status.pinned"), true)
- }
-
if mediaOnly {
// attachments are stored as a json object;
// this implementation differs between sqlite and postgres,
@@ -429,6 +434,24 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li
return a.statusesFromIDs(ctx, statusIDs)
}
+func (a *accountDB) GetAccountPinnedStatuses(ctx context.Context, accountID string) ([]*gtsmodel.Status, db.Error) {
+ statusIDs := []string{}
+
+ q := a.conn.
+ NewSelect().
+ TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
+ Column("status.id").
+ Where("? = ?", bun.Ident("status.account_id"), accountID).
+ Where("? IS NOT NULL", bun.Ident("status.pinned_at")).
+ Order("status.pinned_at DESC")
+
+ if err := q.Scan(ctx, &statusIDs); err != nil {
+ return nil, a.conn.ProcessError(err)
+ }
+
+ return a.statusesFromIDs(ctx, statusIDs)
+}
+
func (a *accountDB) GetAccountWebStatuses(ctx context.Context, accountID string, limit int, maxID string) ([]*gtsmodel.Status, db.Error) {
statusIDs := []string{}
diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go
index 13bdba20d..a510e2152 100644
--- a/internal/db/bundb/account_test.go
+++ b/internal/db/bundb/account_test.go
@@ -28,6 +28,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/bundb"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/uptrace/bun"
@@ -38,25 +39,25 @@ type AccountTestSuite struct {
}
func (suite *AccountTestSuite) TestGetAccountStatuses() {
- statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", false, false, false)
+ statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", false, false)
suite.NoError(err)
suite.Len(statuses, 5)
}
func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogs() {
- statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, false, false)
+ statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, false)
suite.NoError(err)
suite.Len(statuses, 5)
}
func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogsPublicOnly() {
- statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, false, true)
+ statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, true)
suite.NoError(err)
suite.Len(statuses, 1)
}
func (suite *AccountTestSuite) TestGetAccountStatusesMediaOnly() {
- statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", false, true, false)
+ statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", true, false)
suite.NoError(err)
suite.Len(statuses, 1)
}
@@ -214,6 +215,38 @@ func (suite *AccountTestSuite) TestGettingBookmarksWithNoAccount() {
suite.Nil(statuses)
}
+func (suite *AccountTestSuite) TestGetAccountPinnedStatusesSomeResults() {
+ testAccount := suite.testAccounts["admin_account"]
+
+ statuses, err := suite.db.GetAccountPinnedStatuses(context.Background(), testAccount.ID)
+ suite.NoError(err)
+ suite.Len(statuses, 2) // This account has 2 statuses pinned.
+}
+
+func (suite *AccountTestSuite) TestGetAccountPinnedStatusesNothingPinned() {
+ testAccount := suite.testAccounts["local_account_1"]
+
+ statuses, err := suite.db.GetAccountPinnedStatuses(context.Background(), testAccount.ID)
+ suite.ErrorIs(err, db.ErrNoEntries)
+ suite.Empty(statuses) // This account has nothing pinned.
+}
+
+func (suite *AccountTestSuite) TestCountAccountPinnedSomeResults() {
+ testAccount := suite.testAccounts["admin_account"]
+
+ pinned, err := suite.db.CountAccountPinned(context.Background(), testAccount.ID)
+ suite.NoError(err)
+ suite.Equal(pinned, 2) // This account has 2 statuses pinned.
+}
+
+func (suite *AccountTestSuite) TestCountAccountPinnedNothingPinned() {
+ testAccount := suite.testAccounts["local_account_1"]
+
+ pinned, err := suite.db.CountAccountPinned(context.Background(), testAccount.ID)
+ suite.NoError(err)
+ suite.Equal(pinned, 0) // This account has nothing pinned.
+}
+
func TestAccountTestSuite(t *testing.T) {
suite.Run(t, new(AccountTestSuite))
}
diff --git a/internal/db/bundb/migrations/20230221150957_status_pin_client_api.go b/internal/db/bundb/migrations/20230221150957_status_pin_client_api.go
new file mode 100644
index 000000000..b2464fe30
--- /dev/null
+++ b/internal/db/bundb/migrations/20230221150957_status_pin_client_api.go
@@ -0,0 +1,65 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ 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 {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ // Drop the now unused 'pinned' column in statuses.
+ if _, err := tx.ExecContext(ctx, "ALTER TABLE ? DROP COLUMN ?", bun.Ident("statuses"), bun.Ident("pinned")); err != nil &&
+ !(strings.Contains(err.Error(), "no such column") || strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "SQLSTATE 42703")) {
+ return err
+ }
+
+ // Create new (more useful) pinned_at column.
+ if _, err := tx.NewAddColumn().Model(&gtsmodel.Status{}).ColumnExpr("? TIMESTAMPTZ", bun.Ident("pinned_at")).Exec(ctx); 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 column appropriately.
+ if _, err := tx.
+ NewCreateIndex().
+ Model(&gtsmodel.Status{}).
+ Index("statuses_account_id_pinned_at_idx").
+ Column("account_id", "pinned_at").
+ 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/timeline_test.go b/internal/db/bundb/timeline_test.go
index 91a4fef15..3b98de45d 100644
--- a/internal/db/bundb/timeline_test.go
+++ b/internal/db/bundb/timeline_test.go
@@ -113,7 +113,6 @@ func getFutureStatus() *gtsmodel.Status {
Sensitive: testrig.FalseBool(),
Language: "en",
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
- Pinned: testrig.FalseBool(),
Federated: testrig.TrueBool(),
Boostable: testrig.TrueBool(),
Replyable: testrig.TrueBool(),