mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-18 14:07:35 -06:00
[chore] Refactor account deleting/block logic, tidy up some other processing things (#1599)
* start refactoring account deletion * update to use state.DB * further messing about * some more tidying up * more tidying, cleaning, nice-making * further adventures in refactoring and the woes of technical debt * update fr accept/reject * poking + prodding * fix up deleting * create fave uri * don't log using requestingAccount.ID because it might be nil * move getBookmarks function * use exists query to check for status bookmark * use deletenotifications func * fiddle * delete follow request notif * split up some db functions * Fix possible nil pointer panic * fix more possible nil pointers * fix license headers * warn when follow missing (target) account * return wrapped err when bookmark/fave models can't be retrieved * simplify self account delete * warn log likely race condition * de-sillify status delete loop * move error check due north * warn when unfollowSideEffects has no target account * warn when no boost account is found * warn + dump follow when no account * more warnings * warn on fave account not set * move for loop inside anonymous function * fix funky logic * don't remove mutual account items on block; do make sure unfollow occurs in both directions!
This commit is contained in:
parent
276d773438
commit
e8595f0c64
53 changed files with 2472 additions and 1321 deletions
|
|
@ -45,7 +45,7 @@ type Account interface {
|
|||
PutAccount(ctx context.Context, account *gtsmodel.Account) Error
|
||||
|
||||
// UpdateAccount updates one account by ID.
|
||||
UpdateAccount(ctx context.Context, account *gtsmodel.Account) Error
|
||||
UpdateAccount(ctx context.Context, account *gtsmodel.Account, columns ...string) Error
|
||||
|
||||
// DeleteAccount deletes one account from the database by its ID.
|
||||
// DO NOT USE THIS WHEN SUSPENDING ACCOUNTS! In that case you should mark the
|
||||
|
|
@ -86,8 +86,6 @@ type Account interface {
|
|||
// 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)
|
||||
|
||||
GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error)
|
||||
|
||||
// GetAccountLastPosted simply gets the timestamp of the most recent post by the account.
|
||||
|
|
|
|||
|
|
@ -191,9 +191,12 @@ func (a *accountDB) PutAccount(ctx context.Context, account *gtsmodel.Account) d
|
|||
})
|
||||
}
|
||||
|
||||
func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account) db.Error {
|
||||
// Update the account's last-updated
|
||||
func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account, columns ...string) db.Error {
|
||||
account.UpdatedAt = time.Now()
|
||||
if len(columns) > 0 {
|
||||
// If we're updating by column, ensure "updated_at" is included.
|
||||
columns = append(columns, "updated_at")
|
||||
}
|
||||
|
||||
return a.state.Caches.GTS.Account().Store(account, func() error {
|
||||
// It is safe to run this database transaction within cache.Store
|
||||
|
|
@ -226,6 +229,7 @@ func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account
|
|||
_, err := tx.NewUpdate().
|
||||
Model(account).
|
||||
Where("? = ?", bun.Ident("account.id"), account.ID).
|
||||
Column(columns...).
|
||||
Exec(ctx)
|
||||
return err
|
||||
})
|
||||
|
|
@ -477,38 +481,6 @@ func (a *accountDB) GetAccountWebStatuses(ctx context.Context, accountID string,
|
|||
return a.statusesFromIDs(ctx, statusIDs)
|
||||
}
|
||||
|
||||
func (a *accountDB) GetBookmarks(ctx context.Context, accountID string, limit int, maxID string, minID string) ([]*gtsmodel.StatusBookmark, db.Error) {
|
||||
bookmarks := []*gtsmodel.StatusBookmark{}
|
||||
|
||||
q := a.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")).
|
||||
Order("status_bookmark.id DESC").
|
||||
Where("? = ?", bun.Ident("status_bookmark.account_id"), accountID)
|
||||
|
||||
if accountID == "" {
|
||||
return nil, errors.New("must provide an account")
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if maxID != "" {
|
||||
q = q.Where("? < ?", bun.Ident("status_bookmark.id"), maxID)
|
||||
}
|
||||
|
||||
if minID != "" {
|
||||
q = q.Where("? > ?", bun.Ident("status_bookmark.id"), minID)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &bookmarks); err != nil {
|
||||
return nil, a.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return bookmarks, nil
|
||||
}
|
||||
|
||||
func (a *accountDB) GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.Error) {
|
||||
blocks := []*gtsmodel.Block{}
|
||||
|
||||
|
|
|
|||
|
|
@ -208,12 +208,6 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
|
|||
suite.False(*newAccount.HideCollections)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGettingBookmarksWithNoAccount() {
|
||||
statuses, err := suite.db.GetBookmarks(context.Background(), "", 10, "", "")
|
||||
suite.Error(err)
|
||||
suite.Nil(statuses)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountPinnedStatusesSomeResults() {
|
||||
testAccount := suite.testAccounts["admin_account"]
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ type DBService struct {
|
|||
db.Report
|
||||
db.Session
|
||||
db.Status
|
||||
db.StatusBookmark
|
||||
db.StatusFave
|
||||
db.Timeline
|
||||
db.User
|
||||
db.Tombstone
|
||||
|
|
@ -203,6 +205,14 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
|||
conn: conn,
|
||||
state: state,
|
||||
},
|
||||
StatusBookmark: &statusBookmarkDB{
|
||||
conn: conn,
|
||||
state: state,
|
||||
},
|
||||
StatusFave: &statusFaveDB{
|
||||
conn: conn,
|
||||
state: state,
|
||||
},
|
||||
Timeline: &timelineDB{
|
||||
conn: conn,
|
||||
state: state,
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ type BunDBStandardTestSuite struct {
|
|||
testFollows map[string]*gtsmodel.Follow
|
||||
testEmojis map[string]*gtsmodel.Emoji
|
||||
testReports map[string]*gtsmodel.Report
|
||||
testBookmarks map[string]*gtsmodel.StatusBookmark
|
||||
testFaves map[string]*gtsmodel.StatusFave
|
||||
}
|
||||
|
||||
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||
|
|
@ -59,6 +61,8 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
|
|||
suite.testFollows = testrig.NewTestFollows()
|
||||
suite.testEmojis = testrig.NewTestEmojis()
|
||||
suite.testReports = testrig.NewTestReports()
|
||||
suite.testBookmarks = testrig.NewTestBookmarks()
|
||||
suite.testFaves = testrig.NewTestFaves()
|
||||
}
|
||||
|
||||
func (suite *BunDBStandardTestSuite) SetupTest() {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package bundb
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
|
|
@ -104,15 +105,70 @@ func (n *notificationDB) GetNotifications(ctx context.Context, accountID string,
|
|||
return notifs, nil
|
||||
}
|
||||
|
||||
func (n *notificationDB) ClearNotifications(ctx context.Context, accountID string) db.Error {
|
||||
func (n *notificationDB) DeleteNotification(ctx context.Context, id string) db.Error {
|
||||
if _, err := n.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
|
||||
Where("? = ?", bun.Ident("notification.target_account_id"), accountID).
|
||||
Where("? = ?", bun.Ident("notification.id"), id).
|
||||
Exec(ctx); err != nil {
|
||||
return n.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
n.state.Caches.GTS.Notification().Clear()
|
||||
n.state.Caches.GTS.Notification().Invalidate("ID", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *notificationDB) DeleteNotifications(ctx context.Context, targetAccountID string, originAccountID string) db.Error {
|
||||
if targetAccountID == "" && originAccountID == "" {
|
||||
return errors.New("DeleteNotifications: one of targetAccountID or originAccountID must be set")
|
||||
}
|
||||
|
||||
// Capture notification IDs in a RETURNING statement.
|
||||
ids := []string{}
|
||||
|
||||
q := n.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
|
||||
Returning("?", bun.Ident("id"))
|
||||
|
||||
if targetAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("notification.target_account_id"), targetAccountID)
|
||||
}
|
||||
|
||||
if originAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("notification.origin_account_id"), originAccountID)
|
||||
}
|
||||
|
||||
if _, err := q.Exec(ctx, &ids); err != nil {
|
||||
return n.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
// Invalidate each returned ID.
|
||||
for _, id := range ids {
|
||||
n.state.Caches.GTS.Notification().Invalidate("ID", id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *notificationDB) DeleteNotificationsForStatus(ctx context.Context, statusID string) db.Error {
|
||||
// Capture notification IDs in a RETURNING statement.
|
||||
ids := []string{}
|
||||
|
||||
q := n.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
|
||||
Where("? = ?", bun.Ident("notification.status_id"), statusID).
|
||||
Returning("?", bun.Ident("id"))
|
||||
|
||||
if _, err := q.Exec(ctx, &ids); err != nil {
|
||||
return n.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
// Invalidate each returned ID.
|
||||
for _, id := range ids {
|
||||
n.state.Caches.GTS.Notification().Invalidate("ID", id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,13 @@ package bundb_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
|
|
@ -112,10 +114,10 @@ func (suite *NotificationTestSuite) TestGetNotificationsWithoutSpam() {
|
|||
}
|
||||
}
|
||||
|
||||
func (suite *NotificationTestSuite) TestClearNotificationsWithSpam() {
|
||||
func (suite *NotificationTestSuite) TestDeleteNotificationsWithSpam() {
|
||||
suite.spamNotifs()
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
err := suite.db.ClearNotifications(context.Background(), testAccount.ID)
|
||||
err := suite.db.DeleteNotifications(context.Background(), testAccount.ID, "")
|
||||
suite.NoError(err)
|
||||
|
||||
notifications, err := suite.db.GetNotifications(context.Background(), testAccount.ID, []string{}, 20, id.Highest, id.Lowest)
|
||||
|
|
@ -124,10 +126,10 @@ func (suite *NotificationTestSuite) TestClearNotificationsWithSpam() {
|
|||
suite.Empty(notifications)
|
||||
}
|
||||
|
||||
func (suite *NotificationTestSuite) TestClearNotificationsWithTwoAccounts() {
|
||||
func (suite *NotificationTestSuite) TestDeleteNotificationsWithTwoAccounts() {
|
||||
suite.spamNotifs()
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
err := suite.db.ClearNotifications(context.Background(), testAccount.ID)
|
||||
err := suite.db.DeleteNotifications(context.Background(), testAccount.ID, "")
|
||||
suite.NoError(err)
|
||||
|
||||
notifications, err := suite.db.GetNotifications(context.Background(), testAccount.ID, []string{}, 20, id.Highest, id.Lowest)
|
||||
|
|
@ -141,6 +143,69 @@ func (suite *NotificationTestSuite) TestClearNotificationsWithTwoAccounts() {
|
|||
suite.NotEmpty(notif)
|
||||
}
|
||||
|
||||
func (suite *NotificationTestSuite) TestDeleteNotificationsOriginatingFromAccount() {
|
||||
testAccount := suite.testAccounts["local_account_2"]
|
||||
|
||||
if err := suite.db.DeleteNotifications(context.Background(), "", testAccount.ID); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
notif := []*gtsmodel.Notification{}
|
||||
if err := suite.db.GetAll(context.Background(), ¬if); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, n := range notif {
|
||||
if n.OriginAccountID == testAccount.ID {
|
||||
suite.FailNowf("", "no notifications with origin account id %s should remain", testAccount.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *NotificationTestSuite) TestDeleteNotificationsOriginatingFromAndTargetingAccount() {
|
||||
originAccount := suite.testAccounts["local_account_2"]
|
||||
targetAccount := suite.testAccounts["admin_account"]
|
||||
|
||||
if err := suite.db.DeleteNotifications(context.Background(), targetAccount.ID, originAccount.ID); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
notif := []*gtsmodel.Notification{}
|
||||
if err := suite.db.GetAll(context.Background(), ¬if); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, n := range notif {
|
||||
if n.OriginAccountID == originAccount.ID || n.TargetAccountID == targetAccount.ID {
|
||||
suite.FailNowf(
|
||||
"",
|
||||
"no notifications with origin account id %s and target account %s should remain",
|
||||
originAccount.ID,
|
||||
targetAccount.ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *NotificationTestSuite) TestDeleteNotificationsPertainingToStatusID() {
|
||||
testStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
|
||||
if err := suite.db.DeleteNotificationsForStatus(context.Background(), testStatus.ID); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
notif := []*gtsmodel.Notification{}
|
||||
if err := suite.db.GetAll(context.Background(), ¬if); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, n := range notif {
|
||||
if n.StatusID == testStatus.ID {
|
||||
suite.FailNowf("", "no notifications with status id %s should remain", testStatus.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotificationTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(NotificationTestSuite))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ package bundb
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
|
@ -34,14 +34,6 @@ type relationshipDB struct {
|
|||
state *state.State
|
||||
}
|
||||
|
||||
func (r *relationshipDB) newFollowQ(follow interface{}) *bun.SelectQuery {
|
||||
return r.conn.
|
||||
NewSelect().
|
||||
Model(follow).
|
||||
Relation("Account").
|
||||
Relation("TargetAccount")
|
||||
}
|
||||
|
||||
func (r *relationshipDB) IsBlocked(ctx context.Context, account1 string, account2 string, eitherDirection bool) (bool, db.Error) {
|
||||
// Look for a block in direction of account1->account2
|
||||
block1, err := r.getBlock(ctx, account1, account2)
|
||||
|
|
@ -305,169 +297,340 @@ func (r *relationshipDB) IsMutualFollowing(ctx context.Context, account1 *gtsmod
|
|||
}
|
||||
|
||||
func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.Follow, db.Error) {
|
||||
var follow *gtsmodel.Follow
|
||||
|
||||
if err := r.conn.RunInTx(ctx, func(tx bun.Tx) error {
|
||||
// get original follow request
|
||||
followRequest := >smodel.FollowRequest{}
|
||||
if err := tx.
|
||||
NewSelect().
|
||||
Model(followRequest).
|
||||
Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID).
|
||||
Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID).
|
||||
Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create a new follow to 'replace' the request with
|
||||
follow = >smodel.Follow{
|
||||
ID: followRequest.ID,
|
||||
AccountID: originAccountID,
|
||||
TargetAccountID: targetAccountID,
|
||||
URI: followRequest.URI,
|
||||
}
|
||||
|
||||
// if the follow already exists, just update the URI -- we don't need to do anything else
|
||||
if _, err := tx.
|
||||
NewInsert().
|
||||
Model(follow).
|
||||
On("CONFLICT (?,?) DO UPDATE set ? = ?", bun.Ident("account_id"), bun.Ident("target_account_id"), bun.Ident("uri"), follow.URI).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// now remove the follow request
|
||||
if _, err := tx.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||
Where("? = ?", bun.Ident("follow_request.id"), followRequest.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
// Get original follow request.
|
||||
var followRequestID string
|
||||
if err := r.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||
Column("follow_request.id").
|
||||
Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID).
|
||||
Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID).
|
||||
Scan(ctx, &followRequestID); err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
followRequest, err := r.getFollowRequest(ctx, followRequestID)
|
||||
if err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
// Create a new follow to 'replace'
|
||||
// the original follow request with.
|
||||
follow := >smodel.Follow{
|
||||
ID: followRequest.ID,
|
||||
AccountID: originAccountID,
|
||||
Account: followRequest.Account,
|
||||
TargetAccountID: targetAccountID,
|
||||
TargetAccount: followRequest.TargetAccount,
|
||||
URI: followRequest.URI,
|
||||
}
|
||||
|
||||
// If the follow already exists, just
|
||||
// replace the URI with the new one.
|
||||
if _, err := r.conn.
|
||||
NewInsert().
|
||||
Model(follow).
|
||||
On("CONFLICT (?,?) DO UPDATE set ? = ?", bun.Ident("account_id"), bun.Ident("target_account_id"), bun.Ident("uri"), follow.URI).
|
||||
Exec(ctx); err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
// Delete original follow request.
|
||||
if _, err := r.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||
Where("? = ?", bun.Ident("follow_request.id"), followRequest.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
// Delete original follow request notification.
|
||||
if err := r.deleteFollowRequestNotif(ctx, originAccountID, targetAccountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// return the new follow
|
||||
return follow, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) RejectFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.FollowRequest, db.Error) {
|
||||
followRequest := >smodel.FollowRequest{}
|
||||
|
||||
if err := r.conn.RunInTx(ctx, func(tx bun.Tx) error {
|
||||
// get original follow request
|
||||
if err := tx.
|
||||
NewSelect().
|
||||
Model(followRequest).
|
||||
Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID).
|
||||
Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID).
|
||||
Scan(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// now delete it from the database by ID
|
||||
if _, err := tx.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||
Where("? = ?", bun.Ident("follow_request.id"), followRequest.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
// Get original follow request.
|
||||
var followRequestID string
|
||||
if err := r.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||
Column("follow_request.id").
|
||||
Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID).
|
||||
Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID).
|
||||
Scan(ctx, &followRequestID); err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
// return the deleted follow request
|
||||
followRequest, err := r.getFollowRequest(ctx, followRequestID)
|
||||
if err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
// Delete original follow request.
|
||||
if _, err := r.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||
Where("? = ?", bun.Ident("follow_request.id"), followRequest.ID).
|
||||
Exec(ctx); err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
// Delete original follow request notification.
|
||||
if err := r.deleteFollowRequestNotif(ctx, originAccountID, targetAccountID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return the now deleted follow request.
|
||||
return followRequest, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) GetAccountFollowRequests(ctx context.Context, accountID string) ([]*gtsmodel.FollowRequest, db.Error) {
|
||||
followRequests := []*gtsmodel.FollowRequest{}
|
||||
func (r *relationshipDB) deleteFollowRequestNotif(ctx context.Context, originAccountID string, targetAccountID string) db.Error {
|
||||
var id string
|
||||
if err := r.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
|
||||
Column("notification.id").
|
||||
Where("? = ?", bun.Ident("notification.origin_account_id"), originAccountID).
|
||||
Where("? = ?", bun.Ident("notification.target_account_id"), targetAccountID).
|
||||
Where("? = ?", bun.Ident("notification.notification_type"), gtsmodel.NotificationFollowRequest).
|
||||
Limit(1). // There should only be one!
|
||||
Scan(ctx, &id); err != nil {
|
||||
err = r.conn.ProcessError(err)
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
// If no entries, the notif didn't
|
||||
// exist anyway so nothing to do here.
|
||||
return nil
|
||||
}
|
||||
// Return on real error.
|
||||
return err
|
||||
}
|
||||
|
||||
q := r.newFollowQ(&followRequests).
|
||||
Where("? = ?", bun.Ident("follow_request.target_account_id"), accountID).
|
||||
Order("follow_request.updated_at DESC")
|
||||
return r.state.DB.DeleteNotification(ctx, id)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx); err != nil {
|
||||
func (r *relationshipDB) getFollow(ctx context.Context, id string) (*gtsmodel.Follow, db.Error) {
|
||||
follow := >smodel.Follow{}
|
||||
|
||||
err := r.conn.
|
||||
NewSelect().
|
||||
Model(follow).
|
||||
Where("? = ?", bun.Ident("follow.id"), id).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
follow.Account, err = r.state.DB.GetAccountByID(ctx, follow.AccountID)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting follow account %q: %v", follow.AccountID, err)
|
||||
}
|
||||
|
||||
follow.TargetAccount, err = r.state.DB.GetAccountByID(ctx, follow.TargetAccountID)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting follow target account %q: %v", follow.TargetAccountID, err)
|
||||
}
|
||||
|
||||
return follow, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) GetLocalFollowersIDs(ctx context.Context, targetAccountID string) ([]string, db.Error) {
|
||||
accountIDs := []string{}
|
||||
|
||||
// Select only the account ID of each follow.
|
||||
q := r.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow")).
|
||||
ColumnExpr("? AS ?", bun.Ident("follow.account_id"), bun.Ident("account_id")).
|
||||
Where("? = ?", bun.Ident("follow.target_account_id"), targetAccountID)
|
||||
|
||||
// Join on accounts table to select only
|
||||
// those with NULL domain (local accounts).
|
||||
q = q.
|
||||
Join("JOIN ? AS ? ON ? = ?",
|
||||
bun.Ident("accounts"),
|
||||
bun.Ident("account"),
|
||||
bun.Ident("follow.account_id"),
|
||||
bun.Ident("account.id"),
|
||||
).
|
||||
Where("? IS NULL", bun.Ident("account.domain"))
|
||||
|
||||
// We don't *really* need to order these,
|
||||
// but it makes it more consistent to do so.
|
||||
q = q.Order("account_id DESC")
|
||||
|
||||
if err := q.Scan(ctx, &accountIDs); err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return accountIDs, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) GetFollows(ctx context.Context, accountID string, targetAccountID string) ([]*gtsmodel.Follow, db.Error) {
|
||||
ids := []string{}
|
||||
|
||||
q := r.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow")).
|
||||
Column("follow.id").
|
||||
Order("follow.updated_at DESC")
|
||||
|
||||
if accountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("follow.account_id"), accountID)
|
||||
}
|
||||
|
||||
if targetAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("follow.target_account_id"), targetAccountID)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &ids); err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
follows := make([]*gtsmodel.Follow, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
follow, err := r.getFollow(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting follow %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
follows = append(follows, follow)
|
||||
}
|
||||
|
||||
return follows, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) CountFollows(ctx context.Context, accountID string, targetAccountID string) (int, db.Error) {
|
||||
q := r.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow")).
|
||||
Column("follow.id")
|
||||
|
||||
if accountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("follow.account_id"), accountID)
|
||||
}
|
||||
|
||||
if targetAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("follow.target_account_id"), targetAccountID)
|
||||
}
|
||||
|
||||
return q.Count(ctx)
|
||||
}
|
||||
|
||||
func (r *relationshipDB) getFollowRequest(ctx context.Context, id string) (*gtsmodel.FollowRequest, db.Error) {
|
||||
followRequest := >smodel.FollowRequest{}
|
||||
|
||||
err := r.conn.
|
||||
NewSelect().
|
||||
Model(followRequest).
|
||||
Where("? = ?", bun.Ident("follow_request.id"), id).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
followRequest.Account, err = r.state.DB.GetAccountByID(ctx, followRequest.AccountID)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting follow request account %q: %v", followRequest.AccountID, err)
|
||||
}
|
||||
|
||||
followRequest.TargetAccount, err = r.state.DB.GetAccountByID(ctx, followRequest.TargetAccountID)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting follow request target account %q: %v", followRequest.TargetAccountID, err)
|
||||
}
|
||||
|
||||
return followRequest, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) GetFollowRequests(ctx context.Context, accountID string, targetAccountID string) ([]*gtsmodel.FollowRequest, db.Error) {
|
||||
ids := []string{}
|
||||
|
||||
q := r.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||
Column("follow_request.id")
|
||||
|
||||
if accountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("follow_request.account_id"), accountID)
|
||||
}
|
||||
|
||||
if targetAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &ids); err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
followRequests := make([]*gtsmodel.FollowRequest, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
followRequest, err := r.getFollowRequest(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting follow request %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
followRequests = append(followRequests, followRequest)
|
||||
}
|
||||
|
||||
return followRequests, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) GetAccountFollows(ctx context.Context, accountID string) ([]*gtsmodel.Follow, db.Error) {
|
||||
follows := []*gtsmodel.Follow{}
|
||||
|
||||
q := r.newFollowQ(&follows).
|
||||
Where("? = ?", bun.Ident("follow.account_id"), accountID).
|
||||
Order("follow.updated_at DESC")
|
||||
|
||||
if err := q.Scan(ctx); err != nil {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return follows, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) CountAccountFollows(ctx context.Context, accountID string, localOnly bool) (int, db.Error) {
|
||||
func (r *relationshipDB) CountFollowRequests(ctx context.Context, accountID string, targetAccountID string) (int, db.Error) {
|
||||
q := r.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow"))
|
||||
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||
Column("follow_request.id").
|
||||
Order("follow_request.updated_at DESC")
|
||||
|
||||
if localOnly {
|
||||
q = q.
|
||||
Join("JOIN ? AS ? ON ? = ?", bun.Ident("accounts"), bun.Ident("account"), bun.Ident("follow.target_account_id"), bun.Ident("account.id")).
|
||||
Where("? = ?", bun.Ident("follow.account_id"), accountID).
|
||||
Where("? IS NULL", bun.Ident("account.domain"))
|
||||
} else {
|
||||
q = q.Where("? = ?", bun.Ident("follow.account_id"), accountID)
|
||||
if accountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("follow_request.account_id"), accountID)
|
||||
}
|
||||
|
||||
if targetAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID)
|
||||
}
|
||||
|
||||
return q.Count(ctx)
|
||||
}
|
||||
|
||||
func (r *relationshipDB) GetAccountFollowedBy(ctx context.Context, accountID string, localOnly bool) ([]*gtsmodel.Follow, db.Error) {
|
||||
follows := []*gtsmodel.Follow{}
|
||||
func (r *relationshipDB) Unfollow(ctx context.Context, originAccountID string, targetAccountID string) (string, db.Error) {
|
||||
uri := new(string)
|
||||
|
||||
q := r.conn.
|
||||
NewSelect().
|
||||
Model(&follows).
|
||||
Order("follow.updated_at DESC")
|
||||
_, err := r.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow")).
|
||||
Where("? = ?", bun.Ident("follow.target_account_id"), targetAccountID).
|
||||
Where("? = ?", bun.Ident("follow.account_id"), originAccountID).
|
||||
Returning("?", bun.Ident("uri")).Exec(ctx, uri)
|
||||
|
||||
if localOnly {
|
||||
q = q.
|
||||
Join("JOIN ? AS ? ON ? = ?", bun.Ident("accounts"), bun.Ident("account"), bun.Ident("follow.account_id"), bun.Ident("account.id")).
|
||||
Where("? = ?", bun.Ident("follow.target_account_id"), accountID).
|
||||
Where("? IS NULL", bun.Ident("account.domain"))
|
||||
} else {
|
||||
q = q.Where("? = ?", bun.Ident("follow.target_account_id"), accountID)
|
||||
// Only return proper errors.
|
||||
if err = r.conn.ProcessError(err); err != db.ErrNoEntries {
|
||||
return *uri, err
|
||||
}
|
||||
|
||||
err := q.Scan(ctx)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, r.conn.ProcessError(err)
|
||||
}
|
||||
return follows, nil
|
||||
return *uri, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) CountAccountFollowedBy(ctx context.Context, accountID string, localOnly bool) (int, db.Error) {
|
||||
q := r.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow"))
|
||||
func (r *relationshipDB) UnfollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (string, db.Error) {
|
||||
uri := new(string)
|
||||
|
||||
if localOnly {
|
||||
q = q.
|
||||
Join("JOIN ? AS ? ON ? = ?", bun.Ident("accounts"), bun.Ident("account"), bun.Ident("follow.account_id"), bun.Ident("account.id")).
|
||||
Where("? = ?", bun.Ident("follow.target_account_id"), accountID).
|
||||
Where("? IS NULL", bun.Ident("account.domain"))
|
||||
} else {
|
||||
q = q.Where("? = ?", bun.Ident("follow.target_account_id"), accountID)
|
||||
_, err := r.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||
Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID).
|
||||
Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID).
|
||||
Returning("?", bun.Ident("uri")).Exec(ctx, uri)
|
||||
|
||||
// Only return proper errors.
|
||||
if err = r.conn.ProcessError(err); err != db.ErrNoEntries {
|
||||
return *uri, err
|
||||
}
|
||||
|
||||
return q.Count(ctx)
|
||||
return *uri, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,6 +289,47 @@ func (suite *RelationshipTestSuite) TestAcceptFollowRequestOK() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
followRequestNotification := >smodel.Notification{
|
||||
ID: "01GV8MY1Q9KX2ZSWN4FAQ3V1PB",
|
||||
OriginAccountID: account.ID,
|
||||
TargetAccountID: targetAccount.ID,
|
||||
NotificationType: gtsmodel.NotificationFollowRequest,
|
||||
}
|
||||
|
||||
if err := suite.db.Put(ctx, followRequestNotification); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
follow, err := suite.db.AcceptFollowRequest(ctx, account.ID, targetAccount.ID)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(follow)
|
||||
suite.Equal(followRequest.URI, follow.URI)
|
||||
|
||||
// Ensure notification is deleted.
|
||||
notification, err := suite.db.GetNotification(ctx, followRequestNotification.ID)
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
suite.Nil(notification)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestAcceptFollowRequestNoNotification() {
|
||||
ctx := context.Background()
|
||||
account := suite.testAccounts["admin_account"]
|
||||
targetAccount := suite.testAccounts["local_account_2"]
|
||||
|
||||
followRequest := >smodel.FollowRequest{
|
||||
ID: "01GEF753FWHCHRDWR0QEHBXM8W",
|
||||
URI: "http://localhost:8080/weeeeeeeeeeeeeeeee",
|
||||
AccountID: account.ID,
|
||||
TargetAccountID: targetAccount.ID,
|
||||
}
|
||||
|
||||
if err := suite.db.Put(ctx, followRequest); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Unlike the above test, don't create a notification.
|
||||
// Follow request accept should still produce no error.
|
||||
|
||||
follow, err := suite.db.AcceptFollowRequest(ctx, account.ID, targetAccount.ID)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(follow)
|
||||
|
|
@ -352,9 +393,25 @@ func (suite *RelationshipTestSuite) TestRejectFollowRequestOK() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
followRequestNotification := >smodel.Notification{
|
||||
ID: "01GV8MY1Q9KX2ZSWN4FAQ3V1PB",
|
||||
OriginAccountID: account.ID,
|
||||
TargetAccountID: targetAccount.ID,
|
||||
NotificationType: gtsmodel.NotificationFollowRequest,
|
||||
}
|
||||
|
||||
if err := suite.db.Put(ctx, followRequestNotification); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
rejectedFollowRequest, err := suite.db.RejectFollowRequest(ctx, account.ID, targetAccount.ID)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(rejectedFollowRequest)
|
||||
|
||||
// Ensure notification is deleted.
|
||||
notification, err := suite.db.GetNotification(ctx, followRequestNotification.ID)
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
suite.Nil(notification)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestRejectFollowRequestNotExisting() {
|
||||
|
|
@ -383,58 +440,92 @@ func (suite *RelationshipTestSuite) TestGetAccountFollowRequests() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
followRequests, err := suite.db.GetAccountFollowRequests(ctx, targetAccount.ID)
|
||||
followRequests, err := suite.db.GetFollowRequests(ctx, "", targetAccount.ID)
|
||||
suite.NoError(err)
|
||||
suite.Len(followRequests, 1)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestGetAccountFollows() {
|
||||
account := suite.testAccounts["local_account_1"]
|
||||
follows, err := suite.db.GetAccountFollows(context.Background(), account.ID)
|
||||
follows, err := suite.db.GetFollows(context.Background(), account.ID, "")
|
||||
suite.NoError(err)
|
||||
suite.Len(follows, 2)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestCountAccountFollowsLocalOnly() {
|
||||
account := suite.testAccounts["local_account_1"]
|
||||
followsCount, err := suite.db.CountAccountFollows(context.Background(), account.ID, true)
|
||||
suite.NoError(err)
|
||||
suite.Equal(2, followsCount)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestCountAccountFollows() {
|
||||
account := suite.testAccounts["local_account_1"]
|
||||
followsCount, err := suite.db.CountAccountFollows(context.Background(), account.ID, false)
|
||||
followsCount, err := suite.db.CountFollows(context.Background(), account.ID, "")
|
||||
suite.NoError(err)
|
||||
suite.Equal(2, followsCount)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestGetAccountFollowedBy() {
|
||||
account := suite.testAccounts["local_account_1"]
|
||||
follows, err := suite.db.GetAccountFollowedBy(context.Background(), account.ID, false)
|
||||
follows, err := suite.db.GetFollows(context.Background(), "", account.ID)
|
||||
suite.NoError(err)
|
||||
suite.Len(follows, 2)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestGetAccountFollowedByLocalOnly() {
|
||||
func (suite *RelationshipTestSuite) TestGetLocalFollowersIDs() {
|
||||
account := suite.testAccounts["local_account_1"]
|
||||
follows, err := suite.db.GetAccountFollowedBy(context.Background(), account.ID, true)
|
||||
accountIDs, err := suite.db.GetLocalFollowersIDs(context.Background(), account.ID)
|
||||
suite.NoError(err)
|
||||
suite.Len(follows, 2)
|
||||
suite.EqualValues([]string{"01F8MH5NBDF2MV7CTC4Q5128HF", "01F8MH17FWEB39HZJ76B6VXSKF"}, accountIDs)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestCountAccountFollowedBy() {
|
||||
account := suite.testAccounts["local_account_1"]
|
||||
followsCount, err := suite.db.CountAccountFollowedBy(context.Background(), account.ID, false)
|
||||
followsCount, err := suite.db.CountFollows(context.Background(), "", account.ID)
|
||||
suite.NoError(err)
|
||||
suite.Equal(2, followsCount)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestCountAccountFollowedByLocalOnly() {
|
||||
account := suite.testAccounts["local_account_1"]
|
||||
followsCount, err := suite.db.CountAccountFollowedBy(context.Background(), account.ID, true)
|
||||
func (suite *RelationshipTestSuite) TestUnfollowExisting() {
|
||||
originAccount := suite.testAccounts["local_account_1"]
|
||||
targetAccount := suite.testAccounts["admin_account"]
|
||||
|
||||
uri, err := suite.db.Unfollow(context.Background(), originAccount.ID, targetAccount.ID)
|
||||
suite.NoError(err)
|
||||
suite.Equal(2, followsCount)
|
||||
suite.Equal("http://localhost:8080/users/the_mighty_zork/follow/01F8PY8RHWRQZV038T4E8T9YK8", uri)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestUnfollowNotExisting() {
|
||||
originAccount := suite.testAccounts["local_account_1"]
|
||||
targetAccountID := "01GTVD9N484CZ6AM90PGGNY7GQ"
|
||||
|
||||
uri, err := suite.db.Unfollow(context.Background(), originAccount.ID, targetAccountID)
|
||||
suite.NoError(err)
|
||||
suite.Empty(uri)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestUnfollowRequestExisting() {
|
||||
ctx := context.Background()
|
||||
originAccount := suite.testAccounts["admin_account"]
|
||||
targetAccount := suite.testAccounts["local_account_2"]
|
||||
|
||||
followRequest := >smodel.FollowRequest{
|
||||
ID: "01GEF753FWHCHRDWR0QEHBXM8W",
|
||||
URI: "http://localhost:8080/weeeeeeeeeeeeeeeee",
|
||||
AccountID: originAccount.ID,
|
||||
TargetAccountID: targetAccount.ID,
|
||||
}
|
||||
|
||||
if err := suite.db.Put(ctx, followRequest); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
uri, err := suite.db.UnfollowRequest(context.Background(), originAccount.ID, targetAccount.ID)
|
||||
suite.NoError(err)
|
||||
suite.Equal("http://localhost:8080/weeeeeeeeeeeeeeeee", uri)
|
||||
}
|
||||
|
||||
func (suite *RelationshipTestSuite) TestUnfollowRequestNotExisting() {
|
||||
originAccount := suite.testAccounts["local_account_1"]
|
||||
targetAccountID := "01GTVD9N484CZ6AM90PGGNY7GQ"
|
||||
|
||||
uri, err := suite.db.UnfollowRequest(context.Background(), originAccount.ID, targetAccountID)
|
||||
suite.NoError(err)
|
||||
suite.Empty(uri)
|
||||
}
|
||||
|
||||
func TestRelationshipTestSuite(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -46,15 +46,6 @@ func (s *statusDB) newStatusQ(status interface{}) *bun.SelectQuery {
|
|||
Relation("CreatedWithApplication")
|
||||
}
|
||||
|
||||
func (s *statusDB) newFaveQ(faves interface{}) *bun.SelectQuery {
|
||||
return s.conn.
|
||||
NewSelect().
|
||||
Model(faves).
|
||||
Relation("Account").
|
||||
Relation("TargetAccount").
|
||||
Relation("Status")
|
||||
}
|
||||
|
||||
func (s *statusDB) GetStatusByID(ctx context.Context, id string) (*gtsmodel.Status, db.Error) {
|
||||
return s.getStatus(
|
||||
ctx,
|
||||
|
|
@ -542,20 +533,6 @@ func (s *statusDB) IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.St
|
|||
return s.conn.Exists(ctx, q)
|
||||
}
|
||||
|
||||
func (s *statusDB) GetStatusFaves(ctx context.Context, status *gtsmodel.Status) ([]*gtsmodel.StatusFave, db.Error) {
|
||||
faves := []*gtsmodel.StatusFave{}
|
||||
|
||||
q := s.
|
||||
newFaveQ(&faves).
|
||||
Where("? = ?", bun.Ident("status_fave.status_id"), status.ID)
|
||||
|
||||
if err := q.Scan(ctx); err != nil {
|
||||
return nil, s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return faves, nil
|
||||
}
|
||||
|
||||
func (s *statusDB) GetStatusReblogs(ctx context.Context, status *gtsmodel.Status) ([]*gtsmodel.Status, db.Error) {
|
||||
reblogs := []*gtsmodel.Status{}
|
||||
|
||||
|
|
|
|||
198
internal/db/bundb/statusbookmark.go
Normal file
198
internal/db/bundb/statusbookmark.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
// 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"
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type statusBookmarkDB struct {
|
||||
conn *DBConn
|
||||
state *state.State
|
||||
}
|
||||
|
||||
func (s *statusBookmarkDB) GetStatusBookmark(ctx context.Context, id string) (*gtsmodel.StatusBookmark, db.Error) {
|
||||
bookmark := new(gtsmodel.StatusBookmark)
|
||||
|
||||
err := s.conn.
|
||||
NewSelect().
|
||||
Model(bookmark).
|
||||
Where("? = ?", bun.Ident("status_bookmark.ID"), id).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
bookmark.Account, err = s.state.DB.GetAccountByID(ctx, bookmark.AccountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting status bookmark account %q: %w", bookmark.AccountID, err)
|
||||
}
|
||||
|
||||
bookmark.TargetAccount, err = s.state.DB.GetAccountByID(ctx, bookmark.TargetAccountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting status bookmark target account %q: %w", bookmark.TargetAccountID, err)
|
||||
}
|
||||
|
||||
bookmark.Status, err = s.state.DB.GetStatusByID(ctx, bookmark.StatusID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting status bookmark status %q: %w", bookmark.StatusID, err)
|
||||
}
|
||||
|
||||
return bookmark, nil
|
||||
}
|
||||
|
||||
func (s *statusBookmarkDB) GetStatusBookmarkID(ctx context.Context, accountID string, statusID string) (string, db.Error) {
|
||||
var id string
|
||||
|
||||
q := s.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")).
|
||||
Column("status_bookmark.id").
|
||||
Where("? = ?", bun.Ident("status_bookmark.account_id"), accountID).
|
||||
Where("? = ?", bun.Ident("status_bookmark.status_id"), statusID).
|
||||
Limit(1)
|
||||
|
||||
if err := q.Scan(ctx, &id); err != nil {
|
||||
return "", s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *statusBookmarkDB) GetStatusBookmarks(ctx context.Context, accountID string, limit int, maxID string, minID string) ([]*gtsmodel.StatusBookmark, db.Error) {
|
||||
// Ensure reasonable
|
||||
if limit < 0 {
|
||||
limit = 0
|
||||
}
|
||||
|
||||
// Guess size of IDs based on limit.
|
||||
ids := make([]string, 0, limit)
|
||||
|
||||
q := s.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")).
|
||||
Column("status_bookmark.id").
|
||||
Where("? = ?", bun.Ident("status_bookmark.account_id"), accountID).
|
||||
Order("status_bookmark.id DESC")
|
||||
|
||||
if accountID == "" {
|
||||
return nil, errors.New("must provide an account")
|
||||
}
|
||||
|
||||
if maxID != "" {
|
||||
q = q.Where("? < ?", bun.Ident("status_bookmark.id"), maxID)
|
||||
}
|
||||
|
||||
if minID != "" {
|
||||
q = q.Where("? > ?", bun.Ident("status_bookmark.id"), minID)
|
||||
}
|
||||
|
||||
if limit != 0 {
|
||||
q = q.Limit(limit)
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &ids); err != nil {
|
||||
return nil, s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
bookmarks := make([]*gtsmodel.StatusBookmark, 0, len(ids))
|
||||
|
||||
for _, id := range ids {
|
||||
bookmark, err := s.GetStatusBookmark(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting bookmark %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
bookmarks = append(bookmarks, bookmark)
|
||||
}
|
||||
|
||||
return bookmarks, nil
|
||||
}
|
||||
|
||||
func (s *statusBookmarkDB) PutStatusBookmark(ctx context.Context, statusBookmark *gtsmodel.StatusBookmark) db.Error {
|
||||
_, err := s.conn.
|
||||
NewInsert().
|
||||
Model(statusBookmark).
|
||||
Exec(ctx)
|
||||
|
||||
return s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
func (s *statusBookmarkDB) DeleteStatusBookmark(ctx context.Context, id string) db.Error {
|
||||
_, err := s.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")).
|
||||
Where("? = ?", bun.Ident("status_bookmark.id"), id).
|
||||
Exec(ctx)
|
||||
|
||||
return s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
func (s *statusBookmarkDB) DeleteStatusBookmarks(ctx context.Context, targetAccountID string, originAccountID string) db.Error {
|
||||
if targetAccountID == "" && originAccountID == "" {
|
||||
return errors.New("DeleteBookmarks: one of targetAccountID or originAccountID must be set")
|
||||
}
|
||||
|
||||
// TODO: Capture bookmark IDs in a RETURNING
|
||||
// statement (when bookmarks have a cache),
|
||||
// + use the IDs to invalidate cache entries.
|
||||
|
||||
q := s.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark"))
|
||||
|
||||
if targetAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("status_bookmark.target_account_id"), targetAccountID)
|
||||
}
|
||||
|
||||
if originAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("status_bookmark.account_id"), originAccountID)
|
||||
}
|
||||
|
||||
if _, err := q.Exec(ctx); err != nil {
|
||||
return s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *statusBookmarkDB) DeleteStatusBookmarksForStatus(ctx context.Context, statusID string) db.Error {
|
||||
// TODO: Capture bookmark IDs in a RETURNING
|
||||
// statement (when bookmarks have a cache),
|
||||
// + use the IDs to invalidate cache entries.
|
||||
|
||||
q := s.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")).
|
||||
Where("? = ?", bun.Ident("status_bookmark.status_id"), statusID)
|
||||
|
||||
if _, err := q.Exec(ctx); err != nil {
|
||||
return s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
129
internal/db/bundb/statusbookmark_test.go
Normal file
129
internal/db/bundb/statusbookmark_test.go
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
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 bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
type StatusBookmarkTestSuite struct {
|
||||
BunDBStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *StatusBookmarkTestSuite) TestGetStatusBookmarkIDOK() {
|
||||
testBookmark := suite.testBookmarks["local_account_1_admin_account_status_1"]
|
||||
|
||||
id, err := suite.db.GetStatusBookmarkID(context.Background(), testBookmark.AccountID, testBookmark.StatusID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(testBookmark.ID, id)
|
||||
}
|
||||
|
||||
func (suite *StatusBookmarkTestSuite) TestGetStatusBookmarkIDNonexisting() {
|
||||
id, err := suite.db.GetStatusBookmarkID(context.Background(), "01GVAVGD06YJ2FSB5GJSMF8M2K", "01GVAVGKGR1MK9ZN7JCJFYSFZV")
|
||||
suite.Empty(id)
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
}
|
||||
|
||||
func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarksOriginatingFromAccount() {
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
if err := suite.db.DeleteStatusBookmarks(context.Background(), "", testAccount.ID); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
bookmarks := []*gtsmodel.StatusBookmark{}
|
||||
if err := suite.db.GetAll(context.Background(), &bookmarks); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, b := range bookmarks {
|
||||
if b.AccountID == testAccount.ID {
|
||||
suite.FailNowf("", "no StatusBookmarks with account id %s should remain", testAccount.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarksTargetingAccount() {
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
if err := suite.db.DeleteStatusBookmarks(context.Background(), testAccount.ID, ""); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
bookmarks := []*gtsmodel.StatusBookmark{}
|
||||
if err := suite.db.GetAll(context.Background(), &bookmarks); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, b := range bookmarks {
|
||||
if b.TargetAccountID == testAccount.ID {
|
||||
suite.FailNowf("", "no StatusBookmarks with target account id %s should remain", testAccount.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarksTargetingStatus() {
|
||||
testStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
|
||||
if err := suite.db.DeleteStatusBookmarksForStatus(context.Background(), testStatus.ID); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
bookmarks := []*gtsmodel.StatusBookmark{}
|
||||
if err := suite.db.GetAll(context.Background(), &bookmarks); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, b := range bookmarks {
|
||||
if b.StatusID == testStatus.ID {
|
||||
suite.FailNowf("", "no StatusBookmarks with status id %s should remain", testStatus.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmark() {
|
||||
testBookmark := suite.testBookmarks["local_account_1_admin_account_status_1"]
|
||||
ctx := context.Background()
|
||||
|
||||
if err := suite.db.DeleteStatusBookmark(ctx, testBookmark.ID); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
bookmark, err := suite.db.GetStatusBookmark(ctx, testBookmark.ID)
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
suite.Nil(bookmark)
|
||||
}
|
||||
|
||||
func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarkNonExisting() {
|
||||
err := suite.db.DeleteStatusBookmark(context.Background(), "01GVAV715K6Y2SG9ZKS9ZA8G7G")
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
func TestStatusBookmarkTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusBookmarkTestSuite))
|
||||
}
|
||||
172
internal/db/bundb/statusfave.go
Normal file
172
internal/db/bundb/statusfave.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// 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"
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type statusFaveDB struct {
|
||||
conn *DBConn
|
||||
state *state.State
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) GetStatusFave(ctx context.Context, id string) (*gtsmodel.StatusFave, db.Error) {
|
||||
fave := new(gtsmodel.StatusFave)
|
||||
|
||||
err := s.conn.
|
||||
NewSelect().
|
||||
Model(fave).
|
||||
Where("? = ?", bun.Ident("status_fave.ID"), id).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
fave.Account, err = s.state.DB.GetAccountByID(ctx, fave.AccountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting status fave account %q: %w", fave.AccountID, err)
|
||||
}
|
||||
|
||||
fave.TargetAccount, err = s.state.DB.GetAccountByID(ctx, fave.TargetAccountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting status fave target account %q: %w", fave.TargetAccountID, err)
|
||||
}
|
||||
|
||||
fave.Status, err = s.state.DB.GetStatusByID(ctx, fave.StatusID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting status fave status %q: %w", fave.StatusID, err)
|
||||
}
|
||||
|
||||
return fave, nil
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) GetStatusFaveByAccountID(ctx context.Context, accountID string, statusID string) (*gtsmodel.StatusFave, db.Error) {
|
||||
var id string
|
||||
|
||||
err := s.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")).
|
||||
Column("status_fave.id").
|
||||
Where("? = ?", bun.Ident("status_fave.account_id"), accountID).
|
||||
Where("? = ?", bun.Ident("status_fave.status_id"), statusID).
|
||||
Scan(ctx, &id)
|
||||
if err != nil {
|
||||
return nil, s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return s.GetStatusFave(ctx, id)
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) GetStatusFaves(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, db.Error) {
|
||||
ids := []string{}
|
||||
|
||||
if err := s.conn.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")).
|
||||
Column("status_fave.id").
|
||||
Where("? = ?", bun.Ident("status_fave.status_id"), statusID).
|
||||
Scan(ctx, &ids); err != nil {
|
||||
return nil, s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
faves := make([]*gtsmodel.StatusFave, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
fave, err := s.GetStatusFave(ctx, id)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error getting status fave %q: %v", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
faves = append(faves, fave)
|
||||
}
|
||||
|
||||
return faves, nil
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) PutStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) db.Error {
|
||||
_, err := s.conn.
|
||||
NewInsert().
|
||||
Model(statusFave).
|
||||
Exec(ctx)
|
||||
|
||||
return s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) DeleteStatusFave(ctx context.Context, id string) db.Error {
|
||||
_, err := s.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")).
|
||||
Where("? = ?", bun.Ident("status_fave.id"), id).
|
||||
Exec(ctx)
|
||||
|
||||
return s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID string, originAccountID string) db.Error {
|
||||
if targetAccountID == "" && originAccountID == "" {
|
||||
return errors.New("DeleteStatusFaves: one of targetAccountID or originAccountID must be set")
|
||||
}
|
||||
|
||||
// TODO: Capture fave IDs in a RETURNING
|
||||
// statement (when faves have a cache),
|
||||
// + use the IDs to invalidate cache entries.
|
||||
|
||||
q := s.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave"))
|
||||
|
||||
if targetAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("status_fave.target_account_id"), targetAccountID)
|
||||
}
|
||||
|
||||
if originAccountID != "" {
|
||||
q = q.Where("? = ?", bun.Ident("status_fave.account_id"), originAccountID)
|
||||
}
|
||||
|
||||
if _, err := q.Exec(ctx); err != nil {
|
||||
return s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) DeleteStatusFavesForStatus(ctx context.Context, statusID string) db.Error {
|
||||
// TODO: Capture fave IDs in a RETURNING
|
||||
// statement (when faves have a cache),
|
||||
// + use the IDs to invalidate cache entries.
|
||||
|
||||
q := s.conn.
|
||||
NewDelete().
|
||||
TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")).
|
||||
Where("? = ?", bun.Ident("status_fave.status_id"), statusID)
|
||||
|
||||
if _, err := q.Exec(ctx); err != nil {
|
||||
return s.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
148
internal/db/bundb/statusfave_test.go
Normal file
148
internal/db/bundb/statusfave_test.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
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 bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
type StatusFaveTestSuite struct {
|
||||
BunDBStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *StatusFaveTestSuite) TestGetStatusFaves() {
|
||||
testStatus := suite.testStatuses["admin_account_status_1"]
|
||||
|
||||
faves, err := suite.db.GetStatusFaves(context.Background(), testStatus.ID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.NotEmpty(faves)
|
||||
for _, fave := range faves {
|
||||
suite.NotNil(fave.Account)
|
||||
suite.NotNil(fave.TargetAccount)
|
||||
suite.NotNil(fave.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusFaveTestSuite) TestGetStatusFavesNone() {
|
||||
testStatus := suite.testStatuses["admin_account_status_4"]
|
||||
|
||||
faves, err := suite.db.GetStatusFaves(context.Background(), testStatus.ID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Empty(faves)
|
||||
}
|
||||
|
||||
func (suite *StatusFaveTestSuite) TestGetStatusFaveByAccountID() {
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
testStatus := suite.testStatuses["admin_account_status_1"]
|
||||
|
||||
fave, err := suite.db.GetStatusFaveByAccountID(context.Background(), testAccount.ID, testStatus.ID)
|
||||
suite.NoError(err)
|
||||
suite.NotNil(fave)
|
||||
}
|
||||
|
||||
func (suite *StatusFaveTestSuite) TestDeleteStatusFavesOriginatingFromAccount() {
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
if err := suite.db.DeleteStatusFaves(context.Background(), "", testAccount.ID); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
faves := []*gtsmodel.StatusFave{}
|
||||
if err := suite.db.GetAll(context.Background(), &faves); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, b := range faves {
|
||||
if b.AccountID == testAccount.ID {
|
||||
suite.FailNowf("", "no StatusFaves with account id %s should remain", testAccount.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusFaveTestSuite) TestDeleteStatusFavesTargetingAccount() {
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
if err := suite.db.DeleteStatusFaves(context.Background(), testAccount.ID, ""); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
faves := []*gtsmodel.StatusFave{}
|
||||
if err := suite.db.GetAll(context.Background(), &faves); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, b := range faves {
|
||||
if b.TargetAccountID == testAccount.ID {
|
||||
suite.FailNowf("", "no StatusFaves with target account id %s should remain", testAccount.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusFaveTestSuite) TestDeleteStatusFavesTargetingStatus() {
|
||||
testStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
|
||||
if err := suite.db.DeleteStatusFavesForStatus(context.Background(), testStatus.ID); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
faves := []*gtsmodel.StatusFave{}
|
||||
if err := suite.db.GetAll(context.Background(), &faves); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
for _, b := range faves {
|
||||
if b.StatusID == testStatus.ID {
|
||||
suite.FailNowf("", "no StatusFaves with status id %s should remain", testStatus.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusFaveTestSuite) TestDeleteStatusFave() {
|
||||
testFave := suite.testFaves["local_account_1_admin_account_status_1"]
|
||||
ctx := context.Background()
|
||||
|
||||
if err := suite.db.DeleteStatusFave(ctx, testFave.ID); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
fave, err := suite.db.GetStatusFave(ctx, testFave.ID)
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
suite.Nil(fave)
|
||||
}
|
||||
|
||||
func (suite *StatusFaveTestSuite) TestDeleteStatusFaveNonExisting() {
|
||||
err := suite.db.DeleteStatusFave(context.Background(), "01GVAV715K6Y2SG9ZKS9ZA8G7G")
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
func TestStatusFaveTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusFaveTestSuite))
|
||||
}
|
||||
|
|
@ -43,6 +43,8 @@ type DB interface {
|
|||
Report
|
||||
Session
|
||||
Status
|
||||
StatusBookmark
|
||||
StatusFave
|
||||
Timeline
|
||||
User
|
||||
Tombstone
|
||||
|
|
|
|||
|
|
@ -29,8 +29,31 @@ type Notification interface {
|
|||
//
|
||||
// Returned notifications will be ordered ID descending (ie., highest/newest to lowest/oldest).
|
||||
GetNotifications(ctx context.Context, accountID string, excludeTypes []string, limit int, maxID string, sinceID string) ([]*gtsmodel.Notification, Error)
|
||||
|
||||
// GetNotification returns one notification according to its id.
|
||||
GetNotification(ctx context.Context, id string) (*gtsmodel.Notification, Error)
|
||||
// ClearNotifications deletes every notification that pertain to the given accountID.
|
||||
ClearNotifications(ctx context.Context, accountID string) Error
|
||||
|
||||
// DeleteNotification deletes one notification according to its id,
|
||||
// and removes that notification from the in-memory cache.
|
||||
DeleteNotification(ctx context.Context, id string) Error
|
||||
|
||||
// DeleteNotifications mass deletes notifications targeting targetAccountID
|
||||
// and/or originating from originAccountID.
|
||||
//
|
||||
// If targetAccountID is set and originAccountID isn't, all notifications
|
||||
// that target the given account will be deleted.
|
||||
//
|
||||
// If originAccountID is set and targetAccountID isn't, all notifications
|
||||
// originating from the given account will be deleted.
|
||||
//
|
||||
// If both are set, then notifications that target targetAccountID and
|
||||
// originate from originAccountID will be deleted.
|
||||
//
|
||||
// At least one parameter must not be an empty string.
|
||||
DeleteNotifications(ctx context.Context, targetAccountID string, originAccountID string) Error
|
||||
|
||||
// DeleteNotificationsForStatus deletes all notifications that relate to
|
||||
// the given statusID. This function is useful when a status has been deleted,
|
||||
// and so notifications relating to that status must also be deleted.
|
||||
DeleteNotificationsForStatus(ctx context.Context, statusID string) Error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,22 +73,61 @@ type Relationship interface {
|
|||
// The deleted follow request will be returned so that further processing can be done on it.
|
||||
RejectFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.FollowRequest, Error)
|
||||
|
||||
// GetAccountFollowRequests returns all follow requests targeting the given account.
|
||||
GetAccountFollowRequests(ctx context.Context, accountID string) ([]*gtsmodel.FollowRequest, Error)
|
||||
|
||||
// GetAccountFollows returns a slice of follows owned by the given accountID.
|
||||
GetAccountFollows(ctx context.Context, accountID string) ([]*gtsmodel.Follow, Error)
|
||||
|
||||
// CountAccountFollows returns the amount of accounts that the given accountID is following.
|
||||
// GetFollows returns a slice of follows owned by the given accountID, and/or
|
||||
// targeting the given account id.
|
||||
//
|
||||
// If localOnly is set to true, then only follows from *this instance* will be returned.
|
||||
CountAccountFollows(ctx context.Context, accountID string, localOnly bool) (int, Error)
|
||||
|
||||
// GetAccountFollowedBy fetches follows that target given accountID.
|
||||
// If accountID is set and targetAccountID isn't, then all follows created by
|
||||
// accountID will be returned.
|
||||
//
|
||||
// If localOnly is set to true, then only follows from *this instance* will be returned.
|
||||
GetAccountFollowedBy(ctx context.Context, accountID string, localOnly bool) ([]*gtsmodel.Follow, Error)
|
||||
// If targetAccountID is set and accountID isn't, then all follows targeting
|
||||
// targetAccountID will be returned.
|
||||
//
|
||||
// If both accountID and targetAccountID are set, then only 0 or 1 follows will
|
||||
// be in the returned slice.
|
||||
GetFollows(ctx context.Context, accountID string, targetAccountID string) ([]*gtsmodel.Follow, Error)
|
||||
|
||||
// CountAccountFollowedBy returns the amounts that the given ID is followed by.
|
||||
CountAccountFollowedBy(ctx context.Context, accountID string, localOnly bool) (int, Error)
|
||||
// GetLocalFollowersIDs returns a list of local account IDs which follow the
|
||||
// targetAccountID. The returned IDs are not guaranteed to be ordered in any
|
||||
// particular way, so take care.
|
||||
GetLocalFollowersIDs(ctx context.Context, targetAccountID string) ([]string, Error)
|
||||
|
||||
// CountFollows is like GetFollows, but just counts rather than returning.
|
||||
CountFollows(ctx context.Context, accountID string, targetAccountID string) (int, Error)
|
||||
|
||||
// GetFollowRequests returns a slice of follows requests owned by the given
|
||||
// accountID, and/or targeting the given account id.
|
||||
//
|
||||
// If accountID is set and targetAccountID isn't, then all requests created by
|
||||
// accountID will be returned.
|
||||
//
|
||||
// If targetAccountID is set and accountID isn't, then all requests targeting
|
||||
// targetAccountID will be returned.
|
||||
//
|
||||
// If both accountID and targetAccountID are set, then only 0 or 1 requests will
|
||||
// be in the returned slice.
|
||||
GetFollowRequests(ctx context.Context, accountID string, targetAccountID string) ([]*gtsmodel.FollowRequest, Error)
|
||||
|
||||
// CountFollowRequests is like GetFollowRequests, but just counts rather than returning.
|
||||
CountFollowRequests(ctx context.Context, accountID string, targetAccountID string) (int, Error)
|
||||
|
||||
// Unfollow removes a follow targeting targetAccountID and originating
|
||||
// from originAccountID.
|
||||
//
|
||||
// If a follow was removed this way, the AP URI of the follow will be
|
||||
// returned to the caller, so that further processing can take place
|
||||
// if necessary.
|
||||
//
|
||||
// If no follow was removed this way, the returned string will be empty.
|
||||
Unfollow(ctx context.Context, originAccountID string, targetAccountID string) (string, Error)
|
||||
|
||||
// UnfollowRequest removes a follow request targeting targetAccountID
|
||||
// and originating from originAccountID.
|
||||
//
|
||||
// If a follow request was removed this way, the AP URI of the follow
|
||||
// request will be returned to the caller, so that further processing
|
||||
// can take place if necessary.
|
||||
//
|
||||
// If no follow request was removed this way, the returned string will
|
||||
// be empty.
|
||||
UnfollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (string, Error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,10 +77,6 @@ type Status interface {
|
|||
// 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)
|
||||
|
||||
// GetStatusFaves returns a slice of faves/likes of the given status.
|
||||
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
|
||||
GetStatusFaves(ctx context.Context, status *gtsmodel.Status) ([]*gtsmodel.StatusFave, Error)
|
||||
|
||||
// GetStatusReblogs returns a slice of statuses that are a boost/reblog of the given status.
|
||||
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
|
||||
GetStatusReblogs(ctx context.Context, status *gtsmodel.Status) ([]*gtsmodel.Status, Error)
|
||||
|
|
|
|||
66
internal/db/statusbookmark.go
Normal file
66
internal/db/statusbookmark.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
type StatusBookmark interface {
|
||||
// GetStatusBookmark gets one status bookmark with the given ID.
|
||||
GetStatusBookmark(ctx context.Context, id string) (*gtsmodel.StatusBookmark, Error)
|
||||
|
||||
// GetStatusBookmarkID is a shortcut function for returning just the database ID
|
||||
// of a status bookmark created by the given accountID, targeting the given statusID.
|
||||
GetStatusBookmarkID(ctx context.Context, accountID string, statusID string) (string, Error)
|
||||
|
||||
// GetStatusBookmarks retrieves status bookmarks created by the given accountID,
|
||||
// and using the provided parameters. If limit is < 0 then no limit will be set.
|
||||
//
|
||||
// This function is primarily useful for paging through bookmarks in a sort of
|
||||
// timeline view.
|
||||
GetStatusBookmarks(ctx context.Context, accountID string, limit int, maxID string, minID string) ([]*gtsmodel.StatusBookmark, Error)
|
||||
|
||||
// PutStatusBookmark inserts the given statusBookmark into the database.
|
||||
PutStatusBookmark(ctx context.Context, statusBookmark *gtsmodel.StatusBookmark) Error
|
||||
|
||||
// DeleteStatusBookmark deletes one status bookmark with the given ID.
|
||||
DeleteStatusBookmark(ctx context.Context, id string) Error
|
||||
|
||||
// DeleteStatusBookmarks mass deletes status bookmarks targeting targetAccountID
|
||||
// and/or originating from originAccountID and/or bookmarking statusID.
|
||||
//
|
||||
// If targetAccountID is set and originAccountID isn't, all status bookmarks
|
||||
// that target the given account will be deleted.
|
||||
//
|
||||
// If originAccountID is set and targetAccountID isn't, all status bookmarks
|
||||
// originating from the given account will be deleted.
|
||||
//
|
||||
// If both are set, then status bookmarks that target targetAccountID and
|
||||
// originate from originAccountID will be deleted.
|
||||
//
|
||||
// At least one parameter must not be an empty string.
|
||||
DeleteStatusBookmarks(ctx context.Context, targetAccountID string, originAccountID string) Error
|
||||
|
||||
// DeleteStatusBookmarksForStatus deletes all status bookmarks that target the
|
||||
// given status ID. This is useful when a status has been deleted, and you need
|
||||
// to clean up after it.
|
||||
DeleteStatusBookmarksForStatus(ctx context.Context, statusID string) Error
|
||||
}
|
||||
63
internal/db/statusfave.go
Normal file
63
internal/db/statusfave.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
type StatusFave interface {
|
||||
// GetStatusFave returns one status fave with the given id.
|
||||
GetStatusFave(ctx context.Context, id string) (*gtsmodel.StatusFave, Error)
|
||||
|
||||
// GetStatusFaveByAccountID gets one status fave created by the given
|
||||
// accountID, targeting the given statusID.
|
||||
GetStatusFaveByAccountID(ctx context.Context, accountID string, statusID string) (*gtsmodel.StatusFave, Error)
|
||||
|
||||
// GetStatusFaves returns a slice of faves/likes of the given status.
|
||||
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
|
||||
GetStatusFaves(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, Error)
|
||||
|
||||
// PutStatusFave inserts the given statusFave into the database.
|
||||
PutStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) Error
|
||||
|
||||
// DeleteStatusFave deletes one status fave with the given id.
|
||||
DeleteStatusFave(ctx context.Context, id string) Error
|
||||
|
||||
// DeleteStatusFaves mass deletes status faves targeting targetAccountID
|
||||
// and/or originating from originAccountID and/or faving statusID.
|
||||
//
|
||||
// If targetAccountID is set and originAccountID isn't, all status faves
|
||||
// that target the given account will be deleted.
|
||||
//
|
||||
// If originAccountID is set and targetAccountID isn't, all status faves
|
||||
// originating from the given account will be deleted.
|
||||
//
|
||||
// If both are set, then status faves that target targetAccountID and
|
||||
// originate from originAccountID will be deleted.
|
||||
//
|
||||
// At least one parameter must not be an empty string.
|
||||
DeleteStatusFaves(ctx context.Context, targetAccountID string, originAccountID string) Error
|
||||
|
||||
// DeleteStatusFavesForStatus deletes all status faves that target the
|
||||
// given status ID. This is useful when a status has been deleted, and you need
|
||||
// to clean up after it.
|
||||
DeleteStatusFavesForStatus(ctx context.Context, statusID string) Error
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue