summaryrefslogtreecommitdiff
path: root/internal/processing/account
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/account')
-rw-r--r--internal/processing/account/account.go20
-rw-r--r--internal/processing/account/account_test.go52
-rw-r--r--internal/processing/account/alias.go13
-rw-r--r--internal/processing/account/alias_test.go5
-rw-r--r--internal/processing/account/block.go24
-rw-r--r--internal/processing/account/bookmarks.go16
-rw-r--r--internal/processing/account/delete.go649
-rw-r--r--internal/processing/account/delete_test.go7
-rw-r--r--internal/processing/account/export.go10
-rw-r--r--internal/processing/account/follow.go39
-rw-r--r--internal/processing/account/follow_request.go53
-rw-r--r--internal/processing/account/follow_test.go19
-rw-r--r--internal/processing/account/get.go17
-rw-r--r--internal/processing/account/import.go165
-rw-r--r--internal/processing/account/interactionpolicies.go16
-rw-r--r--internal/processing/account/lists.go12
-rw-r--r--internal/processing/account/move.go35
-rw-r--r--internal/processing/account/move_test.go20
-rw-r--r--internal/processing/account/mute.go16
-rw-r--r--internal/processing/account/note.go8
-rw-r--r--internal/processing/account/relationships.go10
-rw-r--r--internal/processing/account/rss.go170
-rw-r--r--internal/processing/account/rss_test.go255
-rw-r--r--internal/processing/account/statuses.go58
-rw-r--r--internal/processing/account/themes.go8
-rw-r--r--internal/processing/account/themes_test.go4
-rw-r--r--internal/processing/account/tokens.go122
-rw-r--r--internal/processing/account/update.go102
-rw-r--r--internal/processing/account/update_test.go76
29 files changed, 1341 insertions, 660 deletions
diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go
index d65d7360c..e94b7e844 100644
--- a/internal/processing/account/account.go
+++ b/internal/processing/account/account.go
@@ -18,14 +18,15 @@
package account
import (
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/text"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/status"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/text"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Processor wraps functionality for updating, creating, and deleting accounts in response to API requests.
@@ -39,6 +40,7 @@ type Processor struct {
converter *typeutils.Converter
mediaManager *media.Manager
visFilter *visibility.Filter
+ statusFilter *status.Filter
formatter *text.Formatter
federator *federation.Federator
parseMention gtsmodel.ParseMentionFunc
@@ -53,6 +55,7 @@ func New(
mediaManager *media.Manager,
federator *federation.Federator,
visFilter *visibility.Filter,
+ statusFilter *status.Filter,
parseMention gtsmodel.ParseMentionFunc,
) Processor {
return Processor{
@@ -61,6 +64,7 @@ func New(
converter: converter,
mediaManager: mediaManager,
visFilter: visFilter,
+ statusFilter: statusFilter,
formatter: text.NewFormatter(state.DB),
federator: federator,
parseMention: parseMention,
diff --git a/internal/processing/account/account_test.go b/internal/processing/account/account_test.go
index 7bd9658dc..b322ee771 100644
--- a/internal/processing/account/account_test.go
+++ b/internal/processing/account/account_test.go
@@ -21,23 +21,25 @@ import (
"context"
"time"
+ "code.superseriousbusiness.org/gotosocial/internal/admin"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/email"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/mutes"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/status"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/processing"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/account"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/storage"
+ "code.superseriousbusiness.org/gotosocial/internal/transport"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/admin"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/processing"
- "github.com/superseriousbusiness/gotosocial/internal/processing/account"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type AccountStandardTestSuite struct {
@@ -55,7 +57,6 @@ type AccountStandardTestSuite struct {
// standard suite models
testTokens map[string]*gtsmodel.Token
- testClients map[string]*gtsmodel.Client
testApplications map[string]*gtsmodel.Application
testUsers map[string]*gtsmodel.User
testAccounts map[string]*gtsmodel.Account
@@ -68,7 +69,7 @@ type AccountStandardTestSuite struct {
}
func (suite *AccountStandardTestSuite) getClientMsg(timeout time.Duration) (*messages.FromClientAPI, bool) {
- ctx := context.Background()
+ ctx := suite.T().Context()
ctx, cncl := context.WithTimeout(ctx, timeout)
defer cncl()
return suite.state.Workers.Client.Queue.PopCtx(ctx)
@@ -76,7 +77,6 @@ func (suite *AccountStandardTestSuite) getClientMsg(timeout time.Duration) (*mes
func (suite *AccountStandardTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
@@ -97,12 +97,6 @@ func (suite *AccountStandardTestSuite) SetupTest() {
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
suite.tc = typeutils.NewConverter(&suite.state)
- testrig.StartTimelines(
- &suite.state,
- visibility.NewFilter(&suite.state),
- suite.tc,
- )
-
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
@@ -112,9 +106,11 @@ func (suite *AccountStandardTestSuite) SetupTest() {
suite.sentEmails = make(map[string]string)
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
- filter := visibility.NewFilter(&suite.state)
- common := common.New(&suite.state, suite.mediaManager, suite.tc, suite.federator, filter)
- suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
+ visFilter := visibility.NewFilter(&suite.state)
+ mutesFilter := mutes.NewFilter(&suite.state)
+ statusFilter := status.NewFilter(&suite.state)
+ common := common.New(&suite.state, suite.mediaManager, suite.tc, suite.federator, visFilter, mutesFilter, statusFilter)
+ suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, visFilter, statusFilter, processing.GetParseMentionFunc(&suite.state, suite.federator))
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
}
diff --git a/internal/processing/account/alias.go b/internal/processing/account/alias.go
index d7d4cf547..01d4e0999 100644
--- a/internal/processing/account/alias.go
+++ b/internal/processing/account/alias.go
@@ -24,10 +24,10 @@ import (
"net/url"
"slices"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/util/xslices"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/util/xslices"
)
func (p *Processor) Alias(
@@ -107,9 +107,14 @@ func (p *Processor) Alias(
}
// Ensure we have account dereferenced.
+ //
+ // As this comes from user input, allow checking
+ // by URL to make things easier, not just to an
+ // exact AP URI (which a user might not even know).
targetAccount, _, err := p.federator.GetAccountByURI(ctx,
account.Username,
newAKA.uri,
+ true,
)
if err != nil {
err := fmt.Errorf(
diff --git a/internal/processing/account/alias_test.go b/internal/processing/account/alias_test.go
index 80fdb81c1..fef2335ec 100644
--- a/internal/processing/account/alias_test.go
+++ b/internal/processing/account/alias_test.go
@@ -18,12 +18,11 @@
package account_test
import (
- "context"
"slices"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
type AliasTestSuite struct {
@@ -145,7 +144,7 @@ func (suite *AliasTestSuite) TestAliasAccount() {
},
} {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
testAcct = new(gtsmodel.Account)
)
diff --git a/internal/processing/account/block.go b/internal/processing/account/block.go
index d3904bffa..3c143e53b 100644
--- a/internal/processing/account/block.go
+++ b/internal/processing/account/block.go
@@ -22,17 +22,17 @@ import (
"errors"
"fmt"
- "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/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/paging"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local.
@@ -137,7 +137,7 @@ func (p *Processor) BlocksGet(
requestingAccount *gtsmodel.Account,
page *paging.Page,
) (*apimodel.PageableResponse, gtserror.WithCode) {
- blocks, err := p.state.DB.GetAccountBlocks(ctx,
+ blocks, err := p.state.DB.GetAccountBlocking(ctx,
requestingAccount.ID,
page,
)
diff --git a/internal/processing/account/bookmarks.go b/internal/processing/account/bookmarks.go
index d64108d3a..468c6ad62 100644
--- a/internal/processing/account/bookmarks.go
+++ b/internal/processing/account/bookmarks.go
@@ -21,13 +21,12 @@ import (
"context"
"errors"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// BookmarksGet returns a pageable response of statuses that are bookmarked by requestingAccount.
@@ -75,11 +74,12 @@ func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmode
}
// Convert the status.
- item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil, nil)
+ item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount)
if err != nil {
log.Errorf(ctx, "error converting bookmarked status to api: %s", err)
continue
}
+
items = append(items, item)
}
diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go
index 2618fdfc5..a45afe754 100644
--- a/internal/processing/account/delete.go
+++ b/internal/processing/account/delete.go
@@ -23,313 +23,313 @@ import (
"net"
"time"
- "codeberg.org/gruf/go-kv"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "codeberg.org/gruf/go-kv/v2"
"github.com/google/uuid"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/util"
"golang.org/x/crypto/bcrypt"
)
-const deleteSelectLimit = 50
+const deleteSelectLimit = 100
// 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.
-func (p *Processor) Delete(
- ctx context.Context,
- account *gtsmodel.Account,
- origin string,
-) gtserror.WithCode {
- l := log.WithContext(ctx).WithFields(kv.Fields{
- {"username", account.Username},
- {"domain", account.Domain},
+//
+// This delete function handles the case of both local and remote accounts, and processes side
+// effects synchronously to not clog worker queues with potentially tens-of-thousands of requests.
+func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) error {
+
+ // Prepare a new log entry for account delete.
+ log := log.WithContext(ctx).WithFields(kv.Fields{
+ {"uri", account.URI},
+ {"origin", origin},
}...)
- l.Trace("beginning account delete process")
- // Delete statuses *before* follows to ensure correct addressing
- // of any outgoing fedi messages generated by deleting statuses.
- if err := p.deleteAccountStatuses(ctx, account); err != nil {
- l.Errorf("continuing after error during account delete: %v", err)
- }
+ var err error
- if err := p.deleteAccountFollows(ctx, account); err != nil {
- l.Errorf("continuing after error during account delete: %v", err)
- }
+ // Log operation start / stop.
+ log.Info("start account delete")
+ defer func() {
+ if err != nil {
+ log.Errorf("fatal error during account delete: %v", err)
+ } else {
+ log.Info("finished account delete")
+ }
+ }()
- if err := p.deleteAccountBlocks(ctx, account); err != nil {
- l.Errorf("continuing after error during account delete: %v", err)
- }
+ // Delete statuses *before* anything else as for local
+ // accounts we need to federate out deletes, which relies
+ // on follows for addressing the appropriate accounts.
+ p.deleteAccountStatuses(ctx, &log, account)
- if err := p.deleteAccountNotifications(ctx, account); err != nil {
- l.Errorf("continuing after error during account delete: %v", err)
- }
+ // Now delete relationships to / from account.
+ p.deleteAccountRelations(ctx, &log, account)
- if err := p.deleteAccountPeripheral(ctx, account); err != nil {
- l.Errorf("continuing after error during account delete: %v", err)
- }
+ // Now delete any notifications to / from account.
+ p.deleteAccountNotifications(ctx, &log, account)
+
+ // Delete other peripheral objects ownable /
+ // manageable by any local / remote account.
+ p.deleteAccountPeripheral(ctx, &log, account)
if account.IsLocal() {
// We delete tokens, applications and clients for
// account as one of the last stages during deletion,
// as other database models rely on these.
- if err := p.deleteUserAndTokensForAccount(ctx, account); err != nil {
- l.Errorf("continuing after error during account delete: %v", err)
+ if err = p.deleteUserAndTokensForAccount(ctx, &log, account); err != nil {
+ return err
}
}
// To prevent the account being created again,
- // stubbify it and update it in the db.
- // The account will not be deleted, but it
- // will become completely unusable.
+ // (which would cause horrible federation shenanigans),
+ // the account will be stubbed out to an unusable state
+ // with no identifying info remaining, but NOT deleted.
columns := stubbifyAccount(account, origin)
- if err := p.state.DB.UpdateAccount(ctx, account, columns...); err != nil {
- return gtserror.NewErrorInternalError(err)
+ if err = p.state.DB.UpdateAccount(ctx, account, columns...); err != nil {
+ return gtserror.Newf("error stubbing out account: %v", err)
}
- l.Info("account delete process complete")
return nil
}
-// deleteUserAndTokensForAccount deletes the gtsmodel.User and
-// any OAuth tokens, applications, and Web Push subscriptions 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 {
+func (p *Processor) deleteUserAndTokensForAccount(
+ ctx context.Context,
+ log *log.Entry,
+ account *gtsmodel.Account,
+) error {
+
+ // Fetch the associated user for account, on fail return
+ // early as all other parts of this func rely on this user.
user, err := p.state.DB.GetUserByAccountID(ctx, account.ID)
if err != nil {
- return gtserror.Newf("db error getting user: %w", err)
+ return gtserror.Newf("error getting account user: %v", err)
}
- tokens := []*gtsmodel.Token{}
- if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "user_id", Value: user.ID}}, &tokens); err != nil {
- return gtserror.Newf("db error getting tokens: %w", err)
+ // Get list of applications managed by deleting user.
+ apps, err := p.state.DB.GetApplicationsManagedByUserID(ctx,
+ user.ID,
+ nil, // i.e. all
+ )
+ if err != nil {
+ log.Errorf("error getting user applications: %v", 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 gtserror.Newf("db error deleting client: %w", err)
+ // Delete each app and any tokens it had created
+ // (not necessarily owned by deleted account).
+ for _, app := range apps {
+ if err := p.state.DB.DeleteTokensByClientID(ctx, app.ClientID); err != nil {
+ log.Errorf("error deleting application token: %v", err)
}
-
- // Delete any OAuth applications associated with this token.
- if err := p.state.DB.DeleteApplicationByClientID(ctx, t.ClientID); err != nil {
- return gtserror.Newf("db error deleting application: %w", err)
+ if err := p.state.DB.DeleteApplicationByID(ctx, app.ID); err != nil {
+ log.Errorf("error deleting user application: %v", err)
}
+ }
- // Delete the token itself.
- if err := p.state.DB.DeleteByID(ctx, t.ID, t); err != nil {
- return gtserror.Newf("db error deleting token: %w", err)
- }
+ // Get any remaining access tokens owned by user.
+ tokens, err := p.state.DB.GetAccessTokens(ctx,
+ user.ID,
+ nil, // i.e. all
+ )
+ if err != nil {
+ log.Errorf("error getting user access tokens: %v", err)
}
- if err := p.state.DB.DeleteWebPushSubscriptionsByAccountID(ctx, account.ID); err != nil {
- return gtserror.Newf("db error deleting Web Push subscriptions: %w", err)
+ // Delete user access tokens.
+ for _, token := range tokens {
+ if err := p.state.DB.DeleteTokenByID(ctx, token.ID); err != nil {
+ log.Errorf("error deleting user access token: %v", err)
+ }
}
- columns, err := stubbifyUser(user)
- if err != nil {
- return gtserror.Newf("error stubbifying user: %w", err)
+ // Delete any web push subscriptions created by this local user account.
+ if err := p.state.DB.DeleteWebPushSubscriptionsByAccountID(ctx, account.ID); err != nil {
+ log.Errorf("error deleting account web push subscriptions: %v", err)
}
+ // To prevent the user being created again,
+ // the user will be stubbed out to an unusable state
+ // with no identifying info remaining, but NOT deleted.
+ columns := stubbifyUser(user)
if err := p.state.DB.UpdateUser(ctx, user, columns...); err != nil {
- return gtserror.Newf("db error updating user: %w", err)
+ return gtserror.Newf("error stubbing out 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.GetAccountFollowers(ctx, account.ID, nil)
+func (p *Processor) deleteAccountRelations(
+ ctx context.Context,
+ log *log.Entry,
+ account *gtsmodel.Account,
+) {
+ // Get a list of the follows targeting this account.
+ followedBy, err := p.state.DB.GetAccountFollowers(ctx,
+ account.ID,
+ nil, // i.e. all
+ )
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("db error getting follows targeting account %s: %w", account.ID, err)
+ log.Errorf("error getting account followed-bys: %v", err)
}
+ // Delete these follows from database.
for _, follow := range followedBy {
if err := p.state.DB.DeleteFollowByID(ctx, follow.ID); err != nil {
- return gtserror.Newf("db error unfollowing account followedBy: %w", err)
+ log.Errorf("error deleting account followed-by %s: %v", follow.URI, err)
}
}
- // Delete follow requests targeting this account.
- followRequestedBy, err := p.state.DB.GetAccountFollowRequests(ctx, account.ID, nil)
+ // Get a list of the follow requests targeting this account.
+ followRequestedBy, err := p.state.DB.GetAccountFollowRequests(ctx,
+ account.ID,
+ nil, // i.e. all
+ )
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("db error getting follow requests targeting account %s: %w", account.ID, err)
+ log.Errorf("error getting account follow-requested-bys: %v", err)
}
- for _, followRequest := range followRequestedBy {
- if err := p.state.DB.DeleteFollowRequestByID(ctx, followRequest.ID); err != nil {
- return gtserror.Newf("db error unfollowing account followRequestedBy: %w", err)
+ // Delete these follow requests from database.
+ for _, followReq := range followRequestedBy {
+ if err := p.state.DB.DeleteFollowRequestByID(ctx, followReq.ID); err != nil {
+ log.Errorf("error deleting account follow-requested-by %s: %v", followReq.URI, 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.IsLocal())
+ // Get a list of the blocks targeting this account.
+ blockedBy, err := p.state.DB.GetAccountBlockedBy(ctx,
+ account.ID,
+ nil, // i.e. all
)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ log.Errorf("error getting account blocked-bys: %v", err)
+ }
- // Delete follows originating from this account.
- following, err := p.state.DB.GetAccountFollows(ctx, account.ID, nil)
+ // Delete these blocks from database.
+ for _, block := range blockedBy {
+ if err := p.state.DB.DeleteBlockByID(ctx, block.ID); err != nil {
+ log.Errorf("error deleting account blocked-by %s: %v", block.URI, err)
+ }
+ }
+
+ // Get the follows originating from this account.
+ following, err := p.state.DB.GetAccountFollows(ctx,
+ account.ID,
+ nil, // i.e. all
+ )
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("db error getting follows owned by account %s: %w", account.ID, err)
+ log.Errorf("error getting account follows: %v", err)
}
- // For each follow owned by this account, unfollow
- // and process side effects (noop if remote account).
+ // Delete these follows from database.
for _, follow := range following {
if err := p.state.DB.DeleteFollowByID(ctx, follow.ID); err != nil {
- return gtserror.Newf("db error unfollowing account: %w", err)
- }
- if msg := unfollowSideEffects(ctx, account, follow); msg != nil {
- // There was a side effect to process.
- msgs = append(msgs, msg)
+ log.Errorf("error deleting account followed %s: %v", follow.URI, err)
}
}
- // Delete follow requests originating from this account.
- followRequesting, err := p.state.DB.GetAccountFollowRequesting(ctx, account.ID, nil)
+ // Get a list of the follow requests originating from this account.
+ followRequesting, err := p.state.DB.GetAccountFollowRequesting(ctx,
+ account.ID,
+ nil, // i.e. all
+ )
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("db error getting follow requests owned by account %s: %w", account.ID, err)
+ log.Errorf("error getting account follow-requests: %v", err)
}
- // For each follow owned by this account, unfollow
- // and process side effects (noop if remote account).
- for _, followRequest := range followRequesting {
- if err := p.state.DB.DeleteFollowRequestByID(ctx, followRequest.ID); err != nil {
- return gtserror.Newf("db error unfollowingRequesting account: %w", err)
- }
-
- // 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 := &gtsmodel.Follow{
- URI: followRequest.URI,
- AccountID: followRequest.AccountID,
- Account: followRequest.Account,
- TargetAccountID: followRequest.TargetAccountID,
- TargetAccount: followRequest.TargetAccount,
+ // Delete these follow requests from database.
+ for _, followReq := range followRequesting {
+ if err := p.state.DB.DeleteFollowRequestByID(ctx, followReq.ID); err != nil {
+ log.Errorf("error deleting account follow-request %s: %v", followReq.URI, err)
}
+ }
- if msg := unfollowSideEffects(ctx, account, follow); msg != nil {
- // There was a side effect to process.
- msgs = append(msgs, msg)
- }
+ // Get the blocks originating from this account.
+ blocking, err := p.state.DB.GetAccountBlocking(ctx,
+ account.ID,
+ nil, // i.e. all
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ log.Errorf("error getting account blocks: %v", err)
}
- // Process accreted messages in serial.
- for _, msg := range msgs {
- if err := p.state.Workers.Client.Process(ctx, msg); err != nil {
- log.Errorf(
- ctx,
- "error processing %s of %s during Delete of account %s: %v",
- msg.APActivityType, msg.APObjectType, account.ID, err,
- )
+ // Delete these blocks from database.
+ for _, block := range blocking {
+ if err := p.state.DB.DeleteBlockByID(ctx, block.ID); err != nil {
+ log.Errorf("error deleting account block %s: %v", block.URI, err)
}
}
- return nil
-}
-
-func (p *Processor) unfollowSideEffectsFunc(local bool) func(
- ctx context.Context,
- account *gtsmodel.Account,
- follow *gtsmodel.Follow,
-) *messages.FromClientAPI {
- if !local {
- // Don't try to process side effects
- // for accounts that aren't local.
- return func(
- _ context.Context,
- _ *gtsmodel.Account,
- _ *gtsmodel.Follow,
- ) *messages.FromClientAPI {
- // noop
- return nil
- }
+ // Delete all mutes targetting / originating from account.
+ if err := p.state.DB.DeleteAccountMutes(ctx, account.ID); // nocollapse
+ err != nil && !errors.Is(err, db.ErrNoEntries) {
+ log.Errorf("error deleting mutes to / from account: %v", err)
}
- 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 account.IsLocal() {
+ // Process side-effects for deleting
+ // of account follows from local user.
+ for _, follow := range following {
+ p.processSideEffect(ctx, log,
+ ap.ActivityUndo,
+ ap.ActivityFollow,
+ follow,
+ account,
+ follow.TargetAccount,
+ )
}
- if follow.TargetAccount.IsLocal() {
- // No side effects
- // for local unfollows.
- return nil
+ // Process side-effects for deleting of account follow requests
+ // from local user. Though handled as though UNDO of a follow.
+ for _, followReq := range followRequesting {
+ p.processSideEffect(ctx, log,
+ ap.ActivityUndo,
+ ap.ActivityFollow,
+ &gtsmodel.Follow{
+ ID: followReq.ID,
+ URI: followReq.URI,
+ AccountID: followReq.AccountID,
+ Account: followReq.Account,
+ TargetAccountID: followReq.TargetAccountID,
+ TargetAccount: followReq.TargetAccount,
+ ShowReblogs: new(bool),
+ Notify: new(bool),
+ },
+ account,
+ followReq.TargetAccount,
+ )
}
- // There was a follow, process side effects.
- return &messages.FromClientAPI{
- APObjectType: ap.ActivityFollow,
- APActivityType: ap.ActivityUndo,
- GTSModel: follow,
- Origin: account,
- Target: follow.TargetAccount,
+ // Process side-effects for deleting
+ // of account blocks from local user.
+ for _, block := range blocking {
+ p.processSideEffect(ctx, log,
+ ap.ActivityUndo,
+ ap.ActivityBlock,
+ block,
+ account,
+ block.TargetAccount,
+ )
}
}
}
-func (p *Processor) deleteAccountBlocks(ctx context.Context, account *gtsmodel.Account) error {
- if err := p.state.DB.DeleteAccountBlocks(ctx, account.ID); err != nil {
- return gtserror.Newf("db error deleting account blocks for %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 processing.
func (p *Processor) deleteAccountStatuses(
ctx context.Context,
+ log *log.Entry,
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 maxID string
- 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,
+ // Page through deleting account's statuses.
+ statuses, err := p.state.DB.GetAccountStatuses(
+ gtscontext.SetBarebones(ctx),
account.ID,
deleteSelectLimit,
false,
@@ -340,12 +340,14 @@ statusLoop:
false,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- // Make sure we don't have a real error.
- return err
+ log.Errorf("error getting account statuses: %v", err)
+ return
}
if len(statuses) == 0 {
- break statusLoop
+ // reached
+ // the end.
+ break
}
// Update next maxID from last status.
@@ -356,140 +358,173 @@ statusLoop:
status.Account = account
// Look for any boosts of this status in DB.
- //
- // We put these in the msgs slice first so
- // that they're handled first, before the
- // parent status that's being boosted.
- //
- // Use a barebones context and just select the
- // origin account separately. The rest will be
- // populated later anyway, and we don't want to
- // stop now because we couldn't get something.
boosts, err := p.state.DB.GetStatusBoosts(
gtscontext.SetBarebones(ctx),
status.ID,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("error fetching status boosts for %s: %w", status.ID, err)
+ log.Errorf("error getting status boosts for %s: %v", status.URI, err)
+ continue
}
// Prepare to Undo each boost.
for _, boost := range boosts {
+
+ // Fetch the owning account of this boost.
boost.Account, err = p.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
boost.AccountID,
)
-
if err != nil {
- log.Warnf(
- ctx,
- "db error getting owner %s of status boost %s: %v",
- boost.AccountID, boost.ID, err,
- )
+ log.Errorf("error getting owner %s of status boost %s: %v",
+ boost.AccountURI, boost.URI, err)
continue
}
- msgs = append(msgs, &messages.FromClientAPI{
- APObjectType: ap.ActivityAnnounce,
- APActivityType: ap.ActivityUndo,
- GTSModel: status,
- Origin: boost.Account,
- Target: account,
- })
+ // Process boost undo event.
+ p.processSideEffect(ctx, log,
+ ap.ActivityUndo,
+ ap.ActivityAnnounce,
+ boost,
+ account,
+ account,
+ )
}
- // Now prepare to Delete status.
- msgs = append(msgs, &messages.FromClientAPI{
- APObjectType: ap.ObjectNote,
- APActivityType: ap.ActivityDelete,
- GTSModel: status,
- Origin: account,
- Target: account,
- })
+ // Process status delete event.
+ p.processSideEffect(ctx, log,
+ ap.ActivityDelete,
+ ap.ObjectNote,
+ status,
+ account,
+ account,
+ )
}
}
+}
- // Process accreted messages in serial.
- for _, msg := range msgs {
- if err := p.state.Workers.Client.Process(ctx, msg); err != nil {
- log.Errorf(
- ctx,
- "error processing %s of %s during Delete of account %s: %v",
- msg.APActivityType, msg.APObjectType, account.ID, err,
- )
+func (p *Processor) deleteAccountNotifications(
+ ctx context.Context,
+ log *log.Entry,
+ account *gtsmodel.Account,
+) {
+ if account.IsLocal() {
+ // Delete all types of notifications targeting this local account.
+ if err := p.state.DB.DeleteNotifications(ctx, nil, account.ID, ""); // nocollapse
+ err != nil && !errors.Is(err, db.ErrNoEntries) {
+ log.Errorf("error deleting notifications targeting account: %v", err)
}
}
- return nil
+ // Delete all types of notifications originating from this account.
+ if err := p.state.DB.DeleteNotifications(ctx, nil, "", account.ID); // nocollapse
+ err != nil && !errors.Is(err, db.ErrNoEntries) {
+ log.Errorf("error deleting notifications originating from account: %v", err)
+ }
}
-func (p *Processor) deleteAccountNotifications(ctx context.Context, account *gtsmodel.Account) error {
- // Delete all notifications of all types targeting given account.
- if err := p.state.DB.DeleteNotifications(ctx, nil, account.ID, ""); err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("error deleting notifications targeting account: %w", err)
- }
+func (p *Processor) deleteAccountPeripheral(
+ ctx context.Context,
+ log *log.Entry,
+ account *gtsmodel.Account,
+) {
+ if account.IsLocal() {
+ // Delete all bookmarks owned by given account, only for local.
+ if err := p.state.DB.DeleteStatusBookmarks(ctx, account.ID, ""); // nocollapse
+ err != nil && !errors.Is(err, db.ErrNoEntries) {
+ log.Errorf("error deleting bookmarks by account: %v", err)
+ }
- // Delete all notifications of all types originating from given account.
- if err := p.state.DB.DeleteNotifications(ctx, nil, "", account.ID); err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("error deleting notifications by account: %w", err)
- }
+ // Delete all faves owned by given account, only for local.
+ if err := p.state.DB.DeleteStatusFaves(ctx, account.ID, ""); // nocollapse
+ err != nil && !errors.Is(err, db.ErrNoEntries) {
+ log.Errorf("error deleting faves by account: %v", err)
+ }
- return nil
-}
+ // Delete all conversations owned by given account, only for local.
+ //
+ // *Participated* conversations will be retained, leaving up to *their* owners.
+ if err := p.state.DB.DeleteConversationsByOwnerAccountID(ctx, account.ID); // nocollapse
+ err != nil && !errors.Is(err, db.ErrNoEntries) {
+ log.Errorf("error deleting conversations by account: %v", 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 gtserror.Newf("error deleting bookmarks by account: %w", err)
- }
+ // Delete all followed tags owned by given account, only for local.
+ if err := p.state.DB.DeleteFollowedTagsByAccountID(ctx, account.ID); // nocollapse
+ err != nil && !errors.Is(err, db.ErrNoEntries) {
+ log.Errorf("error deleting followed tags by account: %v", err)
+ }
- // Delete all bookmarks targeting given account.
- if err := p.state.DB.DeleteStatusBookmarks(ctx, "", account.ID); // nocollapse
- err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("error deleting bookmarks targeting account: %w", err)
- }
+ // Delete stats model stored for given account, only for local.
+ if err := p.state.DB.DeleteAccountStats(ctx, account.ID); err != nil {
+ log.Errorf("error deleting stats for account: %v", err)
+ }
- // Delete all faves owned by given account.
- if err := p.state.DB.DeleteStatusFaves(ctx, account.ID, ""); // nocollapse
- err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("error deleting faves by account: %w", err)
+ // Delete statuses scheduled by given account, only for local.
+ if err := p.state.DB.DeleteScheduledStatusesByAccountID(ctx, account.ID); err != nil {
+ log.Errorf("error deleting scheduled statuses for account: %v", err)
+ }
}
- // Delete all faves targeting given account.
- if err := p.state.DB.DeleteStatusFaves(ctx, "", account.ID); // nocollapse
+ // Delete all bookmarks targeting given account, local and remote.
+ if err := p.state.DB.DeleteStatusBookmarks(ctx, "", account.ID); // nocollapse
err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("error deleting faves targeting account: %w", err)
+ log.Errorf("error deleting bookmarks targeting account: %v", err)
}
- // TODO: add status mutes here when they're implemented.
-
- // Delete all conversations owned by given account.
- // Conversations in which it has only participated will be retained;
- // they can always be deleted by their owners.
- if err := p.state.DB.DeleteConversationsByOwnerAccountID(ctx, account.ID); // nocollapse
+ // Delete all faves targeting given account, local and remote.
+ if err := p.state.DB.DeleteStatusFaves(ctx, "", account.ID); // nocollapse
err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("error deleting conversations owned by account: %w", err)
+ log.Errorf("error deleting faves targeting account: %v", err)
}
- // Delete all poll votes owned by given account.
+ // Delete all poll votes owned by given account, local and remote.
if err := p.state.DB.DeletePollVotesByAccountID(ctx, account.ID); // nocollapse
err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("error deleting poll votes by account: %w", err)
+ log.Errorf("error deleting poll votes by account: %v", err)
}
- // Delete all followed tags owned by given account.
- if err := p.state.DB.DeleteFollowedTagsByAccountID(ctx, account.ID); // nocollapse
+ // Delete all interaction requests from given account, local and remote.
+ if err := p.state.DB.DeleteInteractionRequestsByInteractingAccountID(ctx, account.ID); // nocollapse
err != nil && !errors.Is(err, db.ErrNoEntries) {
- return gtserror.Newf("error deleting followed tags by account: %w", err)
+ log.Errorf("error deleting interaction requests by account: %v", err)
}
+}
- // Delete account stats model.
- if err := p.state.DB.DeleteAccountStats(ctx, account.ID); err != nil {
- return gtserror.Newf("error deleting stats for account: %w", err)
+// processSideEffect will process the given side effect details,
+// with appropriate worker depending on if origin is local / remote.
+func (p *Processor) processSideEffect(
+ ctx context.Context,
+ log *log.Entry,
+ activityType string,
+ objectType string,
+ gtsModel any,
+ origin *gtsmodel.Account,
+ target *gtsmodel.Account,
+) {
+ if origin.IsLocal() {
+ // Process side-effect through our client API as this is a local account.
+ if err := p.state.Workers.Client.Process(ctx, &messages.FromClientAPI{
+ APActivityType: activityType,
+ APObjectType: objectType,
+ GTSModel: gtsModel,
+ Origin: origin,
+ Target: target,
+ }); err != nil {
+ log.Errorf("error processing %s of %s during local account %s delete: %v", activityType, objectType, origin.ID, err)
+ }
+ } else {
+ // Process side-effect through our fedi API as this is a remote account.
+ if err := p.state.Workers.Federator.Process(ctx, &messages.FromFediAPI{
+ APActivityType: activityType,
+ APObjectType: objectType,
+ GTSModel: gtsModel,
+ Requesting: origin,
+ Receiving: target,
+ }); err != nil {
+ log.Errorf("error processing %s of %s during local account %s delete: %v", activityType, objectType, origin.ID, err)
+ }
}
-
- return nil
}
// stubbifyAccount renders the given account as a stub,
@@ -519,7 +554,7 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
account.Fields = nil
account.Note = ""
account.NoteRaw = ""
- account.Memorial = util.Ptr(false)
+ account.MemorializedAt = never
account.AlsoKnownAsURIs = nil
account.MovedToURI = ""
account.Discoverable = util.Ptr(false)
@@ -537,7 +572,7 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
"fields",
"note",
"note_raw",
- "memorial",
+ "memorialized_at",
"also_known_as_uris",
"moved_to_uri",
"discoverable",
@@ -558,15 +593,21 @@ func stubbifyAccount(account *gtsmodel.Account, origin string) []string {
//
// For caller's convenience, this function returns the db
// names of all columns that are updated by it.
-func stubbifyUser(user *gtsmodel.User) ([]string, error) {
+func stubbifyUser(user *gtsmodel.User) []string {
uuid, err := uuid.New().MarshalBinary()
if err != nil {
- return nil, err
+ // this should never happen,
+ // it indicates /dev/random
+ // is misbehaving.
+ panic(err)
}
dummyPassword, err := bcrypt.GenerateFromPassword(uuid, bcrypt.DefaultCost)
if err != nil {
- return nil, err
+ // this should never happen,
+ // it indicates /dev/random
+ // is misbehaving.
+ panic(err)
}
never := time.Time{}
@@ -591,5 +632,5 @@ func stubbifyUser(user *gtsmodel.User) ([]string, error) {
"confirmation_sent_at",
"reset_password_token",
"reset_password_sent_at",
- }, nil
+ }
}
diff --git a/internal/processing/account/delete_test.go b/internal/processing/account/delete_test.go
index ee6fe1dfc..93bc1f9c4 100644
--- a/internal/processing/account/delete_test.go
+++ b/internal/processing/account/delete_test.go
@@ -18,13 +18,12 @@
package account_test
import (
- "context"
"net"
"testing"
"time"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
type AccountDeleteTestSuite struct {
@@ -32,7 +31,7 @@ type AccountDeleteTestSuite struct {
}
func (suite *AccountDeleteTestSuite) TestAccountDeleteLocal() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// Keep a reference around to the original account
// and user, before the delete was processed.
@@ -64,7 +63,7 @@ func (suite *AccountDeleteTestSuite) TestAccountDeleteLocal() {
suite.Nil(updatedAccount.Fields)
suite.Zero(updatedAccount.Note)
suite.Zero(updatedAccount.NoteRaw)
- suite.False(*updatedAccount.Memorial)
+ suite.Zero(updatedAccount.MemorializedAt)
suite.Empty(updatedAccount.AlsoKnownAsURIs)
suite.False(*updatedAccount.Discoverable)
suite.WithinDuration(time.Now(), updatedAccount.SuspendedAt, 1*time.Minute)
diff --git a/internal/processing/account/export.go b/internal/processing/account/export.go
index 68cc17b6d..4f66a8229 100644
--- a/internal/processing/account/export.go
+++ b/internal/processing/account/export.go
@@ -21,10 +21,10 @@ import (
"context"
"errors"
- 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/gtsmodel"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// ExportStats returns the requester's export stats,
@@ -120,7 +120,7 @@ func (p *Processor) ExportBlocks(
ctx context.Context,
requester *gtsmodel.Account,
) ([][]string, gtserror.WithCode) {
- blocks, err := p.state.DB.GetAccountBlocks(ctx, requester.ID, nil)
+ blocks, err := p.state.DB.GetAccountBlocking(ctx, requester.ID, nil)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = gtserror.Newf("db error getting blocks: %w", err)
return nil, gtserror.NewErrorInternalError(err)
diff --git a/internal/processing/account/follow.go b/internal/processing/account/follow.go
index 59de8834b..91955eaa7 100644
--- a/internal/processing/account/follow.go
+++ b/internal/processing/account/follow.go
@@ -21,16 +21,16 @@ import (
"context"
"errors"
- "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/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// FollowCreate handles a follow request to an account, either remote or local.
@@ -82,10 +82,7 @@ func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmode
// Neither follows nor follow requests, so
// create and store a new follow request.
- followID, err := id.NewRandomULID()
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
+ followID := id.NewRandomULID()
followURI := uris.GenerateURIForFollow(requestingAccount.Username, followID)
fr := &gtsmodel.FollowRequest{
@@ -258,13 +255,9 @@ func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Ac
msgs = append(msgs, &messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
- GTSModel: &gtsmodel.Follow{
- AccountID: requestingAccount.ID,
- TargetAccountID: targetAccount.ID,
- URI: follow.URI,
- },
- Origin: requestingAccount,
- Target: targetAccount,
+ GTSModel: follow,
+ Origin: requestingAccount,
+ Target: targetAccount,
})
}
@@ -294,9 +287,13 @@ func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Ac
msgs = append(msgs, &messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
+ // Dummy out a follow to undo,
+ // based on the follow request.
GTSModel: &gtsmodel.Follow{
AccountID: requestingAccount.ID,
+ Account: requestingAccount,
TargetAccountID: targetAccount.ID,
+ TargetAccount: targetAccount,
URI: followReq.URI,
},
Origin: requestingAccount,
diff --git a/internal/processing/account/follow_request.go b/internal/processing/account/follow_request.go
index 6f6c7ba2d..88cadd7d0 100644
--- a/internal/processing/account/follow_request.go
+++ b/internal/processing/account/follow_request.go
@@ -21,13 +21,13 @@ import (
"context"
"errors"
- "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/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/paging"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
)
// FollowRequestAccept handles the accepting of a follow request from the sourceAccountID to the requestingAccount (the currently authorized account).
@@ -117,3 +117,42 @@ func (p *Processor) FollowRequestsGet(ctx context.Context, requestingAccount *gt
Prev: page.Prev(lo, hi),
}), nil
}
+
+// OutgoingFollowRequestsGet fetches a list of the accounts with a pending follow request originating from the given requestingAccount (the currently authorized account).
+func (p *Processor) OutgoingFollowRequestsGet(ctx context.Context, requestingAccount *gtsmodel.Account, page *paging.Page) (*apimodel.PageableResponse, gtserror.WithCode) {
+ // Fetch follow requests originating from the given requesting account model.
+ followRequests, err := p.state.DB.GetAccountFollowRequesting(ctx, requestingAccount.ID, page)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check for empty response.
+ count := len(followRequests)
+ if count == 0 {
+ return paging.EmptyResponse(), nil
+ }
+
+ // Get the lowest and highest
+ // ID values, used for paging.
+ lo := followRequests[count-1].ID
+ hi := followRequests[0].ID
+
+ // Func to fetch follow source at index.
+ getIdx := func(i int) *gtsmodel.Account {
+ return followRequests[i].TargetAccount
+ }
+
+ // Get a filtered slice of public API account models.
+ items := p.c.GetVisibleAPIAccountsPaged(ctx,
+ requestingAccount,
+ getIdx,
+ count,
+ )
+
+ return paging.PackageResponse(paging.ResponseParams{
+ Items: items,
+ Path: "/api/v1/follow_requests/outgoing",
+ Next: page.Next(lo, hi),
+ Prev: page.Prev(lo, hi),
+ }), nil
+}
diff --git a/internal/processing/account/follow_test.go b/internal/processing/account/follow_test.go
index 9ea8ce1b8..d68d8d065 100644
--- a/internal/processing/account/follow_test.go
+++ b/internal/processing/account/follow_test.go
@@ -18,14 +18,13 @@
package account_test
import (
- "context"
"testing"
"time"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/util"
)
type FollowTestSuite struct {
@@ -33,7 +32,7 @@ type FollowTestSuite struct {
}
func (suite *FollowTestSuite) TestUpdateExistingFollowChangeBoth() {
- ctx := context.Background()
+ ctx := suite.T().Context()
requestingAccount := suite.testAccounts["local_account_1"]
targetAccount := suite.testAccounts["admin_account"]
@@ -54,7 +53,7 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowChangeBoth() {
}
func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNotifyIgnoreReblogs() {
- ctx := context.Background()
+ ctx := suite.T().Context()
requestingAccount := suite.testAccounts["local_account_1"]
targetAccount := suite.testAccounts["admin_account"]
@@ -74,7 +73,7 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNotifyIgnoreReblogs(
}
func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNotifySetReblogs() {
- ctx := context.Background()
+ ctx := suite.T().Context()
requestingAccount := suite.testAccounts["local_account_1"]
targetAccount := suite.testAccounts["admin_account"]
@@ -95,7 +94,7 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNotifySetReblogs() {
}
func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNothing() {
- ctx := context.Background()
+ ctx := suite.T().Context()
requestingAccount := suite.testAccounts["local_account_1"]
targetAccount := suite.testAccounts["admin_account"]
@@ -115,7 +114,7 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNothing() {
}
func (suite *FollowTestSuite) TestUpdateExistingFollowSetNothing() {
- ctx := context.Background()
+ ctx := suite.T().Context()
requestingAccount := suite.testAccounts["local_account_1"]
targetAccount := suite.testAccounts["admin_account"]
@@ -133,7 +132,7 @@ func (suite *FollowTestSuite) TestUpdateExistingFollowSetNothing() {
}
func (suite *FollowTestSuite) TestFollowRequestLocal() {
- ctx := context.Background()
+ ctx := suite.T().Context()
requestingAccount := suite.testAccounts["admin_account"]
targetAccount := suite.testAccounts["local_account_2"]
diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go
index eac0f0c3f..7f401ed57 100644
--- a/internal/processing/account/get.go
+++ b/internal/processing/account/get.go
@@ -22,12 +22,12 @@ import (
"errors"
"net/url"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
)
// Get processes the given request for account information.
@@ -66,10 +66,13 @@ func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
// Perform a last-minute fetch of target account to
// ensure remote account header / avatar is cached.
+ //
+ // Match by URI only.
latest, _, err := p.federator.GetAccountByURI(
gtscontext.SetFastFail(ctx),
requestingAccount.Username,
targetAccountURI,
+ false,
)
if err != nil {
log.Errorf(ctx, "error fetching latest target account: %v", err)
@@ -109,7 +112,7 @@ func (p *Processor) GetWeb(ctx context.Context, username string) (*apimodel.WebA
return nil, gtserror.NewErrorInternalError(err)
}
- webAccount, err := p.converter.AccountToWebAccount(ctx, targetAccount)
+ webAccount, err := p.converter.AccountToWebAccount(ctx, targetAccount, nil)
if err != nil {
err := gtserror.Newf("error converting account: %w", err)
return nil, gtserror.NewErrorInternalError(err)
diff --git a/internal/processing/account/import.go b/internal/processing/account/import.go
index 68e843cfa..55415b4c2 100644
--- a/internal/processing/account/import.go
+++ b/internal/processing/account/import.go
@@ -24,10 +24,10 @@ import (
"fmt"
"mime/multipart"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
)
func (p *Processor) ImportData(
@@ -55,6 +55,14 @@ func (p *Processor) ImportData(
overwrite,
)
+ case "mutes":
+ return p.importMutes(
+ ctx,
+ requester,
+ data,
+ overwrite,
+ )
+
default:
const text = "import type not yet supported"
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
@@ -290,7 +298,7 @@ func importBlocksAsyncF(
err error
)
- prevBlocks, err = p.state.DB.GetAccountBlocks(ctx, requester.ID, nil)
+ prevBlocks, err = p.state.DB.GetAccountBlocking(ctx, requester.ID, nil)
if err != nil {
log.Errorf(ctx, "db error getting blocks: %v", err)
return
@@ -377,3 +385,150 @@ func importBlocksAsyncF(
}
}
}
+
+func (p *Processor) importMutes(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ mutesData *multipart.FileHeader,
+ overwrite bool,
+) gtserror.WithCode {
+ file, err := mutesData.Open()
+ if err != nil {
+ err := fmt.Errorf("error opening mutes data file: %w", err)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+ }
+ defer file.Close()
+
+ // Parse records out of the file.
+ records, err := csv.NewReader(file).ReadAll()
+ if err != nil {
+ err := fmt.Errorf("error reading mutes data file: %w", err)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ // Convert the records into a slice of barebones mutes.
+ //
+ // Only TargetAccount.Username, TargetAccount.Domain,
+ // and Notifications will be set on each mute.
+ mutes, err := p.converter.CSVToMutes(ctx, records)
+ if err != nil {
+ err := fmt.Errorf("error converting records to mutes: %w", err)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ // Do remaining processing of this import asynchronously.
+ f := importMutesAsyncF(p, requester, mutes, overwrite)
+ p.state.Workers.Processing.Queue.Push(f)
+
+ return nil
+}
+
+func importMutesAsyncF(
+ p *Processor,
+ requester *gtsmodel.Account,
+ mutes []*gtsmodel.UserMute,
+ overwrite bool,
+) func(context.Context) {
+ return func(ctx context.Context) {
+ // Map used to store wanted
+ // mute targets (if overwriting).
+ var wantedMutes map[string]struct{}
+
+ if overwrite {
+ // If we're overwriting, we need to get current
+ // mutes owned by requester *before* making any
+ // changes, so that we can remove unwanted mutes
+ // after we've created new ones.
+ var (
+ prevMutes []*gtsmodel.UserMute
+ err error
+ )
+
+ prevMutes, err = p.state.DB.GetAccountMutes(ctx, requester.ID, nil)
+ if err != nil {
+ log.Errorf(ctx, "db error getting mutes: %v", err)
+ return
+ }
+
+ // Initialize new mutes map.
+ wantedMutes = make(map[string]struct{}, len(mutes))
+
+ // Once we've created (or tried to create)
+ // the required mutes, go through previous
+ // mutes and remove unwanted ones.
+ defer func() {
+ for _, prev := range prevMutes {
+ username := prev.TargetAccount.Username
+ domain := prev.TargetAccount.Domain
+
+ _, wanted := wantedMutes[username+"@"+domain]
+ if wanted {
+ // Leave this
+ // one alone.
+ continue
+ }
+
+ if _, errWithCode := p.MuteRemove(
+ ctx,
+ requester,
+ prev.TargetAccountID,
+ ); errWithCode != nil {
+ log.Errorf(ctx, "could not unmute account: %v", errWithCode.Unwrap())
+ continue
+ }
+ }
+ }()
+ }
+
+ // Go through the mutes parsed from CSV
+ // file, and create / update each one.
+ for _, mute := range mutes {
+ var (
+ // Username of the target.
+ username = mute.TargetAccount.Username
+
+ // Domain of the target.
+ // Empty for our domain.
+ domain = mute.TargetAccount.Domain
+ )
+
+ if overwrite {
+ // We'll be overwriting, so store
+ // this new mute in our handy map.
+ wantedMutes[username+"@"+domain] = struct{}{}
+ }
+
+ // Get the target account, dereferencing it if necessary.
+ targetAcct, _, err := p.federator.Dereferencer.GetAccountByUsernameDomain(
+ ctx,
+ // Provide empty request user to use the
+ // instance account to deref the account.
+ //
+ // It's pointless to make lots of calls
+ // to a remote from an account that's about
+ // to mute that account.
+ "",
+ username,
+ domain,
+ )
+ if err != nil {
+ log.Errorf(ctx, "could not retrieve account: %v", err)
+ continue
+ }
+
+ // Use the processor's MuteCreate function
+ // to create or update the mute. This takes
+ // account of existing mutes, and also sends
+ // the mute to the FromClientAPI processor.
+ if _, errWithCode := p.MuteCreate(
+ ctx,
+ requester,
+ targetAcct.ID,
+ &apimodel.UserMuteCreateUpdateRequest{Notifications: mute.Notifications},
+ ); errWithCode != nil {
+ log.Errorf(ctx, "could not mute account: %v", errWithCode.Unwrap())
+ continue
+ }
+ }
+ }
+}
diff --git a/internal/processing/account/interactionpolicies.go b/internal/processing/account/interactionpolicies.go
index e02b43e9e..d581112a6 100644
--- a/internal/processing/account/interactionpolicies.go
+++ b/internal/processing/account/interactionpolicies.go
@@ -21,10 +21,10 @@ import (
"cmp"
"context"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
func (p *Processor) DefaultInteractionPoliciesGet(
@@ -89,10 +89,10 @@ func (p *Processor) DefaultInteractionPoliciesGet(
}
return &apimodel.DefaultPolicies{
- Direct: *directAPI,
- Private: *privateAPI,
- Unlisted: *unlistedAPI,
- Public: *publicAPI,
+ Direct: directAPI,
+ Private: privateAPI,
+ Unlisted: unlistedAPI,
+ Public: publicAPI,
}, nil
}
diff --git a/internal/processing/account/lists.go b/internal/processing/account/lists.go
index 04cf4ca73..be6c86d47 100644
--- a/internal/processing/account/lists.go
+++ b/internal/processing/account/lists.go
@@ -22,12 +22,12 @@ import (
"errors"
"fmt"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
)
// ListsGet returns all lists owned by requestingAccount, which contain a follow for targetAccountID.
diff --git a/internal/processing/account/move.go b/internal/processing/account/move.go
index 44f8da268..58db5d452 100644
--- a/internal/processing/account/move.go
+++ b/internal/processing/account/move.go
@@ -25,23 +25,24 @@ import (
"slices"
"time"
- "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/federation/dereferencing"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/federation/dereferencing"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
+ "codeberg.org/gruf/go-byteutil"
"golang.org/x/crypto/bcrypt"
)
func (p *Processor) MoveSelf(
ctx context.Context,
- authed *oauth.Auth,
+ authed *apiutil.Auth,
form *apimodel.AccountMoveRequest,
) gtserror.WithCode {
// Ensure valid MovedToURI.
@@ -70,8 +71,8 @@ func (p *Processor) MoveSelf(
}
if err := bcrypt.CompareHashAndPassword(
- []byte(authed.User.EncryptedPassword),
- []byte(form.Password),
+ byteutil.S2B(authed.User.EncryptedPassword),
+ byteutil.S2B(form.Password),
); err != nil {
const text = "invalid password provided in Move request"
return gtserror.NewErrorBadRequest(errors.New(text), text)
@@ -119,11 +120,15 @@ func (p *Processor) MoveSelf(
unlock := p.state.ProcessingLocks.Lock(lockKey)
defer unlock()
- // Ensure we have a valid, up-to-date representation of the target account.
+ // Ensure we have a valid, up-to-date
+ // representation of the target account.
+ //
+ // Match by uri only.
targetAcct, targetAcctable, err = p.federator.GetAccountByURI(
ctx,
originAcct.Username,
targetAcctURI,
+ false,
)
if err != nil {
const text = "error dereferencing moved_to_uri"
diff --git a/internal/processing/account/move_test.go b/internal/processing/account/move_test.go
index 9d06829ca..55d6548db 100644
--- a/internal/processing/account/move_test.go
+++ b/internal/processing/account/move_test.go
@@ -18,14 +18,14 @@
package account_test
import (
- "context"
"testing"
"time"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/oauth"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
)
type MoveTestSuite struct {
@@ -33,7 +33,7 @@ type MoveTestSuite struct {
}
func (suite *MoveTestSuite) TestMoveAccountOK() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// Copy zork.
requestingAcct := new(gtsmodel.Account)
@@ -56,7 +56,7 @@ func (suite *MoveTestSuite) TestMoveAccountOK() {
// Trigger move from zork to admin.
if err := suite.accountProcessor.MoveSelf(
ctx,
- &oauth.Auth{
+ &apiutil.Auth{
Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
Application: suite.testApplications["local_account_1"],
User: suite.testUsers["local_account_1"],
@@ -105,7 +105,7 @@ func (suite *MoveTestSuite) TestMoveAccountOK() {
}
func (suite *MoveTestSuite) TestMoveAccountNotAliased() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// Copy zork.
requestingAcct := new(gtsmodel.Account)
@@ -120,7 +120,7 @@ func (suite *MoveTestSuite) TestMoveAccountNotAliased() {
// not aliased back to zork.
err := suite.accountProcessor.MoveSelf(
ctx,
- &oauth.Auth{
+ &apiutil.Auth{
Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
Application: suite.testApplications["local_account_1"],
User: suite.testUsers["local_account_1"],
@@ -135,7 +135,7 @@ func (suite *MoveTestSuite) TestMoveAccountNotAliased() {
}
func (suite *MoveTestSuite) TestMoveAccountBadPassword() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// Copy zork.
requestingAcct := new(gtsmodel.Account)
@@ -150,7 +150,7 @@ func (suite *MoveTestSuite) TestMoveAccountBadPassword() {
// not aliased back to zork.
err := suite.accountProcessor.MoveSelf(
ctx,
- &oauth.Auth{
+ &apiutil.Auth{
Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
Application: suite.testApplications["local_account_1"],
User: suite.testUsers["local_account_1"],
diff --git a/internal/processing/account/mute.go b/internal/processing/account/mute.go
index 00bb9dd22..43dc45497 100644
--- a/internal/processing/account/mute.go
+++ b/internal/processing/account/mute.go
@@ -22,14 +22,14 @@ import (
"errors"
"time"
- 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/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/paging"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// MuteCreate handles the creation or updating of a mute from requestingAccount to targetAccountID.
diff --git a/internal/processing/account/note.go b/internal/processing/account/note.go
index 7606c1a91..231bb2ed8 100644
--- a/internal/processing/account/note.go
+++ b/internal/processing/account/note.go
@@ -20,10 +20,10 @@ package account
import (
"context"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
)
// PutNote updates the requesting account's private note on the target account.
diff --git a/internal/processing/account/relationships.go b/internal/processing/account/relationships.go
index 53d2ee3c7..6a82e67a4 100644
--- a/internal/processing/account/relationships.go
+++ b/internal/processing/account/relationships.go
@@ -21,11 +21,11 @@ import (
"context"
"errors"
- 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/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/paging"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
)
// FollowersGet fetches a list of the target account's followers.
diff --git a/internal/processing/account/rss.go b/internal/processing/account/rss.go
index 22ba0fe42..d6f367566 100644
--- a/internal/processing/account/rss.go
+++ b/internal/processing/account/rss.go
@@ -20,21 +20,19 @@ package account
import (
"context"
"errors"
- "fmt"
"time"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
"github.com/gorilla/feeds"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
-const (
- rssFeedLength = 20
-)
+var never time.Time
-type GetRSSFeed func() (string, gtserror.WithCode)
+type GetRSSFeed func() (*feeds.Feed, gtserror.WithCode)
// GetRSSFeedForUsername returns a function to return the RSS feed of a local account
// with the given username, and the last-modified time (time that the account last
@@ -45,33 +43,30 @@ type GetRSSFeed func() (string, gtserror.WithCode)
//
// If the account has not yet posted an RSS-eligible status, the returned last-modified
// time will be zero, and the GetRSSFeed func will return a valid RSS xml with no items.
-func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string) (GetRSSFeed, time.Time, gtserror.WithCode) {
- var (
- never = time.Time{}
- )
+func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string, page *paging.Page) (GetRSSFeed, time.Time, gtserror.WithCode) {
+ // Fetch local (i.e. empty domain) account from database by username.
account, err := p.state.DB.GetAccountByUsernameDomain(ctx, username, "")
if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- // Simply no account with this username.
- err = gtserror.New("account not found")
- return nil, never, gtserror.NewErrorNotFound(err)
- }
-
- // Real db error.
- err = gtserror.Newf("db error getting account %s: %w", username, err)
+ err := gtserror.Newf("db error getting account %s: %w", username, err)
return nil, never, gtserror.NewErrorInternalError(err)
}
+ // Check if exists.
+ if account == nil {
+ err := gtserror.New("account not found")
+ return nil, never, gtserror.NewErrorNotFound(err)
+ }
+
// Ensure account has rss feed enabled.
if !*account.Settings.EnableRSS {
- err = gtserror.New("account RSS feed not enabled")
+ err := gtserror.New("account RSS feed not enabled")
return nil, never, gtserror.NewErrorNotFound(err)
}
- // Ensure account stats populated.
+ // Ensure account stats populated for last status fetch information.
if err := p.state.DB.PopulateAccountStats(ctx, account); err != nil {
- err = gtserror.Newf("db error getting account stats %s: %w", username, err)
+ err := gtserror.Newf("db error getting account stats %s: %w", username, err)
return nil, never, gtserror.NewErrorInternalError(err)
}
@@ -80,17 +75,45 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
// eligible to appear in the RSS feed; that's fine.
lastPostAt := account.Stats.LastStatusAt
- return func() (string, gtserror.WithCode) {
- // Assemble author namestring once only.
- author := "@" + account.Username + "@" + config.GetAccountDomain()
+ return func() (*feeds.Feed, gtserror.WithCode) {
+ var image *feeds.Image
+
+ // Assemble author namestring.
+ author := "@" + account.Username +
+ "@" + config.GetAccountDomain()
- // Derive image/thumbnail for this account (may be nil).
- image, errWithCode := p.rssImageForAccount(ctx, account, author)
- if errWithCode != nil {
- return "", errWithCode
+ // Check if account has an avatar media attachment.
+ if id := account.AvatarMediaAttachmentID; id != "" {
+ if account.AvatarMediaAttachment == nil {
+ var err error
+
+ // Populate the account's avatar media attachment from database by its ID.
+ account.AvatarMediaAttachment, err = p.state.DB.GetAttachmentByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting account avatar: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ // If avatar is found, use as feed image.
+ if account.AvatarMediaAttachment != nil {
+ image = &feeds.Image{
+ Title: "Avatar for " + author,
+ Url: account.AvatarMediaAttachment.Thumbnail.URL,
+ Link: account.URL,
+ }
+ }
}
+ // Start creating feed.
feed := &feeds.Feed{
+ // we specifcally do not set the author, as a lot
+ // of feed readers rely on the RSS standard of the
+ // author being an email with optional name. but
+ // our @username@domain identifiers break this.
+ //
+ // attribution is handled in the title/description.
+
Title: "Posts from " + author,
Description: "Posts from " + author,
Link: &feeds.Link{Href: account.URL},
@@ -106,7 +129,7 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
// since we already know there's no eligible statuses.
if lastPostAt.IsZero() {
feed.Updated = account.CreatedAt
- return stringifyFeed(feed)
+ return feed, nil
}
// Account has posted at least one status that's
@@ -115,65 +138,50 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
// Reuse the lastPostAt value for feed.Updated.
feed.Updated = lastPostAt
- // Retrieve latest statuses as they'd be shown on the web view of the account profile.
- statuses, err := p.state.DB.GetAccountWebStatuses(ctx, account, rssFeedLength, "")
+ // Retrieve latest statuses as they'd be shown
+ // on the web view of the account profile.
+ //
+ // Take into account whether the user wants
+ // their web view laid out in gallery mode.
+ mediaOnly := (account.Settings != nil &&
+ account.Settings.WebLayout == gtsmodel.WebLayoutGallery)
+ statuses, err := p.state.DB.GetAccountWebStatuses(
+ ctx,
+ account,
+ page,
+ mediaOnly,
+ )
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = fmt.Errorf("db error getting account web statuses: %w", err)
- return "", gtserror.NewErrorInternalError(err)
+ err := gtserror.Newf("db error getting account web statuses: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check for no statuses.
+ if len(statuses) == 0 {
+ return feed, nil
}
+ // Get next / prev paging parameters.
+ lo := statuses[len(statuses)-1].ID
+ hi := statuses[0].ID
+ next := page.Next(lo, hi)
+ prev := page.Prev(lo, hi)
+
// Add each status to the rss feed.
for _, status := range statuses {
item, err := p.converter.StatusToRSSItem(ctx, status)
if err != nil {
- err = gtserror.Newf("error converting status to feed item: %w", err)
- return "", gtserror.NewErrorInternalError(err)
+ err := gtserror.Newf("error converting status to feed item: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
-
feed.Add(item)
}
- return stringifyFeed(feed)
- }, lastPostAt, nil
-}
-
-func (p *Processor) rssImageForAccount(ctx context.Context, account *gtsmodel.Account, author string) (*feeds.Image, gtserror.WithCode) {
- if account.AvatarMediaAttachmentID == "" {
- // No image, no problem!
- return nil, nil
- }
+ // TODO: when we have some manner of supporting
+ // atom:link in RSS (and Atom), set the paging
+ // parameters for next / prev feed pages here.
+ _, _ = next, prev
- // Ensure account avatar attachment populated.
- if account.AvatarMediaAttachment == nil {
- var err error
- account.AvatarMediaAttachment, err = p.state.DB.GetAttachmentByID(ctx, account.AvatarMediaAttachmentID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- // No attachment found with this ID (race condition?).
- return nil, nil
- }
-
- // Real db error.
- err = gtserror.Newf("db error fetching avatar media attachment: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
- }
-
- return &feeds.Image{
- Url: account.AvatarMediaAttachment.Thumbnail.URL,
- Title: "Avatar for " + author,
- Link: account.URL,
- }, nil
-}
-
-func stringifyFeed(feed *feeds.Feed) (string, gtserror.WithCode) {
- // Stringify the feed. Even with no statuses,
- // this will still produce valid rss xml.
- rss, err := feed.ToRss()
- if err != nil {
- err := gtserror.Newf("error converting feed to rss string: %w", err)
- return "", gtserror.NewErrorInternalError(err)
- }
-
- return rss, nil
+ return feed, nil
+ }, lastPostAt, nil
}
diff --git a/internal/processing/account/rss_test.go b/internal/processing/account/rss_test.go
index 5606151c2..75aa20891 100644
--- a/internal/processing/account/rss_test.go
+++ b/internal/processing/account/rss_test.go
@@ -18,9 +18,11 @@
package account_test
import (
- "context"
"testing"
+ "time"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
+ "github.com/gorilla/feeds"
"github.com/stretchr/testify/suite"
)
@@ -29,13 +31,8 @@ type GetRSSTestSuite struct {
}
func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
- getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(context.Background(), "admin")
- suite.NoError(err)
- suite.EqualValues(1634726497, lastModified.Unix())
-
- feed, err := getFeed()
- suite.NoError(err)
- suite.Equal(`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+ suite.testGetFeedSerializedAs("admin", &paging.Page{Limit: 20}, (*feeds.Feed).ToRss, 1634726497,
+ `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Posts from @admin@localhost:8080</title>
<link>http://localhost:8080/@admin</link>
@@ -43,11 +40,10 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
<pubDate>Wed, 20 Oct 2021 10:41:37 +0000</pubDate>
<lastBuildDate>Wed, 20 Oct 2021 10:41:37 +0000</lastBuildDate>
<item>
- <title>open to see some puppies</title>
+ <title>open to see some &lt;strong&gt;puppies&lt;/strong&gt;</title>
<link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>
<description>@admin@localhost:8080 made a new post: &#34;🐕🐕🐕🐕🐕&#34;</description>
- <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>
- <author>@admin@localhost:8080</author>
+ <content:encoded><![CDATA[<p>🐕🐕🐕🐕🐕</p>]]></content:encoded>
<guid isPermaLink="true">http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>
<pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>
<source>http://localhost:8080/@admin/feed.rss</source>
@@ -56,25 +52,158 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
<title>hello world! #welcome ! first post on the instance :rainbow: !</title>
<link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>
<description>@admin@localhost:8080 posted 1 attachment: &#34;hello world! #welcome ! first post on the instance :rainbow: !&#34;</description>
- <content:encoded><![CDATA[hello world! #welcome ! first post on the instance <img src="http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png" title=":rainbow:" alt=":rainbow:" width="25" height="25" /> !]]></content:encoded>
- <author>@admin@localhost:8080</author>
+ <content:encoded><![CDATA[<p>hello world! <a href="http://localhost:8080/tags/welcome" class="mention hashtag" rel="tag nofollow noreferrer noopener" target="_blank">#<span>welcome</span></a> ! first post on the instance <img src="http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png" title=":rainbow:" alt=":rainbow:" width="25" height="25" /> !</p>]]></content:encoded>
<enclosure url="http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg" length="62529" type="image/jpeg"></enclosure>
<guid isPermaLink="true">http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>
<pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>
<source>http://localhost:8080/@admin/feed.rss</source>
</item>
</channel>
-</rss>`, feed)
+</rss>`)
+}
+
+func (suite *GetRSSTestSuite) TestGetAccountAtomAdmin() {
+ suite.testGetFeedSerializedAs("admin", &paging.Page{Limit: 20}, (*feeds.Feed).ToAtom, 1634726497,
+ `<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
+ <title>Posts from @admin@localhost:8080</title>
+ <id>http://localhost:8080/@admin</id>
+ <updated>2021-10-20T10:41:37Z</updated>
+ <subtitle>Posts from @admin@localhost:8080</subtitle>
+ <link href="http://localhost:8080/@admin"></link>
+ <entry>
+ <title>open to see some &lt;strong&gt;puppies&lt;/strong&gt;</title>
+ <updated>2021-10-20T12:36:45Z</updated>
+ <id>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</id>
+ <content type="html">&lt;p&gt;🐕🐕🐕🐕🐕&lt;/p&gt;</content>
+ <link href="http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37" rel="alternate"></link>
+ <summary type="html">@admin@localhost:8080 made a new post: &#34;🐕🐕🐕🐕🐕&#34;</summary>
+ </entry>
+ <entry>
+ <title>hello world! #welcome ! first post on the instance :rainbow: !</title>
+ <updated>2021-10-20T11:36:45Z</updated>
+ <id>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</id>
+ <content type="html">&lt;p&gt;hello world! &lt;a href=&#34;http://localhost:8080/tags/welcome&#34; class=&#34;mention hashtag&#34; rel=&#34;tag nofollow noreferrer noopener&#34; target=&#34;_blank&#34;&gt;#&lt;span&gt;welcome&lt;/span&gt;&lt;/a&gt; ! first post on the instance &lt;img src=&#34;http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png&#34; title=&#34;:rainbow:&#34; alt=&#34;:rainbow:&#34; width=&#34;25&#34; height=&#34;25&#34; /&gt; !&lt;/p&gt;</content>
+ <link href="http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R" rel="alternate"></link>
+ <link href="http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg" rel="enclosure" type="image/jpeg" length="62529"></link>
+ <summary type="html">@admin@localhost:8080 posted 1 attachment: &#34;hello world! #welcome ! first post on the instance :rainbow: !&#34;</summary>
+ </entry>
+</feed>`)
+}
+
+func (suite *GetRSSTestSuite) TestGetAccountJSONAdmin() {
+ suite.testGetFeedSerializedAs("admin", &paging.Page{Limit: 20}, (*feeds.Feed).ToJSON, 1634726497,
+ `{
+ "version": "https://jsonfeed.org/version/1.1",
+ "title": "Posts from @admin@localhost:8080",
+ "home_page_url": "http://localhost:8080/@admin",
+ "description": "Posts from @admin@localhost:8080",
+ "items": [
+ {
+ "id": "http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37",
+ "url": "http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37",
+ "external_url": "http://localhost:8080/@admin/feed.rss",
+ "title": "open to see some \u003cstrong\u003epuppies\u003c/strong\u003e",
+ "content_html": "\u003cp\u003e🐕🐕🐕🐕🐕\u003c/p\u003e",
+ "summary": "@admin@localhost:8080 made a new post: \"🐕🐕🐕🐕🐕\"",
+ "date_published": "2021-10-20T12:36:45Z"
+ },
+ {
+ "id": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
+ "url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
+ "external_url": "http://localhost:8080/@admin/feed.rss",
+ "title": "hello world! #welcome ! first post on the instance :rainbow: !",
+ "content_html": "\u003cp\u003ehello world! \u003ca href=\"http://localhost:8080/tags/welcome\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\"\u003e#\u003cspan\u003ewelcome\u003c/span\u003e\u003c/a\u003e ! first post on the instance \u003cimg src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" width=\"25\" height=\"25\" /\u003e !\u003c/p\u003e",
+ "summary": "@admin@localhost:8080 posted 1 attachment: \"hello world! #welcome ! first post on the instance :rainbow: !\"",
+ "image": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
+ "date_published": "2021-10-20T11:36:45Z"
+ }
+ ]
+}`)
}
func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
- getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(context.Background(), "the_mighty_zork")
- suite.NoError(err)
- suite.EqualValues(1730451600, lastModified.Unix())
+ suite.testGetFeedSerializedAs("the_mighty_zork", &paging.Page{Limit: 20}, (*feeds.Feed).ToRss, 1730451600,
+ `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+ <channel>
+ <title>Posts from @the_mighty_zork@localhost:8080</title>
+ <link>http://localhost:8080/@the_mighty_zork</link>
+ <description>Posts from @the_mighty_zork@localhost:8080</description>
+ <pubDate>Fri, 01 Nov 2024 09:00:00 +0000</pubDate>
+ <lastBuildDate>Fri, 01 Nov 2024 09:00:00 +0000</lastBuildDate>
+ <image>
+ <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp</url>
+ <title>Avatar for @the_mighty_zork@localhost:8080</title>
+ <link>http://localhost:8080/@the_mighty_zork</link>
+ </image>
+ <item>
+ <title>edited status</title>
+ <link>http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR</link>
+ <description>@the_mighty_zork@localhost:8080 made a new post: &#34;this is the latest revision of the status, with a content-warning&#34;</description>
+ <content:encoded><![CDATA[<p>this is the latest revision of the status, with a content-warning</p>]]></content:encoded>
+ <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR</guid>
+ <pubDate>Fri, 01 Nov 2024 09:00:00 +0000</pubDate>
+ <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>
+ </item>
+ <item>
+ <title>HTML in post</title>
+ <link>http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</link>
+ <description>@the_mighty_zork@localhost:8080 made a new post: &#34;Here&#39;s a bunch of HTML, read it and weep, weep then!&#xA;&#xA;`+"```"+`html&#xA;&lt;section class=&#34;about-user&#34;&gt;&#xA; &lt;div class=&#34;col-header&#34;&gt;&#xA; &lt;h2&gt;About&lt;/h2&gt;&#xA; &lt;/div&gt; &#xA; &lt;div class=&#34;fields&#34;&gt;&#xA; &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;&#xA; &lt;dl&gt;&#xA;...</description>
+ <content:encoded><![CDATA[<p>Here's a bunch of HTML, read it and weep, weep then!</p><pre><code class="language-html">&lt;section class=&#34;about-user&#34;&gt;
+ &lt;div class=&#34;col-header&#34;&gt;
+ &lt;h2&gt;About&lt;/h2&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;fields&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;
+ &lt;dl&gt;
+ &lt;div class=&#34;field&#34;&gt;
+ &lt;dt&gt;should you follow me?&lt;/dt&gt;
+ &lt;dd&gt;maybe!&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;field&#34;&gt;
+ &lt;dt&gt;age&lt;/dt&gt;
+ &lt;dd&gt;120&lt;/dd&gt;
+ &lt;/div&gt;
+ &lt;/dl&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;bio&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;
+ &lt;p&gt;i post about things that concern me&lt;/p&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;
+ &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;
+ &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;
+ &lt;span&gt;8 posts.&lt;/span&gt;
+ &lt;span&gt;Followed by 1.&lt;/span&gt;
+ &lt;span&gt;Following 1.&lt;/span&gt;
+ &lt;/div&gt;
+ &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;
+ &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;
+ &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;
+ &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;
+ &lt;/div&gt;
+&lt;/section&gt;
+</code></pre><p>There, hope you liked that!</p>]]></content:encoded>
+ <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</guid>
+ <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate>
+ <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>
+ </item>
+ <item>
+ <title>introduction post</title>
+ <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>
+ <description>@the_mighty_zork@localhost:8080 made a new post: &#34;hello everyone!&#34;</description>
+ <content:encoded><![CDATA[<p>hello everyone!</p>]]></content:encoded>
+ <guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>
+ <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>
+ <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>
+ </item>
+ </channel>
+</rss>`)
+}
- feed, err := getFeed()
- suite.NoError(err)
- suite.Equal(`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+func (suite *GetRSSTestSuite) TestGetAccountAtomZork() {
+ suite.testGetFeedSerializedAs("the_mighty_zork", &paging.Page{Limit: 20}, (*feeds.Feed).ToRss, 1730451600,
+ `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Posts from @the_mighty_zork@localhost:8080</title>
<link>http://localhost:8080/@the_mighty_zork</link>
@@ -91,7 +220,6 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
<link>http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR</link>
<description>@the_mighty_zork@localhost:8080 made a new post: &#34;this is the latest revision of the status, with a content-warning&#34;</description>
<content:encoded><![CDATA[<p>this is the latest revision of the status, with a content-warning</p>]]></content:encoded>
- <author>@the_mighty_zork@localhost:8080</author>
<guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR</guid>
<pubDate>Fri, 01 Nov 2024 09:00:00 +0000</pubDate>
<source>http://localhost:8080/@the_mighty_zork/feed.rss</source>
@@ -136,7 +264,6 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
&lt;/div&gt;
&lt;/section&gt;
</code></pre><p>There, hope you liked that!</p>]]></content:encoded>
- <author>@the_mighty_zork@localhost:8080</author>
<guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</guid>
<pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate>
<source>http://localhost:8080/@the_mighty_zork/feed.rss</source>
@@ -145,18 +272,57 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
<title>introduction post</title>
<link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>
<description>@the_mighty_zork@localhost:8080 made a new post: &#34;hello everyone!&#34;</description>
- <content:encoded><![CDATA[hello everyone!]]></content:encoded>
- <author>@the_mighty_zork@localhost:8080</author>
+ <content:encoded><![CDATA[<p>hello everyone!</p>]]></content:encoded>
<guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>
<pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>
<source>http://localhost:8080/@the_mighty_zork/feed.rss</source>
</item>
</channel>
-</rss>`, feed)
+</rss>`)
+}
+
+func (suite *GetRSSTestSuite) TestGetAccountJSONZork() {
+ suite.testGetFeedSerializedAs("the_mighty_zork", &paging.Page{Limit: 20}, (*feeds.Feed).ToJSON, 1730451600,
+ `{
+ "version": "https://jsonfeed.org/version/1.1",
+ "title": "Posts from @the_mighty_zork@localhost:8080",
+ "home_page_url": "http://localhost:8080/@the_mighty_zork",
+ "description": "Posts from @the_mighty_zork@localhost:8080",
+ "items": [
+ {
+ "id": "http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR",
+ "url": "http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR",
+ "external_url": "http://localhost:8080/@the_mighty_zork/feed.rss",
+ "title": "edited status",
+ "content_html": "\u003cp\u003ethis is the latest revision of the status, with a content-warning\u003c/p\u003e",
+ "summary": "@the_mighty_zork@localhost:8080 made a new post: \"this is the latest revision of the status, with a content-warning\"",
+ "date_published": "2024-11-01T09:00:00Z",
+ "date_modified": "2024-11-01T09:02:00Z"
+ },
+ {
+ "id": "http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40",
+ "url": "http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40",
+ "external_url": "http://localhost:8080/@the_mighty_zork/feed.rss",
+ "title": "HTML in post",
+ "content_html": "\u003cp\u003eHere's a bunch of HTML, read it and weep, weep then!\u003c/p\u003e\u003cpre\u003e\u003ccode class=\"language-html\"\u003e\u0026lt;section class=\u0026#34;about-user\u0026#34;\u0026gt;\n \u0026lt;div class=\u0026#34;col-header\u0026#34;\u0026gt;\n \u0026lt;h2\u0026gt;About\u0026lt;/h2\u0026gt;\n \u0026lt;/div\u0026gt; \n \u0026lt;div class=\u0026#34;fields\u0026#34;\u0026gt;\n \u0026lt;h3 class=\u0026#34;sr-only\u0026#34;\u0026gt;Fields\u0026lt;/h3\u0026gt;\n \u0026lt;dl\u0026gt;\n \u0026lt;div class=\u0026#34;field\u0026#34;\u0026gt;\n \u0026lt;dt\u0026gt;should you follow me?\u0026lt;/dt\u0026gt;\n \u0026lt;dd\u0026gt;maybe!\u0026lt;/dd\u0026gt;\n \u0026lt;/div\u0026gt;\n \u0026lt;div class=\u0026#34;field\u0026#34;\u0026gt;\n \u0026lt;dt\u0026gt;age\u0026lt;/dt\u0026gt;\n \u0026lt;dd\u0026gt;120\u0026lt;/dd\u0026gt;\n \u0026lt;/div\u0026gt;\n \u0026lt;/dl\u0026gt;\n \u0026lt;/div\u0026gt;\n \u0026lt;div class=\u0026#34;bio\u0026#34;\u0026gt;\n \u0026lt;h3 class=\u0026#34;sr-only\u0026#34;\u0026gt;Bio\u0026lt;/h3\u0026gt;\n \u0026lt;p\u0026gt;i post about things that concern me\u0026lt;/p\u0026gt;\n \u0026lt;/div\u0026gt;\n \u0026lt;div class=\u0026#34;sr-only\u0026#34; role=\u0026#34;group\u0026#34;\u0026gt;\n \u0026lt;h3 class=\u0026#34;sr-only\u0026#34;\u0026gt;Stats\u0026lt;/h3\u0026gt;\n \u0026lt;span\u0026gt;Joined in Jun, 2022.\u0026lt;/span\u0026gt;\n \u0026lt;span\u0026gt;8 posts.\u0026lt;/span\u0026gt;\n \u0026lt;span\u0026gt;Followed by 1.\u0026lt;/span\u0026gt;\n \u0026lt;span\u0026gt;Following 1.\u0026lt;/span\u0026gt;\n \u0026lt;/div\u0026gt;\n \u0026lt;div class=\u0026#34;accountstats\u0026#34; aria-hidden=\u0026#34;true\u0026#34;\u0026gt;\n \u0026lt;b\u0026gt;Joined\u0026lt;/b\u0026gt;\u0026lt;time datetime=\u0026#34;2022-06-04T13:12:00.000Z\u0026#34;\u0026gt;Jun, 2022\u0026lt;/time\u0026gt;\n \u0026lt;b\u0026gt;Posts\u0026lt;/b\u0026gt;\u0026lt;span\u0026gt;8\u0026lt;/span\u0026gt;\n \u0026lt;b\u0026gt;Followed by\u0026lt;/b\u0026gt;\u0026lt;span\u0026gt;1\u0026lt;/span\u0026gt;\n \u0026lt;b\u0026gt;Following\u0026lt;/b\u0026gt;\u0026lt;span\u0026gt;1\u0026lt;/span\u0026gt;\n \u0026lt;/div\u0026gt;\n\u0026lt;/section\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003eThere, hope you liked that!\u003c/p\u003e",
+ "summary": "@the_mighty_zork@localhost:8080 made a new post: \"Here's a bunch of HTML, read it and weep, weep then!\n\n`+"```"+`html\n\u003csection class=\"about-user\"\u003e\n \u003cdiv class=\"col-header\"\u003e\n \u003ch2\u003eAbout\u003c/h2\u003e\n \u003c/div\u003e \n \u003cdiv class=\"fields\"\u003e\n \u003ch3 class=\"sr-only\"\u003eFields\u003c/h3\u003e\n \u003cdl\u003e\n...",
+ "date_published": "2023-12-10T09:24:00Z"
+ },
+ {
+ "id": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
+ "url": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
+ "external_url": "http://localhost:8080/@the_mighty_zork/feed.rss",
+ "title": "introduction post",
+ "content_html": "\u003cp\u003ehello everyone!\u003c/p\u003e",
+ "summary": "@the_mighty_zork@localhost:8080 made a new post: \"hello everyone!\"",
+ "date_published": "2021-10-20T10:40:37Z"
+ }
+ ]
+}`)
}
func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// Get all of zork's posts.
statuses, err := suite.db.GetAccountStatuses(ctx, suite.testAccounts["local_account_1"].ID, 0, false, false, "", "", false, false)
@@ -171,13 +337,10 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() {
}
}
- getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(ctx, "the_mighty_zork")
- suite.NoError(err)
- suite.Empty(lastModified)
+ var zeroTime time.Time
- feed, err := getFeed()
- suite.NoError(err)
- suite.Equal(`<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+ suite.testGetFeedSerializedAs("the_mighty_zork", &paging.Page{Limit: 20}, (*feeds.Feed).ToRss, zeroTime.Unix(),
+ `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Posts from @the_mighty_zork@localhost:8080</title>
<link>http://localhost:8080/@the_mighty_zork</link>
@@ -190,7 +353,33 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() {
<link>http://localhost:8080/@the_mighty_zork</link>
</image>
</channel>
-</rss>`, feed)
+</rss>`)
+}
+
+// func (suite *GetRSSTestSuite) testGetAccountRSSPaging(username string, page *paging.Page, expectIDs []string) {
+// ctx := suite.T().Context()
+
+// getFeed, _, errWithCode := suite.accountProcessor.GetRSSFeedForUsername(ctx, username, page)
+// suite.NoError(errWithCode)
+
+// feed, errWithCode := getFeed()
+// suite.NoError(errWithCode)
+
+// }
+
+func (suite *GetRSSTestSuite) testGetFeedSerializedAs(username string, page *paging.Page, serialize func(*feeds.Feed) (string, error), expectLastMod int64, expectSerialized string) {
+ ctx := suite.T().Context()
+
+ getFeed, lastMod, errWithCode := suite.accountProcessor.GetRSSFeedForUsername(ctx, username, page)
+ suite.NoError(errWithCode)
+ suite.Equal(expectLastMod, lastMod.Unix())
+
+ feed, errWithCode := getFeed()
+ suite.NoError(errWithCode)
+
+ feedStr, err := serialize(feed)
+ suite.NoError(err)
+ suite.Equal(expectSerialized, feedStr)
}
func TestGetRSSTestSuite(t *testing.T) {
diff --git a/internal/processing/account/statuses.go b/internal/processing/account/statuses.go
index 8029a460b..870019f41 100644
--- a/internal/processing/account/statuses.go
+++ b/internal/processing/account/statuses.go
@@ -22,13 +22,13 @@ import (
"errors"
"fmt"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// StatusesGet fetches a number of statuses (in time descending order) from the
@@ -97,19 +97,33 @@ func (p *Processor) StatusesGet(
return nil, gtserror.NewErrorInternalError(err)
}
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
+ for _, status := range filtered {
+ // ...
+ filtered, hide, err := p.statusFilter.StatusFilterResultsInContext(ctx,
+ requestingAccount,
+ status,
+ gtsmodel.FilterContextAccount,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error filtering status: %v", err)
+ continue
+ }
+
+ if hide {
+ // Don't show.
+ continue
+ }
- for _, s := range filtered {
// Convert filtered statuses to API statuses.
- item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextAccount, filters, nil)
+ item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount)
if err != nil {
log.Errorf(ctx, "error convering to api status: %v", err)
continue
}
+
+ // Set any filter results.
+ item.Filtered = filtered
+
items = append(items, item)
}
@@ -143,7 +157,8 @@ func (p *Processor) StatusesGet(
func (p *Processor) WebStatusesGet(
ctx context.Context,
targetAccountID string,
- maxID string,
+ page *paging.Page,
+ mediaOnly bool,
) (*apimodel.PageableResponse, gtserror.WithCode) {
account, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
if err != nil {
@@ -159,8 +174,13 @@ func (p *Processor) WebStatusesGet(
return nil, gtserror.NewErrorNotFound(err)
}
- statuses, err := p.state.DB.GetAccountWebStatuses(ctx, account, 10, maxID)
+ statuses, err := p.state.DB.GetAccountWebStatuses(ctx,
+ account,
+ page,
+ mediaOnly,
+ )
if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting statuses: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -198,6 +218,7 @@ func (p *Processor) WebStatusesGet(
func (p *Processor) WebStatusesGetPinned(
ctx context.Context,
targetAccountID string,
+ mediaOnly bool,
) ([]*apimodel.WebStatus, gtserror.WithCode) {
statuses, err := p.state.DB.GetAccountPinnedStatuses(ctx, targetAccountID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
@@ -206,6 +227,11 @@ func (p *Processor) WebStatusesGetPinned(
webStatuses := make([]*apimodel.WebStatus, 0, len(statuses))
for _, status := range statuses {
+ if mediaOnly && len(status.Attachments) == 0 {
+ // No media, skip.
+ continue
+ }
+
// Ensure visible via the web.
visible, err := p.visFilter.StatusVisible(ctx, nil, status)
if err != nil {
diff --git a/internal/processing/account/themes.go b/internal/processing/account/themes.go
index 4f8cc49a1..88fd3ff6c 100644
--- a/internal/processing/account/themes.go
+++ b/internal/processing/account/themes.go
@@ -25,11 +25,11 @@ import (
"slices"
"strings"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
"codeberg.org/gruf/go-bytesize"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
)
var (
diff --git a/internal/processing/account/themes_test.go b/internal/processing/account/themes_test.go
index 9506aee50..fff129f68 100644
--- a/internal/processing/account/themes_test.go
+++ b/internal/processing/account/themes_test.go
@@ -20,9 +20,9 @@ package account_test
import (
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/account"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/processing/account"
)
type ThemesTestSuite struct {
diff --git a/internal/processing/account/tokens.go b/internal/processing/account/tokens.go
new file mode 100644
index 000000000..eaeffe38b
--- /dev/null
+++ b/internal/processing/account/tokens.go
@@ -0,0 +1,122 @@
+// 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 account
+
+import (
+ "context"
+ "errors"
+
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
+)
+
+func (p *Processor) TokensGet(
+ ctx context.Context,
+ userID string,
+ page *paging.Page,
+) (*apimodel.PageableResponse, gtserror.WithCode) {
+ tokens, err := p.state.DB.GetAccessTokens(ctx, userID, page)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting tokens: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ count := len(tokens)
+ if count == 0 {
+ return paging.EmptyResponse(), nil
+ }
+
+ var (
+ // Get the lowest and highest
+ // ID values, used for paging.
+ lo = tokens[count-1].ID
+ hi = tokens[0].ID
+
+ // Best-guess items length.
+ items = make([]interface{}, 0, count)
+ )
+
+ for _, token := range tokens {
+ tokenInfo, err := p.converter.TokenToAPITokenInfo(ctx, token)
+ if err != nil {
+ log.Errorf(ctx, "error converting token to api token info: %v", err)
+ continue
+ }
+
+ // Append req to return items.
+ items = append(items, tokenInfo)
+ }
+
+ return paging.PackageResponse(paging.ResponseParams{
+ Items: items,
+ Path: "/api/v1/tokens",
+ Next: page.Next(lo, hi),
+ Prev: page.Prev(lo, hi),
+ }), nil
+}
+
+func (p *Processor) TokenGet(
+ ctx context.Context,
+ userID string,
+ tokenID string,
+) (*apimodel.TokenInfo, gtserror.WithCode) {
+ token, err := p.state.DB.GetTokenByID(ctx, tokenID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting token %s: %w", tokenID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if token == nil {
+ err := gtserror.Newf("token %s not found in the db", tokenID)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ if token.UserID != userID {
+ err := gtserror.Newf("token %s does not belong to user %s", tokenID, userID)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ tokenInfo, err := p.converter.TokenToAPITokenInfo(ctx, token)
+ if err != nil {
+ err := gtserror.Newf("error converting token to api token info: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return tokenInfo, nil
+}
+
+func (p *Processor) TokenInvalidate(
+ ctx context.Context,
+ userID string,
+ tokenID string,
+) (*apimodel.TokenInfo, gtserror.WithCode) {
+ tokenInfo, errWithCode := p.TokenGet(ctx, userID, tokenID)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ if err := p.state.DB.DeleteTokenByID(ctx, tokenID); err != nil {
+ err := gtserror.Newf("db error deleting token %s: %w", tokenID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return tokenInfo, nil
+}
diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go
index 2bdbf96f4..99dd074a5 100644
--- a/internal/processing/account/update.go
+++ b/internal/processing/account/update.go
@@ -24,19 +24,19 @@ import (
"io"
"mime/multipart"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/text"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/internal/validate"
"codeberg.org/gruf/go-iotools"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/text"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/util"
- "github.com/superseriousbusiness/gotosocial/internal/validate"
)
func (p *Processor) selectNoteFormatter(contentType string) text.FormatFunc {
@@ -77,9 +77,17 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
acctColumns = append(acctColumns, "discoverable")
}
- if form.Bot != nil {
- account.Bot = form.Bot
- acctColumns = append(acctColumns, "bot")
+ if bot := form.Bot; bot != nil {
+ if *bot {
+ // Mark account as an Application.
+ // See: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application
+ account.ActorType = gtsmodel.AccountActorTypeApplication
+ } else {
+ // Mark account as a Person.
+ // See: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
+ account.ActorType = gtsmodel.AccountActorTypePerson
+ }
+ acctColumns = append(acctColumns, "actor_type")
}
if form.Locked != nil {
@@ -97,8 +105,8 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- // Parse new display name (always from plaintext).
- account.DisplayName = text.SanitizeToPlaintext(displayName)
+ // HTML tags not allowed in display name.
+ account.DisplayName = text.StripHTMLFromText(displayName)
acctColumns = append(acctColumns, "display_name")
}
@@ -145,7 +153,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
}
if form.AvatarDescription != nil {
- desc := text.SanitizeToPlaintext(*form.AvatarDescription)
+ desc := text.StripHTMLFromText(*form.AvatarDescription)
form.AvatarDescription = &desc
}
@@ -175,7 +183,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
}
if form.HeaderDescription != nil {
- desc := text.SanitizeToPlaintext(*form.HeaderDescription)
+ desc := text.StripHTMLFromText(*form.HeaderDescription)
form.HeaderDescription = util.Ptr(desc)
}
@@ -204,6 +212,37 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
}
}
+ if form.WebVisibility != nil {
+ switch apimodel.Visibility(*form.WebVisibility) {
+
+ // Show none.
+ case apimodel.VisibilityNone:
+ account.HidesToPublicFromUnauthedWeb = util.Ptr(true)
+ account.HidesCcPublicFromUnauthedWeb = util.Ptr(true)
+
+ // Show public only (GtS default).
+ case apimodel.VisibilityPublic:
+ account.HidesToPublicFromUnauthedWeb = util.Ptr(false)
+ account.HidesCcPublicFromUnauthedWeb = util.Ptr(true)
+
+ // Show public and unlisted (Masto default).
+ case apimodel.VisibilityUnlisted:
+ account.HidesToPublicFromUnauthedWeb = util.Ptr(false)
+ account.HidesCcPublicFromUnauthedWeb = util.Ptr(false)
+
+ default:
+ const text = "web_visibility must be one of public, unlisted, or none"
+ err := errors.New(text)
+ return nil, gtserror.NewErrorBadRequest(err, text)
+ }
+
+ acctColumns = append(
+ acctColumns,
+ "hides_to_public_from_unauthed_web",
+ "hides_cc_public_from_unauthed_web",
+ )
+ }
+
// Account settings flags.
if form.Source != nil {
@@ -265,7 +304,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- account.Settings.CustomCSS = text.SanitizeToPlaintext(customCSS)
+ account.Settings.CustomCSS = text.StripHTMLFromText(customCSS)
settingsColumns = append(settingsColumns, "custom_css")
}
@@ -279,19 +318,16 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
settingsColumns = append(settingsColumns, "hide_collections")
}
- if form.WebVisibility != nil {
- apiVis := apimodel.Visibility(*form.WebVisibility)
- webVisibility := typeutils.APIVisToVis(apiVis)
- if webVisibility != gtsmodel.VisibilityPublic &&
- webVisibility != gtsmodel.VisibilityUnlocked &&
- webVisibility != gtsmodel.VisibilityNone {
- const text = "web_visibility must be one of public, unlocked, or none"
+ if form.WebLayout != nil {
+ webLayout := gtsmodel.ParseWebLayout(*form.WebLayout)
+ if webLayout == gtsmodel.WebLayoutUnknown {
+ const text = "web_layout must be one of microblog or gallery"
err := errors.New(text)
return nil, gtserror.NewErrorBadRequest(err, text)
}
- account.Settings.WebVisibility = webVisibility
- settingsColumns = append(settingsColumns, "web_visibility")
+ account.Settings.WebLayout = webLayout
+ settingsColumns = append(settingsColumns, "web_layout")
}
// We've parsed + set everything, do
@@ -356,8 +392,8 @@ func (p *Processor) updateFields(
// Sanitize raw field values.
fieldRaw := &gtsmodel.Field{
- Name: text.SanitizeToPlaintext(name),
- Value: text.SanitizeToPlaintext(value),
+ Name: text.StripHTMLFromText(name),
+ Value: text.StripHTMLFromText(value),
}
fieldsRaw = append(fieldsRaw, fieldRaw)
}
@@ -385,7 +421,7 @@ func (p *Processor) processAccountText(
emojis := make(map[string]*gtsmodel.Emoji)
// Retrieve display name emojis.
- for _, emoji := range p.formatter.FromPlainEmojiOnly(
+ for _, emoji := range p.formatter.FromPlainBasic(
ctx,
p.parseMention,
account.ID,
@@ -413,7 +449,7 @@ func (p *Processor) processAccountText(
// Name stays plain, but we still need to
// see if there are any emojis set in it.
field.Name = fieldRaw.Name
- for _, emoji := range p.formatter.FromPlainEmojiOnly(
+ for _, emoji := range p.formatter.FromPlainBasic(
ctx,
p.parseMention,
account.ID,
diff --git a/internal/processing/account/update_test.go b/internal/processing/account/update_test.go
index a07562544..b6e315d41 100644
--- a/internal/processing/account/update_test.go
+++ b/internal/processing/account/update_test.go
@@ -18,14 +18,14 @@
package account_test
import (
- "context"
"testing"
"time"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
type AccountUpdateTestSuite struct {
@@ -37,7 +37,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateSimple() {
*testAccount = *suite.testAccounts["local_account_1"]
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
locked = true
displayName = "new display name"
note = "#hello here i am!"
@@ -87,7 +87,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMention() {
*testAccount = *suite.testAccounts["local_account_1"]
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
locked = true
displayName = "new display name"
note = "#hello here i am!\n\ngo check out @1happyturtle, they have a cool account!"
@@ -143,7 +143,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() {
testAccount.Settings = settings
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
note = "*hello* ~~here~~ i am!"
noteExpected = `<p><em>hello</em> <del>here</del> i am!</p>`
)
@@ -192,7 +192,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithFields() {
*testAccount = *suite.testAccounts["local_account_1"]
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
updateFields = []apimodel.UpdateField{
{
Name: func() *string { s := "favourite emoji"; return &s }(),
@@ -286,7 +286,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateNoteNotFields() {
*testAccount = *suite.testAccounts["local_account_2"]
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
fieldsRawBefore = len(testAccount.FieldsRaw)
fieldsBefore = len(testAccount.Fields)
note = "#hello here i am!"
@@ -331,6 +331,64 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateNoteNotFields() {
suite.Equal(fieldsBefore, len(dbAccount.Fields))
}
+func (suite *AccountUpdateTestSuite) TestAccountUpdateBotNotBot() {
+ testAccount := &gtsmodel.Account{}
+ *testAccount = *suite.testAccounts["local_account_1"]
+ ctx := suite.T().Context()
+
+ // Call update function to set bot = true.
+ apiAccount, errWithCode := suite.accountProcessor.Update(
+ ctx,
+ testAccount,
+ &apimodel.UpdateCredentialsRequest{
+ Bot: util.Ptr(true),
+ },
+ )
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+
+ // Returned profile should be updated.
+ suite.True(apiAccount.Bot)
+
+ // We should have an update in the client api channel.
+ msg, _ := suite.getClientMsg(5 * time.Second)
+ suite.NotNil(msg)
+
+ // Check database model of account as well.
+ dbAccount, err := suite.db.GetAccountByID(ctx, testAccount.ID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.True(dbAccount.ActorType.IsBot())
+
+ // Call update function to set bot = false.
+ apiAccount, errWithCode = suite.accountProcessor.Update(
+ ctx,
+ testAccount,
+ &apimodel.UpdateCredentialsRequest{
+ Bot: util.Ptr(false),
+ },
+ )
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+
+ // Returned profile should be updated.
+ suite.False(apiAccount.Bot)
+
+ // We should have an update in the client api channel.
+ msg, _ = suite.getClientMsg(5 * time.Second)
+ suite.NotNil(msg)
+
+ // Check database model of account as well.
+ dbAccount, err = suite.db.GetAccountByID(ctx, testAccount.ID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.False(dbAccount.ActorType.IsBot())
+}
+
func TestAccountUpdateTestSuite(t *testing.T) {
suite.Run(t, new(AccountUpdateTestSuite))
}