mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 02:32:25 -05:00 
			
		
		
		
	[chore] Refactor account deleting/block logic, tidy up some other processing things (#1599)
* start refactoring account deletion * update to use state.DB * further messing about * some more tidying up * more tidying, cleaning, nice-making * further adventures in refactoring and the woes of technical debt * update fr accept/reject * poking + prodding * fix up deleting * create fave uri * don't log using requestingAccount.ID because it might be nil * move getBookmarks function * use exists query to check for status bookmark * use deletenotifications func * fiddle * delete follow request notif * split up some db functions * Fix possible nil pointer panic * fix more possible nil pointers * fix license headers * warn when follow missing (target) account * return wrapped err when bookmark/fave models can't be retrieved * simplify self account delete * warn log likely race condition * de-sillify status delete loop * move error check due north * warn when unfollowSideEffects has no target account * warn when no boost account is found * warn + dump follow when no account * more warnings * warn on fave account not set * move for loop inside anonymous function * fix funky logic * don't remove mutual account items on block; do make sure unfollow occurs in both directions!
This commit is contained in:
		
					parent
					
						
							
								276d773438
							
						
					
				
			
			
				commit
				
					
						e8595f0c64
					
				
			
		
					 53 changed files with 2472 additions and 1321 deletions
				
			
		|  | @ -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,49 +297,56 @@ 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 |  | ||||||
| 		followRequest := >smodel.FollowRequest{} |  | ||||||
| 		if err := tx. |  | ||||||
| 		NewSelect(). | 		NewSelect(). | ||||||
| 			Model(followRequest). | 		TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")). | ||||||
|  | 		Column("follow_request.id"). | ||||||
| 		Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID). | 		Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID). | ||||||
| 		Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID). | 		Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID). | ||||||
| 			Scan(ctx); err != nil { | 		Scan(ctx, &followRequestID); err != nil { | ||||||
| 			return err | 		return nil, r.conn.ProcessError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		// create a new follow to 'replace' the request with | 	followRequest, err := r.getFollowRequest(ctx, followRequestID) | ||||||
| 		follow = >smodel.Follow{ | 	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, | 		ID:              followRequest.ID, | ||||||
| 		AccountID:       originAccountID, | 		AccountID:       originAccountID, | ||||||
|  | 		Account:         followRequest.Account, | ||||||
| 		TargetAccountID: targetAccountID, | 		TargetAccountID: targetAccountID, | ||||||
|  | 		TargetAccount:   followRequest.TargetAccount, | ||||||
| 		URI:             followRequest.URI, | 		URI:             followRequest.URI, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		// if the follow already exists, just update the URI -- we don't need to do anything else | 	// If the follow already exists, just | ||||||
| 		if _, err := tx. | 	// replace the URI with the new one. | ||||||
|  | 	if _, err := r.conn. | ||||||
| 		NewInsert(). | 		NewInsert(). | ||||||
| 		Model(follow). | 		Model(follow). | ||||||
| 		On("CONFLICT (?,?) DO UPDATE set ? = ?", bun.Ident("account_id"), bun.Ident("target_account_id"), bun.Ident("uri"), follow.URI). | 		On("CONFLICT (?,?) DO UPDATE set ? = ?", bun.Ident("account_id"), bun.Ident("target_account_id"), bun.Ident("uri"), follow.URI). | ||||||
| 		Exec(ctx); err != nil { | 		Exec(ctx); err != nil { | ||||||
| 			return err | 		return nil, r.conn.ProcessError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		// now remove the follow request | 	// Delete original follow request. | ||||||
| 		if _, err := tx. | 	if _, err := r.conn. | ||||||
| 		NewDelete(). | 		NewDelete(). | ||||||
| 		TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")). | 		TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")). | ||||||
| 		Where("? = ?", bun.Ident("follow_request.id"), followRequest.ID). | 		Where("? = ?", bun.Ident("follow_request.id"), followRequest.ID). | ||||||
| 		Exec(ctx); err != nil { | 		Exec(ctx); err != nil { | ||||||
| 			return err | 		return nil, r.conn.ProcessError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		return nil | 	// Delete original follow request notification. | ||||||
| 	}); err != nil { | 	if err := r.deleteFollowRequestNotif(ctx, originAccountID, targetAccountID); err != nil { | ||||||
| 		return nil, r.conn.ProcessError(err) | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// return the new follow | 	// return the new follow | ||||||
|  | @ -355,119 +354,283 @@ func (r *relationshipDB) AcceptFollowRequest(ctx context.Context, originAccountI | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 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 |  | ||||||
| 		if err := tx. |  | ||||||
| 		NewSelect(). | 		NewSelect(). | ||||||
| 			Model(followRequest). | 		TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")). | ||||||
|  | 		Column("follow_request.id"). | ||||||
| 		Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID). | 		Where("? = ?", bun.Ident("follow_request.account_id"), originAccountID). | ||||||
| 		Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID). | 		Where("? = ?", bun.Ident("follow_request.target_account_id"), targetAccountID). | ||||||
| 			Scan(ctx); err != nil { | 		Scan(ctx, &followRequestID); err != nil { | ||||||
| 			return err | 		return nil, r.conn.ProcessError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		// now delete it from the database by ID | 	followRequest, err := r.getFollowRequest(ctx, followRequestID) | ||||||
| 		if _, err := tx. | 	if err != nil { | ||||||
|  | 		return nil, r.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Delete original follow request. | ||||||
|  | 	if _, err := r.conn. | ||||||
| 		NewDelete(). | 		NewDelete(). | ||||||
| 		TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")). | 		TableExpr("? AS ?", bun.Ident("follow_requests"), bun.Ident("follow_request")). | ||||||
| 		Where("? = ?", bun.Ident("follow_request.id"), followRequest.ID). | 		Where("? = ?", bun.Ident("follow_request.id"), followRequest.ID). | ||||||
| 		Exec(ctx); err != nil { | 		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 | 	// 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{} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										198
									
								
								internal/db/bundb/statusbookmark.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								internal/db/bundb/statusbookmark.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,198 @@ | ||||||
|  | // GoToSocial | ||||||
|  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | ||||||
|  | // SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | package bundb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/state" | ||||||
|  | 	"github.com/uptrace/bun" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type statusBookmarkDB struct { | ||||||
|  | 	conn  *DBConn | ||||||
|  | 	state *state.State | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusBookmarkDB) GetStatusBookmark(ctx context.Context, id string) (*gtsmodel.StatusBookmark, db.Error) { | ||||||
|  | 	bookmark := new(gtsmodel.StatusBookmark) | ||||||
|  | 
 | ||||||
|  | 	err := s.conn. | ||||||
|  | 		NewSelect(). | ||||||
|  | 		Model(bookmark). | ||||||
|  | 		Where("? = ?", bun.Ident("status_bookmark.ID"), id). | ||||||
|  | 		Scan(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, s.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bookmark.Account, err = s.state.DB.GetAccountByID(ctx, bookmark.AccountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error getting status bookmark account %q: %w", bookmark.AccountID, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bookmark.TargetAccount, err = s.state.DB.GetAccountByID(ctx, bookmark.TargetAccountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error getting status bookmark target account %q: %w", bookmark.TargetAccountID, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bookmark.Status, err = s.state.DB.GetStatusByID(ctx, bookmark.StatusID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error getting status bookmark status %q: %w", bookmark.StatusID, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return bookmark, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusBookmarkDB) GetStatusBookmarkID(ctx context.Context, accountID string, statusID string) (string, db.Error) { | ||||||
|  | 	var id string | ||||||
|  | 
 | ||||||
|  | 	q := s.conn. | ||||||
|  | 		NewSelect(). | ||||||
|  | 		TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")). | ||||||
|  | 		Column("status_bookmark.id"). | ||||||
|  | 		Where("? = ?", bun.Ident("status_bookmark.account_id"), accountID). | ||||||
|  | 		Where("? = ?", bun.Ident("status_bookmark.status_id"), statusID). | ||||||
|  | 		Limit(1) | ||||||
|  | 
 | ||||||
|  | 	if err := q.Scan(ctx, &id); err != nil { | ||||||
|  | 		return "", s.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return id, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusBookmarkDB) GetStatusBookmarks(ctx context.Context, accountID string, limit int, maxID string, minID string) ([]*gtsmodel.StatusBookmark, db.Error) { | ||||||
|  | 	// Ensure reasonable | ||||||
|  | 	if limit < 0 { | ||||||
|  | 		limit = 0 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Guess size of IDs based on limit. | ||||||
|  | 	ids := make([]string, 0, limit) | ||||||
|  | 
 | ||||||
|  | 	q := s.conn. | ||||||
|  | 		NewSelect(). | ||||||
|  | 		TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")). | ||||||
|  | 		Column("status_bookmark.id"). | ||||||
|  | 		Where("? = ?", bun.Ident("status_bookmark.account_id"), accountID). | ||||||
|  | 		Order("status_bookmark.id DESC") | ||||||
|  | 
 | ||||||
|  | 	if accountID == "" { | ||||||
|  | 		return nil, errors.New("must provide an account") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if maxID != "" { | ||||||
|  | 		q = q.Where("? < ?", bun.Ident("status_bookmark.id"), maxID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if minID != "" { | ||||||
|  | 		q = q.Where("? > ?", bun.Ident("status_bookmark.id"), minID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if limit != 0 { | ||||||
|  | 		q = q.Limit(limit) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := q.Scan(ctx, &ids); err != nil { | ||||||
|  | 		return nil, s.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bookmarks := make([]*gtsmodel.StatusBookmark, 0, len(ids)) | ||||||
|  | 
 | ||||||
|  | 	for _, id := range ids { | ||||||
|  | 		bookmark, err := s.GetStatusBookmark(ctx, id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Errorf(ctx, "error getting bookmark %q: %v", id, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		bookmarks = append(bookmarks, bookmark) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return bookmarks, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusBookmarkDB) PutStatusBookmark(ctx context.Context, statusBookmark *gtsmodel.StatusBookmark) db.Error { | ||||||
|  | 	_, err := s.conn. | ||||||
|  | 		NewInsert(). | ||||||
|  | 		Model(statusBookmark). | ||||||
|  | 		Exec(ctx) | ||||||
|  | 
 | ||||||
|  | 	return s.conn.ProcessError(err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusBookmarkDB) DeleteStatusBookmark(ctx context.Context, id string) db.Error { | ||||||
|  | 	_, err := s.conn. | ||||||
|  | 		NewDelete(). | ||||||
|  | 		TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")). | ||||||
|  | 		Where("? = ?", bun.Ident("status_bookmark.id"), id). | ||||||
|  | 		Exec(ctx) | ||||||
|  | 
 | ||||||
|  | 	return s.conn.ProcessError(err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusBookmarkDB) DeleteStatusBookmarks(ctx context.Context, targetAccountID string, originAccountID string) db.Error { | ||||||
|  | 	if targetAccountID == "" && originAccountID == "" { | ||||||
|  | 		return errors.New("DeleteBookmarks: one of targetAccountID or originAccountID must be set") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// TODO: Capture bookmark IDs in a RETURNING | ||||||
|  | 	// statement (when bookmarks have a cache), | ||||||
|  | 	// + use the IDs to invalidate cache entries. | ||||||
|  | 
 | ||||||
|  | 	q := s.conn. | ||||||
|  | 		NewDelete(). | ||||||
|  | 		TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")) | ||||||
|  | 
 | ||||||
|  | 	if targetAccountID != "" { | ||||||
|  | 		q = q.Where("? = ?", bun.Ident("status_bookmark.target_account_id"), targetAccountID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if originAccountID != "" { | ||||||
|  | 		q = q.Where("? = ?", bun.Ident("status_bookmark.account_id"), originAccountID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if _, err := q.Exec(ctx); err != nil { | ||||||
|  | 		return s.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusBookmarkDB) DeleteStatusBookmarksForStatus(ctx context.Context, statusID string) db.Error { | ||||||
|  | 	// TODO: Capture bookmark IDs in a RETURNING | ||||||
|  | 	// statement (when bookmarks have a cache), | ||||||
|  | 	// + use the IDs to invalidate cache entries. | ||||||
|  | 
 | ||||||
|  | 	q := s.conn. | ||||||
|  | 		NewDelete(). | ||||||
|  | 		TableExpr("? AS ?", bun.Ident("status_bookmarks"), bun.Ident("status_bookmark")). | ||||||
|  | 		Where("? = ?", bun.Ident("status_bookmark.status_id"), statusID) | ||||||
|  | 
 | ||||||
|  | 	if _, err := q.Exec(ctx); err != nil { | ||||||
|  | 		return s.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										129
									
								
								internal/db/bundb/statusbookmark_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								internal/db/bundb/statusbookmark_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,129 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package bundb_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type StatusBookmarkTestSuite struct { | ||||||
|  | 	BunDBStandardTestSuite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusBookmarkTestSuite) TestGetStatusBookmarkIDOK() { | ||||||
|  | 	testBookmark := suite.testBookmarks["local_account_1_admin_account_status_1"] | ||||||
|  | 
 | ||||||
|  | 	id, err := suite.db.GetStatusBookmarkID(context.Background(), testBookmark.AccountID, testBookmark.StatusID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(testBookmark.ID, id) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusBookmarkTestSuite) TestGetStatusBookmarkIDNonexisting() { | ||||||
|  | 	id, err := suite.db.GetStatusBookmarkID(context.Background(), "01GVAVGD06YJ2FSB5GJSMF8M2K", "01GVAVGKGR1MK9ZN7JCJFYSFZV") | ||||||
|  | 	suite.Empty(id) | ||||||
|  | 	suite.ErrorIs(err, db.ErrNoEntries) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarksOriginatingFromAccount() { | ||||||
|  | 	testAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	if err := suite.db.DeleteStatusBookmarks(context.Background(), "", testAccount.ID); err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bookmarks := []*gtsmodel.StatusBookmark{} | ||||||
|  | 	if err := suite.db.GetAll(context.Background(), &bookmarks); err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, b := range bookmarks { | ||||||
|  | 		if b.AccountID == testAccount.ID { | ||||||
|  | 			suite.FailNowf("", "no StatusBookmarks with account id %s should remain", testAccount.ID) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarksTargetingAccount() { | ||||||
|  | 	testAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	if err := suite.db.DeleteStatusBookmarks(context.Background(), testAccount.ID, ""); err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bookmarks := []*gtsmodel.StatusBookmark{} | ||||||
|  | 	if err := suite.db.GetAll(context.Background(), &bookmarks); err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, b := range bookmarks { | ||||||
|  | 		if b.TargetAccountID == testAccount.ID { | ||||||
|  | 			suite.FailNowf("", "no StatusBookmarks with target account id %s should remain", testAccount.ID) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarksTargetingStatus() { | ||||||
|  | 	testStatus := suite.testStatuses["local_account_1_status_1"] | ||||||
|  | 
 | ||||||
|  | 	if err := suite.db.DeleteStatusBookmarksForStatus(context.Background(), testStatus.ID); err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bookmarks := []*gtsmodel.StatusBookmark{} | ||||||
|  | 	if err := suite.db.GetAll(context.Background(), &bookmarks); err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, b := range bookmarks { | ||||||
|  | 		if b.StatusID == testStatus.ID { | ||||||
|  | 			suite.FailNowf("", "no StatusBookmarks with status id %s should remain", testStatus.ID) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmark() { | ||||||
|  | 	testBookmark := suite.testBookmarks["local_account_1_admin_account_status_1"] | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 
 | ||||||
|  | 	if err := suite.db.DeleteStatusBookmark(ctx, testBookmark.ID); err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bookmark, err := suite.db.GetStatusBookmark(ctx, testBookmark.ID) | ||||||
|  | 	suite.ErrorIs(err, db.ErrNoEntries) | ||||||
|  | 	suite.Nil(bookmark) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusBookmarkTestSuite) TestDeleteStatusBookmarkNonExisting() { | ||||||
|  | 	err := suite.db.DeleteStatusBookmark(context.Background(), "01GVAV715K6Y2SG9ZKS9ZA8G7G") | ||||||
|  | 	suite.NoError(err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestStatusBookmarkTestSuite(t *testing.T) { | ||||||
|  | 	suite.Run(t, new(StatusBookmarkTestSuite)) | ||||||
|  | } | ||||||
							
								
								
									
										172
									
								
								internal/db/bundb/statusfave.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								internal/db/bundb/statusfave.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,172 @@ | ||||||
|  | // GoToSocial | ||||||
|  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | ||||||
|  | // SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | package bundb | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/state" | ||||||
|  | 	"github.com/uptrace/bun" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type statusFaveDB struct { | ||||||
|  | 	conn  *DBConn | ||||||
|  | 	state *state.State | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusFaveDB) GetStatusFave(ctx context.Context, id string) (*gtsmodel.StatusFave, db.Error) { | ||||||
|  | 	fave := new(gtsmodel.StatusFave) | ||||||
|  | 
 | ||||||
|  | 	err := s.conn. | ||||||
|  | 		NewSelect(). | ||||||
|  | 		Model(fave). | ||||||
|  | 		Where("? = ?", bun.Ident("status_fave.ID"), id). | ||||||
|  | 		Scan(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, s.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fave.Account, err = s.state.DB.GetAccountByID(ctx, fave.AccountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error getting status fave account %q: %w", fave.AccountID, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fave.TargetAccount, err = s.state.DB.GetAccountByID(ctx, fave.TargetAccountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error getting status fave target account %q: %w", fave.TargetAccountID, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fave.Status, err = s.state.DB.GetStatusByID(ctx, fave.StatusID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error getting status fave status %q: %w", fave.StatusID, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return fave, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusFaveDB) GetStatusFaveByAccountID(ctx context.Context, accountID string, statusID string) (*gtsmodel.StatusFave, db.Error) { | ||||||
|  | 	var id string | ||||||
|  | 
 | ||||||
|  | 	err := s.conn. | ||||||
|  | 		NewSelect(). | ||||||
|  | 		TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")). | ||||||
|  | 		Column("status_fave.id"). | ||||||
|  | 		Where("? = ?", bun.Ident("status_fave.account_id"), accountID). | ||||||
|  | 		Where("? = ?", bun.Ident("status_fave.status_id"), statusID). | ||||||
|  | 		Scan(ctx, &id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, s.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return s.GetStatusFave(ctx, id) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusFaveDB) GetStatusFaves(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, db.Error) { | ||||||
|  | 	ids := []string{} | ||||||
|  | 
 | ||||||
|  | 	if err := s.conn. | ||||||
|  | 		NewSelect(). | ||||||
|  | 		TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")). | ||||||
|  | 		Column("status_fave.id"). | ||||||
|  | 		Where("? = ?", bun.Ident("status_fave.status_id"), statusID). | ||||||
|  | 		Scan(ctx, &ids); err != nil { | ||||||
|  | 		return nil, s.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	faves := make([]*gtsmodel.StatusFave, 0, len(ids)) | ||||||
|  | 	for _, id := range ids { | ||||||
|  | 		fave, err := s.GetStatusFave(ctx, id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Errorf(ctx, "error getting status fave %q: %v", id, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		faves = append(faves, fave) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return faves, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusFaveDB) PutStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) db.Error { | ||||||
|  | 	_, err := s.conn. | ||||||
|  | 		NewInsert(). | ||||||
|  | 		Model(statusFave). | ||||||
|  | 		Exec(ctx) | ||||||
|  | 
 | ||||||
|  | 	return s.conn.ProcessError(err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusFaveDB) DeleteStatusFave(ctx context.Context, id string) db.Error { | ||||||
|  | 	_, err := s.conn. | ||||||
|  | 		NewDelete(). | ||||||
|  | 		TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")). | ||||||
|  | 		Where("? = ?", bun.Ident("status_fave.id"), id). | ||||||
|  | 		Exec(ctx) | ||||||
|  | 
 | ||||||
|  | 	return s.conn.ProcessError(err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusFaveDB) DeleteStatusFaves(ctx context.Context, targetAccountID string, originAccountID string) db.Error { | ||||||
|  | 	if targetAccountID == "" && originAccountID == "" { | ||||||
|  | 		return errors.New("DeleteStatusFaves: one of targetAccountID or originAccountID must be set") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// TODO: Capture fave IDs in a RETURNING | ||||||
|  | 	// statement (when faves have a cache), | ||||||
|  | 	// + use the IDs to invalidate cache entries. | ||||||
|  | 
 | ||||||
|  | 	q := s.conn. | ||||||
|  | 		NewDelete(). | ||||||
|  | 		TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")) | ||||||
|  | 
 | ||||||
|  | 	if targetAccountID != "" { | ||||||
|  | 		q = q.Where("? = ?", bun.Ident("status_fave.target_account_id"), targetAccountID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if originAccountID != "" { | ||||||
|  | 		q = q.Where("? = ?", bun.Ident("status_fave.account_id"), originAccountID) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if _, err := q.Exec(ctx); err != nil { | ||||||
|  | 		return s.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *statusFaveDB) DeleteStatusFavesForStatus(ctx context.Context, statusID string) db.Error { | ||||||
|  | 	// TODO: Capture fave IDs in a RETURNING | ||||||
|  | 	// statement (when faves have a cache), | ||||||
|  | 	// + use the IDs to invalidate cache entries. | ||||||
|  | 
 | ||||||
|  | 	q := s.conn. | ||||||
|  | 		NewDelete(). | ||||||
|  | 		TableExpr("? AS ?", bun.Ident("status_faves"), bun.Ident("status_fave")). | ||||||
|  | 		Where("? = ?", bun.Ident("status_fave.status_id"), statusID) | ||||||
|  | 
 | ||||||
|  | 	if _, err := q.Exec(ctx); err != nil { | ||||||
|  | 		return s.conn.ProcessError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										148
									
								
								internal/db/bundb/statusfave_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								internal/db/bundb/statusfave_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,148 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package bundb_test | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type StatusFaveTestSuite struct { | ||||||
|  | 	BunDBStandardTestSuite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusFaveTestSuite) TestGetStatusFaves() { | ||||||
|  | 	testStatus := suite.testStatuses["admin_account_status_1"] | ||||||
|  | 
 | ||||||
|  | 	faves, err := suite.db.GetStatusFaves(context.Background(), testStatus.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	suite.NotEmpty(faves) | ||||||
|  | 	for _, fave := range faves { | ||||||
|  | 		suite.NotNil(fave.Account) | ||||||
|  | 		suite.NotNil(fave.TargetAccount) | ||||||
|  | 		suite.NotNil(fave.Status) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusFaveTestSuite) TestGetStatusFavesNone() { | ||||||
|  | 	testStatus := suite.testStatuses["admin_account_status_4"] | ||||||
|  | 
 | ||||||
|  | 	faves, err := suite.db.GetStatusFaves(context.Background(), testStatus.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	suite.Empty(faves) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusFaveTestSuite) TestGetStatusFaveByAccountID() { | ||||||
|  | 	testAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 	testStatus := suite.testStatuses["admin_account_status_1"] | ||||||
|  | 
 | ||||||
|  | 	fave, err := suite.db.GetStatusFaveByAccountID(context.Background(), testAccount.ID, testStatus.ID) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 	suite.NotNil(fave) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusFaveTestSuite) TestDeleteStatusFavesOriginatingFromAccount() { | ||||||
|  | 	testAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	if err := suite.db.DeleteStatusFaves(context.Background(), "", testAccount.ID); err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	faves := []*gtsmodel.StatusFave{} | ||||||
|  | 	if err := suite.db.GetAll(context.Background(), &faves); err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, b := range faves { | ||||||
|  | 		if b.AccountID == testAccount.ID { | ||||||
|  | 			suite.FailNowf("", "no StatusFaves with account id %s should remain", testAccount.ID) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusFaveTestSuite) TestDeleteStatusFavesTargetingAccount() { | ||||||
|  | 	testAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 
 | ||||||
|  | 	if err := suite.db.DeleteStatusFaves(context.Background(), testAccount.ID, ""); err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	faves := []*gtsmodel.StatusFave{} | ||||||
|  | 	if err := suite.db.GetAll(context.Background(), &faves); err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, b := range faves { | ||||||
|  | 		if b.TargetAccountID == testAccount.ID { | ||||||
|  | 			suite.FailNowf("", "no StatusFaves with target account id %s should remain", testAccount.ID) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusFaveTestSuite) TestDeleteStatusFavesTargetingStatus() { | ||||||
|  | 	testStatus := suite.testStatuses["local_account_1_status_1"] | ||||||
|  | 
 | ||||||
|  | 	if err := suite.db.DeleteStatusFavesForStatus(context.Background(), testStatus.ID); err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	faves := []*gtsmodel.StatusFave{} | ||||||
|  | 	if err := suite.db.GetAll(context.Background(), &faves); err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, b := range faves { | ||||||
|  | 		if b.StatusID == testStatus.ID { | ||||||
|  | 			suite.FailNowf("", "no StatusFaves with status id %s should remain", testStatus.ID) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusFaveTestSuite) TestDeleteStatusFave() { | ||||||
|  | 	testFave := suite.testFaves["local_account_1_admin_account_status_1"] | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 
 | ||||||
|  | 	if err := suite.db.DeleteStatusFave(ctx, testFave.ID); err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fave, err := suite.db.GetStatusFave(ctx, testFave.ID) | ||||||
|  | 	suite.ErrorIs(err, db.ErrNoEntries) | ||||||
|  | 	suite.Nil(fave) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *StatusFaveTestSuite) TestDeleteStatusFaveNonExisting() { | ||||||
|  | 	err := suite.db.DeleteStatusFave(context.Background(), "01GVAV715K6Y2SG9ZKS9ZA8G7G") | ||||||
|  | 	suite.NoError(err) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestStatusFaveTestSuite(t *testing.T) { | ||||||
|  | 	suite.Run(t, new(StatusFaveTestSuite)) | ||||||
|  | } | ||||||
|  | @ -43,6 +43,8 @@ type DB interface { | ||||||
| 	Report | 	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) | ||||||
|  |  | ||||||
							
								
								
									
										66
									
								
								internal/db/statusbookmark.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								internal/db/statusbookmark.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,66 @@ | ||||||
|  | // GoToSocial | ||||||
|  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | ||||||
|  | // SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | package db | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 
 | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type StatusBookmark interface { | ||||||
|  | 	// GetStatusBookmark gets one status bookmark with the given ID. | ||||||
|  | 	GetStatusBookmark(ctx context.Context, id string) (*gtsmodel.StatusBookmark, Error) | ||||||
|  | 
 | ||||||
|  | 	// GetStatusBookmarkID is a shortcut function for returning just the database ID | ||||||
|  | 	// of a status bookmark created by the given accountID, targeting the given statusID. | ||||||
|  | 	GetStatusBookmarkID(ctx context.Context, accountID string, statusID string) (string, Error) | ||||||
|  | 
 | ||||||
|  | 	// GetStatusBookmarks retrieves status bookmarks created by the given accountID, | ||||||
|  | 	// and using the provided parameters. If limit is < 0 then no limit will be set. | ||||||
|  | 	// | ||||||
|  | 	// This function is primarily useful for paging through bookmarks in a sort of | ||||||
|  | 	// timeline view. | ||||||
|  | 	GetStatusBookmarks(ctx context.Context, accountID string, limit int, maxID string, minID string) ([]*gtsmodel.StatusBookmark, Error) | ||||||
|  | 
 | ||||||
|  | 	// PutStatusBookmark inserts the given statusBookmark into the database. | ||||||
|  | 	PutStatusBookmark(ctx context.Context, statusBookmark *gtsmodel.StatusBookmark) Error | ||||||
|  | 
 | ||||||
|  | 	// DeleteStatusBookmark deletes one status bookmark with the given ID. | ||||||
|  | 	DeleteStatusBookmark(ctx context.Context, id string) Error | ||||||
|  | 
 | ||||||
|  | 	// DeleteStatusBookmarks mass deletes status bookmarks targeting targetAccountID | ||||||
|  | 	// and/or originating from originAccountID and/or bookmarking statusID. | ||||||
|  | 	// | ||||||
|  | 	// If targetAccountID is set and originAccountID isn't, all status bookmarks | ||||||
|  | 	// that target the given account will be deleted. | ||||||
|  | 	// | ||||||
|  | 	// If originAccountID is set and targetAccountID isn't, all status bookmarks | ||||||
|  | 	// originating from the given account will be deleted. | ||||||
|  | 	// | ||||||
|  | 	// If both are set, then status bookmarks that target targetAccountID and | ||||||
|  | 	// originate from originAccountID will be deleted. | ||||||
|  | 	// | ||||||
|  | 	// At least one parameter must not be an empty string. | ||||||
|  | 	DeleteStatusBookmarks(ctx context.Context, targetAccountID string, originAccountID string) Error | ||||||
|  | 
 | ||||||
|  | 	// DeleteStatusBookmarksForStatus deletes all status bookmarks that target the | ||||||
|  | 	// given status ID. This is useful when a status has been deleted, and you need | ||||||
|  | 	// to clean up after it. | ||||||
|  | 	DeleteStatusBookmarksForStatus(ctx context.Context, statusID string) Error | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								internal/db/statusfave.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								internal/db/statusfave.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | // GoToSocial | ||||||
|  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | ||||||
|  | // SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | package db | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 
 | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type StatusFave interface { | ||||||
|  | 	// GetStatusFave returns one status fave with the given id. | ||||||
|  | 	GetStatusFave(ctx context.Context, id string) (*gtsmodel.StatusFave, Error) | ||||||
|  | 
 | ||||||
|  | 	// GetStatusFaveByAccountID gets one status fave created by the given | ||||||
|  | 	// accountID, targeting the given statusID. | ||||||
|  | 	GetStatusFaveByAccountID(ctx context.Context, accountID string, statusID string) (*gtsmodel.StatusFave, Error) | ||||||
|  | 
 | ||||||
|  | 	// GetStatusFaves returns a slice of faves/likes of the given status. | ||||||
|  | 	// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. | ||||||
|  | 	GetStatusFaves(ctx context.Context, statusID string) ([]*gtsmodel.StatusFave, Error) | ||||||
|  | 
 | ||||||
|  | 	// PutStatusFave inserts the given statusFave into the database. | ||||||
|  | 	PutStatusFave(ctx context.Context, statusFave *gtsmodel.StatusFave) Error | ||||||
|  | 
 | ||||||
|  | 	// DeleteStatusFave deletes one status fave with the given id. | ||||||
|  | 	DeleteStatusFave(ctx context.Context, id string) Error | ||||||
|  | 
 | ||||||
|  | 	// DeleteStatusFaves mass deletes status faves targeting targetAccountID | ||||||
|  | 	// and/or originating from originAccountID and/or faving statusID. | ||||||
|  | 	// | ||||||
|  | 	// If targetAccountID is set and originAccountID isn't, all status faves | ||||||
|  | 	// that target the given account will be deleted. | ||||||
|  | 	// | ||||||
|  | 	// If originAccountID is set and targetAccountID isn't, all status faves | ||||||
|  | 	// originating from the given account will be deleted. | ||||||
|  | 	// | ||||||
|  | 	// If both are set, then status faves that target targetAccountID and | ||||||
|  | 	// originate from originAccountID will be deleted. | ||||||
|  | 	// | ||||||
|  | 	// At least one parameter must not be an empty string. | ||||||
|  | 	DeleteStatusFaves(ctx context.Context, targetAccountID string, originAccountID string) Error | ||||||
|  | 
 | ||||||
|  | 	// DeleteStatusFavesForStatus deletes all status faves that target the | ||||||
|  | 	// given status ID. This is useful when a status has been deleted, and you need | ||||||
|  | 	// to clean up after it. | ||||||
|  | 	DeleteStatusFavesForStatus(ctx context.Context, statusID string) Error | ||||||
|  | } | ||||||
|  | @ -69,9 +69,11 @@ 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) { | ||||||
|  | 		for _, msg := range msgs { | ||||||
| 			suite.fromFederator <- msg | 			suite.fromFederator <- msg | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	suite.db = testrig.NewTestDB(&suite.state) | 	suite.db = testrig.NewTestDB(&suite.state) | ||||||
| 	suite.testActivities = testrig.NewTestActivities(suite.testAccounts) | 	suite.testActivities = testrig.NewTestActivities(suite.testAccounts) | ||||||
|  |  | ||||||
|  | @ -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 { |  | ||||||
| 					// no entry for this account id so it's probably been deleted and we haven't caught up yet |  | ||||||
| 					l.Error(errWrapped) |  | ||||||
| 			continue | 			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 { |  | ||||||
| 					// no entry for this account id so it's probably been deleted and we haven't caught up yet |  | ||||||
| 					l.Error(errWrapped) |  | ||||||
| 			continue | 			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,23 +89,17 @@ 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 | ||||||
| 			var inbox string | 			var inbox string | ||||||
|  |  | ||||||
|  | @ -91,9 +91,11 @@ 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) { | ||||||
|  | 		for _, msg := range msgs { | ||||||
| 			suite.fromClientAPIChan <- msg | 			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")) | ||||||
| 	suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager) | 	suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager) | ||||||
|  |  | ||||||
|  | @ -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 | 	// 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{ | 	p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ | ||||||
| 		APObjectType:   ap.ActivityBlock, | 		APObjectType:   ap.ActivityBlock, | ||||||
| 		APActivityType: ap.ActivityUndo, | 		APActivityType: ap.ActivityUndo, | ||||||
| 			GTSModel:       block, | 		GTSModel:       existingBlock, | ||||||
| 		OriginAccount:  requestingAccount, | 		OriginAccount:  requestingAccount, | ||||||
| 		TargetAccount:  targetAccount, | 		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 |  | ||||||
| 	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 { | 	if err != nil { | ||||||
| 			if !errors.Is(err, db.ErrNoEntries) { | 		return fmt.Errorf("deleteUserAndTokensForAccount: db error getting user: %w", err) | ||||||
| 				// an actual error has occurred |  | ||||||
| 				l.Errorf("Delete: db error selecting statuses for account %s: %s", account.Username, err) |  | ||||||
| 	} | 	} | ||||||
| 			break | 
 | ||||||
|  | 	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 { | ||||||
|  | 			return fmt.Errorf("deleteAccountFollows: db error unfollowRequesting account: %w", err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if uri == "" { | ||||||
|  | 			// 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) { | ||||||
|  | 							// 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 | 							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. | ||||||
| 
 |  | ||||||
| 	// 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{ | 		p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ | ||||||
| 			APObjectType:   ap.ActivityFollow, | 			APObjectType:   ap.ActivityFollow, | ||||||
| 			APActivityType: ap.ActivityCreate, | 			APActivityType: ap.ActivityCreate, | ||||||
| 			GTSModel:       fr, | 			GTSModel:       fr, | ||||||
| 			OriginAccount:  requestingAccount, | 			OriginAccount:  requestingAccount, | ||||||
| 		TargetAccount:  targetAcct, | 			TargetAccount:  targetAccount, | ||||||
| 		}) | 		}) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	// 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) | ||||||
|  | 	} else if blocked { | ||||||
|  | 		err = errors.New("block exists between accounts") | ||||||
|  | 		return nil, gtserror.NewErrorNotFound(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 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("db error looking for target account %s: %w", targetAccountID, err) | ||||||
| 			return nil, gtserror.NewErrorInternalError(err) | 			return nil, gtserror.NewErrorInternalError(err) | ||||||
| 		} | 		} | ||||||
| 	if blocked { | 		// Account not found. | ||||||
| 		return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts")) | 		err = fmt.Errorf("target account %s not found in the db", targetAccountID) | ||||||
|  | 		return nil, gtserror.NewErrorNotFound(err, err.Error()) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// make sure the target account actually exists in our db | 	return targetAccount, nil | ||||||
| 	targetAcct, err := p.state.DB.GetAccountByID(ctx, targetAccountID) | } | ||||||
| 	if err != nil { |  | ||||||
| 		if err == db.ErrNoEntries { |  | ||||||
| 			return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err)) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	// check if a follow request exists, and remove it if it does (storing the URI for later) | // unfollow is a convenience function for having requesting account | ||||||
| 	var frChanged bool | // unfollow (and un follow request) target account, if follows and/or | ||||||
| 	var frURI string | // follow requests exist. | ||||||
| 	fr := >smodel.FollowRequest{} | // | ||||||
| 	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. | ||||||
| 	}, fr); err == nil { | func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) ([]messages.FromClientAPI, error) { | ||||||
| 		frURI = fr.URI | 	msgs := []messages.FromClientAPI{} | ||||||
| 		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 | 	if fURI, err := p.state.DB.Unfollow(ctx, requestingAccount.ID, targetAccount.ID); err != nil { | ||||||
| 	var fChanged bool | 		err = fmt.Errorf("unfollow: error deleting follow from %s targeting %s: %w", requestingAccount.ID, targetAccount.ID, err) | ||||||
| 	var fURI string | 		return nil, err | ||||||
| 	f := >smodel.Follow{} | 	} else if fURI != "" { | ||||||
| 	if err := p.state.DB.GetWhere(ctx, []db.Where{ | 		// Follow status changed, process side effects. | ||||||
| 		{Key: "account_id", Value: requestingAccount.ID}, | 		msgs = append(msgs, messages.FromClientAPI{ | ||||||
| 		{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("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 frChanged { |  | ||||||
| 		p.state.Workers.EnqueueClientAPI(ctx, 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) | 			return nil, gtserror.NewErrorInternalError(err) | ||||||
| 		} | 		} | ||||||
|  | 		return []apimodel.Account{}, nil | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, f := range follows { | 	return p.accountsFromFollows(ctx, follows, requestingAccount.ID) | ||||||
| 		blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, gtserror.NewErrorInternalError(err) |  | ||||||
| 		} |  | ||||||
| 		if blocked { |  | ||||||
| 			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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 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) | 			return nil, gtserror.NewErrorInternalError(err) | ||||||
| 		} | 		} | ||||||
|  | 		return []apimodel.Account{}, nil | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, f := range follows { | 	return p.accountsFromFollows(ctx, follows, requestingAccount.ID) | ||||||
| 		blocked, err := p.state.DB.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, gtserror.NewErrorInternalError(err) |  | ||||||
| 		} |  | ||||||
| 		if blocked { |  | ||||||
| 			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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 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 := make([]apimodel.Account, 0, len(followRequests)) | ||||||
|  | 	for _, followRequest := range followRequests { | ||||||
|  | 		if followRequest.Account == nil { | ||||||
|  | 			// The creator of the follow doesn't exist, | ||||||
|  | 			// just skip this one. | ||||||
|  | 			log.WithContext(ctx).WithField("followRequest", followRequest).Warn("follow request had no associated account") | ||||||
|  | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 	accts := []apimodel.Account{} | 		apiAcct, err := p.tc.AccountToAPIAccountPublic(ctx, followRequest.Account) | ||||||
| 	for _, fr := range frs { |  | ||||||
| 		if fr.Account == nil { |  | ||||||
| 			frAcct, err := p.state.DB.GetAccountByID(ctx, fr.AccountID) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return nil, gtserror.NewErrorInternalError(err) |  | ||||||
| 			} |  | ||||||
| 			fr.Account = frAcct |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		apiAcct, err := p.tc.AccountToAPIAccountPublic(ctx, fr.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 { | ||||||
| 			errors <- fmt.Errorf("timelineStatusForAccount: error converting status %s to frontend representation: %s", status.ID, err) | 		return fmt.Errorf("timelineStatusForAccount: error converting status %s to frontend representation: %w", status.ID, err) | ||||||
| 			return |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := p.stream.Update(apiStatus, timelineAccount, stream.TimelineHome); err != nil { | 	if err := p.stream.Update(apiStatus, timelineAccount, stream.TimelineHome); err != nil { | ||||||
| 			errors <- fmt.Errorf("timelineStatusForAccount: error streaming status %s: %s", status.ID, err) | 		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) { | ||||||
|  | 		for _, msg := range msgs { | ||||||
| 			if err := p.ProcessFromClientAPI(ctx, msg); err != nil { | 			if err := p.ProcessFromClientAPI(ctx, msg); err != nil { | ||||||
| 			log.Errorf(ctx, "error processing client API message: %v", err) | 				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) { | ||||||
|  | 		for _, msg := range msgs { | ||||||
| 			if err := p.ProcessFromFederator(ctx, msg); err != nil { | 			if err := p.ProcessFromFederator(ctx, msg); err != nil { | ||||||
| 			log.Errorf(ctx, "error processing federator message: %v", err) | 				log.WithContext(ctx).WithField("msg", msg).Errorf("error processing federator message: %v", err) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -31,31 +31,17 @@ 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, | ||||||
|  | @ -66,56 +52,46 @@ func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmo | ||||||
| 		Status:          targetStatus, | 		Status:          targetStatus, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		if err := p.state.DB.Put(ctx, gtsBookmark); err != nil { | 	if err := p.state.DB.PutStatusBookmark(ctx, gtsBookmark); err != nil { | ||||||
| 			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting bookmark in database: %s", err)) | 		err = fmt.Errorf("BookmarkCreate: error putting bookmark in database: %w", err) | ||||||
| 		} | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// return the apidon 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)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return apiStatus, nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 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. | ||||||
|  |  | ||||||
							
								
								
									
										63
									
								
								internal/processing/status/common.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								internal/processing/status/common.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | // GoToSocial | ||||||
|  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | ||||||
|  | // SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | package 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,59 +28,42 @@ 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{} |  | ||||||
| 	if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err == nil { |  | ||||||
| 		// we already have a fave for this status |  | ||||||
| 		newFave = false |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if newFave { |  | ||||||
| 		thisFaveID := id.NewULID() |  | ||||||
| 
 |  | ||||||
| 		// we need to create a new fave in the database |  | ||||||
| 	gtsFave := >smodel.StatusFave{ | 	gtsFave := >smodel.StatusFave{ | ||||||
| 			ID:              thisFaveID, | 		ID:              faveID, | ||||||
| 		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, | ||||||
| 			URI:             uris.GenerateURIForLike(requestingAccount.Username, thisFaveID), | 		URI:             uris.GenerateURIForLike(requestingAccount.Username, faveID), | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		if err := p.state.DB.Put(ctx, gtsFave); err != nil { | 	if err := p.state.DB.PutStatusFave(ctx, gtsFave); err != nil { | ||||||
| 			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting fave in database: %s", err)) | 		err = fmt.Errorf("FaveCreate: error putting fave in database: %w", err) | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		// send it back to the processor for async processing | 	// Process new status fave side effects. | ||||||
| 	p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ | 	p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ | ||||||
| 		APObjectType:   ap.ActivityLike, | 		APObjectType:   ap.ActivityLike, | ||||||
| 		APActivityType: ap.ActivityCreate, | 		APActivityType: ap.ActivityCreate, | ||||||
|  | @ -88,121 +71,97 @@ func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel. | ||||||
| 		OriginAccount:  requestingAccount, | 		OriginAccount:  requestingAccount, | ||||||
| 		TargetAccount:  targetStatus.Account, | 		TargetAccount:  targetStatus.Account, | ||||||
| 	}) | 	}) | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	// return the apidon 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)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return apiStatus, nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 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 |  | ||||||
| 		if err := p.state.DB.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err != nil { |  | ||||||
| 			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err)) |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// send it back to the processor for async processing |  | ||||||
| 	p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ | 	p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ | ||||||
| 		APObjectType:   ap.ActivityLike, | 		APObjectType:   ap.ActivityLike, | ||||||
| 		APActivityType: ap.ActivityUndo, | 		APActivityType: ap.ActivityUndo, | ||||||
| 			GTSModel:       gtsFave, | 		GTSModel:       existingFave, | ||||||
| 		OriginAccount:  requestingAccount, | 		OriginAccount:  requestingAccount, | ||||||
| 		TargetAccount:  targetStatus.Account, | 		TargetAccount:  targetStatus.Account, | ||||||
| 	}) | 	}) | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	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 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // 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 { | ||||||
| 		if !blocked { | 			continue | ||||||
| 			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…
	
	Add table
		Add a link
		
	
		Reference in a new issue