mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[feature] Client API endpoints + v. basic web view for pinned posts (#1547)
* implement status pin client api + web handler * make test names + comments more descriptive * don't use separate table for status pins * remove unused add + remove checking * tidy up + add some more tests
This commit is contained in:
@@ -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)
|
||||
|
@@ -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{}
|
||||
|
||||
|
@@ -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))
|
||||
}
|
||||
|
@@ -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(>smodel.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(>smodel.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)
|
||||
}
|
||||
}
|
@@ -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(),
|
||||
|
Reference in New Issue
Block a user