[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
|
@ -30,7 +30,6 @@ import (
|
||||||
"github.com/superseriousbusiness/activity/streams"
|
"github.com/superseriousbusiness/activity/streams"
|
||||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -98,10 +97,7 @@ func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() {
|
||||||
userModule := users.New(suite.processor)
|
userModule := users.New(suite.processor)
|
||||||
targetAccount := suite.testAccounts["local_account_1"]
|
targetAccount := suite.testAccounts["local_account_1"]
|
||||||
|
|
||||||
suite.processor.Account().DeleteLocal(context.Background(), suite.testAccounts["local_account_1"], &apimodel.AccountDeleteRequest{
|
suite.processor.Account().DeleteSelf(context.Background(), suite.testAccounts["local_account_1"])
|
||||||
Password: "password",
|
|
||||||
DeleteOriginID: targetAccount.ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
// wait for the account delete to be processed
|
// wait for the account delete to be processed
|
||||||
if !testrig.WaitFor(func() bool {
|
if !testrig.WaitFor(func() bool {
|
||||||
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AccountDeletePOSTHandler swagger:operation POST /api/v1/accounts/delete accountDelete
|
// AccountDeletePOSTHandler swagger:operation POST /api/v1/accounts/delete accountDelete
|
||||||
|
@ -77,15 +78,20 @@ func (m *Module) AccountDeletePOSTHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Self account delete requires password to ensure it's for real.
|
||||||
if form.Password == "" {
|
if form.Password == "" {
|
||||||
err = errors.New("no password provided in account delete request")
|
err = errors.New("no password provided in account delete request")
|
||||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
form.DeleteOriginID = authed.Account.ID
|
if err := bcrypt.CompareHashAndPassword([]byte(authed.User.EncryptedPassword), []byte(form.Password)); err != nil {
|
||||||
|
err = errors.New("invalid password provided in account delete request")
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if errWithCode := m.processor.Account().DeleteLocal(c.Request.Context(), authed.Account, form); errWithCode != nil {
|
if errWithCode := m.processor.Account().DeleteSelf(c.Request.Context(), authed.Account); errWithCode != nil {
|
||||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,7 +275,7 @@ func (suite *BookmarkTestSuite) TestGetBookmarksNone() {
|
||||||
testUser := suite.testUsers["local_account_1"]
|
testUser := suite.testUsers["local_account_1"]
|
||||||
|
|
||||||
// Remove all bookmarks for this account.
|
// Remove all bookmarks for this account.
|
||||||
if err := suite.db.DeleteWhere(context.Background(), []db.Where{{Key: "account_id", Value: testAccount.ID}}, &[]*gtsmodel.StatusBookmark{}); err != nil {
|
if err := suite.db.DeleteStatusBookmarks(context.Background(), "", testAccount.ID); err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -123,7 +123,7 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
assert.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b))
|
assert.Equal(suite.T(), `{"error":"Forbidden: status is not faveable"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusFaveTestSuite(t *testing.T) {
|
func TestStatusFaveTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -206,9 +206,6 @@ type AccountFollowRequest struct {
|
||||||
type AccountDeleteRequest struct {
|
type AccountDeleteRequest struct {
|
||||||
// Password of the account's user, for confirmation.
|
// Password of the account's user, for confirmation.
|
||||||
Password string `form:"password" json:"password" xml:"password"`
|
Password string `form:"password" json:"password" xml:"password"`
|
||||||
// The origin of the delete account request.
|
|
||||||
// Can be the ID of the account owner, or the ID of an admin account.
|
|
||||||
DeleteOriginID string `form:"-" json:"-" xml:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountRole models the role of an account.
|
// AccountRole models the role of an account.
|
||||||
|
|
|
@ -45,7 +45,7 @@ type Account interface {
|
||||||
PutAccount(ctx context.Context, account *gtsmodel.Account) Error
|
PutAccount(ctx context.Context, account *gtsmodel.Account) Error
|
||||||
|
|
||||||
// UpdateAccount updates one account by ID.
|
// 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.
|
// DeleteAccount deletes one account from the database by its ID.
|
||||||
// DO NOT USE THIS WHEN SUSPENDING ACCOUNTS! In that case you should mark the
|
// 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.
|
// 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)
|
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)
|
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.
|
// 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 {
|
func (a *accountDB) UpdateAccount(ctx context.Context, account *gtsmodel.Account, columns ...string) db.Error {
|
||||||
// Update the account's last-updated
|
|
||||||
account.UpdatedAt = time.Now()
|
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 {
|
return a.state.Caches.GTS.Account().Store(account, func() error {
|
||||||
// It is safe to run this database transaction within cache.Store
|
// 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().
|
_, err := tx.NewUpdate().
|
||||||
Model(account).
|
Model(account).
|
||||||
Where("? = ?", bun.Ident("account.id"), account.ID).
|
Where("? = ?", bun.Ident("account.id"), account.ID).
|
||||||
|
Column(columns...).
|
||||||
Exec(ctx)
|
Exec(ctx)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
@ -477,38 +481,6 @@ func (a *accountDB) GetAccountWebStatuses(ctx context.Context, accountID string,
|
||||||
return a.statusesFromIDs(ctx, statusIDs)
|
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) {
|
func (a *accountDB) GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.Error) {
|
||||||
blocks := []*gtsmodel.Block{}
|
blocks := []*gtsmodel.Block{}
|
||||||
|
|
||||||
|
|
|
@ -208,12 +208,6 @@ func (suite *AccountTestSuite) TestInsertAccountWithDefaults() {
|
||||||
suite.False(*newAccount.HideCollections)
|
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() {
|
func (suite *AccountTestSuite) TestGetAccountPinnedStatusesSomeResults() {
|
||||||
testAccount := suite.testAccounts["admin_account"]
|
testAccount := suite.testAccounts["admin_account"]
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,8 @@ type DBService struct {
|
||||||
db.Report
|
db.Report
|
||||||
db.Session
|
db.Session
|
||||||
db.Status
|
db.Status
|
||||||
|
db.StatusBookmark
|
||||||
|
db.StatusFave
|
||||||
db.Timeline
|
db.Timeline
|
||||||
db.User
|
db.User
|
||||||
db.Tombstone
|
db.Tombstone
|
||||||
|
@ -203,6 +205,14 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
||||||
conn: conn,
|
conn: conn,
|
||||||
state: state,
|
state: state,
|
||||||
},
|
},
|
||||||
|
StatusBookmark: &statusBookmarkDB{
|
||||||
|
conn: conn,
|
||||||
|
state: state,
|
||||||
|
},
|
||||||
|
StatusFave: &statusFaveDB{
|
||||||
|
conn: conn,
|
||||||
|
state: state,
|
||||||
|
},
|
||||||
Timeline: &timelineDB{
|
Timeline: &timelineDB{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
state: state,
|
state: state,
|
||||||
|
|
|
@ -44,6 +44,8 @@ type BunDBStandardTestSuite struct {
|
||||||
testFollows map[string]*gtsmodel.Follow
|
testFollows map[string]*gtsmodel.Follow
|
||||||
testEmojis map[string]*gtsmodel.Emoji
|
testEmojis map[string]*gtsmodel.Emoji
|
||||||
testReports map[string]*gtsmodel.Report
|
testReports map[string]*gtsmodel.Report
|
||||||
|
testBookmarks map[string]*gtsmodel.StatusBookmark
|
||||||
|
testFaves map[string]*gtsmodel.StatusFave
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||||
|
@ -59,6 +61,8 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||||
suite.testFollows = testrig.NewTestFollows()
|
suite.testFollows = testrig.NewTestFollows()
|
||||||
suite.testEmojis = testrig.NewTestEmojis()
|
suite.testEmojis = testrig.NewTestEmojis()
|
||||||
suite.testReports = testrig.NewTestReports()
|
suite.testReports = testrig.NewTestReports()
|
||||||
|
suite.testBookmarks = testrig.NewTestBookmarks()
|
||||||
|
suite.testFaves = testrig.NewTestFaves()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BunDBStandardTestSuite) SetupTest() {
|
func (suite *BunDBStandardTestSuite) SetupTest() {
|
||||||
|
|
|
@ -19,6 +19,7 @@ package bundb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
@ -104,15 +105,70 @@ func (n *notificationDB) GetNotifications(ctx context.Context, accountID string,
|
||||||
return notifs, nil
|
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.
|
if _, err := n.conn.
|
||||||
NewDelete().
|
NewDelete().
|
||||||
TableExpr("? AS ?", bun.Ident("notifications"), bun.Ident("notification")).
|
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 {
|
Exec(ctx); err != nil {
|
||||||
return n.conn.ProcessError(err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,13 @@ package bundb_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
@ -112,10 +114,10 @@ func (suite *NotificationTestSuite) TestGetNotificationsWithoutSpam() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *NotificationTestSuite) TestClearNotificationsWithSpam() {
|
func (suite *NotificationTestSuite) TestDeleteNotificationsWithSpam() {
|
||||||
suite.spamNotifs()
|
suite.spamNotifs()
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
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)
|
suite.NoError(err)
|
||||||
|
|
||||||
notifications, err := suite.db.GetNotifications(context.Background(), testAccount.ID, []string{}, 20, id.Highest, id.Lowest)
|
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)
|
suite.Empty(notifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *NotificationTestSuite) TestClearNotificationsWithTwoAccounts() {
|
func (suite *NotificationTestSuite) TestDeleteNotificationsWithTwoAccounts() {
|
||||||
suite.spamNotifs()
|
suite.spamNotifs()
|
||||||
testAccount := suite.testAccounts["local_account_1"]
|
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)
|
suite.NoError(err)
|
||||||
|
|
||||||
notifications, err := suite.db.GetNotifications(context.Background(), testAccount.ID, []string{}, 20, id.Highest, id.Lowest)
|
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)
|
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) {
|
func TestNotificationTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(NotificationTestSuite))
|
suite.Run(t, new(NotificationTestSuite))
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,12 @@ package bundb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
@ -34,14 +34,6 @@ type relationshipDB struct {
|
||||||
state *state.State
|
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) {
|
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
|
// Look for a block in direction of account1->account2
|
||||||
block1, err := r.getBlock(ctx, 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) {
|
func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.Follow, db.Error) {
|
||||||
var follow *gtsmodel.Follow
|
// Get original follow request.
|
||||||
|
var followRequestID string
|
||||||
if err := r.conn.RunInTx(ctx, func(tx bun.Tx) error {
|
if err := r.conn.
|
||||||
// get original follow request
|
NewSelect().
|
||||||
followRequest := >smodel.FollowRequest{}
|
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||||
if err := tx.
|
Column("follow_request.id").
|
||||||
NewSelect().
|
Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID).
|
||||||
Model(followRequest).
|
Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID).
|
||||||
Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID).
|
Scan(ctx, &followRequestID); err != nil {
|
||||||
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 {
|
|
||||||
return nil, r.conn.ProcessError(err)
|
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 the new follow
|
||||||
return follow, nil
|
return follow, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *relationshipDB) RejectFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.FollowRequest, db.Error) {
|
func (r *relationshipDB) RejectFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.FollowRequest, db.Error) {
|
||||||
followRequest := >smodel.FollowRequest{}
|
// Get original follow request.
|
||||||
|
var followRequestID string
|
||||||
if err := r.conn.RunInTx(ctx, func(tx bun.Tx) error {
|
if err := r.conn.
|
||||||
// get original follow request
|
NewSelect().
|
||||||
if err := tx.
|
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||||
NewSelect().
|
Column("follow_request.id").
|
||||||
Model(followRequest).
|
Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID).
|
||||||
Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID).
|
Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID).
|
||||||
Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID).
|
Scan(ctx, &followRequestID); err != nil {
|
||||||
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 {
|
|
||||||
return nil, r.conn.ProcessError(err)
|
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
|
return followRequest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *relationshipDB) GetAccountFollowRequests(ctx context.Context, accountID string) ([]*gtsmodel.FollowRequest, db.Error) {
|
func (r *relationshipDB) deleteFollowRequestNotif(ctx context.Context, originAccountID string, targetAccountID string) db.Error {
|
||||||
followRequests := []*gtsmodel.FollowRequest{}
|
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).
|
return r.state.DB.DeleteNotification(ctx, id)
|
||||||
Where("? = ?", bun.Ident("follow_request.target_account_id"), accountID).
|
}
|
||||||
Order("follow_request.updated_at DESC")
|
|
||||||
|
|
||||||
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)
|
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
|
return followRequests, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *relationshipDB) GetAccountFollows(ctx context.Context, accountID string) ([]*gtsmodel.Follow, db.Error) {
|
func (r *relationshipDB) CountFollowRequests(ctx context.Context, accountID string, targetAccountID string) (int, 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) {
|
|
||||||
q := r.conn.
|
q := r.conn.
|
||||||
NewSelect().
|
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 {
|
if accountID != "" {
|
||||||
q = q.
|
q = q.Where("? = ?", bun.Ident("follow_request.account_id"), accountID)
|
||||||
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"))
|
if targetAccountID != "" {
|
||||||
} else {
|
q = q.Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID)
|
||||||
q = q.Where("? = ?", bun.Ident("follow.account_id"), accountID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return q.Count(ctx)
|
return q.Count(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *relationshipDB) GetAccountFollowedBy(ctx context.Context, accountID string, localOnly bool) ([]*gtsmodel.Follow, db.Error) {
|
func (r *relationshipDB) Unfollow(ctx context.Context, originAccountID string, targetAccountID string) (string, db.Error) {
|
||||||
follows := []*gtsmodel.Follow{}
|
uri := new(string)
|
||||||
|
|
||||||
q := r.conn.
|
_, err := r.conn.
|
||||||
NewSelect().
|
NewDelete().
|
||||||
Model(&follows).
|
TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow")).
|
||||||
Order("follow.updated_at DESC")
|
Where("? = ?", bun.Ident("follow.target_account_id"), targetAccountID).
|
||||||
|
Where("? = ?", bun.Ident("follow.account_id"), originAccountID).
|
||||||
|
Returning("?", bun.Ident("uri")).Exec(ctx, uri)
|
||||||
|
|
||||||
if localOnly {
|
// Only return proper errors.
|
||||||
q = q.
|
if err = r.conn.ProcessError(err); err != db.ErrNoEntries {
|
||||||
Join("JOIN ? AS ? ON ? = ?", bun.Ident("accounts"), bun.Ident("account"), bun.Ident("follow.account_id"), bun.Ident("account.id")).
|
return *uri, err
|
||||||
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 := q.Scan(ctx)
|
return *uri, nil
|
||||||
if err != nil && err != sql.ErrNoRows {
|
|
||||||
return nil, r.conn.ProcessError(err)
|
|
||||||
}
|
|
||||||
return follows, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *relationshipDB) CountAccountFollowedBy(ctx context.Context, accountID string, localOnly bool) (int, db.Error) {
|
func (r *relationshipDB) UnfollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (string, db.Error) {
|
||||||
q := r.conn.
|
uri := new(string)
|
||||||
NewSelect().
|
|
||||||
TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow"))
|
|
||||||
|
|
||||||
if localOnly {
|
_, err := r.conn.
|
||||||
q = q.
|
NewDelete().
|
||||||
Join("JOIN ? AS ? ON ? = ?", bun.Ident("accounts"), bun.Ident("account"), bun.Ident("follow.account_id"), bun.Ident("account.id")).
|
TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")).
|
||||||
Where("? = ?", bun.Ident("follow.target_account_id"), accountID).
|
Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID).
|
||||||
Where("? IS NULL", bun.Ident("account.domain"))
|
Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID).
|
||||||
} else {
|
Returning("?", bun.Ident("uri")).Exec(ctx, uri)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
return q.Count(ctx)
|
return *uri, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,6 +289,47 @@ func (suite *RelationshipTestSuite) TestAcceptFollowRequestOK() {
|
||||||
suite.FailNow(err.Error())
|
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)
|
follow, err := suite.db.AcceptFollowRequest(ctx, account.ID, targetAccount.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(follow)
|
suite.NotNil(follow)
|
||||||
|
@ -352,9 +393,25 @@ func (suite *RelationshipTestSuite) TestRejectFollowRequestOK() {
|
||||||
suite.FailNow(err.Error())
|
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)
|
rejectedFollowRequest, err := suite.db.RejectFollowRequest(ctx, account.ID, targetAccount.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(rejectedFollowRequest)
|
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() {
|
func (suite *RelationshipTestSuite) TestRejectFollowRequestNotExisting() {
|
||||||
|
@ -383,58 +440,92 @@ func (suite *RelationshipTestSuite) TestGetAccountFollowRequests() {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
followRequests, err := suite.db.GetAccountFollowRequests(ctx, targetAccount.ID)
|
followRequests, err := suite.db.GetFollowRequests(ctx, "", targetAccount.ID)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Len(followRequests, 1)
|
suite.Len(followRequests, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *RelationshipTestSuite) TestGetAccountFollows() {
|
func (suite *RelationshipTestSuite) TestGetAccountFollows() {
|
||||||
account := suite.testAccounts["local_account_1"]
|
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.NoError(err)
|
||||||
suite.Len(follows, 2)
|
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() {
|
func (suite *RelationshipTestSuite) TestCountAccountFollows() {
|
||||||
account := suite.testAccounts["local_account_1"]
|
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.NoError(err)
|
||||||
suite.Equal(2, followsCount)
|
suite.Equal(2, followsCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *RelationshipTestSuite) TestGetAccountFollowedBy() {
|
func (suite *RelationshipTestSuite) TestGetAccountFollowedBy() {
|
||||||
account := suite.testAccounts["local_account_1"]
|
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.NoError(err)
|
||||||
suite.Len(follows, 2)
|
suite.Len(follows, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *RelationshipTestSuite) TestGetAccountFollowedByLocalOnly() {
|
func (suite *RelationshipTestSuite) TestGetLocalFollowersIDs() {
|
||||||
account := suite.testAccounts["local_account_1"]
|
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.NoError(err)
|
||||||
suite.Len(follows, 2)
|
suite.EqualValues([]string{"01F8MH5NBDF2MV7CTC4Q5128HF", "01F8MH17FWEB39HZJ76B6VXSKF"}, accountIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *RelationshipTestSuite) TestCountAccountFollowedBy() {
|
func (suite *RelationshipTestSuite) TestCountAccountFollowedBy() {
|
||||||
account := suite.testAccounts["local_account_1"]
|
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.NoError(err)
|
||||||
suite.Equal(2, followsCount)
|
suite.Equal(2, followsCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *RelationshipTestSuite) TestCountAccountFollowedByLocalOnly() {
|
func (suite *RelationshipTestSuite) TestUnfollowExisting() {
|
||||||
account := suite.testAccounts["local_account_1"]
|
originAccount := suite.testAccounts["local_account_1"]
|
||||||
followsCount, err := suite.db.CountAccountFollowedBy(context.Background(), account.ID, true)
|
targetAccount := suite.testAccounts["admin_account"]
|
||||||
|
|
||||||
|
uri, err := suite.db.Unfollow(context.Background(), originAccount.ID, targetAccount.ID)
|
||||||
suite.NoError(err)
|
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) {
|
func TestRelationshipTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -46,15 +46,6 @@ func (s *statusDB) newStatusQ(status interface{}) *bun.SelectQuery {
|
||||||
Relation("CreatedWithApplication")
|
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) {
|
func (s *statusDB) GetStatusByID(ctx context.Context, id string) (*gtsmodel.Status, db.Error) {
|
||||||
return s.getStatus(
|
return s.getStatus(
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -542,20 +533,6 @@ func (s *statusDB) IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.St
|
||||||
return s.conn.Exists(ctx, q)
|
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) {
|
func (s *statusDB) GetStatusReblogs(ctx context.Context, status *gtsmodel.Status) ([]*gtsmodel.Status, db.Error) {
|
||||||
reblogs := []*gtsmodel.Status{}
|
reblogs := []*gtsmodel.Status{}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
Report
|
||||||
Session
|
Session
|
||||||
Status
|
Status
|
||||||
|
StatusBookmark
|
||||||
|
StatusFave
|
||||||
Timeline
|
Timeline
|
||||||
User
|
User
|
||||||
Tombstone
|
Tombstone
|
||||||
|
|
|
@ -29,8 +29,31 @@ type Notification interface {
|
||||||
//
|
//
|
||||||
// Returned notifications will be ordered ID descending (ie., highest/newest to lowest/oldest).
|
// 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)
|
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 returns one notification according to its id.
|
||||||
GetNotification(ctx context.Context, id string) (*gtsmodel.Notification, Error)
|
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.
|
// 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)
|
RejectFollowRequest(ctx context.Context, originAccountID string, targetAccountID string) (*gtsmodel.FollowRequest, Error)
|
||||||
|
|
||||||
// GetAccountFollowRequests returns all follow requests targeting the given account.
|
// GetFollows returns a slice of follows owned by the given accountID, and/or
|
||||||
GetAccountFollowRequests(ctx context.Context, accountID string) ([]*gtsmodel.FollowRequest, Error)
|
// targeting the given account id.
|
||||||
|
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// If localOnly is set to true, then only follows from *this instance* will be returned.
|
// If accountID is set and targetAccountID isn't, then all follows created by
|
||||||
CountAccountFollows(ctx context.Context, accountID string, localOnly bool) (int, Error)
|
// accountID will be returned.
|
||||||
|
|
||||||
// GetAccountFollowedBy fetches follows that target given accountID.
|
|
||||||
//
|
//
|
||||||
// If localOnly is set to true, then only follows from *this instance* will be returned.
|
// If targetAccountID is set and accountID isn't, then all follows targeting
|
||||||
GetAccountFollowedBy(ctx context.Context, accountID string, localOnly bool) ([]*gtsmodel.Follow, Error)
|
// 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.
|
// GetLocalFollowersIDs returns a list of local account IDs which follow the
|
||||||
CountAccountFollowedBy(ctx context.Context, accountID string, localOnly bool) (int, Error)
|
// 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 checks if a given status has been bookmarked by a given account ID
|
||||||
IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, Error)
|
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.
|
// 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.
|
// 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)
|
GetStatusReblogs(ctx context.Context, status *gtsmodel.Status) ([]*gtsmodel.Status, Error)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -69,8 +69,10 @@ func (suite *FederatingDBTestSuite) SetupTest() {
|
||||||
testrig.StartWorkers(&suite.state)
|
testrig.StartWorkers(&suite.state)
|
||||||
|
|
||||||
suite.fromFederator = make(chan messages.FromFederator, 10)
|
suite.fromFederator = make(chan messages.FromFederator, 10)
|
||||||
suite.state.Workers.EnqueueFederator = func(ctx context.Context, msg messages.FromFederator) {
|
suite.state.Workers.EnqueueFederator = func(ctx context.Context, msgs ...messages.FromFederator) {
|
||||||
suite.fromFederator <- msg
|
for _, msg := range msgs {
|
||||||
|
suite.fromFederator <- msg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.db = testrig.NewTestDB(&suite.state)
|
suite.db = testrig.NewTestDB(&suite.state)
|
||||||
|
|
|
@ -22,9 +22,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"codeberg.org/gruf/go-kv"
|
|
||||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,43 +33,30 @@ import (
|
||||||
//
|
//
|
||||||
// The library makes this call only after acquiring a lock first.
|
// The library makes this call only after acquiring a lock first.
|
||||||
func (f *federatingDB) Followers(ctx context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) {
|
func (f *federatingDB) Followers(ctx context.Context, actorIRI *url.URL) (followers vocab.ActivityStreamsCollection, err error) {
|
||||||
l := log.WithContext(ctx).
|
|
||||||
WithFields(kv.Fields{
|
|
||||||
{"id", actorIRI},
|
|
||||||
}...)
|
|
||||||
l.Debug("entering Followers")
|
|
||||||
|
|
||||||
acct, err := f.getAccountForIRI(ctx, actorIRI)
|
acct, err := f.getAccountForIRI(ctx, actorIRI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
acctFollowers, err := f.state.DB.GetAccountFollowedBy(ctx, acct.ID, false)
|
follows, err := f.state.DB.GetFollows(ctx, "", acct.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Followers: db error getting followers for account id %s: %s", acct.ID, err)
|
return nil, fmt.Errorf("Followers: db error getting followers for account id %s: %s", acct.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
iris := []*url.URL{}
|
iris := make([]*url.URL, 0, len(follows))
|
||||||
for _, follow := range acctFollowers {
|
for _, follow := range follows {
|
||||||
if follow.Account == nil {
|
if follow.Account == nil {
|
||||||
a, err := f.state.DB.GetAccountByID(ctx, follow.AccountID)
|
// Follow account no longer exists,
|
||||||
if err != nil {
|
// for some reason. Skip this one.
|
||||||
errWrapped := fmt.Errorf("Followers: db error getting account id %s: %s", follow.AccountID, err)
|
log.WithContext(ctx).WithField("follow", follow).Warnf("follow missing account %s", follow.AccountID)
|
||||||
if err == db.ErrNoEntries {
|
continue
|
||||||
// no entry for this account id so it's probably been deleted and we haven't caught up yet
|
|
||||||
l.Error(errWrapped)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
// proper error
|
|
||||||
return nil, errWrapped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
follow.Account = a
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(follow.Account.URI)
|
u, err := url.Parse(follow.Account.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
iris = append(iris, u)
|
iris = append(iris, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,9 +22,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"codeberg.org/gruf/go-kv"
|
|
||||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,39 +33,25 @@ import (
|
||||||
//
|
//
|
||||||
// The library makes this call only after acquiring a lock first.
|
// The library makes this call only after acquiring a lock first.
|
||||||
func (f *federatingDB) Following(ctx context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error) {
|
func (f *federatingDB) Following(ctx context.Context, actorIRI *url.URL) (following vocab.ActivityStreamsCollection, err error) {
|
||||||
l := log.WithContext(ctx).
|
|
||||||
WithFields(kv.Fields{
|
|
||||||
{"id", actorIRI},
|
|
||||||
}...)
|
|
||||||
l.Debug("entering Following")
|
|
||||||
|
|
||||||
acct, err := f.getAccountForIRI(ctx, actorIRI)
|
acct, err := f.getAccountForIRI(ctx, actorIRI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
acctFollowing, err := f.state.DB.GetAccountFollows(ctx, acct.ID)
|
follows, err := f.state.DB.GetFollows(ctx, acct.ID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Following: db error getting following for account id %s: %s", acct.ID, err)
|
return nil, fmt.Errorf("Following: db error getting following for account id %s: %w", acct.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
iris := []*url.URL{}
|
iris := make([]*url.URL, 0, len(follows))
|
||||||
for _, follow := range acctFollowing {
|
for _, follow := range follows {
|
||||||
if follow.TargetAccount == nil {
|
if follow.TargetAccount == nil {
|
||||||
a, err := f.state.DB.GetAccountByID(ctx, follow.TargetAccountID)
|
// Follow target account no longer exists,
|
||||||
if err != nil {
|
// for some reason. Skip this one.
|
||||||
errWrapped := fmt.Errorf("Following: db error getting account id %s: %s", follow.TargetAccountID, err)
|
log.WithContext(ctx).WithField("follow", follow).Warnf("follow missing target account %s", follow.TargetAccountID)
|
||||||
if err == db.ErrNoEntries {
|
continue
|
||||||
// no entry for this account id so it's probably been deleted and we haven't caught up yet
|
|
||||||
l.Error(errWrapped)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
// proper error
|
|
||||||
return nil, errWrapped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
follow.TargetAccount = a
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(follow.TargetAccount.URI)
|
u, err := url.Parse(follow.TargetAccount.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -89,22 +89,16 @@ func (f *federatingDB) InboxesForIRI(c context.Context, iri *url.URL) (inboxIRIs
|
||||||
return nil, fmt.Errorf("couldn't find local account with username %s: %s", localAccountUsername, err)
|
return nil, fmt.Errorf("couldn't find local account with username %s: %s", localAccountUsername, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
follows, err := f.state.DB.GetAccountFollowedBy(c, account.ID, false)
|
follows, err := f.state.DB.GetFollows(c, "", account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't get followers of local account %s: %s", localAccountUsername, err)
|
return nil, fmt.Errorf("couldn't get followers of local account %s: %s", localAccountUsername, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, follow := range follows {
|
for _, follow := range follows {
|
||||||
// make sure we retrieved the following account from the db
|
|
||||||
if follow.Account == nil {
|
if follow.Account == nil {
|
||||||
followingAccount, err := f.state.DB.GetAccountByID(c, follow.AccountID)
|
// No account exists for this follow,
|
||||||
if err != nil {
|
// for some reason. Just skip it.
|
||||||
if err == db.ErrNoEntries {
|
continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("error retrieving account with id %s: %s", follow.AccountID, err)
|
|
||||||
}
|
|
||||||
follow.Account = followingAccount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// deliver to a shared inbox if we have that option
|
// deliver to a shared inbox if we have that option
|
||||||
|
|
|
@ -91,8 +91,10 @@ func (suite *AccountStandardTestSuite) SetupTest() {
|
||||||
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
||||||
|
|
||||||
suite.fromClientAPIChan = make(chan messages.FromClientAPI, 100)
|
suite.fromClientAPIChan = make(chan messages.FromClientAPI, 100)
|
||||||
suite.state.Workers.EnqueueClientAPI = func(ctx context.Context, msg messages.FromClientAPI) {
|
suite.state.Workers.EnqueueClientAPI = func(ctx context.Context, msgs ...messages.FromClientAPI) {
|
||||||
suite.fromClientAPIChan <- msg
|
for _, msg := range msgs {
|
||||||
|
suite.fromClientAPIChan <- msg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
|
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
|
||||||
|
|
|
@ -34,118 +34,53 @@ import (
|
||||||
|
|
||||||
// BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local.
|
// BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local.
|
||||||
func (p *Processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
func (p *Processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
// make sure the target account actually exists in our db
|
targetAccount, existingBlock, errWithCode := p.getBlockTarget(ctx, requestingAccount, targetAccountID)
|
||||||
targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
if errWithCode != nil {
|
||||||
if err != nil {
|
return nil, errWithCode
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if requestingAccount already blocks target account, we don't need to do anything
|
if existingBlock != nil {
|
||||||
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, false); err != nil {
|
// Block already exists, nothing to do.
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error checking existence of block: %s", err))
|
|
||||||
} else if blocked {
|
|
||||||
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't block yourself, silly
|
// Create and store a new block.
|
||||||
if requestingAccount.ID == targetAccountID {
|
blockID := id.NewULID()
|
||||||
return nil, gtserror.NewErrorNotAcceptable(fmt.Errorf("BlockCreate: account %s cannot block itself", requestingAccount.ID))
|
blockURI := uris.GenerateURIForBlock(requestingAccount.Username, blockID)
|
||||||
|
block := >smodel.Block{
|
||||||
|
ID: blockID,
|
||||||
|
URI: blockURI,
|
||||||
|
AccountID: requestingAccount.ID,
|
||||||
|
Account: requestingAccount,
|
||||||
|
TargetAccountID: targetAccountID,
|
||||||
|
TargetAccount: targetAccount,
|
||||||
}
|
}
|
||||||
|
|
||||||
// make the block
|
|
||||||
block := >smodel.Block{}
|
|
||||||
newBlockID := id.NewULID()
|
|
||||||
block.ID = newBlockID
|
|
||||||
block.AccountID = requestingAccount.ID
|
|
||||||
block.Account = requestingAccount
|
|
||||||
block.TargetAccountID = targetAccountID
|
|
||||||
block.TargetAccount = targetAccount
|
|
||||||
block.URI = uris.GenerateURIForBlock(requestingAccount.Username, newBlockID)
|
|
||||||
|
|
||||||
// whack it in the database
|
|
||||||
if err := p.state.DB.PutBlock(ctx, block); err != nil {
|
if err := p.state.DB.PutBlock(ctx, block); err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error creating block in db: %s", err))
|
err = fmt.Errorf("BlockCreate: error creating block in db: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear any follows or follow requests from the blocked account to the target account -- this is a simple delete
|
// Ensure each account unfollows the other.
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{
|
// We only care about processing unfollow side
|
||||||
{Key: "account_id", Value: targetAccountID},
|
// effects from requesting account -> target
|
||||||
{Key: "target_account_id", Value: requestingAccount.ID},
|
// account, since requesting account is ours,
|
||||||
}, >smodel.Follow{}); err != nil {
|
// and target account might not be.
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err))
|
msgs, err := p.unfollow(ctx, requestingAccount, targetAccount)
|
||||||
}
|
if err != nil {
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{
|
err = fmt.Errorf("BlockCreate: error unfollowing: %w", err)
|
||||||
{Key: "account_id", Value: targetAccountID},
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
{Key: "target_account_id", Value: requestingAccount.ID},
|
|
||||||
}, >smodel.FollowRequest{}); err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear any follows or follow requests from the requesting account to the target account --
|
// Ensure unfollowed in other direction;
|
||||||
// this might require federation so we need to pass some messages around
|
// ignore/don't process returned messages.
|
||||||
|
if _, err := p.unfollow(ctx, targetAccount, requestingAccount); err != nil {
|
||||||
// check if a follow request exists from the requesting account to the target account, and remove it if it does (storing the URI for later)
|
err = fmt.Errorf("BlockCreate: error unfollowing: %w", err)
|
||||||
var frChanged bool
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
var frURI string
|
|
||||||
fr := >smodel.FollowRequest{}
|
|
||||||
if err := p.state.DB.GetWhere(ctx, []db.Where{
|
|
||||||
{Key: "account_id", Value: requestingAccount.ID},
|
|
||||||
{Key: "target_account_id", Value: targetAccountID},
|
|
||||||
}, fr); err == nil {
|
|
||||||
frURI = fr.URI
|
|
||||||
if err := p.state.DB.DeleteByID(ctx, fr.ID, fr); err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow request from db: %s", err))
|
|
||||||
}
|
|
||||||
frChanged = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// now do the same thing for any existing follow
|
// Process block side effects (federation etc).
|
||||||
var fChanged bool
|
msgs = append(msgs, messages.FromClientAPI{
|
||||||
var fURI string
|
|
||||||
f := >smodel.Follow{}
|
|
||||||
if err := p.state.DB.GetWhere(ctx, []db.Where{
|
|
||||||
{Key: "account_id", Value: requestingAccount.ID},
|
|
||||||
{Key: "target_account_id", Value: targetAccountID},
|
|
||||||
}, f); err == nil {
|
|
||||||
fURI = f.URI
|
|
||||||
if err := p.state.DB.DeleteByID(ctx, f.ID, f); err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow from db: %s", err))
|
|
||||||
}
|
|
||||||
fChanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// follow request status changed so send the UNDO activity to the channel for async processing
|
|
||||||
if frChanged {
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
|
||||||
APObjectType: ap.ActivityFollow,
|
|
||||||
APActivityType: ap.ActivityUndo,
|
|
||||||
GTSModel: >smodel.Follow{
|
|
||||||
AccountID: requestingAccount.ID,
|
|
||||||
TargetAccountID: targetAccountID,
|
|
||||||
URI: frURI,
|
|
||||||
},
|
|
||||||
OriginAccount: requestingAccount,
|
|
||||||
TargetAccount: targetAccount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// follow status changed so send the UNDO activity to the channel for async processing
|
|
||||||
if fChanged {
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
|
||||||
APObjectType: ap.ActivityFollow,
|
|
||||||
APActivityType: ap.ActivityUndo,
|
|
||||||
GTSModel: >smodel.Follow{
|
|
||||||
AccountID: requestingAccount.ID,
|
|
||||||
TargetAccountID: targetAccountID,
|
|
||||||
URI: fURI,
|
|
||||||
},
|
|
||||||
OriginAccount: requestingAccount,
|
|
||||||
TargetAccount: targetAccount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle the rest of the block process asynchronously
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
|
||||||
APObjectType: ap.ActivityBlock,
|
APObjectType: ap.ActivityBlock,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: block,
|
GTSModel: block,
|
||||||
|
@ -153,39 +88,72 @@ func (p *Processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel
|
||||||
TargetAccount: targetAccount,
|
TargetAccount: targetAccount,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Batch queue accreted client api messages.
|
||||||
|
p.state.Workers.EnqueueClientAPI(ctx, msgs...)
|
||||||
|
|
||||||
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local.
|
// BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local.
|
||||||
func (p *Processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
func (p *Processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
// make sure the target account actually exists in our db
|
targetAccount, existingBlock, errWithCode := p.getBlockTarget(ctx, requestingAccount, targetAccountID)
|
||||||
targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
if errWithCode != nil {
|
||||||
if err != nil {
|
return nil, errWithCode
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if a block exists, and remove it if it does
|
if existingBlock == nil {
|
||||||
block, err := p.state.DB.GetBlock(ctx, requestingAccount.ID, targetAccountID)
|
// Already not blocked, nothing to do.
|
||||||
if err == nil {
|
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
||||||
// we got a block, remove it
|
|
||||||
block.Account = requestingAccount
|
|
||||||
block.TargetAccount = targetAccount
|
|
||||||
if err := p.state.DB.DeleteBlockByID(ctx, block.ID); err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error removing block from db: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// send the UNDO activity to the client worker for async processing
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
|
||||||
APObjectType: ap.ActivityBlock,
|
|
||||||
APActivityType: ap.ActivityUndo,
|
|
||||||
GTSModel: block,
|
|
||||||
OriginAccount: requestingAccount,
|
|
||||||
TargetAccount: targetAccount,
|
|
||||||
})
|
|
||||||
} else if !errors.Is(err, db.ErrNoEntries) {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error getting possible block from db: %s", err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// return whatever relationship results from all this
|
// We got a block, remove it from the db.
|
||||||
|
if err := p.state.DB.DeleteBlockByID(ctx, existingBlock.ID); err != nil {
|
||||||
|
err := fmt.Errorf("BlockRemove: error removing block from db: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate account fields for convenience.
|
||||||
|
existingBlock.Account = requestingAccount
|
||||||
|
existingBlock.TargetAccount = targetAccount
|
||||||
|
|
||||||
|
// Process block removal side effects (federation etc).
|
||||||
|
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||||
|
APObjectType: ap.ActivityBlock,
|
||||||
|
APActivityType: ap.ActivityUndo,
|
||||||
|
GTSModel: existingBlock,
|
||||||
|
OriginAccount: requestingAccount,
|
||||||
|
TargetAccount: targetAccount,
|
||||||
|
})
|
||||||
|
|
||||||
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Processor) getBlockTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*gtsmodel.Account, *gtsmodel.Block, gtserror.WithCode) {
|
||||||
|
// Account should not block or unblock itself.
|
||||||
|
if requestingAccount.ID == targetAccountID {
|
||||||
|
err := fmt.Errorf("getBlockTarget: account %s cannot block or unblock itself", requestingAccount.ID)
|
||||||
|
return nil, nil, gtserror.NewErrorNotAcceptable(err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure target account retrievable.
|
||||||
|
targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
// Real db error.
|
||||||
|
err = fmt.Errorf("getBlockTarget: db error looking for target account %s: %w", targetAccountID, err)
|
||||||
|
return nil, nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
// Account not found.
|
||||||
|
err = fmt.Errorf("getBlockTarget: target account %s not found in the db", targetAccountID)
|
||||||
|
return nil, nil, gtserror.NewErrorNotFound(err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if currently blocked.
|
||||||
|
block, err := p.state.DB.GetBlock(ctx, requestingAccount.ID, targetAccountID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err = fmt.Errorf("getBlockTarget: db error checking existing block: %w", err)
|
||||||
|
return nil, nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetAccount, block, nil
|
||||||
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ import (
|
||||||
// BookmarksGet returns a pageable response of statuses that are bookmarked by requestingAccount.
|
// BookmarksGet returns a pageable response of statuses that are bookmarked by requestingAccount.
|
||||||
// Paging for this response is done based on bookmark ID rather than status ID.
|
// Paging for this response is done based on bookmark ID rather than status ID.
|
||||||
func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode) {
|
func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode) {
|
||||||
bookmarks, err := p.state.DB.GetBookmarks(ctx, requestingAccount.ID, limit, maxID, minID)
|
bookmarks, err := p.state.DB.GetStatusBookmarks(ctx, requestingAccount.ID, limit, maxID, minID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,135 +20,322 @@ package account
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"codeberg.org/gruf/go-kv"
|
"codeberg.org/gruf/go-kv"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const deleteSelectLimit = 50
|
||||||
|
|
||||||
// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc.
|
// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc.
|
||||||
// The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block.
|
// The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block.
|
||||||
func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode {
|
func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode {
|
||||||
fields := kv.Fields{{"username", account.Username}}
|
l := log.WithContext(ctx).WithFields(kv.Fields{
|
||||||
|
{"username", account.Username},
|
||||||
if account.Domain != "" {
|
{"domain", account.Domain},
|
||||||
fields = append(fields, kv.Field{
|
}...)
|
||||||
"domain", account.Domain,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
l := log.WithContext(ctx).WithFields(fields...)
|
|
||||||
l.Trace("beginning account delete process")
|
l.Trace("beginning account delete process")
|
||||||
|
|
||||||
// 1. Delete account's application(s), clients, and oauth tokens
|
if account.IsLocal() {
|
||||||
// we only need to do this step for local account since remote ones won't have any tokens or applications on our server
|
if err := p.deleteUserAndTokensForAccount(ctx, account); err != nil {
|
||||||
var user *gtsmodel.User
|
return gtserror.NewErrorInternalError(err)
|
||||||
if account.Domain == "" {
|
|
||||||
// see if we can get a user for this account
|
|
||||||
var err error
|
|
||||||
if user, err = p.state.DB.GetUserByAccountID(ctx, account.ID); err == nil {
|
|
||||||
// we got one! select all tokens with the user's ID
|
|
||||||
tokens := []*gtsmodel.Token{}
|
|
||||||
if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "user_id", Value: user.ID}}, &tokens); err == nil {
|
|
||||||
// we have some tokens to delete
|
|
||||||
for _, t := range tokens {
|
|
||||||
// delete client(s) associated with this token
|
|
||||||
if err := p.state.DB.DeleteByID(ctx, t.ClientID, >smodel.Client{}); err != nil {
|
|
||||||
l.Errorf("error deleting oauth client: %s", err)
|
|
||||||
}
|
|
||||||
// delete application(s) associated with this token
|
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "client_id", Value: t.ClientID}}, >smodel.Application{}); err != nil {
|
|
||||||
l.Errorf("error deleting application: %s", err)
|
|
||||||
}
|
|
||||||
// delete the token itself
|
|
||||||
if err := p.state.DB.DeleteByID(ctx, t.ID, t); err != nil {
|
|
||||||
l.Errorf("error deleting oauth token: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Delete account's blocks
|
if err := p.deleteAccountFollows(ctx, account); err != nil {
|
||||||
l.Trace("deleting account blocks")
|
return gtserror.NewErrorInternalError(err)
|
||||||
// first delete any blocks that this account created
|
|
||||||
if err := p.state.DB.DeleteBlocksByOriginAccountID(ctx, account.ID); err != nil {
|
|
||||||
l.Errorf("error deleting blocks created by account: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// now delete any blocks that target this account
|
if err := p.deleteAccountBlocks(ctx, account); err != nil {
|
||||||
if err := p.state.DB.DeleteBlocksByTargetAccountID(ctx, account.ID); err != nil {
|
return gtserror.NewErrorInternalError(err)
|
||||||
l.Errorf("error deleting blocks targeting account: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Delete account's emoji
|
if err := p.deleteAccountStatuses(ctx, account); err != nil {
|
||||||
// nothing to do here
|
return gtserror.NewErrorInternalError(err)
|
||||||
|
|
||||||
// 4. Delete account's follow requests
|
|
||||||
// TODO: federate these if necessary
|
|
||||||
l.Trace("deleting account follow requests")
|
|
||||||
// first delete any follow requests that this account created
|
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.FollowRequest{}); err != nil {
|
|
||||||
l.Errorf("error deleting follow requests created by account: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// now delete any follow requests that target this account
|
if err := p.deleteAccountNotifications(ctx, account); err != nil {
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.FollowRequest{}); err != nil {
|
return gtserror.NewErrorInternalError(err)
|
||||||
l.Errorf("error deleting follow requests targeting account: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Delete account's follows
|
if err := p.deleteAccountPeripheral(ctx, account); err != nil {
|
||||||
// TODO: federate these if necessary
|
return gtserror.NewErrorInternalError(err)
|
||||||
l.Trace("deleting account follows")
|
|
||||||
// first delete any follows that this account created
|
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.Follow{}); err != nil {
|
|
||||||
l.Errorf("error deleting follows created by account: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// now delete any follows that target this account
|
// To prevent the account being created again,
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.Follow{}); err != nil {
|
// stubbify it and update it in the db.
|
||||||
l.Errorf("error deleting follows targeting account: %s", err)
|
// The account will not be deleted, but it
|
||||||
|
// will become completely unusable.
|
||||||
|
columns := stubbifyAccount(account, origin)
|
||||||
|
if err := p.state.DB.UpdateAccount(ctx, account, columns...); err != nil {
|
||||||
|
return gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxID string
|
l.Info("account deleted")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Delete account's statuses
|
// DeleteSelf is like Delete, but specifically for local accounts deleting themselves.
|
||||||
l.Trace("deleting account statuses")
|
//
|
||||||
|
// Calling DeleteSelf results in a delete message being enqueued in the processor,
|
||||||
|
// which causes side effects to occur: delete will be federated out to other instances,
|
||||||
|
// and the above Delete function will be called afterwards from the processor, to clear
|
||||||
|
// out the account's bits and bobs, and stubbify it.
|
||||||
|
func (p *Processor) DeleteSelf(ctx context.Context, account *gtsmodel.Account) gtserror.WithCode {
|
||||||
|
fromClientAPIMessage := messages.FromClientAPI{
|
||||||
|
APObjectType: ap.ActorPerson,
|
||||||
|
APActivityType: ap.ActivityDelete,
|
||||||
|
OriginAccount: account,
|
||||||
|
TargetAccount: account,
|
||||||
|
}
|
||||||
|
|
||||||
// we'll select statuses 20 at a time so we don't wreck the db, and pass them through to the client api channel
|
// Process the delete side effects asynchronously.
|
||||||
// Deleting the statuses in this way also handles 7. Delete account's media attachments, 8. Delete account's mentions, and 9. Delete account's polls,
|
p.state.Workers.EnqueueClientAPI(ctx, fromClientAPIMessage)
|
||||||
// since these are all attached to statuses.
|
|
||||||
|
|
||||||
for {
|
return nil
|
||||||
// Fetch next block of account statuses from database
|
}
|
||||||
statuses, err := p.state.DB.GetAccountStatuses(ctx, account.ID, 20, false, false, maxID, "", false, false)
|
|
||||||
|
// deleteUserAndTokensForAccount deletes the gtsmodel.User and
|
||||||
|
// any OAuth tokens and applications for the given account.
|
||||||
|
//
|
||||||
|
// Callers to this function should already have checked that
|
||||||
|
// this is a local account, or else it won't have a user associated
|
||||||
|
// with it, and this will fail.
|
||||||
|
func (p *Processor) deleteUserAndTokensForAccount(ctx context.Context, account *gtsmodel.Account) error {
|
||||||
|
user, err := p.state.DB.GetUserByAccountID(ctx, account.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("deleteUserAndTokensForAccount: db error getting user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens := []*gtsmodel.Token{}
|
||||||
|
if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "user_id", Value: user.ID}}, &tokens); err != nil {
|
||||||
|
return fmt.Errorf("deleteUserAndTokensForAccount: db error getting tokens: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range tokens {
|
||||||
|
// Delete any OAuth clients associated with this token.
|
||||||
|
if err := p.state.DB.DeleteByID(ctx, t.ClientID, &[]*gtsmodel.Client{}); err != nil {
|
||||||
|
return fmt.Errorf("deleteUserAndTokensForAccount: db error deleting client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete any OAuth applications associated with this token.
|
||||||
|
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "client_id", Value: t.ClientID}}, &[]*gtsmodel.Application{}); err != nil {
|
||||||
|
return fmt.Errorf("deleteUserAndTokensForAccount: db error deleting application: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the token itself.
|
||||||
|
if err := p.state.DB.DeleteByID(ctx, t.ID, t); err != nil {
|
||||||
|
return fmt.Errorf("deleteUserAndTokensForAccount: db error deleting token: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.state.DB.DeleteUserByID(ctx, user.ID); err != nil {
|
||||||
|
return fmt.Errorf("deleteUserAndTokensForAccount: db error deleting user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteAccountFollows deletes:
|
||||||
|
// - Follows targeting account.
|
||||||
|
// - Follow requests targeting account.
|
||||||
|
// - Follows created by account.
|
||||||
|
// - Follow requests created by account.
|
||||||
|
func (p *Processor) deleteAccountFollows(ctx context.Context, account *gtsmodel.Account) error {
|
||||||
|
// Delete follows targeting this account.
|
||||||
|
followedBy, err := p.state.DB.GetFollows(ctx, "", account.ID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
return fmt.Errorf("deleteAccountFollows: db error getting follows targeting account %s: %w", account.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, follow := range followedBy {
|
||||||
|
if _, err := p.state.DB.Unfollow(ctx, follow.AccountID, account.ID); err != nil {
|
||||||
|
return fmt.Errorf("deleteAccountFollows: db error unfollowing account followedBy: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete follow requests targeting this account.
|
||||||
|
followRequestedBy, err := p.state.DB.GetFollowRequests(ctx, "", account.ID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
return fmt.Errorf("deleteAccountFollows: db error getting follow requests targeting account %s: %w", account.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, followRequest := range followRequestedBy {
|
||||||
|
if _, err := p.state.DB.UnfollowRequest(ctx, followRequest.AccountID, account.ID); err != nil {
|
||||||
|
return fmt.Errorf("deleteAccountFollows: db error unfollowing account followRequestedBy: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Use this slice to batch unfollow messages.
|
||||||
|
msgs = []messages.FromClientAPI{}
|
||||||
|
// To avoid checking if account is local over + over
|
||||||
|
// inside the subsequent loops, just generate static
|
||||||
|
// side effects function once now.
|
||||||
|
unfollowSideEffects = p.unfollowSideEffectsFunc(account)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete follows originating from this account.
|
||||||
|
following, err := p.state.DB.GetFollows(ctx, account.ID, "")
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
return fmt.Errorf("deleteAccountFollows: db error getting follows owned by account %s: %w", account.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each follow owned by this account, unfollow
|
||||||
|
// and process side effects (noop if remote account).
|
||||||
|
for _, follow := range following {
|
||||||
|
if uri, err := p.state.DB.Unfollow(ctx, account.ID, follow.TargetAccountID); err != nil {
|
||||||
|
return fmt.Errorf("deleteAccountFollows: db error unfollowing account: %w", err)
|
||||||
|
} else if uri == "" {
|
||||||
|
// There was no follow after all.
|
||||||
|
// Some race condition? Skip.
|
||||||
|
log.WithContext(ctx).WithField("follow", follow).Warn("Unfollow did not return uri, likely race condition")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg := unfollowSideEffects(ctx, account, follow); msg != nil {
|
||||||
|
// There was a side effect to process.
|
||||||
|
msgs = append(msgs, *msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete follow requests originating from this account.
|
||||||
|
followRequesting, err := p.state.DB.GetFollowRequests(ctx, account.ID, "")
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
return fmt.Errorf("deleteAccountFollows: db error getting follow requests owned by account %s: %w", account.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each follow owned by this account, unfollow
|
||||||
|
// and process side effects (noop if remote account).
|
||||||
|
for _, followRequest := range followRequesting {
|
||||||
|
uri, err := p.state.DB.UnfollowRequest(ctx, account.ID, followRequest.TargetAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, db.ErrNoEntries) {
|
return fmt.Errorf("deleteAccountFollows: db error unfollowRequesting account: %w", err)
|
||||||
// an actual error has occurred
|
}
|
||||||
l.Errorf("Delete: db error selecting statuses for account %s: %s", account.Username, err)
|
|
||||||
}
|
if uri == "" {
|
||||||
break
|
// There was no follow request after all.
|
||||||
|
// Some race condition? Skip.
|
||||||
|
log.WithContext(ctx).WithField("followRequest", followRequest).Warn("UnfollowRequest did not return uri, likely race condition")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dummy out a follow so our side effects func
|
||||||
|
// has something to work with. This follow will
|
||||||
|
// never enter the db, it's just for convenience.
|
||||||
|
follow := >smodel.Follow{
|
||||||
|
URI: uri,
|
||||||
|
AccountID: followRequest.AccountID,
|
||||||
|
Account: followRequest.Account,
|
||||||
|
TargetAccountID: followRequest.TargetAccountID,
|
||||||
|
TargetAccount: followRequest.TargetAccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg := unfollowSideEffects(ctx, account, follow); msg != nil {
|
||||||
|
// There was a side effect to process.
|
||||||
|
msgs = append(msgs, *msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process accreted messages asynchronously.
|
||||||
|
p.state.Workers.EnqueueClientAPI(ctx, msgs...)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Processor) unfollowSideEffectsFunc(deletedAccount *gtsmodel.Account) func(ctx context.Context, account *gtsmodel.Account, follow *gtsmodel.Follow) *messages.FromClientAPI {
|
||||||
|
if !deletedAccount.IsLocal() {
|
||||||
|
// Don't try to process side effects
|
||||||
|
// for accounts that aren't local.
|
||||||
|
return func(ctx context.Context, account *gtsmodel.Account, follow *gtsmodel.Follow) *messages.FromClientAPI {
|
||||||
|
return nil // noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(ctx context.Context, account *gtsmodel.Account, follow *gtsmodel.Follow) *messages.FromClientAPI {
|
||||||
|
if follow.TargetAccount == nil {
|
||||||
|
// TargetAccount seems to have gone;
|
||||||
|
// race condition? db corruption?
|
||||||
|
log.WithContext(ctx).WithField("follow", follow).Warn("follow had no TargetAccount, likely race condition")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if follow.TargetAccount.IsLocal() {
|
||||||
|
// No side effects for local unfollows.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// There was a follow, process side effects.
|
||||||
|
return &messages.FromClientAPI{
|
||||||
|
APObjectType: ap.ActivityFollow,
|
||||||
|
APActivityType: ap.ActivityUndo,
|
||||||
|
GTSModel: follow,
|
||||||
|
OriginAccount: account,
|
||||||
|
TargetAccount: follow.TargetAccount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Processor) deleteAccountBlocks(ctx context.Context, account *gtsmodel.Account) error {
|
||||||
|
// Delete blocks created by this account.
|
||||||
|
if err := p.state.DB.DeleteBlocksByOriginAccountID(ctx, account.ID); err != nil {
|
||||||
|
return fmt.Errorf("deleteAccountBlocks: db error deleting blocks created by account %s: %w", account.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete blocks targeting this account.
|
||||||
|
if err := p.state.DB.DeleteBlocksByTargetAccountID(ctx, account.ID); err != nil {
|
||||||
|
return fmt.Errorf("deleteAccountBlocks: db error deleting blocks targeting account %s: %w", account.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteAccountStatuses iterates through all statuses owned by
|
||||||
|
// the given account, passing each discovered status (and boosts
|
||||||
|
// thereof) to the processor workers for further async processing.
|
||||||
|
func (p *Processor) deleteAccountStatuses(ctx context.Context, account *gtsmodel.Account) error {
|
||||||
|
// We'll select statuses 50 at a time so we don't wreck the db,
|
||||||
|
// and pass them through to the client api worker to handle.
|
||||||
|
//
|
||||||
|
// Deleting the statuses in this way also handles deleting the
|
||||||
|
// account's media attachments, mentions, and polls, since these
|
||||||
|
// are all attached to statuses.
|
||||||
|
|
||||||
|
var (
|
||||||
|
statuses []*gtsmodel.Status
|
||||||
|
err error
|
||||||
|
maxID string
|
||||||
|
msgs = []messages.FromClientAPI{}
|
||||||
|
)
|
||||||
|
|
||||||
|
statusLoop:
|
||||||
|
for {
|
||||||
|
// Page through account's statuses.
|
||||||
|
statuses, err = p.state.DB.GetAccountStatuses(ctx, account.ID, deleteSelectLimit, false, false, maxID, "", false, false)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
// Make sure we don't have a real error.
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(statuses) == 0 {
|
if len(statuses) == 0 {
|
||||||
break // reached end
|
break statusLoop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update next maxID from last status.
|
||||||
|
maxID = statuses[len(statuses)-1].ID
|
||||||
|
|
||||||
for _, status := range statuses {
|
for _, status := range statuses {
|
||||||
// Ensure account is set
|
status.Account = account // ensure account is set
|
||||||
status.Account = account
|
|
||||||
|
|
||||||
l.Tracef("queue client API status delete: %s", status.ID)
|
// Pass the status delete through the client api worker for processing.
|
||||||
|
msgs = append(msgs, messages.FromClientAPI{
|
||||||
// pass the status delete through the client api channel for processing
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
|
||||||
APObjectType: ap.ObjectNote,
|
APObjectType: ap.ObjectNote,
|
||||||
APActivityType: ap.ActivityDelete,
|
APActivityType: ap.ActivityDelete,
|
||||||
GTSModel: status,
|
GTSModel: status,
|
||||||
|
@ -156,30 +343,32 @@ func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origi
|
||||||
TargetAccount: account,
|
TargetAccount: account,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Look for any boosts of this status in DB
|
// Look for any boosts of this status in DB.
|
||||||
boosts, err := p.state.DB.GetStatusReblogs(ctx, status)
|
boosts, err := p.state.DB.GetStatusReblogs(ctx, status)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
l.Errorf("error fetching status reblogs for %q: %v", status.ID, err)
|
return fmt.Errorf("deleteAccountStatuses: error fetching status reblogs for %s: %w", status.ID, err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, boost := range boosts {
|
for _, boost := range boosts {
|
||||||
if boost.Account == nil {
|
if boost.Account == nil {
|
||||||
// Fetch the relevant account for this status boost
|
// Fetch the relevant account for this status boost.
|
||||||
boostAcc, err := p.state.DB.GetAccountByID(ctx, boost.AccountID)
|
boostAcc, err := p.state.DB.GetAccountByID(ctx, boost.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorf("error fetching boosted status account for %q: %v", boost.AccountID, err)
|
if errors.Is(err, db.ErrNoEntries) {
|
||||||
continue
|
// We don't have an account for this boost
|
||||||
|
// for some reason, so just skip processing.
|
||||||
|
log.WithContext(ctx).WithField("boost", boost).Warnf("no account found with id %s for boost %s", boost.AccountID, boost.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("deleteAccountStatuses: error fetching boosted status account for %s: %w", boost.AccountID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set account model
|
// Set account model
|
||||||
boost.Account = boostAcc
|
boost.Account = boostAcc
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Tracef("queue client API boost delete: %s", status.ID)
|
// Pass the boost delete through the client api worker for processing.
|
||||||
|
msgs = append(msgs, messages.FromClientAPI{
|
||||||
// pass the boost delete through the client api channel for processing
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
|
||||||
APObjectType: ap.ActivityAnnounce,
|
APObjectType: ap.ActivityAnnounce,
|
||||||
APActivityType: ap.ActivityUndo,
|
APActivityType: ap.ActivityUndo,
|
||||||
GTSModel: status,
|
GTSModel: status,
|
||||||
|
@ -188,128 +377,120 @@ func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origi
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update next maxID from last status
|
|
||||||
maxID = statuses[len(statuses)-1].ID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Delete account's notifications
|
// Batch process all accreted messages.
|
||||||
l.Trace("deleting account notifications")
|
p.state.Workers.EnqueueClientAPI(ctx, msgs...)
|
||||||
// first notifications created by account
|
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "origin_account_id", Value: account.ID}}, &[]*gtsmodel.Notification{}); err != nil {
|
return nil
|
||||||
l.Errorf("error deleting notifications created by account: %s", err)
|
}
|
||||||
|
|
||||||
|
func (p *Processor) deleteAccountNotifications(ctx context.Context, account *gtsmodel.Account) error {
|
||||||
|
// Delete all notifications targeting given account.
|
||||||
|
if err := p.state.DB.DeleteNotifications(ctx, account.ID, ""); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// now notifications targeting account
|
// Delete all notifications originating from given account.
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.Notification{}); err != nil {
|
if err := p.state.DB.DeleteNotifications(ctx, "", account.ID); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
l.Errorf("error deleting notifications targeting account: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 11. Delete account's bookmarks
|
return nil
|
||||||
l.Trace("deleting account bookmarks")
|
}
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusBookmark{}); err != nil {
|
|
||||||
l.Errorf("error deleting bookmarks created by account: %s", err)
|
func (p *Processor) deleteAccountPeripheral(ctx context.Context, account *gtsmodel.Account) error {
|
||||||
|
// Delete all bookmarks owned by given account.
|
||||||
|
if err := p.state.DB.DeleteStatusBookmarks(ctx, account.ID, ""); // nocollapse
|
||||||
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 12. Delete account's faves
|
// Delete all bookmarks targeting given account.
|
||||||
// TODO: federate these if necessary
|
if err := p.state.DB.DeleteStatusBookmarks(ctx, "", account.ID); // nocollapse
|
||||||
l.Trace("deleting account faves")
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusFave{}); err != nil {
|
return err
|
||||||
l.Errorf("error deleting faves created by account: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 13. Delete account's mutes
|
// Delete all faves owned by given account.
|
||||||
l.Trace("deleting account mutes")
|
if err := p.state.DB.DeleteStatusFaves(ctx, account.ID, ""); // nocollapse
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusMute{}); err != nil {
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
l.Errorf("error deleting status mutes created by account: %s", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 14. Delete account's streams
|
// Delete all faves targeting given account.
|
||||||
// TODO
|
if err := p.state.DB.DeleteStatusFaves(ctx, "", account.ID); // nocollapse
|
||||||
|
err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
// 15. Delete account's tags
|
return err
|
||||||
// TODO
|
|
||||||
|
|
||||||
// 16. Delete account's user
|
|
||||||
if user != nil {
|
|
||||||
l.Trace("deleting account user")
|
|
||||||
if err := p.state.DB.DeleteUserByID(ctx, user.ID); err != nil {
|
|
||||||
return gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 17. Delete account's timeline
|
// TODO: add status mutes here when they're implemented.
|
||||||
// TODO
|
|
||||||
|
|
||||||
// 18. Delete account itself
|
return nil
|
||||||
// to prevent the account being created again, set all these fields and update it in the db
|
}
|
||||||
// the account won't actually be *removed* from the database but it will be set to just a stub
|
|
||||||
account.Note = ""
|
// stubbifyAccount renders the given account as a stub,
|
||||||
account.DisplayName = ""
|
// removing most information from it and marking it as
|
||||||
|
// suspended.
|
||||||
|
//
|
||||||
|
// The origin parameter refers to the origin of the
|
||||||
|
// suspension action; should be an account ID or domain
|
||||||
|
// block ID.
|
||||||
|
//
|
||||||
|
// For caller's convenience, this function returns the db
|
||||||
|
// names of all columns that are updated by it.
|
||||||
|
func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
|
||||||
|
var (
|
||||||
|
falseBool = func() *bool { b := false; return &b }
|
||||||
|
trueBool = func() *bool { b := true; return &b }
|
||||||
|
now = time.Now()
|
||||||
|
never = time.Time{}
|
||||||
|
)
|
||||||
|
|
||||||
|
account.FetchedAt = never
|
||||||
account.AvatarMediaAttachmentID = ""
|
account.AvatarMediaAttachmentID = ""
|
||||||
account.AvatarRemoteURL = ""
|
account.AvatarRemoteURL = ""
|
||||||
account.HeaderMediaAttachmentID = ""
|
account.HeaderMediaAttachmentID = ""
|
||||||
account.HeaderRemoteURL = ""
|
account.HeaderRemoteURL = ""
|
||||||
|
account.DisplayName = ""
|
||||||
|
account.EmojiIDs = nil
|
||||||
|
account.Emojis = nil
|
||||||
|
account.Fields = nil
|
||||||
|
account.Note = ""
|
||||||
|
account.NoteRaw = ""
|
||||||
|
account.Memorial = falseBool()
|
||||||
|
account.AlsoKnownAs = ""
|
||||||
|
account.MovedToAccountID = ""
|
||||||
account.Reason = ""
|
account.Reason = ""
|
||||||
account.Emojis = []*gtsmodel.Emoji{}
|
account.Discoverable = falseBool()
|
||||||
account.EmojiIDs = []string{}
|
account.StatusContentType = ""
|
||||||
account.Fields = []gtsmodel.Field{}
|
account.CustomCSS = ""
|
||||||
hideCollections := true
|
account.SuspendedAt = now
|
||||||
account.HideCollections = &hideCollections
|
|
||||||
discoverable := false
|
|
||||||
account.Discoverable = &discoverable
|
|
||||||
account.SuspendedAt = time.Now()
|
|
||||||
account.SuspensionOrigin = origin
|
account.SuspensionOrigin = origin
|
||||||
err := p.state.DB.UpdateAccount(ctx, account)
|
account.HideCollections = trueBool()
|
||||||
if err != nil {
|
account.EnableRSS = falseBool()
|
||||||
return gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Infof("deleted account with username %s from domain %s", account.Username, account.Domain)
|
return []string{
|
||||||
return nil
|
"fetched_at",
|
||||||
}
|
"avatar_media_attachment_id",
|
||||||
|
"avatar_remote_url",
|
||||||
// DeleteLocal is like Delete, but specifically for deletion of local accounts rather than federated ones.
|
"header_media_attachment_id",
|
||||||
// Unlike Delete, it will propagate the deletion out across the federating API to other instances.
|
"header_remote_url",
|
||||||
func (p *Processor) DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode {
|
"display_name",
|
||||||
fromClientAPIMessage := messages.FromClientAPI{
|
"emojis",
|
||||||
APObjectType: ap.ActorPerson,
|
"fields",
|
||||||
APActivityType: ap.ActivityDelete,
|
"note",
|
||||||
TargetAccount: account,
|
"note_raw",
|
||||||
}
|
"memorial",
|
||||||
|
"also_known_as",
|
||||||
if form.DeleteOriginID == account.ID {
|
"moved_to_account_id",
|
||||||
// the account owner themself has requested deletion via the API, get their user from the db
|
"reason",
|
||||||
user, err := p.state.DB.GetUserByAccountID(ctx, account.ID)
|
"discoverable",
|
||||||
if err != nil {
|
"status_content_type",
|
||||||
return gtserror.NewErrorInternalError(err)
|
"custom_css",
|
||||||
}
|
"suspended_at",
|
||||||
|
"suspension_origin",
|
||||||
// now check that the password they supplied is correct
|
"hide_collections",
|
||||||
// make sure a password is actually set and bail if not
|
"enable_rss",
|
||||||
if user.EncryptedPassword == "" {
|
}
|
||||||
return gtserror.NewErrorForbidden(errors.New("user password was not set"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare the provided password with the encrypted one from the db, bail if they don't match
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(form.Password)); err != nil {
|
|
||||||
return gtserror.NewErrorForbidden(errors.New("invalid password"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fromClientAPIMessage.OriginAccount = account
|
|
||||||
} else {
|
|
||||||
// the delete has been requested by some other account, grab it;
|
|
||||||
// if we've reached this point we know it has permission already
|
|
||||||
requestingAccount, err := p.state.DB.GetAccountByID(ctx, form.DeleteOriginID)
|
|
||||||
if err != nil {
|
|
||||||
return gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fromClientAPIMessage.OriginAccount = requestingAccount
|
|
||||||
}
|
|
||||||
|
|
||||||
// put the delete in the processor queue to handle the rest of it asynchronously
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, fromClientAPIMessage)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
|
@ -33,172 +34,182 @@ import (
|
||||||
|
|
||||||
// FollowCreate handles a follow request to an account, either remote or local.
|
// FollowCreate handles a follow request to an account, either remote or local.
|
||||||
func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
|
func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
// if there's a block between the accounts we shouldn't create the request ofc
|
targetAccount, errWithCode := p.getFollowTarget(ctx, requestingAccount.ID, form.ID)
|
||||||
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, form.ID, true); err != nil {
|
if errWithCode != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, errWithCode
|
||||||
} else if blocked {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the target account actually exists in our db
|
// Check if a follow exists already.
|
||||||
targetAcct, err := p.state.DB.GetAccountByID(ctx, form.ID)
|
if follows, err := p.state.DB.IsFollowing(ctx, requestingAccount, targetAccount); err != nil {
|
||||||
if err != nil {
|
err = fmt.Errorf("FollowCreate: db error checking follow: %w", err)
|
||||||
if err == db.ErrNoEntries {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.ID, err))
|
|
||||||
}
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
|
||||||
|
|
||||||
// check if a follow exists already
|
|
||||||
if follows, err := p.state.DB.IsFollowing(ctx, requestingAccount, targetAcct); err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
|
|
||||||
} else if follows {
|
} else if follows {
|
||||||
// already follows so just return the relationship
|
// Already follows, just return current relationship.
|
||||||
return p.RelationshipGet(ctx, requestingAccount, form.ID)
|
return p.RelationshipGet(ctx, requestingAccount, form.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if a follow request exists already
|
// Check if a follow request exists already.
|
||||||
if followRequested, err := p.state.DB.IsFollowRequested(ctx, requestingAccount, targetAcct); err != nil {
|
if followRequested, err := p.state.DB.IsFollowRequested(ctx, requestingAccount, targetAccount); err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
|
err = fmt.Errorf("FollowCreate: db error checking follow request: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
} else if followRequested {
|
} else if followRequested {
|
||||||
// already follow requested so just return the relationship
|
// Already follow requested, just return current relationship.
|
||||||
return p.RelationshipGet(ctx, requestingAccount, form.ID)
|
return p.RelationshipGet(ctx, requestingAccount, form.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for attempt to follow self
|
// Create and store a new follow request.
|
||||||
if requestingAccount.ID == targetAcct.ID {
|
followID, err := id.NewRandomULID()
|
||||||
return nil, gtserror.NewErrorNotAcceptable(fmt.Errorf("accountfollowcreate: account %s cannot follow itself", requestingAccount.ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
// make the follow request
|
|
||||||
newFollowID, err := id.NewRandomULID()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
followURI := uris.GenerateURIForFollow(requestingAccount.Username, followID)
|
||||||
|
|
||||||
showReblogs := true
|
|
||||||
notify := false
|
|
||||||
fr := >smodel.FollowRequest{
|
fr := >smodel.FollowRequest{
|
||||||
ID: newFollowID,
|
ID: followID,
|
||||||
|
URI: followURI,
|
||||||
AccountID: requestingAccount.ID,
|
AccountID: requestingAccount.ID,
|
||||||
|
Account: requestingAccount,
|
||||||
TargetAccountID: form.ID,
|
TargetAccountID: form.ID,
|
||||||
ShowReblogs: &showReblogs,
|
TargetAccount: targetAccount,
|
||||||
URI: uris.GenerateURIForFollow(requestingAccount.Username, newFollowID),
|
ShowReblogs: form.Reblogs,
|
||||||
Notify: ¬ify,
|
Notify: form.Notify,
|
||||||
}
|
|
||||||
if form.Reblogs != nil {
|
|
||||||
fr.ShowReblogs = form.Reblogs
|
|
||||||
}
|
|
||||||
if form.Notify != nil {
|
|
||||||
fr.Notify = form.Notify
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// whack it in the database
|
|
||||||
if err := p.state.DB.Put(ctx, fr); err != nil {
|
if err := p.state.DB.Put(ctx, fr); err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
|
err = fmt.Errorf("FollowCreate: error creating follow request in db: %s", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's a local account that's not locked we can just straight up accept the follow request
|
if targetAccount.IsLocal() && !*targetAccount.Locked {
|
||||||
if !*targetAcct.Locked && targetAcct.Domain == "" {
|
// If the target account is local and not locked,
|
||||||
|
// we can already accept the follow request and
|
||||||
|
// skip any further processing.
|
||||||
|
//
|
||||||
|
// Because we know the requestingAccount is also
|
||||||
|
// local, we don't need to federate the accept out.
|
||||||
if _, err := p.state.DB.AcceptFollowRequest(ctx, requestingAccount.ID, form.ID); err != nil {
|
if _, err := p.state.DB.AcceptFollowRequest(ctx, requestingAccount.ID, form.ID); err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
|
err = fmt.Errorf("FollowCreate: error accepting follow request for local unlocked account: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
// return the new relationship
|
} else if targetAccount.IsRemote() {
|
||||||
return p.RelationshipGet(ctx, requestingAccount, form.ID)
|
// Otherwise we leave the follow request as it is,
|
||||||
|
// and we handle the rest of the process async.
|
||||||
|
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||||
|
APObjectType: ap.ActivityFollow,
|
||||||
|
APActivityType: ap.ActivityCreate,
|
||||||
|
GTSModel: fr,
|
||||||
|
OriginAccount: requestingAccount,
|
||||||
|
TargetAccount: targetAccount,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
|
||||||
APObjectType: ap.ActivityFollow,
|
|
||||||
APActivityType: ap.ActivityCreate,
|
|
||||||
GTSModel: fr,
|
|
||||||
OriginAccount: requestingAccount,
|
|
||||||
TargetAccount: targetAcct,
|
|
||||||
})
|
|
||||||
|
|
||||||
// return whatever relationship results from this
|
|
||||||
return p.RelationshipGet(ctx, requestingAccount, form.ID)
|
return p.RelationshipGet(ctx, requestingAccount, form.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FollowRemove handles the removal of a follow/follow request to an account, either remote or local.
|
// FollowRemove handles the removal of a follow/follow request to an account, either remote or local.
|
||||||
func (p *Processor) FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
func (p *Processor) FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
// if there's a block between the accounts we shouldn't do anything
|
targetAccount, errWithCode := p.getFollowTarget(ctx, requestingAccount.ID, targetAccountID)
|
||||||
blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true)
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unfollow and deal with side effects.
|
||||||
|
msgs, err := p.unfollow(ctx, requestingAccount, targetAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("FollowRemove: account %s not found in the db: %s", targetAccountID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch queue accreted client api messages.
|
||||||
|
p.state.Workers.EnqueueClientAPI(ctx, msgs...)
|
||||||
|
|
||||||
|
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Utility functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// getFollowTarget is a convenience function which:
|
||||||
|
// - Checks if account is trying to follow/unfollow itself.
|
||||||
|
// - Returns not found if there's a block in place between accounts.
|
||||||
|
// - Returns target account according to its id.
|
||||||
|
func (p *Processor) getFollowTarget(ctx context.Context, requestingAccountID string, targetAccountID string) (*gtsmodel.Account, gtserror.WithCode) {
|
||||||
|
// Account can't follow or unfollow itself.
|
||||||
|
if requestingAccountID == targetAccountID {
|
||||||
|
err := errors.New("account can't follow or unfollow itself")
|
||||||
|
return nil, gtserror.NewErrorNotAcceptable(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do nothing if a block exists in either direction between accounts.
|
||||||
|
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccountID, targetAccountID, true); err != nil {
|
||||||
|
err = fmt.Errorf("db error checking block between accounts: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
} else if blocked {
|
||||||
if blocked {
|
err = errors.New("block exists between accounts")
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the target account actually exists in our db
|
// Ensure target account retrievable.
|
||||||
targetAcct, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == db.ErrNoEntries {
|
if !errors.Is(err, db.ErrNoEntries) {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
|
// Real db error.
|
||||||
|
err = fmt.Errorf("db error looking for target account %s: %w", targetAccountID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
// Account not found.
|
||||||
|
err = fmt.Errorf("target account %s not found in the db", targetAccountID)
|
||||||
|
return nil, gtserror.NewErrorNotFound(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if a follow request exists, and remove it if it does (storing the URI for later)
|
return targetAccount, nil
|
||||||
var frChanged bool
|
}
|
||||||
var frURI string
|
|
||||||
fr := >smodel.FollowRequest{}
|
|
||||||
if err := p.state.DB.GetWhere(ctx, []db.Where{
|
|
||||||
{Key: "account_id", Value: requestingAccount.ID},
|
|
||||||
{Key: "target_account_id", Value: targetAccountID},
|
|
||||||
}, fr); err == nil {
|
|
||||||
frURI = fr.URI
|
|
||||||
if err := p.state.DB.DeleteByID(ctx, fr.ID, fr); err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
|
|
||||||
}
|
|
||||||
frChanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// now do the same thing for any existing follow
|
// unfollow is a convenience function for having requesting account
|
||||||
var fChanged bool
|
// unfollow (and un follow request) target account, if follows and/or
|
||||||
var fURI string
|
// follow requests exist.
|
||||||
f := >smodel.Follow{}
|
//
|
||||||
if err := p.state.DB.GetWhere(ctx, []db.Where{
|
// If a follow and/or follow request was removed this way, one or two
|
||||||
{Key: "account_id", Value: requestingAccount.ID},
|
// messages will be returned which should then be processed by a client
|
||||||
{Key: "target_account_id", Value: targetAccountID},
|
// api worker.
|
||||||
}, f); err == nil {
|
func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) ([]messages.FromClientAPI, error) {
|
||||||
fURI = f.URI
|
msgs := []messages.FromClientAPI{}
|
||||||
if err := p.state.DB.DeleteByID(ctx, f.ID, f); err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
|
|
||||||
}
|
|
||||||
fChanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// follow request status changed so send the UNDO activity to the channel for async processing
|
if fURI, err := p.state.DB.Unfollow(ctx, requestingAccount.ID, targetAccount.ID); err != nil {
|
||||||
if frChanged {
|
err = fmt.Errorf("unfollow: error deleting follow from %s targeting %s: %w", requestingAccount.ID, targetAccount.ID, err)
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
return nil, err
|
||||||
|
} else if fURI != "" {
|
||||||
|
// Follow status changed, process side effects.
|
||||||
|
msgs = append(msgs, messages.FromClientAPI{
|
||||||
APObjectType: ap.ActivityFollow,
|
APObjectType: ap.ActivityFollow,
|
||||||
APActivityType: ap.ActivityUndo,
|
APActivityType: ap.ActivityUndo,
|
||||||
GTSModel: >smodel.Follow{
|
GTSModel: >smodel.Follow{
|
||||||
AccountID: requestingAccount.ID,
|
AccountID: requestingAccount.ID,
|
||||||
TargetAccountID: targetAccountID,
|
TargetAccountID: targetAccount.ID,
|
||||||
URI: frURI,
|
|
||||||
},
|
|
||||||
OriginAccount: requestingAccount,
|
|
||||||
TargetAccount: targetAcct,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// follow status changed so send the UNDO activity to the channel for async processing
|
|
||||||
if fChanged {
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
|
||||||
APObjectType: ap.ActivityFollow,
|
|
||||||
APActivityType: ap.ActivityUndo,
|
|
||||||
GTSModel: >smodel.Follow{
|
|
||||||
AccountID: requestingAccount.ID,
|
|
||||||
TargetAccountID: targetAccountID,
|
|
||||||
URI: fURI,
|
URI: fURI,
|
||||||
},
|
},
|
||||||
OriginAccount: requestingAccount,
|
OriginAccount: requestingAccount,
|
||||||
TargetAccount: targetAcct,
|
TargetAccount: targetAccount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// return whatever relationship results from all this
|
if frURI, err := p.state.DB.UnfollowRequest(ctx, requestingAccount.ID, targetAccount.ID); err != nil {
|
||||||
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
|
err = fmt.Errorf("unfollow: error deleting follow request from %s targeting %s: %w", requestingAccount.ID, targetAccount.ID, err)
|
||||||
|
return nil, err
|
||||||
|
} else if frURI != "" {
|
||||||
|
// Follow request status changed, process side effects.
|
||||||
|
msgs = append(msgs, messages.FromClientAPI{
|
||||||
|
APObjectType: ap.ActivityFollow,
|
||||||
|
APActivityType: ap.ActivityUndo,
|
||||||
|
GTSModel: >smodel.Follow{
|
||||||
|
AccountID: requestingAccount.ID,
|
||||||
|
TargetAccountID: targetAccount.ID,
|
||||||
|
URI: frURI,
|
||||||
|
},
|
||||||
|
OriginAccount: requestingAccount,
|
||||||
|
TargetAccount: targetAccount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return msgs, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,98 +26,51 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FollowersGet fetches a list of the target account's followers.
|
// FollowersGet fetches a list of the target account's followers.
|
||||||
func (p *Processor) FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
func (p *Processor) FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||||
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
|
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
|
||||||
|
err = fmt.Errorf("FollowersGet: db error checking block: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
} else if blocked {
|
} else if blocked {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
err = errors.New("FollowersGet: block exists between accounts")
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts := []apimodel.Account{}
|
follows, err := p.state.DB.GetFollows(ctx, "", targetAccountID)
|
||||||
follows, err := p.state.DB.GetAccountFollowedBy(ctx, targetAccountID, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == db.ErrNoEntries {
|
if !errors.Is(err, db.ErrNoEntries) {
|
||||||
return accounts, nil
|
err = fmt.Errorf("FollowersGet: db error getting followers: %w", err)
|
||||||
}
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range follows {
|
|
||||||
blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
if blocked {
|
return []apimodel.Account{}, nil
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.Account == nil {
|
|
||||||
a, err := p.state.DB.GetAccountByID(ctx, f.AccountID)
|
|
||||||
if err != nil {
|
|
||||||
if err == db.ErrNoEntries {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
f.Account = a
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := p.tc.AccountToAPIAccountPublic(ctx, f.Account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
accounts = append(accounts, *account)
|
|
||||||
}
|
}
|
||||||
return accounts, nil
|
|
||||||
|
return p.accountsFromFollows(ctx, follows, requestingAccount.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FollowingGet fetches a list of the accounts that target account is following.
|
// FollowingGet fetches a list of the accounts that target account is following.
|
||||||
func (p *Processor) FollowingGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
func (p *Processor) FollowingGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||||
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
|
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil {
|
||||||
|
err = fmt.Errorf("FollowingGet: db error checking block: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
} else if blocked {
|
} else if blocked {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
err = errors.New("FollowingGet: block exists between accounts")
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts := []apimodel.Account{}
|
follows, err := p.state.DB.GetFollows(ctx, targetAccountID, "")
|
||||||
follows, err := p.state.DB.GetAccountFollows(ctx, targetAccountID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == db.ErrNoEntries {
|
if !errors.Is(err, db.ErrNoEntries) {
|
||||||
return accounts, nil
|
err = fmt.Errorf("FollowingGet: db error getting followers: %w", err)
|
||||||
}
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range follows {
|
|
||||||
blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
if blocked {
|
return []apimodel.Account{}, nil
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.TargetAccount == nil {
|
|
||||||
a, err := p.state.DB.GetAccountByID(ctx, f.TargetAccountID)
|
|
||||||
if err != nil {
|
|
||||||
if err == db.ErrNoEntries {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
f.TargetAccount = a
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := p.tc.AccountToAPIAccountPublic(ctx, f.TargetAccount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
accounts = append(accounts, *account)
|
|
||||||
}
|
}
|
||||||
return accounts, nil
|
|
||||||
|
return p.accountsFromFollows(ctx, follows, requestingAccount.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
|
// RelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
|
||||||
|
@ -138,3 +91,30 @@ func (p *Processor) RelationshipGet(ctx context.Context, requestingAccount *gtsm
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Processor) accountsFromFollows(ctx context.Context, follows []*gtsmodel.Follow, requestingAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||||
|
accounts := make([]apimodel.Account, 0, len(follows))
|
||||||
|
for _, follow := range follows {
|
||||||
|
if follow.Account == nil {
|
||||||
|
// No account set for some reason; just skip.
|
||||||
|
log.WithContext(ctx).WithField("follow", follow).Warn("follow had no associated account")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccountID, follow.AccountID, true); err != nil {
|
||||||
|
err = fmt.Errorf("accountsFromFollows: db error checking block: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
} else if blocked {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := p.tc.AccountToAPIAccountPublic(ctx, follow.Account)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("accountsFromFollows: error converting account to api account: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
accounts = append(accounts, *account)
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/activity/pub"
|
"github.com/superseriousbusiness/activity/pub"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
@ -52,10 +51,7 @@ func (suite *AccountTestSuite) TestAccountDeleteLocal() {
|
||||||
err := suite.db.Put(ctx, follow)
|
err := suite.db.Put(ctx, follow)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
errWithCode := suite.processor.Account().DeleteLocal(ctx, suite.testAccounts["local_account_1"], &apimodel.AccountDeleteRequest{
|
errWithCode := suite.processor.Account().DeleteSelf(ctx, suite.testAccounts["local_account_1"])
|
||||||
Password: "password",
|
|
||||||
DeleteOriginID: deletingAccount.ID,
|
|
||||||
})
|
|
||||||
suite.NoError(errWithCode)
|
suite.NoError(errWithCode)
|
||||||
|
|
||||||
// the delete should be federated outwards to the following account's inbox
|
// the delete should be federated outwards to the following account's inbox
|
||||||
|
|
|
@ -19,34 +19,33 @@ package processing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Processor) FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) {
|
func (p *Processor) FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) {
|
||||||
frs, err := p.state.DB.GetAccountFollowRequests(ctx, auth.Account.ID)
|
followRequests, err := p.state.DB.GetFollowRequests(ctx, "", auth.Account.ID)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
if err != db.ErrNoEntries {
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
accts := []apimodel.Account{}
|
accts := make([]apimodel.Account, 0, len(followRequests))
|
||||||
for _, fr := range frs {
|
for _, followRequest := range followRequests {
|
||||||
if fr.Account == nil {
|
if followRequest.Account == nil {
|
||||||
frAcct, err := p.state.DB.GetAccountByID(ctx, fr.AccountID)
|
// The creator of the follow doesn't exist,
|
||||||
if err != nil {
|
// just skip this one.
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
log.WithContext(ctx).WithField("followRequest", followRequest).Warn("follow request had no associated account")
|
||||||
}
|
continue
|
||||||
fr.Account = frAcct
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiAcct, err := p.tc.AccountToAPIAccountPublic(ctx, fr.Account)
|
apiAcct, err := p.tc.AccountToAPIAccountPublic(ctx, followRequest.Account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
@ -62,19 +61,10 @@ func (p *Processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, a
|
||||||
}
|
}
|
||||||
|
|
||||||
if follow.Account == nil {
|
if follow.Account == nil {
|
||||||
followAccount, err := p.state.DB.GetAccountByID(ctx, follow.AccountID)
|
// The creator of the follow doesn't exist,
|
||||||
if err != nil {
|
// so we can't do further processing.
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
log.WithContext(ctx).WithField("follow", follow).Warn("follow had no associated account")
|
||||||
}
|
return p.relationship(ctx, auth.Account.ID, accountID)
|
||||||
follow.Account = followAccount
|
|
||||||
}
|
|
||||||
|
|
||||||
if follow.TargetAccount == nil {
|
|
||||||
followTargetAccount, err := p.state.DB.GetAccountByID(ctx, follow.TargetAccountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
follow.TargetAccount = followTargetAccount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||||
|
@ -85,17 +75,7 @@ func (p *Processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, a
|
||||||
TargetAccount: follow.TargetAccount,
|
TargetAccount: follow.TargetAccount,
|
||||||
})
|
})
|
||||||
|
|
||||||
gtsR, err := p.state.DB.GetRelationship(ctx, auth.Account.ID, accountID)
|
return p.relationship(ctx, auth.Account.ID, accountID)
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := p.tc.RelationshipToAPIRelationship(ctx, gtsR)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
func (p *Processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
|
@ -105,19 +85,9 @@ func (p *Processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, a
|
||||||
}
|
}
|
||||||
|
|
||||||
if followRequest.Account == nil {
|
if followRequest.Account == nil {
|
||||||
a, err := p.state.DB.GetAccountByID(ctx, followRequest.AccountID)
|
// The creator of the request doesn't exist,
|
||||||
if err != nil {
|
// so we can't do further processing.
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return p.relationship(ctx, auth.Account.ID, accountID)
|
||||||
}
|
|
||||||
followRequest.Account = a
|
|
||||||
}
|
|
||||||
|
|
||||||
if followRequest.TargetAccount == nil {
|
|
||||||
a, err := p.state.DB.GetAccountByID(ctx, followRequest.TargetAccountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
followRequest.TargetAccount = a
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||||
|
@ -128,15 +98,19 @@ func (p *Processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, a
|
||||||
TargetAccount: followRequest.TargetAccount,
|
TargetAccount: followRequest.TargetAccount,
|
||||||
})
|
})
|
||||||
|
|
||||||
gtsR, err := p.state.DB.GetRelationship(ctx, auth.Account.ID, accountID)
|
return p.relationship(ctx, auth.Account.ID, accountID)
|
||||||
if err != nil {
|
}
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
func (p *Processor) relationship(ctx context.Context, accountID string, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
|
relationship, err := p.state.DB.GetRelationship(ctx, accountID, targetAccountID)
|
||||||
r, err := p.tc.RelationshipToAPIRelationship(ctx, gtsR)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
}
|
||||||
}
|
|
||||||
|
apiRelationship, err := p.tc.RelationshipToAPIRelationship(ctx, relationship)
|
||||||
return r, nil
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiRelationship, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,8 +139,8 @@ func (p *Processor) processCreateAccountFromClientAPI(ctx context.Context, clien
|
||||||
return errors.New("account was not parseable as *gtsmodel.Account")
|
return errors.New("account was not parseable as *gtsmodel.Account")
|
||||||
}
|
}
|
||||||
|
|
||||||
// return if the account isn't from this domain
|
// Do nothing if this isn't our activity.
|
||||||
if account.Domain != "" {
|
if !account.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,8 +383,8 @@ func (p *Processor) processReportAccountFromClientAPI(ctx context.Context, clien
|
||||||
// TODO: move all the below functions into federation.Federator
|
// TODO: move all the below functions into federation.Federator
|
||||||
|
|
||||||
func (p *Processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error {
|
func (p *Processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error {
|
||||||
// do nothing if this isn't our account
|
// Do nothing if this isn't our activity.
|
||||||
if account.Domain != "" {
|
if !account.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -449,8 +449,8 @@ func (p *Processor) federateStatus(ctx context.Context, status *gtsmodel.Status)
|
||||||
status.Account = statusAccount
|
status.Account = statusAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
// do nothing if this isn't our status
|
// Do nothing if this isn't our activity.
|
||||||
if status.Account.Domain != "" {
|
if !status.Account.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,8 +482,8 @@ func (p *Processor) federateStatusDelete(ctx context.Context, status *gtsmodel.S
|
||||||
status.Account = statusAccount
|
status.Account = statusAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
// do nothing if this isn't our status
|
// Do nothing if this isn't our activity.
|
||||||
if status.Account.Domain != "" {
|
if !status.Account.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -502,8 +502,8 @@ func (p *Processor) federateStatusDelete(ctx context.Context, status *gtsmodel.S
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) federateFollow(ctx context.Context, followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
func (p *Processor) federateFollow(ctx context.Context, followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
// if both accounts are local there's nothing to do here
|
// Do nothing if both accounts are local.
|
||||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
if originAccount.IsLocal() && targetAccount.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -524,8 +524,8 @@ func (p *Processor) federateFollow(ctx context.Context, followRequest *gtsmodel.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
func (p *Processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
// if both accounts are local there's nothing to do here
|
// Do nothing if both accounts are local.
|
||||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
if originAccount.IsLocal() && targetAccount.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -565,8 +565,8 @@ func (p *Processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
func (p *Processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
// if both accounts are local there's nothing to do here
|
// Do nothing if both accounts are local.
|
||||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
if originAccount.IsLocal() && targetAccount.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -604,8 +604,8 @@ func (p *Processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFav
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
func (p *Processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
if originAccount.Domain != "" {
|
// Do nothing if this isn't our activity.
|
||||||
// nothing to do here
|
if !originAccount.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -657,13 +657,9 @@ func (p *Processor) federateAcceptFollowRequest(ctx context.Context, follow *gts
|
||||||
}
|
}
|
||||||
targetAccount := follow.TargetAccount
|
targetAccount := follow.TargetAccount
|
||||||
|
|
||||||
// if target account isn't from our domain we shouldn't do anything
|
// Do nothing if target account *isn't* local,
|
||||||
if targetAccount.Domain != "" {
|
// or both origin + target *are* local.
|
||||||
return nil
|
if targetAccount.IsRemote() || originAccount.IsLocal() {
|
||||||
}
|
|
||||||
|
|
||||||
// if both accounts are local there's nothing to do here
|
|
||||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -730,13 +726,9 @@ func (p *Processor) federateRejectFollowRequest(ctx context.Context, followReque
|
||||||
}
|
}
|
||||||
targetAccount := followRequest.TargetAccount
|
targetAccount := followRequest.TargetAccount
|
||||||
|
|
||||||
// if target account isn't from our domain we shouldn't do anything
|
// Do nothing if target account *isn't* local,
|
||||||
if targetAccount.Domain != "" {
|
// or both origin + target *are* local.
|
||||||
return nil
|
if targetAccount.IsRemote() || originAccount.IsLocal() {
|
||||||
}
|
|
||||||
|
|
||||||
// if both accounts are local there's nothing to do here
|
|
||||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -786,8 +778,8 @@ func (p *Processor) federateRejectFollowRequest(ctx context.Context, followReque
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
func (p *Processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
// if both accounts are local there's nothing to do here
|
// Do nothing if both accounts are local.
|
||||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
if originAccount.IsLocal() && targetAccount.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -857,8 +849,8 @@ func (p *Processor) federateBlock(ctx context.Context, block *gtsmodel.Block) er
|
||||||
block.TargetAccount = blockTargetAccount
|
block.TargetAccount = blockTargetAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
// if both accounts are local there's nothing to do here
|
// Do nothing if both accounts are local.
|
||||||
if block.Account.Domain == "" && block.TargetAccount.Domain == "" {
|
if block.Account.IsLocal() && block.TargetAccount.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -893,8 +885,8 @@ func (p *Processor) federateUnblock(ctx context.Context, block *gtsmodel.Block)
|
||||||
block.TargetAccount = blockTargetAccount
|
block.TargetAccount = blockTargetAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
// if both accounts are local there's nothing to do here
|
// Do nothing if both accounts are local.
|
||||||
if block.Account.Domain == "" && block.TargetAccount.Domain == "" {
|
if block.Account.IsLocal() && block.TargetAccount.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/stream"
|
"github.com/superseriousbusiness/gotosocial/internal/stream"
|
||||||
|
@ -413,45 +412,30 @@ func (p *Processor) timelineStatus(ctx context.Context, status *gtsmodel.Status)
|
||||||
status.Account = a
|
status.Account = a
|
||||||
}
|
}
|
||||||
|
|
||||||
// get local followers of the account that posted the status
|
// Get LOCAL followers of the account that posted the status;
|
||||||
follows, err := p.state.DB.GetAccountFollowedBy(ctx, status.AccountID, true)
|
// we know that remote accounts don't have timelines on this
|
||||||
|
// instance, so there's no point selecting them too.
|
||||||
|
accountIDs, err := p.state.DB.GetLocalFollowersIDs(ctx, status.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("timelineStatus: error getting followers for account id %s: %s", status.AccountID, err)
|
return fmt.Errorf("timelineStatus: error getting followers for account id %s: %s", status.AccountID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the poster is local, add a fake entry for them to the followers list so they can see their own status in their timeline
|
// If the poster is also local, add a fake entry for them
|
||||||
if status.Account.Domain == "" {
|
// so they can see their own status in their timeline.
|
||||||
follows = append(follows, >smodel.Follow{
|
if status.Account.IsLocal() {
|
||||||
AccountID: status.AccountID,
|
accountIDs = append(accountIDs, status.AccountID)
|
||||||
Account: status.Account,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wg := sync.WaitGroup{}
|
// Timeline the status for each local following account.
|
||||||
wg.Add(len(follows))
|
errors := gtserror.MultiError{}
|
||||||
errors := make(chan error, len(follows))
|
for _, accountID := range accountIDs {
|
||||||
|
if err := p.timelineStatusForAccount(ctx, status, accountID); err != nil {
|
||||||
for _, f := range follows {
|
errors.Append(err)
|
||||||
go p.timelineStatusForAccount(ctx, status, f.AccountID, errors, &wg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// read any errors that come in from the async functions
|
|
||||||
errs := []string{}
|
|
||||||
go func(errs []string) {
|
|
||||||
for range errors {
|
|
||||||
if e := <-errors; e != nil {
|
|
||||||
errs = append(errs, e.Error())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}(errs)
|
}
|
||||||
|
|
||||||
// wait til all functions have returned and then close the error channel
|
if len(errors) != 0 {
|
||||||
wg.Wait()
|
return fmt.Errorf("timelineStatus: one or more errors timelining statuses: %w", errors.Combine())
|
||||||
close(errors)
|
|
||||||
|
|
||||||
if len(errs) != 0 {
|
|
||||||
// we have at least one error
|
|
||||||
return fmt.Errorf("timelineStatus: one or more errors timelining statuses: %s", strings.Join(errs, ";"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -462,46 +446,38 @@ func (p *Processor) timelineStatus(ctx context.Context, status *gtsmodel.Status)
|
||||||
//
|
//
|
||||||
// If the status was inserted into the home timeline of the given account,
|
// If the status was inserted into the home timeline of the given account,
|
||||||
// it will also be streamed via websockets to the user.
|
// it will also be streamed via websockets to the user.
|
||||||
func (p *Processor) timelineStatusForAccount(ctx context.Context, status *gtsmodel.Status, accountID string, errors chan error, wg *sync.WaitGroup) {
|
func (p *Processor) timelineStatusForAccount(ctx context.Context, status *gtsmodel.Status, accountID string) error {
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
// get the timeline owner account
|
// get the timeline owner account
|
||||||
timelineAccount, err := p.state.DB.GetAccountByID(ctx, accountID)
|
timelineAccount, err := p.state.DB.GetAccountByID(ctx, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errors <- fmt.Errorf("timelineStatusForAccount: error getting account for timeline with id %s: %s", accountID, err)
|
return fmt.Errorf("timelineStatusForAccount: error getting account for timeline with id %s: %w", accountID, err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the status is timelineable
|
// make sure the status is timelineable
|
||||||
timelineable, err := p.filter.StatusHometimelineable(ctx, status, timelineAccount)
|
if timelineable, err := p.filter.StatusHometimelineable(ctx, status, timelineAccount); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("timelineStatusForAccount: error getting timelineability for status for timeline with id %s: %w", accountID, err)
|
||||||
errors <- fmt.Errorf("timelineStatusForAccount: error getting timelineability for status for timeline with id %s: %s", accountID, err)
|
} else if !timelineable {
|
||||||
return
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
if !timelineable {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// stick the status in the timeline for the account and then immediately prepare it so they can see it right away
|
// stick the status in the timeline for the account and then immediately prepare it so they can see it right away
|
||||||
inserted, err := p.statusTimelines.IngestAndPrepare(ctx, status, timelineAccount.ID)
|
if inserted, err := p.statusTimelines.IngestAndPrepare(ctx, status, timelineAccount.ID); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("timelineStatusForAccount: error ingesting status %s: %w", status.ID, err)
|
||||||
errors <- fmt.Errorf("timelineStatusForAccount: error ingesting status %s: %s", status.ID, err)
|
} else if !inserted {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// the status was inserted so stream it to the user
|
// the status was inserted so stream it to the user
|
||||||
if inserted {
|
apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, timelineAccount)
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, timelineAccount)
|
if err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("timelineStatusForAccount: error converting status %s to frontend representation: %w", status.ID, err)
|
||||||
errors <- fmt.Errorf("timelineStatusForAccount: error converting status %s to frontend representation: %s", status.ID, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.stream.Update(apiStatus, timelineAccount, stream.TimelineHome); err != nil {
|
|
||||||
errors <- fmt.Errorf("timelineStatusForAccount: error streaming status %s: %s", status.ID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := p.stream.Update(apiStatus, timelineAccount, stream.TimelineHome); err != nil {
|
||||||
|
return fmt.Errorf("timelineStatusForAccount: error streaming update for status %s: %w", status.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteStatusFromTimelines completely removes the given status from all timelines.
|
// deleteStatusFromTimelines completely removes the given status from all timelines.
|
||||||
|
@ -544,12 +520,17 @@ func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete all notification entries generated by this status
|
// delete all notification entries generated by this status
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil {
|
if err := p.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete all bookmarks that point to this status
|
// delete all bookmarks that point to this status
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.StatusBookmark{}); err != nil {
|
if err := p.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete all faves of this status
|
||||||
|
if err := p.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,10 @@ package processing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
@ -39,7 +41,7 @@ func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ex
|
||||||
return util.EmptyPageableResponse(), nil
|
return util.EmptyPageableResponse(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
items := []interface{}{}
|
items := make([]interface{}, 0, count)
|
||||||
nextMaxIDValue := ""
|
nextMaxIDValue := ""
|
||||||
prevMinIDValue := ""
|
prevMinIDValue := ""
|
||||||
for i, n := range notifs {
|
for i, n := range notifs {
|
||||||
|
@ -71,8 +73,8 @@ func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode {
|
func (p *Processor) NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode {
|
||||||
err := p.state.DB.ClearNotifications(ctx, authed.Account.ID)
|
// Delete all notifications that target the authorized account.
|
||||||
if err != nil {
|
if err := p.state.DB.DeleteNotifications(ctx, authed.Account.ID, ""); err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
return gtserror.NewErrorInternalError(err)
|
return gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -138,20 +138,24 @@ func NewProcessor(
|
||||||
return processor
|
return processor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) EnqueueClientAPI(ctx context.Context, msg messages.FromClientAPI) {
|
func (p *Processor) EnqueueClientAPI(ctx context.Context, msgs ...messages.FromClientAPI) {
|
||||||
log.WithContext(ctx).WithField("msg", msg).Trace("enqueuing client API")
|
log.Trace(ctx, "enqueuing client API")
|
||||||
_ = p.state.Workers.ClientAPI.MustEnqueueCtx(ctx, func(ctx context.Context) {
|
_ = p.state.Workers.ClientAPI.MustEnqueueCtx(ctx, func(ctx context.Context) {
|
||||||
if err := p.ProcessFromClientAPI(ctx, msg); err != nil {
|
for _, msg := range msgs {
|
||||||
log.Errorf(ctx, "error processing client API message: %v", err)
|
if err := p.ProcessFromClientAPI(ctx, msg); err != nil {
|
||||||
|
log.WithContext(ctx).WithField("msg", msg).Errorf("error processing client API message: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) EnqueueFederator(ctx context.Context, msg messages.FromFederator) {
|
func (p *Processor) EnqueueFederator(ctx context.Context, msgs ...messages.FromFederator) {
|
||||||
log.WithContext(ctx).WithField("msg", msg).Trace("enqueuing federator")
|
log.Trace(ctx, "enqueuing federator")
|
||||||
_ = p.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
|
_ = p.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
|
||||||
if err := p.ProcessFromFederator(ctx, msg); err != nil {
|
for _, msg := range msgs {
|
||||||
log.Errorf(ctx, "error processing federator message: %v", err)
|
if err := p.ProcessFromFederator(ctx, msg); err != nil {
|
||||||
|
log.WithContext(ctx).WithField("msg", msg).Errorf("error processing federator message: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,91 +31,67 @@ import (
|
||||||
|
|
||||||
// BookmarkCreate adds a bookmark for the requestingAccount, targeting the given status (no-op if bookmark already exists).
|
// BookmarkCreate adds a bookmark for the requestingAccount, targeting the given status (no-op if bookmark already exists).
|
||||||
func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
|
targetStatus, existingBookmarkID, errWithCode := p.getBookmarkTarget(ctx, requestingAccount, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
return nil, errWithCode
|
||||||
}
|
|
||||||
if targetStatus.Account == nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
|
||||||
}
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// first check if the status is already bookmarked, if so we don't need to do anything
|
if existingBookmarkID != "" {
|
||||||
newBookmark := true
|
// Status is already bookmarked.
|
||||||
gtsBookmark := >smodel.StatusBookmark{}
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil {
|
|
||||||
// we already have a bookmark for this status
|
|
||||||
newBookmark = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if newBookmark {
|
// Create and store a new bookmark.
|
||||||
// we need to create a new bookmark in the database
|
gtsBookmark := >smodel.StatusBookmark{
|
||||||
gtsBookmark := >smodel.StatusBookmark{
|
ID: id.NewULID(),
|
||||||
ID: id.NewULID(),
|
AccountID: requestingAccount.ID,
|
||||||
AccountID: requestingAccount.ID,
|
Account: requestingAccount,
|
||||||
Account: requestingAccount,
|
TargetAccountID: targetStatus.AccountID,
|
||||||
TargetAccountID: targetStatus.AccountID,
|
TargetAccount: targetStatus.Account,
|
||||||
TargetAccount: targetStatus.Account,
|
StatusID: targetStatus.ID,
|
||||||
StatusID: targetStatus.ID,
|
Status: targetStatus,
|
||||||
Status: targetStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.state.DB.Put(ctx, gtsBookmark); err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting bookmark in database: %s", err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the apidon representation of the target status
|
if err := p.state.DB.PutStatusBookmark(ctx, gtsBookmark); err != nil {
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
err = fmt.Errorf("BookmarkCreate: error putting bookmark in database: %w", err)
|
||||||
if err != nil {
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiStatus, nil
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BookmarkRemove removes a bookmark for the requesting account, targeting the given status (no-op if bookmark doesn't exist).
|
// BookmarkRemove removes a bookmark for the requesting account, targeting the given status (no-op if bookmark doesn't exist).
|
||||||
func (p *Processor) BookmarkRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) BookmarkRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
|
targetStatus, existingBookmarkID, errWithCode := p.getBookmarkTarget(ctx, requestingAccount, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
return nil, errWithCode
|
||||||
}
|
|
||||||
if targetStatus.Account == nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
|
||||||
}
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// first check if the status is actually bookmarked
|
if existingBookmarkID == "" {
|
||||||
toUnbookmark := false
|
// Status isn't bookmarked.
|
||||||
gtsBookmark := >smodel.StatusBookmark{}
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil {
|
|
||||||
// we have a bookmark for this status
|
|
||||||
toUnbookmark = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if toUnbookmark {
|
// We have a bookmark to remove.
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err != nil {
|
if err := p.state.DB.DeleteStatusBookmark(ctx, existingBookmarkID); err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
|
err = fmt.Errorf("BookmarkRemove: error removing status bookmark: %w", err)
|
||||||
}
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the api representation of the target status
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
func (p *Processor) getBookmarkTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, string, gtserror.WithCode) {
|
||||||
}
|
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
return apiStatus, nil
|
return nil, "", errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkID, err := p.state.DB.GetStatusBookmarkID(ctx, requestingAccount.ID, targetStatus.ID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err = fmt.Errorf("getBookmarkTarget: error checking existing bookmark: %w", err)
|
||||||
|
return nil, "", gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetStatus, bookmarkID, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,13 +86,7 @@ func (p *Processor) BoostCreate(ctx context.Context, requestingAccount *gtsmodel
|
||||||
TargetAccount: targetStatus.Account,
|
TargetAccount: targetStatus.Account,
|
||||||
})
|
})
|
||||||
|
|
||||||
// return the frontend representation of the new status to the submitter
|
return p.apiStatus(ctx, boostWrapperStatus, requestingAccount)
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, boostWrapperStatus, requestingAccount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well.
|
// BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well.
|
||||||
|
@ -159,12 +153,7 @@ func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
||||||
|
|
|
@ -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 status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Processor) apiStatus(ctx context.Context, targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("error converting status %s to frontend representation: %w", targetStatus.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Processor) getVisibleStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, gtserror.WithCode) {
|
||||||
|
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("getVisibleStatus: db error fetching status %s: %w", targetStatusID, err)
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetStatus.Account == nil {
|
||||||
|
err = fmt.Errorf("getVisibleStatus: no status owner for status %s", targetStatusID)
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("getVisibleStatus: error seeing if status %s is visible: %w", targetStatus.ID, err)
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visible {
|
||||||
|
err = fmt.Errorf("getVisibleStatus: status %s is not visible to requesting account", targetStatusID)
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetStatus, nil
|
||||||
|
}
|
|
@ -93,13 +93,7 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, appli
|
||||||
OriginAccount: account,
|
OriginAccount: account,
|
||||||
})
|
})
|
||||||
|
|
||||||
// return the frontend representation of the new status to the submitter
|
return p.apiStatus(ctx, newStatus, account)
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, newStatus, account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", newStatus.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func processReplyToID(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
|
func processReplyToID(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
|
||||||
|
|
|
@ -35,6 +35,7 @@ func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Acco
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetStatus.Account == nil {
|
if targetStatus.Account == nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
||||||
}
|
}
|
||||||
|
@ -43,12 +44,13 @@ func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Acco
|
||||||
return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account"))
|
return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account"))
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
// Parse the status to API model BEFORE deleting it.
|
||||||
if err != nil {
|
apiStatus, errWithCode := p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// send the status back to the processor for async processing
|
// Process delete side effects.
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||||
APObjectType: ap.ObjectNote,
|
APObjectType: ap.ObjectNote,
|
||||||
APActivityType: ap.ActivityDelete,
|
APActivityType: ap.ActivityDelete,
|
||||||
|
|
|
@ -28,181 +28,140 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FaveCreate processes the faving of a given status, returning the updated status if the fave goes through.
|
// FaveCreate adds a fave for the requestingAccount, targeting the given status (no-op if fave already exists).
|
||||||
func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
|
targetStatus, existingFave, errWithCode := p.getFaveTarget(ctx, requestingAccount, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
return nil, errWithCode
|
||||||
}
|
|
||||||
if targetStatus.Account == nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
if existingFave != nil {
|
||||||
if err != nil {
|
// Status is already faveed.
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
|
||||||
}
|
|
||||||
if !*targetStatus.Likeable {
|
|
||||||
return nil, gtserror.NewErrorForbidden(errors.New("status is not faveable"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// first check if the status is already faved, if so we don't need to do anything
|
// Create and store a new fave
|
||||||
newFave := true
|
faveID := id.NewULID()
|
||||||
gtsFave := >smodel.StatusFave{}
|
gtsFave := >smodel.StatusFave{
|
||||||
if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err == nil {
|
ID: faveID,
|
||||||
// we already have a fave for this status
|
AccountID: requestingAccount.ID,
|
||||||
newFave = false
|
Account: requestingAccount,
|
||||||
|
TargetAccountID: targetStatus.AccountID,
|
||||||
|
TargetAccount: targetStatus.Account,
|
||||||
|
StatusID: targetStatus.ID,
|
||||||
|
Status: targetStatus,
|
||||||
|
URI: uris.GenerateURIForLike(requestingAccount.Username, faveID),
|
||||||
}
|
}
|
||||||
|
|
||||||
if newFave {
|
if err := p.state.DB.PutStatusFave(ctx, gtsFave); err != nil {
|
||||||
thisFaveID := id.NewULID()
|
err = fmt.Errorf("FaveCreate: error putting fave in database: %w", err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
// we need to create a new fave in the database
|
|
||||||
gtsFave := >smodel.StatusFave{
|
|
||||||
ID: thisFaveID,
|
|
||||||
AccountID: requestingAccount.ID,
|
|
||||||
Account: requestingAccount,
|
|
||||||
TargetAccountID: targetStatus.AccountID,
|
|
||||||
TargetAccount: targetStatus.Account,
|
|
||||||
StatusID: targetStatus.ID,
|
|
||||||
Status: targetStatus,
|
|
||||||
URI: uris.GenerateURIForLike(requestingAccount.Username, thisFaveID),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.state.DB.Put(ctx, gtsFave); err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting fave in database: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// send it back to the processor for async processing
|
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
|
||||||
APObjectType: ap.ActivityLike,
|
|
||||||
APActivityType: ap.ActivityCreate,
|
|
||||||
GTSModel: gtsFave,
|
|
||||||
OriginAccount: requestingAccount,
|
|
||||||
TargetAccount: targetStatus.Account,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the apidon representation of the target status
|
// Process new status fave side effects.
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||||
if err != nil {
|
APObjectType: ap.ActivityLike,
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
APActivityType: ap.ActivityCreate,
|
||||||
}
|
GTSModel: gtsFave,
|
||||||
|
OriginAccount: requestingAccount,
|
||||||
|
TargetAccount: targetStatus.Account,
|
||||||
|
})
|
||||||
|
|
||||||
return apiStatus, nil
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FaveRemove processes the unfaving of a given status, returning the updated status if the fave goes through.
|
// FaveRemove removes a fave for the requesting account, targeting the given status (no-op if fave doesn't exist).
|
||||||
func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
|
targetStatus, existingFave, errWithCode := p.getFaveTarget(ctx, requestingAccount, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
return nil, errWithCode
|
||||||
}
|
|
||||||
if targetStatus.Account == nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
if existingFave == nil {
|
||||||
if err != nil {
|
// Status isn't faveed.
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we actually have a fave for this status
|
// We have a fave to remove.
|
||||||
var toUnfave bool
|
if err := p.state.DB.DeleteStatusFave(ctx, existingFave.ID); err != nil {
|
||||||
|
err = fmt.Errorf("FaveRemove: error removing status fave: %w", err)
|
||||||
gtsFave := >smodel.StatusFave{}
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
err = p.state.DB.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave)
|
|
||||||
if err == nil {
|
|
||||||
// we have a fave
|
|
||||||
toUnfave = true
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// something went wrong in the db finding the fave
|
|
||||||
if err != db.ErrNoEntries {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err))
|
|
||||||
}
|
|
||||||
// we just don't have a fave
|
|
||||||
toUnfave = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if toUnfave {
|
// Process remove status fave side effects.
|
||||||
// we had a fave, so take some action to get rid of it
|
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||||
if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err != nil {
|
APObjectType: ap.ActivityLike,
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
|
APActivityType: ap.ActivityUndo,
|
||||||
}
|
GTSModel: existingFave,
|
||||||
|
OriginAccount: requestingAccount,
|
||||||
|
TargetAccount: targetStatus.Account,
|
||||||
|
})
|
||||||
|
|
||||||
// send it back to the processor for async processing
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
|
||||||
APObjectType: ap.ActivityLike,
|
|
||||||
APActivityType: ap.ActivityUndo,
|
|
||||||
GTSModel: gtsFave,
|
|
||||||
OriginAccount: requestingAccount,
|
|
||||||
TargetAccount: targetStatus.Account,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
||||||
func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
|
func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
|
||||||
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
|
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
return nil, errWithCode
|
||||||
}
|
|
||||||
if targetStatus.Account == nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
statusFaves, err := p.state.DB.GetStatusFaves(ctx, targetStatus.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("FavedBy: error seeing who faved status: %s", err))
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
statusFaves, err := p.state.DB.GetStatusFaves(ctx, targetStatus)
|
// For each fave, ensure that we're only showing
|
||||||
if err != nil {
|
// the requester accounts that they don't block,
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing who faved status: %s", err))
|
// and which don't block them.
|
||||||
}
|
apiAccounts := make([]*apimodel.Account, 0, len(statusFaves))
|
||||||
|
|
||||||
// filter the list so the user doesn't see accounts they blocked or which blocked them
|
|
||||||
filteredAccounts := []*gtsmodel.Account{}
|
|
||||||
for _, fave := range statusFaves {
|
for _, fave := range statusFaves {
|
||||||
blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, fave.AccountID, true)
|
if blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, fave.AccountID, true); err != nil {
|
||||||
if err != nil {
|
err = fmt.Errorf("FavedBy: error checking blocks: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err))
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
} else if blocked {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if !blocked {
|
|
||||||
filteredAccounts = append(filteredAccounts, fave.Account)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now we can return the api representation of those accounts
|
if fave.Account == nil {
|
||||||
apiAccounts := []*apimodel.Account{}
|
// Account isn't set for some reason, just skip.
|
||||||
for _, acc := range filteredAccounts {
|
log.WithContext(ctx).WithField("fave", fave).Warn("fave had no associated account")
|
||||||
apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, acc)
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, fave.Account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
err = fmt.Errorf("FavedBy: error converting account %s to frontend representation: %w", fave.AccountID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
apiAccounts = append(apiAccounts, apiAccount)
|
apiAccounts = append(apiAccounts, apiAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiAccounts, nil
|
return apiAccounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Processor) getFaveTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, *gtsmodel.StatusFave, gtserror.WithCode) {
|
||||||
|
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*targetStatus.Likeable {
|
||||||
|
err := errors.New("status is not faveable")
|
||||||
|
return nil, nil, gtserror.NewErrorForbidden(err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fave, err := p.state.DB.GetStatusFaveByAccountID(ctx, requestingAccount.ID, targetStatusID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err)
|
||||||
|
return nil, nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetStatus, fave, nil
|
||||||
|
}
|
||||||
|
|
|
@ -19,8 +19,6 @@ package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
@ -30,46 +28,19 @@ import (
|
||||||
|
|
||||||
// Get gets the given status, taking account of privacy settings and blocks etc.
|
// Get gets the given status, taking account of privacy settings and blocks etc.
|
||||||
func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
|
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
return nil, errWithCode
|
||||||
}
|
|
||||||
if targetStatus.Account == nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
|
||||||
}
|
|
||||||
|
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextGet returns the context (previous and following posts) from the given status ID.
|
// ContextGet returns the context (previous and following posts) from the given status ID.
|
||||||
func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
|
func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
|
||||||
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
|
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
return nil, errWithCode
|
||||||
}
|
|
||||||
if targetStatus.Account == nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
|
|
||||||
}
|
|
||||||
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
if !visible {
|
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context := &apimodel.Context{
|
context := &apimodel.Context{
|
||||||
|
|
|
@ -38,40 +38,24 @@ const allowedPinnedCount = 10
|
||||||
// - Status belongs to requesting account.
|
// - Status belongs to requesting account.
|
||||||
// - Status is public, unlisted, or followers-only.
|
// - Status is public, unlisted, or followers-only.
|
||||||
// - Status is not a boost.
|
// - Status is not a boost.
|
||||||
func (p *Processor) getPinnableStatus(ctx context.Context, targetStatusID string, requestingAccountID string) (*gtsmodel.Status, gtserror.WithCode) {
|
func (p *Processor) getPinnableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
|
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
err = fmt.Errorf("error fetching status %s: %w", targetStatusID, err)
|
return nil, errWithCode
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestingAccount, err := p.state.DB.GetAccountByID(ctx, requestingAccountID)
|
if targetStatus.AccountID != requestingAccount.ID {
|
||||||
if err != nil {
|
err := fmt.Errorf("status %s does not belong to account %s", targetStatusID, requestingAccount.ID)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
|
||||||
err = fmt.Errorf("status %s not visible to account %s", targetStatusID, requestingAccountID)
|
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetStatus.AccountID != requestingAccountID {
|
|
||||||
err = fmt.Errorf("status %s does not belong to account %s", targetStatusID, requestingAccountID)
|
|
||||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetStatus.Visibility == gtsmodel.VisibilityDirect {
|
if targetStatus.Visibility == gtsmodel.VisibilityDirect {
|
||||||
err = errors.New("cannot pin direct messages")
|
err := errors.New("cannot pin direct messages")
|
||||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetStatus.BoostOfID != "" {
|
if targetStatus.BoostOfID != "" {
|
||||||
err = errors.New("cannot pin boosts")
|
err := errors.New("cannot pin boosts")
|
||||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +73,7 @@ func (p *Processor) getPinnableStatus(ctx context.Context, targetStatusID string
|
||||||
//
|
//
|
||||||
// If the conditions can't be met, then code 422 Unprocessable Entity will be returned.
|
// If the conditions can't be met, then code 422 Unprocessable Entity will be returned.
|
||||||
func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, errWithCode := p.getPinnableStatus(ctx, targetStatusID, requestingAccount.ID)
|
targetStatus, errWithCode := p.getPinnableStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
@ -114,12 +98,7 @@ func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.A
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error pinning status: %w", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error pinning status: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %w", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PinRemove unpins the target status from the top of requestingAccount's profile, if possible.
|
// PinRemove unpins the target status from the top of requestingAccount's profile, if possible.
|
||||||
|
@ -134,7 +113,7 @@ func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.A
|
||||||
// Unlike with PinCreate, statuses that are already unpinned will not return 422, but just do
|
// Unlike with PinCreate, statuses that are already unpinned will not return 422, but just do
|
||||||
// nothing and return the api model representation of the status, to conform to the masto API.
|
// nothing and return the api model representation of the status, to conform to the masto API.
|
||||||
func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, errWithCode := p.getPinnableStatus(ctx, targetStatusID, requestingAccount.ID)
|
targetStatus, errWithCode := p.getPinnableStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
@ -146,10 +125,5 @@ func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.A
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
||||||
if err != nil {
|
|
||||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %w", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ import (
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -60,15 +59,9 @@ func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
|
||||||
// then adding the Source object to it...
|
// then adding the Source object to it...
|
||||||
|
|
||||||
// check pending follow requests aimed at this account
|
// check pending follow requests aimed at this account
|
||||||
frs, err := c.db.GetAccountFollowRequests(ctx, a.ID)
|
frc, err := c.db.CountFollowRequests(ctx, "", a.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != db.ErrNoEntries {
|
return nil, fmt.Errorf("error counting follow requests: %s", err)
|
||||||
return nil, fmt.Errorf("error getting follow requests: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var frc int
|
|
||||||
if frs != nil {
|
|
||||||
frc = len(frs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
statusContentType := string(apimodel.StatusContentTypeDefault)
|
statusContentType := string(apimodel.StatusContentTypeDefault)
|
||||||
|
@ -91,13 +84,13 @@ func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
|
||||||
|
|
||||||
func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
|
||||||
// count followers
|
// count followers
|
||||||
followersCount, err := c.db.CountAccountFollowedBy(ctx, a.ID, false)
|
followersCount, err := c.db.CountFollows(ctx, "", a.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error counting followers: %s", err)
|
return nil, fmt.Errorf("error counting followers: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// count following
|
// count following
|
||||||
followingCount, err := c.db.CountAccountFollows(ctx, a.ID, false)
|
followingCount, err := c.db.CountFollows(ctx, a.ID, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error counting following: %s", err)
|
return nil, fmt.Errorf("error counting following: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,8 @@ type Workers struct {
|
||||||
// Enqueue functions for clientAPI / federator worker pools,
|
// Enqueue functions for clientAPI / federator worker pools,
|
||||||
// these are pointers to Processor{}.Enqueue___() msg functions.
|
// these are pointers to Processor{}.Enqueue___() msg functions.
|
||||||
// This prevents dependency cycling as Processor depends on Workers.
|
// This prevents dependency cycling as Processor depends on Workers.
|
||||||
EnqueueClientAPI func(context.Context, messages.FromClientAPI)
|
EnqueueClientAPI func(context.Context, ...messages.FromClientAPI)
|
||||||
EnqueueFederator func(context.Context, messages.FromFederator)
|
EnqueueFederator func(context.Context, ...messages.FromFederator)
|
||||||
|
|
||||||
// Media manager worker pools.
|
// Media manager worker pools.
|
||||||
Media runners.WorkerPool
|
Media runners.WorkerPool
|
||||||
|
|
|
@ -1845,6 +1845,15 @@ func NewTestNotifications() map[string]*gtsmodel.Notification {
|
||||||
StatusID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
StatusID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
Read: FalseBool(),
|
Read: FalseBool(),
|
||||||
},
|
},
|
||||||
|
"local_account_2_like": {
|
||||||
|
ID: "01GTS6PRPXJYZBPFFQ56PP0XR8",
|
||||||
|
NotificationType: gtsmodel.NotificationFave,
|
||||||
|
CreatedAt: TimeMustParse("2022-01-13T12:45:01+02:00"),
|
||||||
|
TargetAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
|
OriginAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
StatusID: "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||||
|
Read: FalseBool(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartWorkers(state *state.State) {
|
func StartWorkers(state *state.State) {
|
||||||
state.Workers.EnqueueClientAPI = func(context.Context, messages.FromClientAPI) {}
|
state.Workers.EnqueueClientAPI = func(context.Context, ...messages.FromClientAPI) {}
|
||||||
state.Workers.EnqueueFederator = func(context.Context, messages.FromFederator) {}
|
state.Workers.EnqueueFederator = func(context.Context, ...messages.FromFederator) {}
|
||||||
|
|
||||||
_ = state.Workers.Scheduler.Start(nil)
|
_ = state.Workers.Scheduler.Start(nil)
|
||||||
_ = state.Workers.ClientAPI.Start(1, 10)
|
_ = state.Workers.ClientAPI.Start(1, 10)
|
||||||
|
|
Loading…
Reference in New Issue