summaryrefslogtreecommitdiff
path: root/internal/processing
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing')
-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
-rw-r--r--internal/processing/admin/account_test.go11
-rw-r--r--internal/processing/admin/accountaction.go12
-rw-r--r--internal/processing/admin/accountget.go6
-rw-r--r--internal/processing/admin/accounts.go12
-rw-r--r--internal/processing/admin/admin.go18
-rw-r--r--internal/processing/admin/admin_test.go52
-rw-r--r--internal/processing/admin/debug_apurl.go10
-rw-r--r--internal/processing/admin/domainallow.go68
-rw-r--r--internal/processing/admin/domainblock.go66
-rw-r--r--internal/processing/admin/domainkeysexpire.go6
-rw-r--r--internal/processing/admin/domainpermission.go191
-rw-r--r--internal/processing/admin/domainpermission_test.go22
-rw-r--r--internal/processing/admin/domainpermissiondraft.go20
-rw-r--r--internal/processing/admin/domainpermissionexclude.go14
-rw-r--r--internal/processing/admin/domainpermissionsubscription.go27
-rw-r--r--internal/processing/admin/email.go8
-rw-r--r--internal/processing/admin/emoji.go24
-rw-r--r--internal/processing/admin/emoji_test.go9
-rw-r--r--internal/processing/admin/headerfilter.go14
-rw-r--r--internal/processing/admin/media.go16
-rw-r--r--internal/processing/admin/report.go16
-rw-r--r--internal/processing/admin/rule.go23
-rw-r--r--internal/processing/admin/signupapprove.go12
-rw-r--r--internal/processing/admin/signupapprove_test.go7
-rw-r--r--internal/processing/admin/signupreject.go12
-rw-r--r--internal/processing/admin/signupreject_test.go13
-rw-r--r--internal/processing/admin/util.go6
-rw-r--r--internal/processing/admin/workertask.go12
-rw-r--r--internal/processing/admin/workertask_test.go18
-rw-r--r--internal/processing/advancedmigrations/advancedmigrations.go2
-rw-r--r--internal/processing/app.go88
-rw-r--r--internal/processing/application/application.go (renamed from internal/processing/filters/v2/convert.go)30
-rw-r--r--internal/processing/application/create.go113
-rw-r--r--internal/processing/application/delete.go76
-rw-r--r--internal/processing/application/get.go104
-rw-r--r--internal/processing/common/account.go10
-rw-r--r--internal/processing/common/common.go38
-rw-r--r--internal/processing/common/media.go8
-rw-r--r--internal/processing/common/status.go83
-rw-r--r--internal/processing/conversations/conversations.go67
-rw-r--r--internal/processing/conversations/conversations_test.go56
-rw-r--r--internal/processing/conversations/delete.go6
-rw-r--r--internal/processing/conversations/delete_test.go4
-rw-r--r--internal/processing/conversations/get.go45
-rw-r--r--internal/processing/conversations/get_test.go9
-rw-r--r--internal/processing/conversations/migrate.go12
-rw-r--r--internal/processing/conversations/migrate_test.go10
-rw-r--r--internal/processing/conversations/read.go28
-rw-r--r--internal/processing/conversations/read_test.go6
-rw-r--r--internal/processing/conversations/update.go170
-rw-r--r--internal/processing/conversations/update_test.go6
-rw-r--r--internal/processing/fedi/accept.go85
-rw-r--r--internal/processing/fedi/authorization.go142
-rw-r--r--internal/processing/fedi/collections.go101
-rw-r--r--internal/processing/fedi/common.go63
-rw-r--r--internal/processing/fedi/emoji.go61
-rw-r--r--internal/processing/fedi/fedi.go10
-rw-r--r--internal/processing/fedi/status.go50
-rw-r--r--internal/processing/fedi/user.go129
-rw-r--r--internal/processing/fedi/wellknown.go17
-rw-r--r--internal/processing/filters/common/common.go212
-rw-r--r--internal/processing/filters/v1/convert.go38
-rw-r--r--internal/processing/filters/v1/create.go94
-rw-r--r--internal/processing/filters/v1/delete.go58
-rw-r--r--internal/processing/filters/v1/filters.go15
-rw-r--r--internal/processing/filters/v1/get.go69
-rw-r--r--internal/processing/filters/v1/update.go197
-rw-r--r--internal/processing/filters/v2/create.go218
-rw-r--r--internal/processing/filters/v2/delete.go33
-rw-r--r--internal/processing/filters/v2/filters.go15
-rw-r--r--internal/processing/filters/v2/get.go59
-rw-r--r--internal/processing/filters/v2/keywordcreate.go67
-rw-r--r--internal/processing/filters/v2/keyworddelete.go41
-rw-r--r--internal/processing/filters/v2/keywordget.go76
-rw-r--r--internal/processing/filters/v2/keywordupdate.go55
-rw-r--r--internal/processing/filters/v2/statuscreate.go73
-rw-r--r--internal/processing/filters/v2/statusdelete.go41
-rw-r--r--internal/processing/filters/v2/statusget.go76
-rw-r--r--internal/processing/filters/v2/update.go453
-rw-r--r--internal/processing/followrequest_test.go15
-rw-r--r--internal/processing/instance.go171
-rw-r--r--internal/processing/interactionrequests/accept.go28
-rw-r--r--internal/processing/interactionrequests/accept_test.go11
-rw-r--r--internal/processing/interactionrequests/get.go14
-rw-r--r--internal/processing/interactionrequests/interactionrequests.go6
-rw-r--r--internal/processing/interactionrequests/interactionrequests_test.go4
-rw-r--r--internal/processing/interactionrequests/reject.go16
-rw-r--r--internal/processing/interactionrequests/reject_test.go11
-rw-r--r--internal/processing/list/create.go10
-rw-r--r--internal/processing/list/delete.go6
-rw-r--r--internal/processing/list/get.go14
-rw-r--r--internal/processing/list/list.go4
-rw-r--r--internal/processing/list/update.go10
-rw-r--r--internal/processing/list/updateentries.go12
-rw-r--r--internal/processing/list/util.go8
-rw-r--r--internal/processing/markers/get.go10
-rw-r--r--internal/processing/markers/markers.go4
-rw-r--r--internal/processing/markers/update.go8
-rw-r--r--internal/processing/media/create.go22
-rw-r--r--internal/processing/media/delete.go6
-rw-r--r--internal/processing/media/getemoji.go8
-rw-r--r--internal/processing/media/getemoji_test.go3
-rw-r--r--internal/processing/media/getfile.go260
-rw-r--r--internal/processing/media/getfile_test.go19
-rw-r--r--internal/processing/media/getmedia.go15
-rw-r--r--internal/processing/media/media.go12
-rw-r--r--internal/processing/media/media_test.go34
-rw-r--r--internal/processing/media/profile.go6
-rw-r--r--internal/processing/media/unattach.go15
-rw-r--r--internal/processing/media/unattach_test.go3
-rw-r--r--internal/processing/media/update.go23
-rw-r--r--internal/processing/oauth.go19
-rw-r--r--internal/processing/parsemention.go44
-rw-r--r--internal/processing/parsemention_test.go9
-rw-r--r--internal/processing/polls/expiry.go12
-rw-r--r--internal/processing/polls/get.go6
-rw-r--r--internal/processing/polls/poll.go12
-rw-r--r--internal/processing/polls/poll_test.go43
-rw-r--r--internal/processing/polls/vote.go14
-rw-r--r--internal/processing/preferences.go6
-rw-r--r--internal/processing/preferences_test.go7
-rw-r--r--internal/processing/processor.go102
-rw-r--r--internal/processing/processor_test.go55
-rw-r--r--internal/processing/push/create.go10
-rw-r--r--internal/processing/push/delete.go2
-rw-r--r--internal/processing/push/get.go6
-rw-r--r--internal/processing/push/push.go10
-rw-r--r--internal/processing/push/update.go8
-rw-r--r--internal/processing/report/create.go16
-rw-r--r--internal/processing/report/get.go12
-rw-r--r--internal/processing/report/report.go4
-rw-r--r--internal/processing/search/accounts.go16
-rw-r--r--internal/processing/search/get.go61
-rw-r--r--internal/processing/search/lookup.go12
-rw-r--r--internal/processing/search/search.go8
-rw-r--r--internal/processing/search/util.go59
-rw-r--r--internal/processing/status/bookmark.go12
-rw-r--r--internal/processing/status/bookmark_test.go5
-rw-r--r--internal/processing/status/boost.go37
-rw-r--r--internal/processing/status/boost_test.go3
-rw-r--r--internal/processing/status/common.go93
-rw-r--r--internal/processing/status/context.go54
-rw-r--r--internal/processing/status/context_test.go4
-rw-r--r--internal/processing/status/create.go299
-rw-r--r--internal/processing/status/create_test.go124
-rw-r--r--internal/processing/status/delete.go17
-rw-r--r--internal/processing/status/edit.go45
-rw-r--r--internal/processing/status/edit_test.go161
-rw-r--r--internal/processing/status/fave.go45
-rw-r--r--internal/processing/status/get.go21
-rw-r--r--internal/processing/status/mute.go10
-rw-r--r--internal/processing/status/pin.go6
-rw-r--r--internal/processing/status/scheduledstatus.go357
-rw-r--r--internal/processing/status/scheduledstatus_test.go69
-rw-r--r--internal/processing/status/status.go20
-rw-r--r--internal/processing/status/status_test.go65
-rw-r--r--internal/processing/status/util.go8
-rw-r--r--internal/processing/stream/authorize.go27
-rw-r--r--internal/processing/stream/authorize_test.go9
-rw-r--r--internal/processing/stream/conversation.go6
-rw-r--r--internal/processing/stream/delete.go2
-rw-r--r--internal/processing/stream/filterschanged.go4
-rw-r--r--internal/processing/stream/notification.go8
-rw-r--r--internal/processing/stream/notification_test.go13
-rw-r--r--internal/processing/stream/open.go10
-rw-r--r--internal/processing/stream/open_test.go3
-rw-r--r--internal/processing/stream/statusupdate.go8
-rw-r--r--internal/processing/stream/statusupdate_test.go31
-rw-r--r--internal/processing/stream/stream.go6
-rw-r--r--internal/processing/stream/stream_test.go16
-rw-r--r--internal/processing/stream/update.go8
-rw-r--r--internal/processing/tags/follow.go15
-rw-r--r--internal/processing/tags/followed.go20
-rw-r--r--internal/processing/tags/followedtags.go22
-rw-r--r--internal/processing/tags/get.go12
-rw-r--r--internal/processing/tags/unfollow.go13
-rw-r--r--internal/processing/timeline/common.go71
-rw-r--r--internal/processing/timeline/faved.go18
-rw-r--r--internal/processing/timeline/home.go211
-rw-r--r--internal/processing/timeline/home_test.go89
-rw-r--r--internal/processing/timeline/list.go234
-rw-r--r--internal/processing/timeline/notification.go146
-rw-r--r--internal/processing/timeline/public.go272
-rw-r--r--internal/processing/timeline/public_test.go90
-rw-r--r--internal/processing/timeline/tag.go168
-rw-r--r--internal/processing/timeline/timeline.go150
-rw-r--r--internal/processing/timeline/timeline_test.go20
-rw-r--r--internal/processing/user/create.go18
-rw-r--r--internal/processing/user/create_test.go5
-rw-r--r--internal/processing/user/delete.go8
-rw-r--r--internal/processing/user/email.go20
-rw-r--r--internal/processing/user/email_test.go5
-rw-r--r--internal/processing/user/get.go6
-rw-r--r--internal/processing/user/password.go14
-rw-r--r--internal/processing/user/password_test.go36
-rw-r--r--internal/processing/user/twofactor.go285
-rw-r--r--internal/processing/user/user.go8
-rw-r--r--internal/processing/user/user_test.go22
-rw-r--r--internal/processing/workers/federate.go36
-rw-r--r--internal/processing/workers/fromclientapi.go319
-rw-r--r--internal/processing/workers/fromclientapi_test.go293
-rw-r--r--internal/processing/workers/fromfediapi.go752
-rw-r--r--internal/processing/workers/fromfediapi_move.go24
-rw-r--r--internal/processing/workers/fromfediapi_test.go252
-rw-r--r--internal/processing/workers/surface.go18
-rw-r--r--internal/processing/workers/surfaceemail.go12
-rw-r--r--internal/processing/workers/surfacenotify.go302
-rw-r--r--internal/processing/workers/surfacenotify_test.go18
-rw-r--r--internal/processing/workers/surfacetimeline.go437
-rw-r--r--internal/processing/workers/util.go308
-rw-r--r--internal/processing/workers/workers.go30
-rw-r--r--internal/processing/workers/workers_test.go16
241 files changed, 8695 insertions, 5292 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))
}
diff --git a/internal/processing/admin/account_test.go b/internal/processing/admin/account_test.go
index baa6eb646..34eb1d2fd 100644
--- a/internal/processing/admin/account_test.go
+++ b/internal/processing/admin/account_test.go
@@ -18,13 +18,12 @@
package admin_test
import (
- "context"
"testing"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type AccountTestSuite struct {
@@ -33,7 +32,7 @@ type AccountTestSuite struct {
func (suite *AccountTestSuite) TestAccountActionSuspend() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
adminAcct = suite.testAccounts["admin_account"]
request = &apimodel.AdminActionRequest{
Category: gtsmodel.AdminActionCategoryAccount.String(),
@@ -79,7 +78,7 @@ func (suite *AccountTestSuite) TestAccountActionSuspend() {
func (suite *AccountTestSuite) TestAccountActionUnsupported() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
adminAcct = suite.testAccounts["admin_account"]
request = &apimodel.AdminActionRequest{
Category: gtsmodel.AdminActionCategoryAccount.String(),
diff --git a/internal/processing/admin/accountaction.go b/internal/processing/admin/accountaction.go
index 959f2cfcd..19b04a145 100644
--- a/internal/processing/admin/accountaction.go
+++ b/internal/processing/admin/accountaction.go
@@ -21,12 +21,12 @@ import (
"context"
"fmt"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- 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"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ 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"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
)
func (p *Processor) AccountAction(
diff --git a/internal/processing/admin/accountget.go b/internal/processing/admin/accountget.go
index 5a3c34c62..06a47cedc 100644
--- a/internal/processing/admin/accountget.go
+++ b/internal/processing/admin/accountget.go
@@ -22,9 +22,9 @@ import (
"errors"
"fmt"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
)
func (p *Processor) AccountGet(ctx context.Context, accountID string) (*apimodel.AdminAccountInfo, gtserror.WithCode) {
diff --git a/internal/processing/admin/accounts.go b/internal/processing/admin/accounts.go
index ba2a88ce6..2be71ddc3 100644
--- a/internal/processing/admin/accounts.go
+++ b/internal/processing/admin/accounts.go
@@ -25,12 +25,12 @@ import (
"net/url"
"slices"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/paging"
+ 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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
)
var (
diff --git a/internal/processing/admin/admin.go b/internal/processing/admin/admin.go
index 08e6bf0d5..7ad2450de 100644
--- a/internal/processing/admin/admin.go
+++ b/internal/processing/admin/admin.go
@@ -18,15 +18,15 @@
package admin
import (
- "github.com/superseriousbusiness/gotosocial/internal/cleaner"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/subscriptions"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/cleaner"
+ "code.superseriousbusiness.org/gotosocial/internal/email"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/subscriptions"
+ "code.superseriousbusiness.org/gotosocial/internal/transport"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/admin/admin_test.go b/internal/processing/admin/admin_test.go
index ad9d9b2ae..857afcae1 100644
--- a/internal/processing/admin/admin_test.go
+++ b/internal/processing/admin/admin_test.go
@@ -18,26 +18,28 @@
package admin_test
import (
+ adminactions "code.superseriousbusiness.org/gotosocial/internal/admin"
+ "code.superseriousbusiness.org/gotosocial/internal/cleaner"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/email"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/interaction"
+ "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/oauth"
+ "code.superseriousbusiness.org/gotosocial/internal/processing"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/admin"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/storage"
+ "code.superseriousbusiness.org/gotosocial/internal/subscriptions"
+ "code.superseriousbusiness.org/gotosocial/internal/transport"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- adminactions "github.com/superseriousbusiness/gotosocial/internal/admin"
- "github.com/superseriousbusiness/gotosocial/internal/cleaner"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
- "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/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/processing"
- "github.com/superseriousbusiness/gotosocial/internal/processing/admin"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/internal/subscriptions"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type AdminStandardTestSuite struct {
@@ -58,7 +60,6 @@ type AdminStandardTestSuite 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
@@ -73,7 +74,6 @@ type AdminStandardTestSuite struct {
func (suite *AdminStandardTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
@@ -94,16 +94,10 @@ func (suite *AdminStandardTestSuite) SetupTest() {
suite.state.AdminActions = adminactions.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)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
+ suite.oauthServer = testrig.NewTestOauthServer(&suite.state)
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
@@ -121,7 +115,9 @@ func (suite *AdminStandardTestSuite) SetupTest() {
suite.emailSender,
testrig.NewNoopWebPushSender(),
visibility.NewFilter(&suite.state),
+ mutes.NewFilter(&suite.state),
interaction.NewFilter(&suite.state),
+ status.NewFilter(&suite.state),
)
testrig.StartWorkers(&suite.state, suite.processor.Workers())
diff --git a/internal/processing/admin/debug_apurl.go b/internal/processing/admin/debug_apurl.go
index dbf337dc3..f6098c534 100644
--- a/internal/processing/admin/debug_apurl.go
+++ b/internal/processing/admin/debug_apurl.go
@@ -23,11 +23,11 @@ import (
"net/http"
"net/url"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// DebugAPUrl performs a GET to the given url, using the
diff --git a/internal/processing/admin/domainallow.go b/internal/processing/admin/domainallow.go
index 13f0307f2..bdf70642b 100644
--- a/internal/processing/admin/domainallow.go
+++ b/internal/processing/admin/domainallow.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/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/text"
+ 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/text"
)
func (p *Processor) createDomainAllow(
@@ -53,14 +53,14 @@ func (p *Processor) createDomainAllow(
ID: id.NewULID(),
Domain: domain,
CreatedByAccountID: adminAcct.ID,
- PrivateComment: text.SanitizeToPlaintext(privateComment),
- PublicComment: text.SanitizeToPlaintext(publicComment),
+ PrivateComment: text.StripHTMLFromText(privateComment),
+ PublicComment: text.StripHTMLFromText(publicComment),
Obfuscate: &obfuscate,
SubscriptionID: subscriptionID,
}
// Insert the new allow into the database.
- if err := p.state.DB.CreateDomainAllow(ctx, domainAllow); err != nil {
+ if err := p.state.DB.PutDomainAllow(ctx, domainAllow); err != nil {
err = gtserror.Newf("db error putting domain allow %s: %w", domain, err)
return nil, "", gtserror.NewErrorInternalError(err)
}
@@ -92,6 +92,54 @@ func (p *Processor) createDomainAllow(
return apiDomainAllow, action.ID, nil
}
+func (p *Processor) updateDomainAllow(
+ ctx context.Context,
+ domainAllowID string,
+ obfuscate *bool,
+ publicComment *string,
+ privateComment *string,
+ subscriptionID *string,
+) (*apimodel.DomainPermission, gtserror.WithCode) {
+ domainAllow, err := p.state.DB.GetDomainAllowByID(ctx, domainAllowID)
+ if err != nil {
+ if !errors.Is(err, db.ErrNoEntries) {
+ // Real error.
+ err = gtserror.Newf("db error getting domain allow: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // There are just no entries for this ID.
+ err = fmt.Errorf("no domain allow entry exists with ID %s", domainAllowID)
+ return nil, gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ var columns []string
+ if obfuscate != nil {
+ domainAllow.Obfuscate = obfuscate
+ columns = append(columns, "obfuscate")
+ }
+ if publicComment != nil {
+ domainAllow.PublicComment = *publicComment
+ columns = append(columns, "public_comment")
+ }
+ if privateComment != nil {
+ domainAllow.PrivateComment = *privateComment
+ columns = append(columns, "private_comment")
+ }
+ if subscriptionID != nil {
+ domainAllow.SubscriptionID = *subscriptionID
+ columns = append(columns, "subscription_id")
+ }
+
+ // Update the domain allow.
+ if err := p.state.DB.UpdateDomainAllow(ctx, domainAllow, columns...); err != nil {
+ err = gtserror.Newf("db error updating domain allow: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.apiDomainPerm(ctx, domainAllow, false)
+}
+
func (p *Processor) deleteDomainAllow(
ctx context.Context,
adminAcct *gtsmodel.Account,
@@ -128,7 +176,7 @@ func (p *Processor) deleteDomainAllow(
ID: id.NewULID(),
TargetCategory: gtsmodel.AdminActionCategoryDomain,
TargetID: domainAllow.Domain,
- Type: gtsmodel.AdminActionUnsuspend,
+ Type: gtsmodel.AdminActionUnallow,
AccountID: adminAcct.ID,
}
diff --git a/internal/processing/admin/domainblock.go b/internal/processing/admin/domainblock.go
index f8c1a6708..1c5d004bd 100644
--- a/internal/processing/admin/domainblock.go
+++ b/internal/processing/admin/domainblock.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/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/text"
+ 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/text"
)
func (p *Processor) createDomainBlock(
@@ -53,14 +53,14 @@ func (p *Processor) createDomainBlock(
ID: id.NewULID(),
Domain: domain,
CreatedByAccountID: adminAcct.ID,
- PrivateComment: text.SanitizeToPlaintext(privateComment),
- PublicComment: text.SanitizeToPlaintext(publicComment),
+ PrivateComment: text.StripHTMLFromText(privateComment),
+ PublicComment: text.StripHTMLFromText(publicComment),
Obfuscate: &obfuscate,
SubscriptionID: subscriptionID,
}
// Insert the new block into the database.
- if err := p.state.DB.CreateDomainBlock(ctx, domainBlock); err != nil {
+ if err := p.state.DB.PutDomainBlock(ctx, domainBlock); err != nil {
err = gtserror.Newf("db error putting domain block %s: %w", domain, err)
return nil, "", gtserror.NewErrorInternalError(err)
}
@@ -93,6 +93,54 @@ func (p *Processor) createDomainBlock(
return apiDomainBlock, action.ID, nil
}
+func (p *Processor) updateDomainBlock(
+ ctx context.Context,
+ domainBlockID string,
+ obfuscate *bool,
+ publicComment *string,
+ privateComment *string,
+ subscriptionID *string,
+) (*apimodel.DomainPermission, gtserror.WithCode) {
+ domainBlock, err := p.state.DB.GetDomainBlockByID(ctx, domainBlockID)
+ if err != nil {
+ if !errors.Is(err, db.ErrNoEntries) {
+ // Real error.
+ err = gtserror.Newf("db error getting domain block: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // There are just no entries for this ID.
+ err = fmt.Errorf("no domain block entry exists with ID %s", domainBlockID)
+ return nil, gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ var columns []string
+ if obfuscate != nil {
+ domainBlock.Obfuscate = obfuscate
+ columns = append(columns, "obfuscate")
+ }
+ if publicComment != nil {
+ domainBlock.PublicComment = *publicComment
+ columns = append(columns, "public_comment")
+ }
+ if privateComment != nil {
+ domainBlock.PrivateComment = *privateComment
+ columns = append(columns, "private_comment")
+ }
+ if subscriptionID != nil {
+ domainBlock.SubscriptionID = *subscriptionID
+ columns = append(columns, "subscription_id")
+ }
+
+ // Update the domain block.
+ if err := p.state.DB.UpdateDomainBlock(ctx, domainBlock, columns...); err != nil {
+ err = gtserror.Newf("db error updating domain block: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.apiDomainPerm(ctx, domainBlock, false)
+}
+
func (p *Processor) deleteDomainBlock(
ctx context.Context,
adminAcct *gtsmodel.Account,
diff --git a/internal/processing/admin/domainkeysexpire.go b/internal/processing/admin/domainkeysexpire.go
index 0613f502d..39fe77dc7 100644
--- a/internal/processing/admin/domainkeysexpire.go
+++ b/internal/processing/admin/domainkeysexpire.go
@@ -20,9 +20,9 @@ package admin
import (
"context"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
)
// DomainKeysExpire iterates through all
diff --git a/internal/processing/admin/domainpermission.go b/internal/processing/admin/domainpermission.go
index 55800f458..d7546fb15 100644
--- a/internal/processing/admin/domainpermission.go
+++ b/internal/processing/admin/domainpermission.go
@@ -18,6 +18,7 @@
package admin
import (
+ "cmp"
"context"
"encoding/json"
"errors"
@@ -25,10 +26,11 @@ import (
"mime/multipart"
"net/http"
- 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"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// DomainPermissionCreate creates an instance-level permission
@@ -84,6 +86,50 @@ func (p *Processor) DomainPermissionCreate(
}
}
+// DomainPermissionUpdate updates a domain permission
+// of the given permissionType, with the given ID.
+func (p *Processor) DomainPermissionUpdate(
+ ctx context.Context,
+ permissionType gtsmodel.DomainPermissionType,
+ permID string,
+ obfuscate *bool,
+ publicComment *string,
+ privateComment *string,
+ subscriptionID *string,
+) (*apimodel.DomainPermission, gtserror.WithCode) {
+ switch permissionType {
+
+ // Explicitly block a domain.
+ case gtsmodel.DomainPermissionBlock:
+ return p.updateDomainBlock(
+ ctx,
+ permID,
+ obfuscate,
+ publicComment,
+ privateComment,
+ subscriptionID,
+ )
+
+ // Explicitly allow a domain.
+ case gtsmodel.DomainPermissionAllow:
+ return p.updateDomainAllow(
+ ctx,
+ permID,
+ obfuscate,
+ publicComment,
+ privateComment,
+ subscriptionID,
+ )
+
+ // 🎵 Why don't we all strap bombs to our chests,
+ // and ride our bikes to the next G7 picnic?
+ // Seems easier with every clock-tick. 🎵
+ default:
+ err := gtserror.Newf("unrecognized permission type %d", permissionType)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+}
+
// DomainPermissionDelete removes one domain block with the given ID,
// and processes side effects of removing the block asynchronously.
//
@@ -153,14 +199,14 @@ func (p *Processor) DomainPermissionsImport(
}
defer file.Close()
- // Parse file as slice of domain blocks.
- domainPerms := make([]*apimodel.DomainPermission, 0)
- if err := json.NewDecoder(file).Decode(&domainPerms); err != nil {
+ // Parse file as slice of domain permissions.
+ apiDomainPerms := make([]*apimodel.DomainPermission, 0)
+ if err := json.NewDecoder(file).Decode(&apiDomainPerms); err != nil {
err = gtserror.Newf("error parsing attachment as domain permissions: %w", err)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- count := len(domainPerms)
+ count := len(apiDomainPerms)
if count == 0 {
err = gtserror.New("error importing domain permissions: 0 entries provided")
return nil, gtserror.NewErrorBadRequest(err, err.Error())
@@ -170,50 +216,95 @@ func (p *Processor) DomainPermissionsImport(
// between successes and errors so that the caller can
// try failed imports again if desired.
multiStatusEntries := make([]apimodel.MultiStatusEntry, 0, count)
-
- for _, domainPerm := range domainPerms {
- var (
- domain = domainPerm.Domain.Domain
- obfuscate = domainPerm.Obfuscate
- publicComment = domainPerm.PublicComment
- privateComment = domainPerm.PrivateComment
- subscriptionID = "" // No sub ID for imports.
- errWithCode gtserror.WithCode
+ for _, apiDomainPerm := range apiDomainPerms {
+ multiStatusEntries = append(
+ multiStatusEntries,
+ p.importOrUpdateDomainPerm(
+ ctx,
+ permissionType,
+ account,
+ apiDomainPerm,
+ ),
)
+ }
+
+ return apimodel.NewMultiStatus(multiStatusEntries), nil
+}
- domainPerm, _, errWithCode = p.DomainPermissionCreate(
+func (p *Processor) importOrUpdateDomainPerm(
+ ctx context.Context,
+ permType gtsmodel.DomainPermissionType,
+ account *gtsmodel.Account,
+ apiDomainPerm *apimodel.DomainPermission,
+) apimodel.MultiStatusEntry {
+ var (
+ domain = apiDomainPerm.Domain.Domain
+ obfuscate = apiDomainPerm.Obfuscate
+ publicComment = cmp.Or(apiDomainPerm.PublicComment, apiDomainPerm.Comment)
+ privateComment = apiDomainPerm.PrivateComment
+ subscriptionID = "" // No sub ID for imports.
+ )
+
+ // Check if this domain
+ // perm already exists.
+ var (
+ domainPerm gtsmodel.DomainPermission
+ err error
+ )
+ if permType == gtsmodel.DomainPermissionBlock {
+ domainPerm, err = p.state.DB.GetDomainBlock(ctx, domain)
+ } else {
+ domainPerm, err = p.state.DB.GetDomainAllow(ctx, domain)
+ }
+
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ // Real db error.
+ return apimodel.MultiStatusEntry{
+ Resource: domain,
+ Message: "db error checking for existence of domain permission",
+ Status: http.StatusInternalServerError,
+ }
+ }
+
+ var errWithCode gtserror.WithCode
+ if !util.IsNil(domainPerm) {
+ // Permission already exists, update it.
+ apiDomainPerm, errWithCode = p.DomainPermissionUpdate(
ctx,
- permissionType,
- account,
- domain,
+ permType,
+ domainPerm.GetID(),
obfuscate,
publicComment,
privateComment,
+ nil,
+ )
+ } else {
+ // Permission didn't exist yet, create it.
+ apiDomainPerm, _, errWithCode = p.DomainPermissionCreate(
+ ctx,
+ permType,
+ account,
+ domain,
+ util.PtrOrZero(obfuscate),
+ util.PtrOrZero(publicComment),
+ util.PtrOrZero(privateComment),
subscriptionID,
)
+ }
- var entry *apimodel.MultiStatusEntry
-
- if errWithCode != nil {
- entry = &apimodel.MultiStatusEntry{
- // Use the failed domain entry as the resource value.
- Resource: domain,
- Message: errWithCode.Safe(),
- Status: errWithCode.Code(),
- }
- } else {
- entry = &apimodel.MultiStatusEntry{
- // Use successfully created API model domain block as the resource value.
- Resource: domainPerm,
- Message: http.StatusText(http.StatusOK),
- Status: http.StatusOK,
- }
+ if errWithCode != nil {
+ return apimodel.MultiStatusEntry{
+ Resource: domain,
+ Message: errWithCode.Safe(),
+ Status: errWithCode.Code(),
}
-
- multiStatusEntries = append(multiStatusEntries, *entry)
}
- return apimodel.NewMultiStatus(multiStatusEntries), nil
+ return apimodel.MultiStatusEntry{
+ Resource: apiDomainPerm,
+ Message: http.StatusText(http.StatusOK),
+ Status: http.StatusOK,
+ }
}
// DomainPermissionsGet returns all existing domain
@@ -303,15 +394,21 @@ func (p *Processor) DomainPermissionGet(
err = gtserror.New("unrecognized permission type")
}
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- err = fmt.Errorf("no domain %s exists with id %s", permissionType.String(), id)
- return nil, gtserror.NewErrorNotFound(err, err.Error())
- }
-
- err = gtserror.Newf("error getting domain %s with id %s: %w", permissionType.String(), id, err)
+ if err != nil && errors.Is(err, db.ErrNoEntries) {
+ err = gtserror.Newf(
+ "db error getting domain %s with id %s: %w",
+ permissionType.String(), id, err,
+ )
return nil, gtserror.NewErrorInternalError(err)
}
+ if util.IsNil(domainPerm) {
+ errText := fmt.Sprintf(
+ "no domain %s exists with id %s",
+ permissionType.String(), id,
+ )
+ return nil, gtserror.NewErrorNotFound(errors.New(errText), errText)
+ }
+
return p.apiDomainPerm(ctx, domainPerm, export)
}
diff --git a/internal/processing/admin/domainpermission_test.go b/internal/processing/admin/domainpermission_test.go
index c8f3560c3..fccb9e10f 100644
--- a/internal/processing/admin/domainpermission_test.go
+++ b/internal/processing/admin/domainpermission_test.go
@@ -22,13 +22,13 @@ import (
"net/http"
"testing"
+ 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/id"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- 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/id"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type DomainBlockTestSuite struct {
@@ -82,7 +82,7 @@ func (suite *DomainBlockTestSuite) runDomainPermTest(t domainPermTest) {
config.SetInstanceFederationMode(t.instanceFederationMode)
for _, action := range t.actions {
- ctx, cancel := context.WithCancel(context.Background())
+ ctx, cancel := context.WithCancel(suite.T().Context())
defer cancel()
// Run the desired action.
@@ -102,7 +102,7 @@ func (suite *DomainBlockTestSuite) runDomainPermTest(t domainPermTest) {
// Check expected results
// against each account.
accounts, err := suite.db.GetInstanceAccounts(
- context.Background(),
+ suite.T().Context(),
action.domain,
"", 0,
)
@@ -123,7 +123,7 @@ func (suite *DomainBlockTestSuite) createDomainPerm(
permissionType gtsmodel.DomainPermissionType,
domain string,
) (*apimodel.DomainPermission, string) {
- ctx := context.Background()
+ ctx := suite.T().Context()
apiPerm, actionID, errWithCode := suite.adminProcessor.DomainPermissionCreate(
ctx,
@@ -148,7 +148,7 @@ func (suite *DomainBlockTestSuite) deleteDomainPerm(
domain string,
) (*apimodel.DomainPermission, string) {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
domainPermission gtsmodel.DomainPermission
)
@@ -183,7 +183,7 @@ func (suite *DomainBlockTestSuite) deleteDomainPerm(
// waits for given actionID to be completed.
func (suite *DomainBlockTestSuite) awaitAction(actionID string) {
- ctx := context.Background()
+ ctx := suite.T().Context()
if !testrig.WaitFor(func() bool {
return suite.state.AdminActions.TotalRunning() == 0
diff --git a/internal/processing/admin/domainpermissiondraft.go b/internal/processing/admin/domainpermissiondraft.go
index 0dc17a45a..9e067f20b 100644
--- a/internal/processing/admin/domainpermissiondraft.go
+++ b/internal/processing/admin/domainpermissiondraft.go
@@ -23,16 +23,16 @@ import (
"fmt"
"net/url"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "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/log"
- "github.com/superseriousbusiness/gotosocial/internal/paging"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ 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/gtscontext"
+ "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"
)
// DomainPermissionDraftGet returns one
diff --git a/internal/processing/admin/domainpermissionexclude.go b/internal/processing/admin/domainpermissionexclude.go
index 761ca8b9c..b2134777d 100644
--- a/internal/processing/admin/domainpermissionexclude.go
+++ b/internal/processing/admin/domainpermissionexclude.go
@@ -23,13 +23,13 @@ import (
"fmt"
"net/url"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "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/paging"
+ 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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
)
func (p *Processor) DomainPermissionExcludeCreate(
diff --git a/internal/processing/admin/domainpermissionsubscription.go b/internal/processing/admin/domainpermissionsubscription.go
index bdc38df63..9afe6ee5c 100644
--- a/internal/processing/admin/domainpermissionsubscription.go
+++ b/internal/processing/admin/domainpermissionsubscription.go
@@ -24,13 +24,13 @@ import (
"net/url"
"slices"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "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/paging"
+ 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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
)
// DomainPermissionSubscriptionGet returns one
@@ -142,6 +142,8 @@ func (p *Processor) DomainPermissionSubscriptionCreate(
contentType gtsmodel.DomainPermSubContentType,
permType gtsmodel.DomainPermissionType,
asDraft bool,
+ adoptOrphans *bool,
+ removeRetracted *bool,
fetchUsername string,
fetchPassword string,
) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
@@ -151,12 +153,14 @@ func (p *Processor) DomainPermissionSubscriptionCreate(
Title: title,
PermissionType: permType,
AsDraft: &asDraft,
+ AdoptOrphans: adoptOrphans,
CreatedByAccountID: acct.ID,
CreatedByAccount: acct,
URI: uri,
ContentType: contentType,
FetchUsername: fetchUsername,
FetchPassword: fetchPassword,
+ RemoveRetracted: removeRetracted,
}
err := p.state.DB.PutDomainPermissionSubscription(ctx, permSub)
@@ -184,6 +188,7 @@ func (p *Processor) DomainPermissionSubscriptionUpdate(
contentType *gtsmodel.DomainPermSubContentType,
asDraft *bool,
adoptOrphans *bool,
+ removeRetracted *bool,
fetchUsername *string,
fetchPassword *string,
) (*apimodel.DomainPermissionSubscription, gtserror.WithCode) {
@@ -230,6 +235,11 @@ func (p *Processor) DomainPermissionSubscriptionUpdate(
columns = append(columns, "adopt_orphans")
}
+ if removeRetracted != nil {
+ permSub.RemoveRetracted = removeRetracted
+ columns = append(columns, "remove_retracted")
+ }
+
if fetchPassword != nil {
permSub.FetchPassword = *fetchPassword
columns = append(columns, "fetch_password")
@@ -342,12 +352,13 @@ func (p *Processor) DomainPermissionSubscriptionTest(
// Call the permSub.URI and parse a list of perms from it.
// Any error returned here is a "real" one, not an error
// from fetching / parsing the list.
- createdPerms, err := p.subscriptions.ProcessDomainPermissionSubscription(
+ createdPerms, _, err := p.subscriptions.ProcessDomainPermissionSubscription(
ctx,
permSub,
tsport,
higherPrios,
true, // Dry run.
+ true, // Skip caching.
)
if err != nil {
err := gtserror.Newf("error doing dry-run: %w", err)
diff --git a/internal/processing/admin/email.go b/internal/processing/admin/email.go
index 949be6e4b..fe69236db 100644
--- a/internal/processing/admin/email.go
+++ b/internal/processing/admin/email.go
@@ -21,10 +21,10 @@ import (
"context"
"fmt"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/email"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// EmailTest sends a generic test email to the given
diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/emoji.go
index 5a7da445e..8d568b9a8 100644
--- a/internal/processing/admin/emoji.go
+++ b/internal/processing/admin/emoji.go
@@ -25,15 +25,16 @@ import (
"mime/multipart"
"strings"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "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/id"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
"codeberg.org/gruf/go-iotools"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "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/media"
- "github.com/superseriousbusiness/gotosocial/internal/util"
)
// EmojiCreate creates a custom emoji on this instance.
@@ -262,11 +263,7 @@ func (p *Processor) EmojiCategoriesGet(
apiCategories := make([]*apimodel.EmojiCategory, 0, len(categories))
for _, category := range categories {
- apiCategory, err := p.converter.EmojiCategoryToAPIEmojiCategory(ctx, category)
- if err != nil {
- err := gtserror.Newf("error converting emoji category to api emoji category: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
+ apiCategory := typeutils.EmojiCategoryToAPIEmojiCategory(category)
apiCategories = append(apiCategories, apiCategory)
}
@@ -293,6 +290,7 @@ func (p *Processor) emojiUpdateCopy(
// Ensure target emoji is locally cached.
target, err := p.federator.RecacheEmoji(ctx,
target,
+ false,
)
if err != nil {
err := gtserror.Newf("error recaching emoji %s: %w", target.ImageRemoteURL, err)
diff --git a/internal/processing/admin/emoji_test.go b/internal/processing/admin/emoji_test.go
index 17f5fc864..b050cefe8 100644
--- a/internal/processing/admin/emoji_test.go
+++ b/internal/processing/admin/emoji_test.go
@@ -18,13 +18,12 @@
package admin_test
import (
- "context"
"testing"
+ 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"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/util"
)
type EmojiTestSuite struct {
@@ -32,7 +31,7 @@ type EmojiTestSuite struct {
}
func (suite *EmojiTestSuite) TestUpdateEmojiCategory() {
- ctx := context.Background()
+ ctx := suite.T().Context()
testEmoji := new(gtsmodel.Emoji)
*testEmoji = *suite.testEmojis["rainbow"]
diff --git a/internal/processing/admin/headerfilter.go b/internal/processing/admin/headerfilter.go
index 13105d191..dc74c151b 100644
--- a/internal/processing/admin/headerfilter.go
+++ b/internal/processing/admin/headerfilter.go
@@ -23,13 +23,13 @@ import (
"net/textproto"
"regexp"
- 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/headerfilter"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "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/headerfilter"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// GetAllowHeaderFilter fetches allow HTTP header filter with provided ID from the database.
diff --git a/internal/processing/admin/media.go b/internal/processing/admin/media.go
index 9cd68d88b..11394bbed 100644
--- a/internal/processing/admin/media.go
+++ b/internal/processing/admin/media.go
@@ -21,10 +21,10 @@ import (
"context"
"fmt"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
)
// MediaRefetch forces a refetch of remote emojis.
@@ -56,11 +56,11 @@ func (p *Processor) MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gt
return gtserror.NewErrorBadRequest(err, err.Error())
}
- // Start background task performing all media cleanup tasks.
go func() {
- ctx := context.Background()
- p.cleaner.Media().All(ctx, mediaRemoteCacheDays)
- p.cleaner.Emoji().All(ctx, mediaRemoteCacheDays)
+ // Start background task performing all media cleanup tasks.
+ ctx := gtscontext.WithValues(context.Background(), ctx)
+ p.cleaner.Media().AllAndFix(ctx, mediaRemoteCacheDays)
+ p.cleaner.Emoji().AllAndFix(ctx, mediaRemoteCacheDays)
}()
return nil
diff --git a/internal/processing/admin/report.go b/internal/processing/admin/report.go
index ed34a4e83..bb94e67f5 100644
--- a/internal/processing/admin/report.go
+++ b/internal/processing/admin/report.go
@@ -25,14 +25,14 @@ import (
"strconv"
"time"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "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"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+ "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"
)
// ReportsGet returns reports stored on this
diff --git a/internal/processing/admin/rule.go b/internal/processing/admin/rule.go
index 8134c21cd..de19cba0b 100644
--- a/internal/processing/admin/rule.go
+++ b/internal/processing/admin/rule.go
@@ -22,13 +22,13 @@ import (
"errors"
"fmt"
- 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/typeutils"
- "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/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// RulesGet returns all rules stored on this instance.
@@ -64,17 +64,12 @@ func (p *Processor) RuleGet(ctx context.Context, id string) (*apimodel.AdminInst
// RuleCreate adds a new rule to the instance.
func (p *Processor) RuleCreate(ctx context.Context, form *apimodel.InstanceRuleCreateRequest) (*apimodel.AdminInstanceRule, gtserror.WithCode) {
- ruleID, err := id.NewRandomULID()
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating id for new instance rule: %s", err), "error creating rule ID")
- }
-
rule := &gtsmodel.Rule{
- ID: ruleID,
+ ID: id.NewRandomULID(),
Text: form.Text,
}
- if err = p.state.DB.PutRule(ctx, rule); err != nil {
+ if err := p.state.DB.PutRule(ctx, rule); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/admin/signupapprove.go b/internal/processing/admin/signupapprove.go
index 84e04fa8d..d33227bb2 100644
--- a/internal/processing/admin/signupapprove.go
+++ b/internal/processing/admin/signupapprove.go
@@ -22,12 +22,12 @@ 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/messages"
+ "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"
)
func (p *Processor) SignupApprove(
diff --git a/internal/processing/admin/signupapprove_test.go b/internal/processing/admin/signupapprove_test.go
index 58b8fdade..5739325c2 100644
--- a/internal/processing/admin/signupapprove_test.go
+++ b/internal/processing/admin/signupapprove_test.go
@@ -18,12 +18,11 @@
package admin_test
import (
- "context"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type AdminApproveTestSuite struct {
@@ -32,7 +31,7 @@ type AdminApproveTestSuite struct {
func (suite *AdminApproveTestSuite) TestApprove() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
adminAcct = suite.testAccounts["admin_account"]
targetAcct = suite.testAccounts["unconfirmed_account"]
targetUser = new(gtsmodel.User)
diff --git a/internal/processing/admin/signupreject.go b/internal/processing/admin/signupreject.go
index 39eff0b87..5b7872a19 100644
--- a/internal/processing/admin/signupreject.go
+++ b/internal/processing/admin/signupreject.go
@@ -22,12 +22,12 @@ 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/messages"
+ "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"
)
func (p *Processor) SignupReject(
diff --git a/internal/processing/admin/signupreject_test.go b/internal/processing/admin/signupreject_test.go
index cb6a25eb3..ea517f579 100644
--- a/internal/processing/admin/signupreject_test.go
+++ b/internal/processing/admin/signupreject_test.go
@@ -18,13 +18,12 @@
package admin_test
import (
- "context"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type AdminRejectTestSuite struct {
@@ -33,7 +32,7 @@ type AdminRejectTestSuite struct {
func (suite *AdminRejectTestSuite) TestReject() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
adminAcct = suite.testAccounts["admin_account"]
targetAcct = suite.testAccounts["unconfirmed_account"]
targetUser = suite.testUsers["unconfirmed_account"]
@@ -95,7 +94,7 @@ func (suite *AdminRejectTestSuite) TestReject() {
func (suite *AdminRejectTestSuite) TestRejectRemote() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
adminAcct = suite.testAccounts["admin_account"]
targetAcct = suite.testAccounts["remote_account_1"]
privateComment = "It's a no from me chief."
@@ -117,7 +116,7 @@ func (suite *AdminRejectTestSuite) TestRejectRemote() {
func (suite *AdminRejectTestSuite) TestRejectApproved() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
adminAcct = suite.testAccounts["admin_account"]
targetAcct = suite.testAccounts["local_account_1"]
privateComment = "It's a no from me chief."
diff --git a/internal/processing/admin/util.go b/internal/processing/admin/util.go
index f04b3654b..2ad0069d7 100644
--- a/internal/processing/admin/util.go
+++ b/internal/processing/admin/util.go
@@ -20,9 +20,9 @@ package admin
import (
"context"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// apiDomainPerm is a cheeky shortcut for returning
diff --git a/internal/processing/admin/workertask.go b/internal/processing/admin/workertask.go
index 6d7cc7b7a..9fcd1f5a1 100644
--- a/internal/processing/admin/workertask.go
+++ b/internal/processing/admin/workertask.go
@@ -23,12 +23,12 @@ import (
"slices"
"time"
- "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/transport"
- "github.com/superseriousbusiness/gotosocial/internal/transport/delivery"
+ "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/transport"
+ "code.superseriousbusiness.org/gotosocial/internal/transport/delivery"
)
// NOTE:
diff --git a/internal/processing/admin/workertask_test.go b/internal/processing/admin/workertask_test.go
index bf326bafd..8ce0a3436 100644
--- a/internal/processing/admin/workertask_test.go
+++ b/internal/processing/admin/workertask_test.go
@@ -27,14 +27,14 @@ import (
"net/url"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/httpclient"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/transport/delivery"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/httpclient"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/transport/delivery"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
var (
@@ -94,7 +94,7 @@ type WorkerTaskTestSuite struct {
}
func (suite *WorkerTaskTestSuite) TestFillWorkerQueues() {
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
var tasks []*gtsmodel.WorkerTask
@@ -255,7 +255,7 @@ func (suite *WorkerTaskTestSuite) TestFillWorkerQueues() {
}
func (suite *WorkerTaskTestSuite) TestPersistWorkerQueues() {
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// Push all test worker tasks to their respective queues.
diff --git a/internal/processing/advancedmigrations/advancedmigrations.go b/internal/processing/advancedmigrations/advancedmigrations.go
index 3f1876539..d4404bb85 100644
--- a/internal/processing/advancedmigrations/advancedmigrations.go
+++ b/internal/processing/advancedmigrations/advancedmigrations.go
@@ -21,7 +21,7 @@ import (
"context"
"fmt"
- "github.com/superseriousbusiness/gotosocial/internal/processing/conversations"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/conversations"
)
// Processor holds references to any other processor that has migrations to run.
diff --git a/internal/processing/app.go b/internal/processing/app.go
deleted file mode 100644
index d492b3bc4..000000000
--- a/internal/processing/app.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// 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 processing
-
-import (
- "context"
-
- "github.com/google/uuid"
- 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"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
-)
-
-func (p *Processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) {
- // set default 'read' for scopes if it's not set
- var scopes string
- if form.Scopes == "" {
- scopes = "read"
- } else {
- scopes = form.Scopes
- }
-
- // generate new IDs for this application and its associated client
- clientID, err := id.NewRandomULID()
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- clientSecret := uuid.NewString()
-
- appID, err := id.NewRandomULID()
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // generate the application to put in the database
- app := &gtsmodel.Application{
- ID: appID,
- Name: form.ClientName,
- Website: form.Website,
- RedirectURI: form.RedirectURIs,
- ClientID: clientID,
- ClientSecret: clientSecret,
- Scopes: scopes,
- }
-
- // chuck it in the db
- if err := p.state.DB.PutApplication(ctx, app); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // now we need to model an oauth client from the application that the oauth library can use
- oc := &gtsmodel.Client{
- ID: clientID,
- Secret: clientSecret,
- Domain: form.RedirectURIs,
- // This client isn't yet associated with a specific user, it's just an app client right now
- UserID: "",
- }
-
- // chuck it in the db
- if err := p.state.DB.PutClient(ctx, oc); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- apiApp, err := p.converter.AppToAPIAppSensitive(ctx, app)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return apiApp, nil
-}
diff --git a/internal/processing/filters/v2/convert.go b/internal/processing/application/application.go
index 1e544e6e4..acecd7f85 100644
--- a/internal/processing/filters/v2/convert.go
+++ b/internal/processing/application/application.go
@@ -15,24 +15,24 @@
// 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 v2
+package application
import (
- "context"
- "fmt"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
-// apiFilter is a shortcut to return the API v2 filter version of the given
-// filter, or return an appropriate error if conversion fails.
-func (p *Processor) apiFilter(ctx context.Context, filterKeyword *gtsmodel.Filter) (*apimodel.FilterV2, gtserror.WithCode) {
- apiFilter, err := p.converter.FilterToAPIFilterV2(ctx, filterKeyword)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting filter to API v2 filter: %w", err))
- }
+type Processor struct {
+ state *state.State
+ converter *typeutils.Converter
+}
- return apiFilter, nil
+func New(
+ state *state.State,
+ converter *typeutils.Converter,
+) Processor {
+ return Processor{
+ state: state,
+ converter: converter,
+ }
}
diff --git a/internal/processing/application/create.go b/internal/processing/application/create.go
new file mode 100644
index 000000000..d63b682d6
--- /dev/null
+++ b/internal/processing/application/create.go
@@ -0,0 +1,113 @@
+// 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 application
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/url"
+ "strings"
+
+ 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"
+ "code.superseriousbusiness.org/gotosocial/internal/oauth"
+ "github.com/google/uuid"
+)
+
+func (p *Processor) Create(
+ ctx context.Context,
+ managedByUserID string,
+ form *apimodel.ApplicationCreateRequest,
+) (*apimodel.Application, gtserror.WithCode) {
+ // Set default 'read' for
+ // scopes if it's not set.
+ var scopes string
+ if form.Scopes == "" {
+ scopes = "read"
+ } else {
+ scopes = form.Scopes
+ }
+
+ // Normalize + parse requested redirect URIs.
+ form.RedirectURIs = strings.TrimSpace(form.RedirectURIs)
+ var redirectURIs []string
+ if form.RedirectURIs != "" {
+ // Redirect URIs can be just one value, or can be passed
+ // as a newline-separated list of strings. Ensure each URI
+ // is parseable + normalize it by reconstructing from *url.URL.
+ // Also ensure we don't add multiple copies of the same URI.
+ redirectStrs := strings.Split(form.RedirectURIs, "\n")
+ added := make(map[string]struct{}, len(redirectStrs))
+
+ for _, redirectStr := range redirectStrs {
+ redirectStr = strings.TrimSpace(redirectStr)
+ if redirectStr == "" {
+ continue
+ }
+
+ redirectURI, err := url.Parse(redirectStr)
+ if err != nil {
+ errText := fmt.Sprintf("error parsing redirect URI: %v", err)
+ return nil, gtserror.NewErrorBadRequest(err, errText)
+ }
+
+ redirectURIStr := redirectURI.String()
+ if _, alreadyAdded := added[redirectURIStr]; !alreadyAdded {
+ redirectURIs = append(redirectURIs, redirectURIStr)
+ added[redirectURIStr] = struct{}{}
+ }
+ }
+
+ if len(redirectURIs) == 0 {
+ errText := "no redirect URIs left after trimming space"
+ return nil, gtserror.NewErrorBadRequest(errors.New(errText), errText)
+ }
+ } else {
+ // No redirect URI(s) provided, just set default oob.
+ redirectURIs = append(redirectURIs, oauth.OOBURI)
+ }
+
+ // Generate random client ID.
+ clientID := id.NewRandomULID()
+
+ // Generate + store app
+ // to put in the database.
+ app := &gtsmodel.Application{
+ ID: id.NewULID(),
+ Name: form.ClientName,
+ Website: form.Website,
+ RedirectURIs: redirectURIs,
+ ClientID: clientID,
+ ClientSecret: uuid.NewString(),
+ Scopes: scopes,
+ ManagedByUserID: managedByUserID,
+ }
+ if err := p.state.DB.PutApplication(ctx, app); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ apiApp, err := p.converter.AppToAPIAppSensitive(ctx, app)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiApp, nil
+}
diff --git a/internal/processing/application/delete.go b/internal/processing/application/delete.go
new file mode 100644
index 000000000..6b3856bf0
--- /dev/null
+++ b/internal/processing/application/delete.go
@@ -0,0 +1,76 @@
+// 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 application
+
+import (
+ "context"
+ "errors"
+
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+)
+
+func (p *Processor) Delete(
+ ctx context.Context,
+ userID string,
+ appID string,
+) (*apimodel.Application, gtserror.WithCode) {
+ app, err := p.state.DB.GetApplicationByID(ctx, appID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting app %s: %w", appID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if app == nil {
+ err := gtserror.Newf("app %s not found in the db", appID)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ if app.ManagedByUserID != userID {
+ err := gtserror.Newf("app %s not managed by user %s", appID, userID)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ // Convert app before deletion.
+ apiApp, err := p.converter.AppToAPIAppSensitive(ctx, app)
+ if err != nil {
+ err := gtserror.Newf("error converting app to api app: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Delete app itself.
+ if err := p.state.DB.DeleteApplicationByID(ctx, appID); err != nil {
+ err := gtserror.Newf("db error deleting app %s: %w", appID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Delete all tokens owned by app.
+ if err := p.state.DB.DeleteTokensByClientID(ctx, app.ClientID); err != nil {
+ err := gtserror.Newf("db error deleting tokens for app %s: %w", appID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Delete all scheduled statuses posted from the app.
+ if err := p.state.DB.DeleteScheduledStatusesByApplicationID(ctx, appID); err != nil {
+ err := gtserror.Newf("db error deleting scheduled statuses for app %s: %w", appID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiApp, nil
+}
diff --git a/internal/processing/application/get.go b/internal/processing/application/get.go
new file mode 100644
index 000000000..25cc634a6
--- /dev/null
+++ b/internal/processing/application/get.go
@@ -0,0 +1,104 @@
+// 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 application
+
+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) Get(
+ ctx context.Context,
+ userID string,
+ appID string,
+) (*apimodel.Application, gtserror.WithCode) {
+ app, err := p.state.DB.GetApplicationByID(ctx, appID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting app %s: %w", appID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if app == nil {
+ err := gtserror.Newf("app %s not found in the db", appID)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ if app.ManagedByUserID != userID {
+ err := gtserror.Newf("app %s not managed by user %s", appID, userID)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ apiApp, err := p.converter.AppToAPIAppSensitive(ctx, app)
+ if err != nil {
+ err := gtserror.Newf("error converting app to api app: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiApp, nil
+}
+
+func (p *Processor) GetPage(
+ ctx context.Context,
+ userID string,
+ page *paging.Page,
+) (*apimodel.PageableResponse, gtserror.WithCode) {
+ apps, err := p.state.DB.GetApplicationsManagedByUserID(ctx, userID, page)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting apps: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ count := len(apps)
+ if count == 0 {
+ return paging.EmptyResponse(), nil
+ }
+
+ var (
+ // Get the lowest and highest
+ // ID values, used for paging.
+ lo = apps[count-1].ID
+ hi = apps[0].ID
+
+ // Best-guess items length.
+ items = make([]interface{}, 0, count)
+ )
+
+ for _, app := range apps {
+ apiApp, err := p.converter.AppToAPIAppSensitive(ctx, app)
+ if err != nil {
+ log.Errorf(ctx, "error converting app to api app: %v", err)
+ continue
+ }
+
+ // Append req to return items.
+ items = append(items, apiApp)
+ }
+
+ return paging.PackageResponse(paging.ResponseParams{
+ Items: items,
+ Path: "/api/v1/apps",
+ Next: page.Next(lo, hi),
+ Prev: page.Prev(lo, hi),
+ }), nil
+}
diff --git a/internal/processing/common/account.go b/internal/processing/common/account.go
index ae26e4ebd..07644e839 100644
--- a/internal/processing/common/account.go
+++ b/internal/processing/common/account.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/log"
+ 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"
)
// GetTargetAccountBy fetches the target account with db load function, given the authorized (or, nil) requester's
diff --git a/internal/processing/common/common.go b/internal/processing/common/common.go
index 29def3506..2b3adb9a0 100644
--- a/internal/processing/common/common.go
+++ b/internal/processing/common/common.go
@@ -18,22 +18,26 @@
package common
import (
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "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/media"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Processor provides a processor with logic
// common to multiple logical domains of the
// processing subsection of the codebase.
type Processor struct {
- state *state.State
- media *media.Manager
- converter *typeutils.Converter
- federator *federation.Federator
- visFilter *visibility.Filter
+ state *state.State
+ media *media.Manager
+ converter *typeutils.Converter
+ federator *federation.Federator
+ visFilter *visibility.Filter
+ muteFilter *mutes.Filter
+ statusFilter *status.Filter
}
// New returns a new Processor instance.
@@ -43,12 +47,16 @@ func New(
converter *typeutils.Converter,
federator *federation.Federator,
visFilter *visibility.Filter,
+ muteFilter *mutes.Filter,
+ statusFilter *status.Filter,
) Processor {
return Processor{
- state: state,
- media: media,
- converter: converter,
- federator: federator,
- visFilter: visFilter,
+ state: state,
+ media: media,
+ converter: converter,
+ federator: federator,
+ visFilter: visFilter,
+ muteFilter: muteFilter,
+ statusFilter: statusFilter,
}
}
diff --git a/internal/processing/common/media.go b/internal/processing/common/media.go
index 7957470cd..e379bfe40 100644
--- a/internal/processing/common/media.go
+++ b/internal/processing/common/media.go
@@ -22,10 +22,10 @@ import (
"errors"
"fmt"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
)
// StoreLocalMedia is a wrapper around CreateMedia() and
diff --git a/internal/processing/common/status.go b/internal/processing/common/status.go
index 01f2ab72d..2bcf89a02 100644
--- a/internal/processing/common/status.go
+++ b/internal/processing/common/status.go
@@ -21,14 +21,12 @@ import (
"context"
"errors"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
- statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
- "github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
- "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/federation/dereferencing"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
)
// GetOwnStatus fetches the given status with ID,
@@ -214,9 +212,6 @@ func (p *Processor) GetAPIStatus(
apiStatus, err := p.converter.StatusToAPIStatus(ctx,
target,
requester,
- statusfilter.FilterContextNone,
- nil,
- nil,
)
if err != nil {
err := gtserror.Newf("error converting: %w", err)
@@ -236,9 +231,7 @@ func (p *Processor) GetVisibleAPIStatuses(
ctx context.Context,
requester *gtsmodel.Account,
statuses []*gtsmodel.Status,
- filterContext statusfilter.FilterContext,
- filters []*gtsmodel.Filter,
- userMutes []*gtsmodel.UserMute,
+ filterCtx gtsmodel.FilterContext,
) []apimodel.Status {
// Start new log entry with
@@ -247,9 +240,6 @@ func (p *Processor) GetVisibleAPIStatuses(
l := log.WithContext(ctx).
WithField("caller", log.Caller(3))
- // Compile mutes to useable user mutes for type converter.
- compUserMutes := usermute.NewCompiledUserMuteList(userMutes)
-
// Iterate filtered statuses for conversion to API model.
apiStatuses := make([]apimodel.Status, 0, len(statuses))
for _, status := range statuses {
@@ -268,24 +258,44 @@ func (p *Processor) GetVisibleAPIStatuses(
continue
}
+ // Check whether this status is muted by requesting account.
+ muted, err := p.muteFilter.StatusMuted(ctx, requester, status)
+ if err != nil {
+ log.Errorf(ctx, "error checking mute: %v", err)
+ continue
+ }
+
+ if muted {
+ continue
+ }
+
+ // Check whether status is filtered in context by requesting account.
+ filtered, hide, err := p.statusFilter.StatusFilterResultsInContext(ctx,
+ requester,
+ status,
+ filterCtx,
+ )
+ if err != nil {
+ l.Errorf("error filtering: %v", err)
+ continue
+ }
+
+ if hide {
+ continue
+ }
+
// Convert to API status, taking mute / filter into account.
apiStatus, err := p.converter.StatusToAPIStatus(ctx,
status,
requester,
- filterContext,
- filters,
- compUserMutes,
)
- if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) {
+ if err != nil {
l.Errorf("error converting: %v", err)
continue
}
- if apiStatus == nil {
- // Status was
- // filtered out.
- continue
- }
+ // Set filter results on status.
+ apiStatus.Filtered = filtered
// Append converted status to return slice.
apiStatuses = append(apiStatuses, *apiStatus)
@@ -306,25 +316,10 @@ func (p *Processor) InvalidateTimelinedStatus(ctx context.Context, accountID str
return gtserror.Newf("db error getting lists for account %s: %w", accountID, err)
}
- // Start new log entry with
- // the above calling func's name.
- l := log.
- WithContext(ctx).
- WithField("caller", log.Caller(3)).
- WithField("accountID", accountID).
- WithField("statusID", statusID)
-
- // Unprepare item from home + list timelines, just log
- // if something goes wrong since this is not a showstopper.
-
- if err := p.state.Timelines.Home.UnprepareItem(ctx, accountID, statusID); err != nil {
- l.Errorf("error unpreparing item from home timeline: %v", err)
- }
-
+ // Unprepare item from home + list timelines.
+ p.state.Caches.Timelines.Home.MustGet(accountID).UnprepareByStatusIDs(statusID)
for _, list := range lists {
- if err := p.state.Timelines.List.UnprepareItem(ctx, list.ID, statusID); err != nil {
- l.Errorf("error unpreparing item from list timeline %s: %v", list.ID, err)
- }
+ p.state.Caches.Timelines.List.MustGet(list.ID).UnprepareByStatusIDs(statusID)
}
return nil
diff --git a/internal/processing/conversations/conversations.go b/internal/processing/conversations/conversations.go
index d95740605..b80ba659a 100644
--- a/internal/processing/conversations/conversations.go
+++ b/internal/processing/conversations/conversations.go
@@ -21,31 +21,37 @@ import (
"context"
"errors"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
- state *state.State
- converter *typeutils.Converter
- filter *visibility.Filter
+ state *state.State
+ converter *typeutils.Converter
+ visFilter *visibility.Filter
+ muteFilter *mutes.Filter
+ statusFilter *status.Filter
}
func New(
state *state.State,
converter *typeutils.Converter,
- filter *visibility.Filter,
+ visFilter *visibility.Filter,
+ muteFilter *mutes.Filter,
+ statusFilter *status.Filter,
) Processor {
return Processor{
- state: state,
- converter: converter,
- filter: filter,
+ state: state,
+ converter: converter,
+ visFilter: visFilter,
+ muteFilter: muteFilter,
+ statusFilter: statusFilter,
}
}
@@ -93,34 +99,3 @@ func (p *Processor) getConversationOwnedBy(
return conversation, nil
}
-
-// getFiltersAndMutes gets the given account's filters and compiled mute list.
-func (p *Processor) getFiltersAndMutes(
- ctx context.Context,
- requestingAccount *gtsmodel.Account,
-) ([]*gtsmodel.Filter, *usermute.CompiledUserMuteList, gtserror.WithCode) {
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID)
- if err != nil {
- return nil, nil, gtserror.NewErrorInternalError(
- gtserror.Newf(
- "DB error getting filters for account %s: %w",
- requestingAccount.ID,
- err,
- ),
- )
- }
-
- mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), requestingAccount.ID, nil)
- if err != nil {
- return nil, nil, gtserror.NewErrorInternalError(
- gtserror.Newf(
- "DB error getting mutes for account %s: %w",
- requestingAccount.ID,
- err,
- ),
- )
- }
- compiledMutes := usermute.NewCompiledUserMuteList(mutes)
-
- return filters, compiledMutes, nil
-}
diff --git a/internal/processing/conversations/conversations_test.go b/internal/processing/conversations/conversations_test.go
index 831ba1a43..407623964 100644
--- a/internal/processing/conversations/conversations_test.go
+++ b/internal/processing/conversations/conversations_test.go
@@ -22,23 +22,25 @@ import (
"testing"
"time"
+ "code.superseriousbusiness.org/gotosocial/internal/admin"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ dbtest "code.superseriousbusiness.org/gotosocial/internal/db/test"
+ "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/log"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/conversations"
+ "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"
- dbtest "github.com/superseriousbusiness/gotosocial/internal/db/test"
- "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/log"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/processing/conversations"
- "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 ConversationsTestSuite struct {
@@ -53,11 +55,11 @@ type ConversationsTestSuite struct {
federator *federation.Federator
emailSender email.Sender
sentEmails map[string]string
- filter *visibility.Filter
+ visFilter *visibility.Filter
+ muteFilter *mutes.Filter
// 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
@@ -76,7 +78,7 @@ type ConversationsTestSuite struct {
}
func (suite *ConversationsTestSuite) 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)
@@ -84,7 +86,6 @@ func (suite *ConversationsTestSuite) getClientMsg(timeout time.Duration) (*messa
func (suite *ConversationsTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
@@ -106,13 +107,8 @@ func (suite *ConversationsTestSuite) SetupTest() {
suite.state.DB = suite.db
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
suite.tc = typeutils.NewConverter(&suite.state)
- suite.filter = visibility.NewFilter(&suite.state)
-
- testrig.StartTimelines(
- &suite.state,
- suite.filter,
- suite.tc,
- )
+ suite.visFilter = visibility.NewFilter(&suite.state)
+ suite.muteFilter = mutes.NewFilter(&suite.state)
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
@@ -123,7 +119,7 @@ func (suite *ConversationsTestSuite) SetupTest() {
suite.sentEmails = make(map[string]string)
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
- suite.conversationsProcessor = conversations.New(&suite.state, suite.tc, suite.filter)
+ suite.conversationsProcessor = conversations.New(&suite.state, suite.tc, suite.visFilter, suite.muteFilter, status.NewFilter(&suite.state))
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
@@ -138,8 +134,8 @@ func (suite *ConversationsTestSuite) TearDownTest() {
(*gtsmodel.ConversationToStatus)(nil),
}
for _, model := range conversationModels {
- if err := suite.db.DropTable(context.Background(), model); err != nil {
- log.Error(context.Background(), err)
+ if err := suite.db.DropTable(suite.T().Context(), model); err != nil {
+ log.Error(suite.T().Context(), err)
}
}
diff --git a/internal/processing/conversations/delete.go b/internal/processing/conversations/delete.go
index 5cbdd00a5..0f8281716 100644
--- a/internal/processing/conversations/delete.go
+++ b/internal/processing/conversations/delete.go
@@ -20,9 +20,9 @@ package conversations
import (
"context"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
func (p *Processor) Delete(
diff --git a/internal/processing/conversations/delete_test.go b/internal/processing/conversations/delete_test.go
index 23b4f1c1a..d3d8d47a6 100644
--- a/internal/processing/conversations/delete_test.go
+++ b/internal/processing/conversations/delete_test.go
@@ -17,11 +17,9 @@
package conversations_test
-import "context"
-
func (suite *ConversationsTestSuite) TestDelete() {
conversation := suite.NewTestConversation(suite.testAccount, 0)
- err := suite.conversationsProcessor.Delete(context.Background(), suite.testAccount, conversation.ID)
+ err := suite.conversationsProcessor.Delete(suite.T().Context(), suite.testAccount, conversation.ID)
suite.NoError(err)
}
diff --git a/internal/processing/conversations/get.go b/internal/processing/conversations/get.go
index 0c7832cae..cdc0756c3 100644
--- a/internal/processing/conversations/get.go
+++ b/internal/processing/conversations/get.go
@@ -21,13 +21,13 @@ 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/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/log"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// GetAll returns conversations owned by the given account.
@@ -64,23 +64,29 @@ func (p *Processor) GetAll(
items := make([]interface{}, 0, count)
- filters, mutes, errWithCode := p.getFiltersAndMutes(ctx, requestingAccount)
- if errWithCode != nil {
- return nil, errWithCode
- }
-
for _, conversation := range conversations {
+ // Check whether status if filtered by local participant in context.
+ filtered, hide, err := p.statusFilter.StatusFilterResultsInContext(ctx,
+ requestingAccount,
+ conversation.LastStatus,
+ gtsmodel.FilterContextNotifications,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error filtering status: %v", err)
+ continue
+ }
+
+ if hide {
+ continue
+ }
+
// Convert conversation to frontend API model.
- apiConversation, err := p.converter.ConversationToAPIConversation(
- ctx,
+ apiConversation, err := p.converter.ConversationToAPIConversation(ctx,
conversation,
requestingAccount,
- filters,
- mutes,
)
if err != nil {
- log.Errorf(
- ctx,
+ log.Errorf(ctx,
"error converting conversation %s to API representation: %v",
conversation.ID,
err,
@@ -88,6 +94,9 @@ func (p *Processor) GetAll(
continue
}
+ // Set filter results on attached status model.
+ apiConversation.LastStatus.Filtered = filtered
+
// Append conversation to return items.
items = append(items, apiConversation)
}
diff --git a/internal/processing/conversations/get_test.go b/internal/processing/conversations/get_test.go
index 7b3d60749..9106c930c 100644
--- a/internal/processing/conversations/get_test.go
+++ b/internal/processing/conversations/get_test.go
@@ -18,16 +18,15 @@
package conversations_test
import (
- "context"
"time"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
)
func (suite *ConversationsTestSuite) TestGetAll() {
conversation := suite.NewTestConversation(suite.testAccount, 0)
- resp, err := suite.conversationsProcessor.GetAll(context.Background(), suite.testAccount, nil)
+ resp, err := suite.conversationsProcessor.GetAll(suite.T().Context(), suite.testAccount, nil)
if suite.NoError(err) && suite.Len(resp.Items, 1) && suite.IsType((*apimodel.Conversation)(nil), resp.Items[0]) {
apiConversation := resp.Items[0].(*apimodel.Conversation)
suite.Equal(conversation.ID, apiConversation.ID)
@@ -46,11 +45,11 @@ func (suite *ConversationsTestSuite) TestGetAllOrder() {
// Add an even newer status than that to conversation1.
conversation1Status2 := suite.NewTestStatus(suite.testAccount, conversation1.LastStatus.ThreadID, 2*time.Second, conversation1.LastStatus)
conversation1.LastStatusID = conversation1Status2.ID
- if err := suite.db.UpsertConversation(context.Background(), conversation1, "last_status_id"); err != nil {
+ if err := suite.db.UpsertConversation(suite.T().Context(), conversation1, "last_status_id"); err != nil {
suite.FailNow(err.Error())
}
- resp, err := suite.conversationsProcessor.GetAll(context.Background(), suite.testAccount, nil)
+ resp, err := suite.conversationsProcessor.GetAll(suite.T().Context(), suite.testAccount, nil)
if suite.NoError(err) && suite.Len(resp.Items, 2) {
// conversation1 should be the first conversation returned.
apiConversation1 := resp.Items[0].(*apimodel.Conversation)
diff --git a/internal/processing/conversations/migrate.go b/internal/processing/conversations/migrate.go
index 959ffcca4..902024944 100644
--- a/internal/processing/conversations/migrate.go
+++ b/internal/processing/conversations/migrate.go
@@ -22,12 +22,12 @@ import (
"encoding/json"
"errors"
- "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/util"
+ "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/util"
)
const advancedMigrationID = "20240611190733_add_conversations"
diff --git a/internal/processing/conversations/migrate_test.go b/internal/processing/conversations/migrate_test.go
index b625e59ba..a6e506e49 100644
--- a/internal/processing/conversations/migrate_test.go
+++ b/internal/processing/conversations/migrate_test.go
@@ -18,18 +18,16 @@
package conversations_test
import (
- "context"
-
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/db/bundb"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/db/bundb"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// Test that we can migrate DMs to conversations.
// This test assumes that we're using the standard test fixtures, which contain some conversation-eligible DMs.
func (suite *ConversationsTestSuite) TestMigrateDMsToConversations() {
advancedMigrationID := "20240611190733_add_conversations"
- ctx := context.Background()
+ ctx := suite.T().Context()
rawDB := (suite.db).(*bundb.DBService).DB()
// Precondition: we shouldn't have any conversations yet.
diff --git a/internal/processing/conversations/read.go b/internal/processing/conversations/read.go
index 512a004a3..c7e4f1acd 100644
--- a/internal/processing/conversations/read.go
+++ b/internal/processing/conversations/read.go
@@ -20,10 +20,11 @@ package conversations
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/util"
+ 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"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
func (p *Processor) Read(
@@ -44,22 +45,27 @@ func (p *Processor) Read(
return nil, gtserror.NewErrorInternalError(err)
}
- filters, mutes, errWithCode := p.getFiltersAndMutes(ctx, requestingAccount)
- if errWithCode != nil {
- return nil, errWithCode
+ // Check whether status if filtered by local participant in context.
+ filtered, _, err := p.statusFilter.StatusFilterResultsInContext(ctx,
+ requestingAccount,
+ conversation.LastStatus,
+ gtsmodel.FilterContextNotifications,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error filtering status: %v", err)
}
- apiConversation, err := p.converter.ConversationToAPIConversation(
- ctx,
+ apiConversation, err := p.converter.ConversationToAPIConversation(ctx,
conversation,
requestingAccount,
- filters,
- mutes,
)
if err != nil {
err = gtserror.Newf("error converting conversation %s to API representation: %w", id, err)
return nil, gtserror.NewErrorInternalError(err)
}
+ // Set filter results on attached status model.
+ apiConversation.LastStatus.Filtered = filtered
+
return apiConversation, nil
}
diff --git a/internal/processing/conversations/read_test.go b/internal/processing/conversations/read_test.go
index ebd8f7fe5..a15ddcca3 100644
--- a/internal/processing/conversations/read_test.go
+++ b/internal/processing/conversations/read_test.go
@@ -18,16 +18,14 @@
package conversations_test
import (
- "context"
-
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
func (suite *ConversationsTestSuite) TestRead() {
conversation := suite.NewTestConversation(suite.testAccount, 0)
suite.False(util.PtrOrValue(conversation.Read, false))
- apiConversation, err := suite.conversationsProcessor.Read(context.Background(), suite.testAccount, conversation.ID)
+ apiConversation, err := suite.conversationsProcessor.Read(suite.T().Context(), suite.testAccount, conversation.ID)
if suite.NoError(err) {
suite.False(apiConversation.Unread)
}
diff --git a/internal/processing/conversations/update.go b/internal/processing/conversations/update.go
index 7445994ae..21f1cf915 100644
--- a/internal/processing/conversations/update.go
+++ b/internal/processing/conversations/update.go
@@ -21,20 +21,23 @@ 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/id"
- "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/id"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
-// ConversationNotification carries the arguments to processing/stream.Processor.Conversation.
+// ConversationNotification carries the arguments
+// to processing/stream.Processor.Conversation.
type ConversationNotification struct {
- // AccountID of a local account to deliver the notification to.
+
+ // AccountID of a local account to
+ // deliver the notification to.
AccountID string
+
// Conversation as the notification payload.
Conversation *apimodel.Conversation
}
@@ -46,11 +49,13 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt
// Only DMs are considered part of conversations.
return nil, nil
}
+
if status.BoostOfID != "" {
// Boosts can't be part of conversations.
// FUTURE: This may change if we ever implement quote posts.
return nil, nil
}
+
if status.ThreadID == "" {
// If the status doesn't have a thread ID, it didn't mention a local account,
// and thus can't be part of a conversation.
@@ -77,51 +82,15 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt
}
localAccount := participant
- // If the status is not visible to this account, skip processing it for this account.
- visible, err := p.filter.StatusVisible(ctx, localAccount, status)
+ // If status not visible to this account, skip further processing.
+ visible, err := p.visFilter.StatusVisible(ctx, localAccount, status)
if err != nil {
- log.Errorf(
- ctx,
- "error checking status %s visibility for account %s: %v",
- status.ID,
- localAccount.ID,
- err,
- )
+ log.Errorf(ctx, "error checking status %s visibility for account %s: %v", status.URI, localAccount.URI, err)
continue
} else if !visible {
continue
}
- // Is the status filtered or muted for this user?
- // Converting the status to an API status runs the filter/mute checks.
- filters, mutes, errWithCode := p.getFiltersAndMutes(ctx, localAccount)
- if errWithCode != nil {
- log.Error(ctx, errWithCode)
- continue
- }
- _, err = p.converter.StatusToAPIStatus(
- ctx,
- status,
- localAccount,
- statusfilter.FilterContextNotifications,
- filters,
- mutes,
- )
- if err != nil {
- // If the status matched a hide filter, skip processing it for this account.
- // If there was another kind of error, log that and skip it anyway.
- if !errors.Is(err, statusfilter.ErrHideStatus) {
- log.Errorf(
- ctx,
- "error checking status %s filtering/muting for account %s: %v",
- status.ID,
- localAccount.ID,
- err,
- )
- }
- continue
- }
-
// Collect other accounts participating in the conversation.
otherAccounts := make([]*gtsmodel.Account, 0, len(allParticipantsSet)-1)
otherAccountIDs := make([]string, 0, len(allParticipantsSet)-1)
@@ -133,20 +102,14 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt
}
// Check for a previously existing conversation, if there is one.
- conversation, err := p.state.DB.GetConversationByThreadAndAccountIDs(
- ctx,
+ conversation, err := p.state.DB.GetConversationByThreadAndAccountIDs(ctx,
status.ThreadID,
localAccount.ID,
otherAccountIDs,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- log.Errorf(
- ctx,
- "error trying to find a previous conversation for status %s and account %s: %v",
- status.ID,
- localAccount.ID,
- err,
- )
+ log.Errorf(ctx, "error finding previous conversation for status %s and account %s: %v",
+ status.URI, localAccount.URI, err)
continue
}
@@ -172,6 +135,7 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt
conversation.LastStatusID = status.ID
conversation.LastStatus = status
}
+
// If the conversation is unread, leave it marked as unread.
// If the conversation is read but this status might not have been, mark the conversation as unread.
if !statusAuthoredByConversationOwner {
@@ -181,61 +145,75 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt
// Create or update the conversation.
err = p.state.DB.UpsertConversation(ctx, conversation)
if err != nil {
- log.Errorf(
- ctx,
- "error creating or updating conversation %s for status %s and account %s: %v",
- conversation.ID,
- status.ID,
- localAccount.ID,
- err,
- )
+ log.Errorf(ctx, "error creating or updating conversation %s for status %s and account %s: %v",
+ conversation.ID, status.URI, localAccount.URI, err)
continue
}
// Link the conversation to the status.
if err := p.state.DB.LinkConversationToStatus(ctx, conversation.ID, status.ID); err != nil {
- log.Errorf(
- ctx,
- "error linking conversation %s to status %s: %v",
- conversation.ID,
- status.ID,
- err,
- )
+ log.Errorf(ctx, "error linking conversation %s to status %s: %v",
+ conversation.ID, status.URI, err)
+ continue
+ }
+
+ // If status was authored by this participant,
+ // don't bother notifying, they already know!
+ if status.AccountID == localAccount.ID {
+ continue
+ }
+
+ // Check whether status is muted to local participant.
+ muted, err := p.muteFilter.StatusNotificationsMuted(ctx,
+ localAccount,
+ status,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error checking status mute: %v", err)
+ continue
+ }
+
+ if muted {
+ continue
+ }
+
+ // Check whether status if filtered by local participant in context.
+ filtered, hide, err := p.statusFilter.StatusFilterResultsInContext(ctx,
+ localAccount,
+ status,
+ gtsmodel.FilterContextNotifications,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error filtering status: %v", err)
+ continue
+ }
+
+ if hide {
continue
}
// Convert the conversation to API representation.
- apiConversation, err := p.converter.ConversationToAPIConversation(
- ctx,
+ apiConversation, err := p.converter.ConversationToAPIConversation(ctx,
conversation,
localAccount,
- filters,
- mutes,
)
if err != nil {
- // If the conversation's last status matched a hide filter, skip it.
- // If there was another kind of error, log that and skip it anyway.
- if !errors.Is(err, statusfilter.ErrHideStatus) {
- log.Errorf(
- ctx,
- "error converting conversation %s to API representation for account %s: %v",
- status.ID,
- localAccount.ID,
- err,
- )
- }
+ log.Errorf(ctx, "error converting conversation %s to API representation for account %s: %v",
+ status.ID,
+ localAccount.ID,
+ err,
+ )
continue
}
+ // Set filter results on attached status model.
+ apiConversation.LastStatus.Filtered = filtered
+
// Generate a notification,
- // unless the status was authored by the user who would be notified,
- // in which case they already know.
- if status.AccountID != localAccount.ID {
- notifications = append(notifications, ConversationNotification{
- AccountID: localAccount.ID,
- Conversation: apiConversation,
- })
- }
+ notifications = append(notifications, ConversationNotification{
+ AccountID: localAccount.ID,
+ Conversation: apiConversation,
+ })
}
return notifications, nil
diff --git a/internal/processing/conversations/update_test.go b/internal/processing/conversations/update_test.go
index 8ba2800fe..b547fae7c 100644
--- a/internal/processing/conversations/update_test.go
+++ b/internal/processing/conversations/update_test.go
@@ -17,13 +17,9 @@
package conversations_test
-import (
- "context"
-)
-
// Test that we can create conversations when a new status comes in.
func (suite *ConversationsTestSuite) TestUpdateConversationsForStatus() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// Precondition: the test user shouldn't have any conversations yet.
conversations, err := suite.db.GetConversationsByOwnerAccountID(ctx, suite.testAccount.ID, nil)
diff --git a/internal/processing/fedi/accept.go b/internal/processing/fedi/accept.go
deleted file mode 100644
index fc699ee08..000000000
--- a/internal/processing/fedi/accept.go
+++ /dev/null
@@ -1,85 +0,0 @@
-// 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 fedi
-
-import (
- "context"
- "errors"
-
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
-)
-
-// AcceptGet handles the getting of a fedi/activitypub
-// representation of a local interaction acceptance.
-//
-// It performs appropriate authentication before
-// returning a JSON serializable interface.
-func (p *Processor) AcceptGet(
- ctx context.Context,
- requestedUser string,
- reqID string,
-) (interface{}, gtserror.WithCode) {
- // Authenticate incoming request, getting related accounts.
- auth, errWithCode := p.authenticate(ctx, requestedUser)
- if errWithCode != nil {
- return nil, errWithCode
- }
-
- if auth.handshakingURI != nil {
- // We're currently handshaking, which means
- // we don't know this account yet. This should
- // be a very rare race condition.
- err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- receivingAcct := auth.receivingAcct
-
- req, err := p.state.DB.GetInteractionRequestByID(ctx, reqID)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err := gtserror.Newf("db error getting interaction request %s: %w", reqID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if req == nil || !req.IsAccepted() {
- // Request doesn't exist or hasn't been accepted.
- err := gtserror.Newf("interaction request %s not found", reqID)
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- if req.TargetAccountID != receivingAcct.ID {
- const text = "interaction request does not belong to receiving account"
- return nil, gtserror.NewErrorNotFound(errors.New(text))
- }
-
- accept, err := p.converter.InteractionReqToASAccept(ctx, req)
- if err != nil {
- err := gtserror.Newf("error converting accept: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- data, err := ap.Serialize(accept)
- if err != nil {
- err := gtserror.Newf("error serializing accept: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return data, nil
-}
diff --git a/internal/processing/fedi/authorization.go b/internal/processing/fedi/authorization.go
new file mode 100644
index 000000000..276dd3a82
--- /dev/null
+++ b/internal/processing/fedi/authorization.go
@@ -0,0 +1,142 @@
+// 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 fedi
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+)
+
+// AuthorizationGet handles the getting of a fedi/activitypub
+// representation of a local interaction authorization.
+//
+// It performs appropriate authentication before
+// returning a JSON serializable interface.
+func (p *Processor) AuthorizationGet(
+ ctx context.Context,
+ requestedUser string,
+ intReqID string,
+) (any, gtserror.WithCode) {
+ // Ensure valid request, intReq exists, etc.
+ intReq, errWithCode := p.validateAuthGetRequest(ctx, requestedUser, intReqID)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Convert + serialize the Authorization.
+ authorization, err := p.converter.InteractionReqToASAuthorization(ctx, intReq)
+ if err != nil {
+ err := gtserror.Newf("error converting to authorization: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ data, err := ap.Serialize(authorization)
+ if err != nil {
+ err := gtserror.Newf("error serializing accept: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
+
+// AcceptGet handles the getting of a fedi/activitypub
+// representation of a local interaction acceptance.
+//
+// It performs appropriate authentication before
+// returning a JSON serializable interface.
+func (p *Processor) AcceptGet(
+ ctx context.Context,
+ requestedUser string,
+ intReqID string,
+) (any, gtserror.WithCode) {
+ // Ensure valid request, intReq exists, etc.
+ intReq, errWithCode := p.validateAuthGetRequest(ctx, requestedUser, intReqID)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Convert + serialize the Accept.
+ accept, err := p.converter.InteractionReqToASAccept(ctx, intReq)
+ if err != nil {
+ err := gtserror.Newf("error converting to accept: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ data, err := ap.Serialize(accept)
+ if err != nil {
+ err := gtserror.Newf("error serializing accept: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return data, nil
+}
+
+// validateAuthGetRequest is a shortcut function
+// for returning an accepted interaction request
+// targeting `requestedUser`.
+func (p *Processor) validateAuthGetRequest(
+ ctx context.Context,
+ requestedUser string,
+ intReqID string,
+) (*gtsmodel.InteractionRequest, gtserror.WithCode) {
+ // Authenticate incoming request, getting related accounts.
+ auth, errWithCode := p.authenticate(ctx, requestedUser)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ if auth.handshakingURI != nil {
+ // We're currently handshaking, which means we don't know
+ // this account yet. This should be a very rare race condition.
+ err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Fetch interaction request with the given ID.
+ req, err := p.state.DB.GetInteractionRequestByID(ctx, intReqID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting interaction request %s: %w", intReqID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Ensure that this is an existing
+ // and *accepted* interaction request.
+ if req == nil || !req.IsAccepted() {
+ const text = "interaction request not found"
+ return nil, gtserror.NewErrorNotFound(errors.New(text))
+ }
+
+ // Ensure interaction request was accepted
+ // by the account in the request path.
+ if req.TargetAccountID != auth.receiver.ID {
+ text := fmt.Sprintf(
+ "account %s is not targeted by interaction request %s and therefore can't accept it",
+ requestedUser, intReqID,
+ )
+ return nil, gtserror.NewErrorNotFound(errors.New(text))
+ }
+
+ // All fine.
+ return req, nil
+}
diff --git a/internal/processing/fedi/collections.go b/internal/processing/fedi/collections.go
index fd84e7688..b67651dff 100644
--- a/internal/processing/fedi/collections.go
+++ b/internal/processing/fedi/collections.go
@@ -23,14 +23,14 @@ import (
"net/http"
"net/url"
- "github.com/superseriousbusiness/activity/streams/vocab"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/paging"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "code.superseriousbusiness.org/activity/streams/vocab"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// InboxPost handles POST requests to a user's inbox for new activitypub messages.
@@ -54,24 +54,24 @@ func (p *Processor) OutboxGet(
ctx context.Context,
requestedUser string,
page *paging.Page,
-) (interface{}, gtserror.WithCode) {
+) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
return nil, errWithCode
}
- receivingAcct := auth.receivingAcct
+ receiver := auth.receiver
// Parse the collection ID object from account's followers URI.
- collectionID, err := url.Parse(receivingAcct.OutboxURI)
+ collectionID, err := url.Parse(receiver.OutboxURI)
if err != nil {
- err := gtserror.Newf("error parsing account outbox uri %s: %w", receivingAcct.OutboxURI, err)
+ err := gtserror.Newf("error parsing account outbox uri %s: %w", receiver.OutboxURI, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Ensure we have stats for this account.
- if err := p.state.DB.PopulateAccountStats(ctx, receivingAcct); err != nil {
- err := gtserror.Newf("error getting stats for account %s: %w", receivingAcct.ID, err)
+ if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil {
+ err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -83,8 +83,8 @@ func (p *Processor) OutboxGet(
switch {
- case *receivingAcct.Settings.HideCollections ||
- receivingAcct.IsInstance():
+ case receiver.IsInstance() ||
+ *receiver.Settings.HideCollections:
// If account that hides collections, or instance
// account (ie., can't post / have relationships),
// just return barest stub of collection.
@@ -94,7 +94,7 @@ func (p *Processor) OutboxGet(
// If paging disabled, or we're currently handshaking
// the requester, just return collection that links
// to first page (i.e. path below), with no items.
- params.Total = util.Ptr(*receivingAcct.Stats.StatusesCount)
+ params.Total = util.Ptr(*receiver.Stats.StatusesCount)
params.First = new(paging.Page)
params.Query = make(url.Values, 1)
params.Query.Set("limit", "40") // enables paging
@@ -105,7 +105,7 @@ func (p *Processor) OutboxGet(
// Get page of full public statuses.
statuses, err := p.state.DB.GetAccountStatuses(
ctx,
- receivingAcct.ID,
+ receiver.ID,
page.GetLimit(), // limit
true, // excludeReplies
true, // excludeReblogs
@@ -133,7 +133,7 @@ func (p *Processor) OutboxGet(
// (eg., local-only statuses, if the requester is remote).
statuses, err = p.visFilter.StatusesVisible(
ctx,
- auth.requestingAcct,
+ auth.requester,
statuses,
)
if err != nil {
@@ -142,7 +142,7 @@ func (p *Processor) OutboxGet(
}
// Start building AS collection page params.
- params.Total = util.Ptr(*receivingAcct.Stats.StatusesCount)
+ params.Total = util.Ptr(*receiver.Stats.StatusesCount)
var pageParams ap.CollectionPageParams
pageParams.CollectionParams = params
@@ -194,24 +194,24 @@ func (p *Processor) FollowersGet(
ctx context.Context,
requestedUser string,
page *paging.Page,
-) (interface{}, gtserror.WithCode) {
+) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
return nil, errWithCode
}
- receivingAcct := auth.receivingAcct
+ receiver := auth.receiver
// Parse the collection ID object from account's followers URI.
- collectionID, err := url.Parse(receivingAcct.FollowersURI)
+ collectionID, err := url.Parse(receiver.FollowersURI)
if err != nil {
- err := gtserror.Newf("error parsing account followers uri %s: %w", receivingAcct.FollowersURI, err)
+ err := gtserror.Newf("error parsing account followers uri %s: %w", receiver.FollowersURI, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Ensure we have stats for this account.
- if err := p.state.DB.PopulateAccountStats(ctx, receivingAcct); err != nil {
- err := gtserror.Newf("error getting stats for account %s: %w", receivingAcct.ID, err)
+ if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil {
+ err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -223,8 +223,8 @@ func (p *Processor) FollowersGet(
switch {
- case receivingAcct.IsInstance() ||
- *receivingAcct.Settings.HideCollections:
+ case receiver.IsInstance() ||
+ *receiver.Settings.HideCollections:
// If account that hides collections, or instance
// account (ie., can't post / have relationships),
// just return barest stub of collection.
@@ -234,7 +234,7 @@ func (p *Processor) FollowersGet(
// If paging disabled, or we're currently handshaking
// the requester, just return collection that links
// to first page (i.e. path below), with no items.
- params.Total = util.Ptr(*receivingAcct.Stats.FollowersCount)
+ params.Total = util.Ptr(*receiver.Stats.FollowersCount)
params.First = new(paging.Page)
params.Query = make(url.Values, 1)
params.Query.Set("limit", "40") // enables paging
@@ -243,7 +243,7 @@ func (p *Processor) FollowersGet(
default:
// Paging enabled.
// Get page of full follower objects with attached accounts.
- followers, err := p.state.DB.GetAccountFollowers(ctx, receivingAcct.ID, page)
+ followers, err := p.state.DB.GetAccountFollowers(ctx, receiver.ID, page)
if err != nil {
err := gtserror.Newf("error getting followers: %w", err)
return nil, gtserror.NewErrorInternalError(err)
@@ -260,7 +260,7 @@ func (p *Processor) FollowersGet(
}
// Start building AS collection page params.
- params.Total = util.Ptr(*receivingAcct.Stats.FollowersCount)
+ params.Total = util.Ptr(*receiver.Stats.FollowersCount)
var pageParams ap.CollectionPageParams
pageParams.CollectionParams = params
@@ -306,24 +306,24 @@ func (p *Processor) FollowersGet(
// FollowingGet returns the serialized ActivityPub
// collection of a local account's following collection,
// which contains links to accounts followed by this account.
-func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page *paging.Page) (interface{}, gtserror.WithCode) {
+func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page *paging.Page) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
return nil, errWithCode
}
- receivingAcct := auth.receivingAcct
+ receiver := auth.receiver
// Parse collection ID from account's following URI.
- collectionID, err := url.Parse(receivingAcct.FollowingURI)
+ collectionID, err := url.Parse(receiver.FollowingURI)
if err != nil {
- err := gtserror.Newf("error parsing account following uri %s: %w", receivingAcct.FollowingURI, err)
+ err := gtserror.Newf("error parsing account following uri %s: %w", receiver.FollowingURI, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Ensure we have stats for this account.
- if err := p.state.DB.PopulateAccountStats(ctx, receivingAcct); err != nil {
- err := gtserror.Newf("error getting stats for account %s: %w", receivingAcct.ID, err)
+ if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil {
+ err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err)
return nil, gtserror.NewErrorInternalError(err)
}
@@ -334,8 +334,8 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
params.ID = collectionID
switch {
- case receivingAcct.IsInstance() ||
- *receivingAcct.Settings.HideCollections:
+ case receiver.IsInstance() ||
+ *receiver.Settings.HideCollections:
// If account that hides collections, or instance
// account (ie., can't post / have relationships),
// just return barest stub of collection.
@@ -345,7 +345,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
// If paging disabled, or we're currently handshaking
// the requester, just return collection that links
// to first page (i.e. path below), with no items.
- params.Total = util.Ptr(*receivingAcct.Stats.FollowingCount)
+ params.Total = util.Ptr(*receiver.Stats.FollowingCount)
params.First = new(paging.Page)
params.Query = make(url.Values, 1)
params.Query.Set("limit", "40") // enables paging
@@ -354,7 +354,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
default:
// Paging enabled.
// Get page of full follower objects with attached accounts.
- follows, err := p.state.DB.GetAccountFollows(ctx, receivingAcct.ID, page)
+ follows, err := p.state.DB.GetAccountFollows(ctx, receiver.ID, page)
if err != nil {
err := gtserror.Newf("error getting follows: %w", err)
return nil, gtserror.NewErrorInternalError(err)
@@ -371,7 +371,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
}
// Start AS collection page params.
- params.Total = util.Ptr(*receivingAcct.Stats.FollowingCount)
+ params.Total = util.Ptr(*receiver.Stats.FollowingCount)
var pageParams ap.CollectionPageParams
pageParams.CollectionParams = params
@@ -416,28 +416,29 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
// FeaturedCollectionGet returns an ordered collection of the requested username's Pinned posts.
// The returned collection have an `items` property which contains an ordered list of status URIs.
-func (p *Processor) FeaturedCollectionGet(ctx context.Context, requestedUser string) (interface{}, gtserror.WithCode) {
+func (p *Processor) FeaturedCollectionGet(ctx context.Context, requestedUser string) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
return nil, errWithCode
}
- receivingAcct := auth.receivingAcct
+ receiver := auth.receiver
- statuses, err := p.state.DB.GetAccountPinnedStatuses(ctx, receivingAcct.ID)
- if err != nil {
- if !errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorInternalError(err)
- }
+ statuses, err := p.state.DB.GetAccountPinnedStatuses(ctx, receiver.ID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting pinned statuses: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- collection, err := p.converter.StatusesToASFeaturedCollection(ctx, receivingAcct.FeaturedCollectionURI, statuses)
+ collection, err := p.converter.StatusesToASFeaturedCollection(ctx, receiver.FeaturedCollectionURI, statuses)
if err != nil {
+ err := gtserror.Newf("error converting pinned statuses: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
data, err := ap.Serialize(collection)
if err != nil {
+ err := gtserror.Newf("error serializing: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/fedi/common.go b/internal/processing/fedi/common.go
index 1a4d38bc1..ff6ed6fd4 100644
--- a/internal/processing/fedi/common.go
+++ b/internal/processing/fedi/common.go
@@ -22,28 +22,30 @@ import (
"errors"
"net/url"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
type commonAuth struct {
handshakingURI *url.URL // Set to requestingAcct's URI if we're currently handshaking them.
- requestingAcct *gtsmodel.Account // Remote account making request to this instance.
- receivingAcct *gtsmodel.Account // Local account receiving the request.
+ requester *gtsmodel.Account // Remote account making request to this instance.
+ receiver *gtsmodel.Account // Local account receiving the request.
}
+// authenticate is a util function for authenticating a signed GET
+// request to one of the AP/fedi resources handled in this package.
func (p *Processor) authenticate(ctx context.Context, requestedUser string) (*commonAuth, gtserror.WithCode) {
- // First get the requested (receiving) LOCAL account with username from database.
+ // Get the requested local account
+ // with given username from database.
receiver, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUser, "")
- if err != nil {
- if !errors.Is(err, db.ErrNoEntries) {
- // Real db error.
- err = gtserror.Newf("db error getting account %s: %w", requestedUser, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err = gtserror.Newf("db error getting account %s: %w", requestedUser, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
- // Account just not found in the db.
+ if receiver == nil {
+ err := gtserror.Newf("account %s not found in the db", requestedUser)
return nil, gtserror.NewErrorNotFound(err)
}
@@ -59,25 +61,44 @@ func (p *Processor) authenticate(ctx context.Context, requestedUser string) (*co
// don't know the requester yet.
return &commonAuth{
handshakingURI: pubKeyAuth.OwnerURI,
- receivingAcct: receiver,
+ receiver: receiver,
}, nil
}
// Get requester from auth.
requester := pubKeyAuth.Owner
- // Ensure block does not exist between receiver and requester.
- blocked, err := p.state.DB.IsEitherBlocked(ctx, receiver.ID, requester.ID)
+ // Check if requester is suspended.
+ switch {
+ case !requester.IsSuspended():
+ // No problem.
+
+ case requester.DeletedSelf():
+ // Requester deleted their own account.
+ // Why are they now requesting something?
+ err := gtserror.Newf("requester %s self-deleted", requester.UsernameDomain())
+ return nil, gtserror.NewErrorUnauthorized(err)
+
+ default:
+ // Admin from our instance likely suspended account.
+ err := gtserror.Newf("requester %s is suspended", requester.UsernameDomain())
+ return nil, gtserror.NewErrorForbidden(err)
+ }
+
+ // Ensure receiver does not block requester.
+ blocked, err := p.state.DB.IsBlocked(ctx, receiver.ID, requester.ID)
if err != nil {
- err := gtserror.Newf("error checking block: %w", err)
+ err := gtserror.Newf("db error checking block: %w", err)
return nil, gtserror.NewErrorInternalError(err)
- } else if blocked {
- const text = "block exists between accounts"
+ }
+
+ if blocked {
+ var text = requestedUser + " blocks " + requester.Username
return nil, gtserror.NewErrorForbidden(errors.New(text))
}
return &commonAuth{
- requestingAcct: requester,
- receivingAcct: receiver,
+ requester: requester,
+ receiver: receiver,
}, nil
}
diff --git a/internal/processing/fedi/emoji.go b/internal/processing/fedi/emoji.go
index 9ac0ea244..e7e3ec406 100644
--- a/internal/processing/fedi/emoji.go
+++ b/internal/processing/fedi/emoji.go
@@ -19,38 +19,69 @@ package fedi
import (
"context"
- "fmt"
+ "errors"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
)
-// EmojiGet handles the GET for a federated emoji originating from this instance.
-func (p *Processor) EmojiGet(ctx context.Context, requestedEmojiID string) (interface{}, gtserror.WithCode) {
- if _, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, ""); errWithCode != nil {
+// EmojiGet handles the GET for an emoji originating from this instance.
+func (p *Processor) EmojiGet(ctx context.Context, emojiID string) (any, gtserror.WithCode) {
+ // Authenticate incoming request.
+ //
+ // Pass hostname string to this function to indicate
+ // it's the instance account being requested, as
+ // emojis are always owned by the instance account.
+ auth, errWithCode := p.authenticate(ctx, config.GetHost())
+ if errWithCode != nil {
return nil, errWithCode
}
- requestedEmoji, err := p.state.DB.GetEmojiByID(ctx, requestedEmojiID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting emoji with id %s: %s", requestedEmojiID, err))
+ if auth.handshakingURI != nil {
+ // We're currently handshaking, which means
+ // we don't know this account yet. This should
+ // be a very rare race condition.
+ err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Get the requested emoji.
+ emoji, err := p.state.DB.GetEmojiByID(ctx, emojiID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting emoji %s: %w", emojiID, err)
+ return nil, gtserror.NewErrorNotFound(err)
}
- if !requestedEmoji.IsLocal() {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s doesn't belong to this instance (domain %s)", requestedEmojiID, requestedEmoji.Domain))
+ if emoji == nil {
+ err := gtserror.Newf("emoji %s not found in the db", emojiID)
+ return nil, gtserror.NewErrorNotFound(err)
}
- if *requestedEmoji.Disabled {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s has been disabled", requestedEmojiID))
+ // Only serve *our*
+ // emojis on this path.
+ if !emoji.IsLocal() {
+ err := gtserror.Newf("emoji %s doesn't belong to this instance (domain is %s)", emojiID, emoji.Domain)
+ return nil, gtserror.NewErrorNotFound(err)
}
- apEmoji, err := p.converter.EmojiToAS(ctx, requestedEmoji)
+ // Don't serve emojis that have
+ // been disabled by an admin.
+ if *emoji.Disabled {
+ err := gtserror.Newf("emoji with id %s has been disabled by an admin", emojiID)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ apEmoji, err := p.converter.EmojiToAS(ctx, emoji)
if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting gtsmodel emoji with id %s to ap emoji: %s", requestedEmojiID, err))
+ err := gtserror.Newf("error converting emoji %s to ap: %s", emojiID, err)
+ return nil, gtserror.NewErrorInternalError(err)
}
data, err := ap.Serialize(apEmoji)
if err != nil {
+ err := gtserror.Newf("error serializing emoji %s: %w", emojiID, err)
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/fedi/fedi.go b/internal/processing/fedi/fedi.go
index 52a9d70bf..d770f3927 100644
--- a/internal/processing/fedi/fedi.go
+++ b/internal/processing/fedi/fedi.go
@@ -18,11 +18,11 @@
package fedi
import (
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/fedi/status.go b/internal/processing/fedi/status.go
index fe07b5a95..d1de6f4c1 100644
--- a/internal/processing/fedi/status.go
+++ b/internal/processing/fedi/status.go
@@ -24,18 +24,23 @@ import (
"slices"
"strconv"
- "github.com/superseriousbusiness/activity/streams/vocab"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/paging"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "code.superseriousbusiness.org/activity/streams/vocab"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "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"
)
-// StatusGet handles the getting of a fedi/activitypub representation of a local status.
+// StatusGet handles getting an AP representation of a local status.
// It performs appropriate authentication before returning a JSON serializable interface.
-func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusID string) (interface{}, gtserror.WithCode) {
+func (p *Processor) StatusGet(
+ ctx context.Context,
+ requestedUser string,
+ statusID string,
+) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
@@ -49,16 +54,23 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI
err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
return nil, gtserror.NewErrorInternalError(err)
}
-
- receivingAcct := auth.receivingAcct
- requestingAcct := auth.requestingAcct
+ receiver := auth.receiver
+ requester := auth.requester
status, err := p.state.DB.GetStatusByID(ctx, statusID)
- if err != nil {
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting status: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if status == nil {
+ // TODO: Update this to serve "gone"
+ // when a status has been deleted.
+ err := gtserror.Newf("status %s not found in the db", statusID)
return nil, gtserror.NewErrorNotFound(err)
}
- if status.AccountID != receivingAcct.ID {
+ if status.AccountID != receiver.ID {
const text = "status does not belong to receiving account"
return nil, gtserror.NewErrorNotFound(errors.New(text))
}
@@ -68,7 +80,7 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI
return nil, gtserror.NewErrorNotFound(errors.New(text))
}
- visible, err := p.visFilter.StatusVisible(ctx, requestingAcct, status)
+ visible, err := p.visFilter.StatusVisible(ctx, requester, status)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -93,7 +105,7 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI
return data, nil
}
-// GetStatus handles the getting of a fedi/activitypub representation of replies to a status,
+// GetStatus handles getting an AP representation of replies to a status,
// performing appropriate authentication before returning a JSON serializable interface to the caller.
func (p *Processor) StatusRepliesGet(
ctx context.Context,
@@ -101,7 +113,7 @@ func (p *Processor) StatusRepliesGet(
statusID string,
page *paging.Page,
onlyOtherAccounts bool,
-) (interface{}, gtserror.WithCode) {
+) (any, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
@@ -116,8 +128,8 @@ func (p *Processor) StatusRepliesGet(
return nil, gtserror.NewErrorInternalError(err)
}
- receivingAcct := auth.receivingAcct
- requestingAcct := auth.requestingAcct
+ receivingAcct := auth.receiver
+ requestingAcct := auth.requester
// Get target status and ensure visible to requester.
status, errWithCode := p.c.GetVisibleTargetStatus(ctx,
diff --git a/internal/processing/fedi/user.go b/internal/processing/fedi/user.go
index 79c1b4fdb..9fb338673 100644
--- a/internal/processing/fedi/user.go
+++ b/internal/processing/fedi/user.go
@@ -20,96 +20,83 @@ package fedi
import (
"context"
"errors"
- "fmt"
- "net/url"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
)
-// UserGet handles the getting of a fedi/activitypub representation of a user/account,
-// performing authentication before returning a JSON serializable interface to the caller.
-func (p *Processor) UserGet(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
- // (Try to) get the requested local account from the db.
- receiver, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUsername, "")
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- // Account just not found w/ this username.
- err := fmt.Errorf("account with username %s not found in the db", requestedUsername)
- return nil, gtserror.NewErrorNotFound(err)
- }
+// UserGet handles getting an AP representation of an account.
+// It does auth before returning a JSON serializable interface to the caller.
+func (p *Processor) UserGet(
+ ctx context.Context,
+ requestedUser string,
+) (any, gtserror.WithCode) {
+ // Authenticate incoming request, getting related accounts.
+ //
+ // We may currently be handshaking with the remote account
+ // making the request. Unlike with other fedi endpoints,
+ // don't bother checking this; if we're still handshaking
+ // just serve the AP representation of our account anyway.
+ //
+ // This ensures that we don't get stuck in a loop with another
+ // GtS instance, where each instance is trying repeatedly to
+ // dereference the other account that's making the request
+ // before it will reveal its own account.
+ //
+ // Instead, we end up in an 'I'll show you mine if you show me
+ // yours' situation, where we sort of agree to reveal each
+ // other's profiles at the same time.
+ auth, errWithCode := p.authenticate(ctx, requestedUser)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
- // Real db error.
- err := fmt.Errorf("db error getting account with username %s: %w", requestedUsername, err)
+ // Generate the proper AP representation.
+ accountable, err := p.converter.AccountToAS(ctx, auth.receiver)
+ if err != nil {
+ err := gtserror.Newf("error converting to accountable: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- if uris.IsPublicKeyPath(requestURL) {
- // If request is on a public key path, we don't need to
- // authenticate this request. However, we'll only serve
- // the bare minimum user profile needed for the pubkey.
- //
- // TODO: https://github.com/superseriousbusiness/gotosocial/issues/1186
- minimalPerson, err := p.converter.AccountToASMinimal(ctx, receiver)
- if err != nil {
- err := gtserror.Newf("error converting to minimal account: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // Return early with bare minimum data.
- return data(minimalPerson)
+ data, err := ap.Serialize(accountable)
+ if err != nil {
+ err := gtserror.Newf("error serializing accountable: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- // If the request is not on a public key path, we want to
- // try to authenticate it before we serve any data, so that
- // we can serve a more complete profile.
- pubKeyAuth, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
- if errWithCode != nil {
- return nil, errWithCode // likely 401
- }
+ return data, nil
+}
- // Auth passed, generate the proper AP representation.
- accountable, err := p.converter.AccountToAS(ctx, receiver)
- if err != nil {
- err := gtserror.Newf("error converting account: %w", err)
+// UserGetMinimal returns a minimal AP representation
+// of the requested account, containing just the public
+// key, without doing authentication.
+func (p *Processor) UserGetMinimal(
+ ctx context.Context,
+ requestedUser string,
+) (any, gtserror.WithCode) {
+ acct, err := p.state.DB.GetAccountByUsernameDomain(
+ gtscontext.SetBarebones(ctx),
+ requestedUser, "",
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting account %s: %w", requestedUser, err)
return nil, gtserror.NewErrorInternalError(err)
}
- if pubKeyAuth.Handshaking {
- // If we are currently handshaking with the remote account
- // making the request, then don't be coy: just serve the AP
- // representation of the target account.
- //
- // This handshake check ensures that we don't get stuck in
- // a loop with another GtS instance, where each instance is
- // trying repeatedly to dereference the other account that's
- // making the request before it will reveal its own account.
- //
- // Instead, we end up in an 'I'll show you mine if you show me
- // yours' situation, where we sort of agree to reveal each
- // other's profiles at the same time.
- return data(accountable)
+ if acct == nil {
+ err := gtserror.Newf("account %s not found in the db", requestedUser)
+ return nil, gtserror.NewErrorNotFound(err)
}
- // Get requester from auth.
- requester := pubKeyAuth.Owner
-
- // Check that block does not exist between receiver and requester.
- blocked, err := p.state.DB.IsBlocked(ctx, receiver.ID, requester.ID)
+ // Generate minimal AP representation.
+ accountable, err := p.converter.AccountToASMinimal(ctx, acct)
if err != nil {
- err := gtserror.Newf("error checking block: %w", err)
+ err := gtserror.Newf("error converting to accountable: %w", err)
return nil, gtserror.NewErrorInternalError(err)
- } else if blocked {
- const text = "block exists between accounts"
- return nil, gtserror.NewErrorForbidden(errors.New(text))
}
- return data(accountable)
-}
-
-func data(accountable ap.Accountable) (interface{}, gtserror.WithCode) {
data, err := ap.Serialize(accountable)
if err != nil {
err := gtserror.Newf("error serializing accountable: %w", err)
diff --git a/internal/processing/fedi/wellknown.go b/internal/processing/fedi/wellknown.go
index 93fd3b28f..236f09257 100644
--- a/internal/processing/fedi/wellknown.go
+++ b/internal/processing/fedi/wellknown.go
@@ -21,9 +21,9 @@ import (
"context"
"fmt"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
)
const (
@@ -34,7 +34,7 @@ const (
nodeInfoSoftwareName = "gotosocial"
nodeInfo20Rel = "http://nodeinfo.diaspora.software/ns/schema/2.0"
nodeInfo21Rel = "http://nodeinfo.diaspora.software/ns/schema/2.1"
- nodeInfoRepo = "https://github.com/superseriousbusiness/gotosocial"
+ nodeInfoRepo = "https://codeberg.org/superseriousbusiness/gotosocial"
nodeInfoHomepage = "https://docs.gotosocial.org"
webfingerProfilePage = "http://webfinger.net/rel/profile-page"
webFingerProfilePageContentType = "text/html"
@@ -47,7 +47,7 @@ var (
nodeInfoProtocols = []string{"activitypub"}
nodeInfoInbound = []string{}
nodeInfoOutbound = []string{}
- nodeInfoMetadata = make(map[string]interface{})
+ nodeInfoMetadata = make(map[string]any)
)
// NodeInfoRelGet returns a well known response giving the path to node info.
@@ -156,11 +156,12 @@ func (p *Processor) HostMetaGet() *apimodel.HostMeta {
}
// WebfingerGet handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
-func (p *Processor) WebfingerGet(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
+func (p *Processor) WebfingerGet(ctx context.Context, requestedUser string) (*apimodel.WellKnownResponse, gtserror.WithCode) {
// Get the local account the request is referring to.
- requestedAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUsername, "")
+ requestedAccount, err := p.state.DB.GetAccountByUsernameDomain(ctx, requestedUser, "")
if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
+ err := gtserror.Newf("db error getting account %s: %s", requestedUser, err)
+ return nil, gtserror.NewErrorNotFound(err)
}
return &apimodel.WellKnownResponse{
diff --git a/internal/processing/filters/common/common.go b/internal/processing/filters/common/common.go
new file mode 100644
index 000000000..8930b3aaf
--- /dev/null
+++ b/internal/processing/filters/common/common.go
@@ -0,0 +1,212 @@
+// 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 common
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ 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"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+)
+
+type Processor struct {
+ state *state.State
+ stream *stream.Processor
+}
+
+func New(state *state.State, stream *stream.Processor) *Processor {
+ return &Processor{state, stream}
+}
+
+// CheckFilterExists calls .GetFilter() with a barebones context to not
+// fetch any sub-models, and not returning the result. this functionally
+// just uses .GetFilter() for the ownership and existence checks.
+func (p *Processor) CheckFilterExists(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) gtserror.WithCode {
+ _, errWithCode := p.GetFilter(gtscontext.SetBarebones(ctx), requester, id)
+ return errWithCode
+}
+
+// GetFilter fetches the filter with given ID, also checking
+// the given requesting account is the owner of the filter.
+func (p *Processor) GetFilter(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (
+ *gtsmodel.Filter,
+ gtserror.WithCode,
+) {
+ // Get the filter from the database with given ID.
+ filter, err := p.state.DB.GetFilterByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check it exists.
+ if filter == nil {
+ const text = "filter not found"
+ return nil, gtserror.NewWithCode(http.StatusNotFound, text)
+ }
+
+ // Check that the requester owns it.
+ if filter.AccountID != requester.ID {
+ const text = "filter not found"
+ err := gtserror.New("filter does not belong to account")
+ return nil, gtserror.NewErrorNotFound(err, text)
+ }
+
+ return filter, nil
+}
+
+// GetFilterStatus fetches the filter status with given ID, also
+// checking the given requesting account is the owner of it.
+func (p *Processor) GetFilterStatus(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (
+ *gtsmodel.FilterStatus,
+ *gtsmodel.Filter,
+ gtserror.WithCode,
+) {
+
+ // Get the filter status from the database with given ID.
+ filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter status: %w", err)
+ return nil, nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check it even exists.
+ if filterStatus == nil {
+ const text = "filter status not found"
+ return nil, nil, gtserror.NewWithCode(http.StatusNotFound, text)
+ }
+
+ // Get the filter this filter status is
+ // associated with, without sub-models.
+ // (this also checks filter ownership).
+ filter, errWithCode := p.GetFilter(
+ gtscontext.SetBarebones(ctx),
+ requester,
+ filterStatus.FilterID,
+ )
+ if errWithCode != nil {
+ return nil, nil, errWithCode
+ }
+
+ return filterStatus, filter, nil
+}
+
+// GetFilterKeyword fetches the filter keyword with given ID,
+// also checking the given requesting account is the owner of it.
+func (p *Processor) GetFilterKeyword(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (
+ *gtsmodel.FilterKeyword,
+ *gtsmodel.Filter,
+ gtserror.WithCode,
+) {
+
+ // Get the filter keyword from the database with given ID.
+ keyword, err := p.state.DB.GetFilterKeywordByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter keyword: %w", err)
+ return nil, nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check it exists.
+ if keyword == nil {
+ const text = "filter keyword not found"
+ return nil, nil, gtserror.NewWithCode(http.StatusNotFound, text)
+ }
+
+ // Get the filter this filter keyword is
+ // associated with, without sub-models.
+ // (this also checks filter ownership).
+ filter, errWithCode := p.GetFilter(
+ gtscontext.SetBarebones(ctx),
+ requester,
+ keyword.FilterID,
+ )
+ if errWithCode != nil {
+ return nil, nil, errWithCode
+ }
+
+ return keyword, filter, nil
+}
+
+// OnFilterChanged ...
+func (p *Processor) OnFilterChanged(ctx context.Context, requester *gtsmodel.Account) {
+
+ // Get list of list IDs created by this requesting account.
+ listIDs, err := p.state.DB.GetListIDsByAccountID(ctx, requester.ID)
+ if err != nil {
+ log.Errorf(ctx, "error getting account '%s' lists: %v", requester.Username, err)
+ }
+
+ // Unprepare this requester's home timeline.
+ p.state.Caches.Timelines.Home.Unprepare(requester.ID)
+
+ // Unprepare list timelines.
+ for _, id := range listIDs {
+ p.state.Caches.Timelines.List.Unprepare(id)
+ }
+
+ // Send filter changed event for account.
+ p.stream.FiltersChanged(ctx, requester)
+}
+
+// FromAPIContexts converts a slice of frontend API model FilterContext types to our internal FilterContexts bit field.
+func FromAPIContexts(apiContexts []apimodel.FilterContext) (gtsmodel.FilterContexts, gtserror.WithCode) {
+ var contexts gtsmodel.FilterContexts
+ for _, context := range apiContexts {
+ switch context {
+ case apimodel.FilterContextHome:
+ contexts.SetHome()
+ case apimodel.FilterContextNotifications:
+ contexts.SetNotifications()
+ case apimodel.FilterContextPublic:
+ contexts.SetPublic()
+ case apimodel.FilterContextThread:
+ contexts.SetThread()
+ case apimodel.FilterContextAccount:
+ contexts.SetAccount()
+ default:
+ text := fmt.Sprintf("unsupported filter context: %s", context)
+ return 0, gtserror.NewWithCode(http.StatusBadRequest, text)
+ }
+ }
+ return contexts, nil
+}
diff --git a/internal/processing/filters/v1/convert.go b/internal/processing/filters/v1/convert.go
deleted file mode 100644
index 1e0db5ff1..000000000
--- a/internal/processing/filters/v1/convert.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// 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 v1
-
-import (
- "context"
- "fmt"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-// apiFilter is a shortcut to return the API v1 filter version of the given
-// filter keyword, or return an appropriate error if conversion fails.
-func (p *Processor) apiFilter(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword) (*apimodel.FilterV1, gtserror.WithCode) {
- apiFilter, err := p.converter.FilterKeywordToAPIFilterV1(ctx, filterKeyword)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting filter keyword to API v1 filter: %w", err))
- }
-
- return apiFilter, nil
-}
diff --git a/internal/processing/filters/v1/create.go b/internal/processing/filters/v1/create.go
index 86019d2a6..9f3fc17e0 100644
--- a/internal/processing/filters/v1/create.go
+++ b/internal/processing/filters/v1/create.go
@@ -20,76 +20,80 @@ package v1
import (
"context"
"errors"
- "fmt"
+ "net/http"
"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/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/processing/filters/common"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// Create a new filter and filter keyword for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
-func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.FilterCreateUpdateRequestV1) (*apimodel.FilterV1, gtserror.WithCode) {
+func (p *Processor) Create(ctx context.Context, requester *gtsmodel.Account, form *apimodel.FilterCreateUpdateRequestV1) (*apimodel.FilterV1, gtserror.WithCode) {
+ var errWithCode gtserror.WithCode
+
+ // Create new wrapping filter.
filter := &gtsmodel.Filter{
ID: id.NewULID(),
- AccountID: account.ID,
+ AccountID: requester.ID,
Title: form.Phrase,
- Action: gtsmodel.FilterActionWarn,
}
+
if *form.Irreversible {
+ // Irreversible = action hide.
filter.Action = gtsmodel.FilterActionHide
+ } else {
+ // Default action = action warn.
+ filter.Action = gtsmodel.FilterActionWarn
}
- if form.ExpiresIn != nil && *form.ExpiresIn != 0 {
- filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn))
+
+ // Check form for valid expiry and set on filter.
+ if form.ExpiresIn != nil && *form.ExpiresIn > 0 {
+ expiresIn := time.Duration(*form.ExpiresIn) * time.Second
+ filter.ExpiresAt = time.Now().Add(expiresIn)
}
- for _, context := range form.Context {
- switch context {
- case apimodel.FilterContextHome:
- filter.ContextHome = util.Ptr(true)
- case apimodel.FilterContextNotifications:
- filter.ContextNotifications = util.Ptr(true)
- case apimodel.FilterContextPublic:
- filter.ContextPublic = util.Ptr(true)
- case apimodel.FilterContextThread:
- filter.ContextThread = util.Ptr(true)
- case apimodel.FilterContextAccount:
- filter.ContextAccount = util.Ptr(true)
- default:
- return nil, gtserror.NewErrorUnprocessableEntity(
- fmt.Errorf("unsupported filter context '%s'", context),
- )
- }
+
+ // Parse contexts filter applies in from incoming request form data.
+ filter.Contexts, errWithCode = common.FromAPIContexts(form.Context)
+ if errWithCode != nil {
+ return nil, errWithCode
}
+ // Create new keyword attached to filter.
filterKeyword := &gtsmodel.FilterKeyword{
ID: id.NewULID(),
- AccountID: account.ID,
FilterID: filter.ID,
- Filter: filter,
Keyword: form.Phrase,
WholeWord: util.Ptr(util.PtrOrValue(form.WholeWord, false)),
}
- filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
- if err := p.state.DB.PutFilter(ctx, filter); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("you already have a filter with this title")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
+ // Attach the new keyword to filter before insert.
+ filter.Keywords = append(filter.Keywords, filterKeyword)
+ filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID)
- apiFilter, errWithCode := p.apiFilter(ctx, filterKeyword)
- if errWithCode != nil {
- return nil, errWithCode
+ // Insert newly created filter into the database.
+ switch err := p.state.DB.PutFilter(ctx, filter); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate title"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error inserting filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
- return apiFilter, nil
+ // Return as converted frontend filter keyword model.
+ return typeutils.FilterKeywordToAPIFilterV1(filter, filterKeyword), nil
}
diff --git a/internal/processing/filters/v1/delete.go b/internal/processing/filters/v1/delete.go
index 89282c65d..65768140a 100644
--- a/internal/processing/filters/v1/delete.go
+++ b/internal/processing/filters/v1/delete.go
@@ -19,52 +19,52 @@ package v1
import (
"context"
- "errors"
+ "slices"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
-// Delete an existing filter keyword and (if empty afterwards) filter for the given account.
+// Delete an existing filter keyword and (if empty
+// afterwards) filter for the given account.
func (p *Processor) Delete(
ctx context.Context,
- account *gtsmodel.Account,
+ requester *gtsmodel.Account,
filterKeywordID string,
) gtserror.WithCode {
- // Get enough of the filter keyword that we can look up its filter ID.
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(gtscontext.SetBarebones(ctx), filterKeywordID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return gtserror.NewErrorNotFound(err)
- }
- return gtserror.NewErrorInternalError(err)
- }
- if filterKeyword.AccountID != account.ID {
- return gtserror.NewErrorNotFound(nil)
- }
-
- // Get the filter for this keyword.
- filter, err := p.state.DB.GetFilterByID(ctx, filterKeyword.FilterID)
- if err != nil {
- return gtserror.NewErrorNotFound(err)
+ // Get the filter keyword with given ID, and associated filter, also checking ownership.
+ filterKeyword, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return errWithCode
}
if len(filter.Keywords) > 1 || len(filter.Statuses) > 0 {
- // The filter has other keywords or statuses. Delete only the requested filter keyword.
- if err := p.state.DB.DeleteFilterKeywordByID(ctx, filterKeyword.ID); err != nil {
+ // The filter has other keywords or statuses, just delete the one filter keyword.
+ if err := p.state.DB.DeleteFilterKeywordsByIDs(ctx, filterKeyword.ID); err != nil {
+ err := gtserror.Newf("error deleting filter keyword: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ // Delete this filter keyword from the slice of IDs attached to filter.
+ filter.KeywordIDs = slices.DeleteFunc(filter.KeywordIDs, func(id string) bool {
+ return filterKeyword.ID == id
+ })
+
+ // Update filter in the database now the keyword has been unattached.
+ if err := p.state.DB.UpdateFilter(ctx, filter, "keywords"); err != nil {
+ err := gtserror.Newf("error updating filter: %w", err)
return gtserror.NewErrorInternalError(err)
}
} else {
- // Delete the entire filter.
- if err := p.state.DB.DeleteFilterByID(ctx, filter.ID); err != nil {
+ // Delete the filter and this keyword that is attached to it.
+ if err := p.state.DB.DeleteFilter(ctx, filter); err != nil {
+ err := gtserror.Newf("error deleting filter: %w", err)
return gtserror.NewErrorInternalError(err)
}
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
return nil
}
diff --git a/internal/processing/filters/v1/filters.go b/internal/processing/filters/v1/filters.go
index daa9087a9..4492b4e76 100644
--- a/internal/processing/filters/v1/filters.go
+++ b/internal/processing/filters/v1/filters.go
@@ -18,21 +18,24 @@
package v1
import (
- "github.com/superseriousbusiness/gotosocial/internal/processing/stream"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
+ // embedded common logic
+ c *common.Processor
+
state *state.State
converter *typeutils.Converter
- stream *stream.Processor
}
-func New(state *state.State, converter *typeutils.Converter, stream *stream.Processor) Processor {
+func New(state *state.State, converter *typeutils.Converter, common *common.Processor) Processor {
return Processor{
+ c: common,
+
state: state,
converter: converter,
- stream: stream,
}
}
diff --git a/internal/processing/filters/v1/get.go b/internal/processing/filters/v1/get.go
index 3ead09b20..bdde123e9 100644
--- a/internal/processing/filters/v1/get.go
+++ b/internal/processing/filters/v1/get.go
@@ -23,49 +23,60 @@ import (
"slices"
"strings"
- 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/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Get looks up a filter keyword by ID and returns it as a v1 filter.
-func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterV1, gtserror.WithCode) {
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterKeywordID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filterKeyword.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(nil)
+func (p *Processor) Get(ctx context.Context, requester *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterV1, gtserror.WithCode) {
+ filterKeyword, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
-
- return p.apiFilter(ctx, filterKeyword)
+ return typeutils.FilterKeywordToAPIFilterV1(filter, filterKeyword), nil
}
// GetAll looks up all filter keywords for the current account and returns them as v1 filters.
-func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*apimodel.FilterV1, gtserror.WithCode) {
- filters, err := p.state.DB.GetFilterKeywordsForAccountID(
- ctx,
- account.ID,
+func (p *Processor) GetAll(ctx context.Context, requester *gtsmodel.Account) ([]*apimodel.FilterV1, gtserror.WithCode) {
+ var totalKeywords int
+
+ // Get a list of all filters owned by this account,
+ // (without any sub-models attached, done later).
+ filters, err := p.state.DB.GetFiltersByAccountID(
+ gtscontext.SetBarebones(ctx),
+ requester.ID,
)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, nil
- }
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filters: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- apiFilters := make([]*apimodel.FilterV1, 0, len(filters))
+ // Get a total count of all expected
+ // keywords for slice preallocation.
for _, filter := range filters {
- apiFilter, errWithCode := p.apiFilter(ctx, filter)
- if errWithCode != nil {
- return nil, errWithCode
+ totalKeywords += len(filter.KeywordIDs)
+ }
+
+ // Create a slice to store converted V1 frontend models.
+ apiFilters := make([]*apimodel.FilterV1, 0, totalKeywords)
+
+ for _, filter := range filters {
+ // For each of the fetched filters, fetch all of their associated keywords.
+ keywords, err := p.state.DB.GetFilterKeywordsByIDs(ctx, filter.KeywordIDs)
+ if err != nil {
+ err := gtserror.Newf("error getting filter keywords: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- apiFilters = append(apiFilters, apiFilter)
+ // Convert each keyword to frontend.
+ for _, keyword := range keywords {
+ apiFilter := typeutils.FilterKeywordToAPIFilterV1(filter, keyword)
+ apiFilters = append(apiFilters, apiFilter)
+ }
}
// Sort them by ID so that they're in a stable order.
diff --git a/internal/processing/filters/v1/update.go b/internal/processing/filters/v1/update.go
index 15c5de365..19699f328 100644
--- a/internal/processing/filters/v1/update.go
+++ b/internal/processing/filters/v1/update.go
@@ -21,77 +21,59 @@ import (
"context"
"errors"
"fmt"
+ "net/http"
"strings"
"time"
- 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/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/processing/filters/common"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Update an existing filter and filter keyword for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
func (p *Processor) Update(
ctx context.Context,
- account *gtsmodel.Account,
+ requester *gtsmodel.Account,
filterKeywordID string,
form *apimodel.FilterCreateUpdateRequestV1,
) (*apimodel.FilterV1, gtserror.WithCode) {
- // Get enough of the filter keyword that we can look up its filter ID.
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(gtscontext.SetBarebones(ctx), filterKeywordID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filterKeyword.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(nil)
+ // Get the filter keyword with given ID, and associated filter, also checking ownership.
+ filterKeyword, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
- // Get the filter for this keyword.
- filter, err := p.state.DB.GetFilterByID(ctx, filterKeyword.FilterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
+ var title string
+ var action gtsmodel.FilterAction
+ var contexts gtsmodel.FilterContexts
+ var expiresAt time.Time
+ var wholeword bool
+
+ // Get filter title.
+ title = form.Phrase
- title := form.Phrase
- action := gtsmodel.FilterActionWarn
if *form.Irreversible {
+ // Irreversible = action hide.
action = gtsmodel.FilterActionHide
+ } else {
+ // Default action = action warn.
+ action = gtsmodel.FilterActionWarn
}
- expiresAt := time.Time{}
- if form.ExpiresIn != nil && *form.ExpiresIn != 0 {
- expiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn))
+
+ // Check form for valid expiry and set on filter.
+ if form.ExpiresIn != nil && *form.ExpiresIn > 0 {
+ expiresIn := time.Duration(*form.ExpiresIn) * time.Second
+ expiresAt = time.Now().Add(expiresIn)
}
- contextHome := false
- contextNotifications := false
- contextPublic := false
- contextThread := false
- contextAccount := false
- for _, context := range form.Context {
- switch context {
- case apimodel.FilterContextHome:
- contextHome = true
- case apimodel.FilterContextNotifications:
- contextNotifications = true
- case apimodel.FilterContextPublic:
- contextPublic = true
- case apimodel.FilterContextThread:
- contextThread = true
- case apimodel.FilterContextAccount:
- contextAccount = true
- default:
- return nil, gtserror.NewErrorUnprocessableEntity(
- fmt.Errorf("unsupported filter context '%s'", context),
- )
- }
+
+ // Parse contexts filter applies in from incoming form data.
+ contexts, errWithCode = common.FromAPIContexts(form.Context)
+ if errWithCode != nil {
+ return nil, errWithCode
}
// v1 filter APIs can't change certain fields for a filter with multiple keywords or any statuses,
@@ -108,11 +90,7 @@ func (p *Processor) Update(
if expiresAt != filter.ExpiresAt {
forbiddenFields = append(forbiddenFields, "expires_in")
}
- if contextHome != util.PtrOrValue(filter.ContextHome, false) ||
- contextNotifications != util.PtrOrValue(filter.ContextNotifications, false) ||
- contextPublic != util.PtrOrValue(filter.ContextPublic, false) ||
- contextThread != util.PtrOrValue(filter.ContextThread, false) ||
- contextAccount != util.PtrOrValue(filter.ContextAccount, false) {
+ if contexts != filter.Contexts {
forbiddenFields = append(forbiddenFields, "context")
}
if len(forbiddenFields) > 0 {
@@ -122,54 +100,75 @@ func (p *Processor) Update(
}
}
- // Now that we've checked that the changes are legal, apply them to the filter and keyword.
- filter.Title = title
- filter.Action = action
- filter.ExpiresAt = expiresAt
- filter.ContextHome = &contextHome
- filter.ContextNotifications = &contextNotifications
- filter.ContextPublic = &contextPublic
- filter.ContextThread = &contextThread
- filter.ContextAccount = &contextAccount
- filterKeyword.Keyword = form.Phrase
- filterKeyword.WholeWord = util.Ptr(util.PtrOrValue(form.WholeWord, false))
-
- // We only want to update the relevant filter keyword.
- filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
- filter.Statuses = nil
- filterKeyword.Filter = filter
-
- filterColumns := []string{
- "title",
- "action",
- "expires_at",
- "context_home",
- "context_notifications",
- "context_public",
- "context_thread",
- "context_account",
+ // Filter columns that
+ // we're going to update.
+ var filterCols []string
+ var keywordCols []string
+
+ // Check for changed filter title / filter keyword phrase.
+ if title != filter.Title || title != filterKeyword.Keyword {
+ keywordCols = append(keywordCols, "keyword")
+ filterCols = append(filterCols, "title")
+ filterKeyword.Keyword = title
+ filter.Title = title
}
- filterKeywordColumns := [][]string{
- {
- "keyword",
- "whole_word",
- },
+
+ // Check for changed action.
+ if action != filter.Action {
+ filterCols = append(filterCols, "action")
+ filter.Action = action
}
- if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, filterKeywordColumns, nil, nil); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("you already have a filter with this title")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
+
+ // Check for changed filter expiry time.
+ if !expiresAt.Equal(filter.ExpiresAt) {
+ filterCols = append(filterCols, "expires_at")
+ filter.ExpiresAt = expiresAt
+ }
+
+ // Check for changed filter context.
+ if contexts != filter.Contexts {
+ filterCols = append(filterCols, "contexts")
+ filter.Contexts = contexts
+ }
+
+ // Check for changed wholeword flag.
+ if form.WholeWord != nil &&
+ *form.WholeWord != *filterKeyword.WholeWord {
+ keywordCols = append(keywordCols, "whole_word")
+ filterKeyword.WholeWord = &wholeword
+ }
+
+ // Update filter keyword model in the database with determined changed cols.
+ switch err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, keywordCols...); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate keyword"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error updating filter: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- apiFilter, errWithCode := p.apiFilter(ctx, filterKeyword)
- if errWithCode != nil {
- return nil, errWithCode
+ // Update filter model in the database with determined changed cols.
+ switch err := p.state.DB.UpdateFilter(ctx, filter, filterCols...); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate title"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error updating filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
- return apiFilter, nil
+ // Return as converted frontend filter keyword model.
+ return typeutils.FilterKeywordToAPIFilterV1(filter, filterKeyword), nil
}
diff --git a/internal/processing/filters/v2/create.go b/internal/processing/filters/v2/create.go
index 60dd46f43..93f93d493 100644
--- a/internal/processing/filters/v2/create.go
+++ b/internal/processing/filters/v2/create.go
@@ -20,87 +20,193 @@ package v2
import (
"context"
"errors"
- "fmt"
+ "net/http"
"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/typeutils"
- "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/processing/filters/common"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Create a new filter for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
-func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.FilterCreateRequestV2) (*apimodel.FilterV2, gtserror.WithCode) {
+func (p *Processor) Create(ctx context.Context, requester *gtsmodel.Account, form *apimodel.FilterCreateRequestV2) (*apimodel.FilterV2, gtserror.WithCode) {
+ var errWithCode gtserror.WithCode
+
+ // Create new filter model.
filter := &gtsmodel.Filter{
ID: id.NewULID(),
- AccountID: account.ID,
+ AccountID: requester.ID,
Title: form.Title,
- Action: typeutils.APIFilterActionToFilterAction(*form.FilterAction),
}
- if form.ExpiresIn != nil && *form.ExpiresIn != 0 {
- filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn))
+
+ // Parse filter action from form and set on filter, checking for validity.
+ filter.Action = typeutils.APIFilterActionToFilterAction(*form.FilterAction)
+ if filter.Action == 0 {
+ const text = "invalid filter action"
+ return nil, gtserror.NewWithCode(http.StatusBadRequest, text)
}
- for _, context := range form.Context {
- switch context {
- case apimodel.FilterContextHome:
- filter.ContextHome = util.Ptr(true)
- case apimodel.FilterContextNotifications:
- filter.ContextNotifications = util.Ptr(true)
- case apimodel.FilterContextPublic:
- filter.ContextPublic = util.Ptr(true)
- case apimodel.FilterContextThread:
- filter.ContextThread = util.Ptr(true)
- case apimodel.FilterContextAccount:
- filter.ContextAccount = util.Ptr(true)
- default:
- return nil, gtserror.NewErrorUnprocessableEntity(
- fmt.Errorf("unsupported filter context '%s'", context),
- )
+
+ // Parse contexts filter applies in from incoming request form data.
+ filter.Contexts, errWithCode = common.FromAPIContexts(form.Context)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Check form for valid expiry and set on filter.
+ if form.ExpiresIn != nil && *form.ExpiresIn > 0 {
+ expiresIn := time.Duration(*form.ExpiresIn) * time.Second
+ filter.ExpiresAt = time.Now().Add(expiresIn)
+ }
+
+ // Create new attached filter keywords.
+ keywordQueries, errWithCode := p.createFilterKeywords(ctx,
+ filter, form.Keywords)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Create new attached filter statuses.
+ statusQueries, errWithCode := p.createFilterStatuses(ctx,
+ filter, form.Statuses)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ for _, keywordCreate := range keywordQueries {
+ if errWithCode := keywordCreate(); errWithCode != nil {
+ return nil, errWithCode
}
}
- for _, formKeyword := range form.Keywords {
- filterKeyword := &gtsmodel.FilterKeyword{
- ID: id.NewULID(),
- AccountID: account.ID,
- FilterID: filter.ID,
- Filter: filter,
- Keyword: formKeyword.Keyword,
- WholeWord: formKeyword.WholeWord,
+ for _, statusCreate := range statusQueries {
+ if errWithCode := statusCreate(); errWithCode != nil {
+ return nil, errWithCode
}
- filter.Keywords = append(filter.Keywords, filterKeyword)
}
- for _, formStatus := range form.Statuses {
- filterStatus := &gtsmodel.FilterStatus{
+ // Insert the new filter model into the database.
+ switch err := p.state.DB.PutFilter(ctx, filter); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate title, keyword or status"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error inserting filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
+
+ // Return as converted frontend filter model.
+ return typeutils.FilterToAPIFilterV2(filter), nil
+}
+
+func (p *Processor) createFilterKeywords(ctx context.Context, filter *gtsmodel.Filter, form []apimodel.FilterKeywordCreateUpdateRequest) ([]func() gtserror.WithCode, gtserror.WithCode) {
+ if len(form) == 0 {
+ // No keywords created.
+ return nil, nil
+ }
+
+ var deferred []func() gtserror.WithCode
+
+ // Create filter keywords in the database.
+ for _, request := range form {
+ // Check for valid request.
+ if request.Keyword == "" {
+ const text = "missing keyword"
+ return deferred, gtserror.NewWithCode(http.StatusBadRequest, text)
+ }
+
+ // Create new filter keyword for insert.
+ filterKeyword := &gtsmodel.FilterKeyword{
ID: id.NewULID(),
- AccountID: account.ID,
FilterID: filter.ID,
- Filter: filter,
- StatusID: formStatus.StatusID,
+ Keyword: request.Keyword,
+ WholeWord: request.WholeWord,
}
- filter.Statuses = append(filter.Statuses, filterStatus)
- }
- if err := p.state.DB.PutFilter(ctx, filter); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("duplicate title, keyword, or status")
- return nil, gtserror.NewErrorConflict(err, err.Error())
+ // Verify that this is valid regular expression.
+ if err := filterKeyword.Compile(); err != nil {
+ const text = "invalid regular expression"
+ err := gtserror.Newf("invalid regular expression: %w", err)
+ return deferred, gtserror.NewWithCodeSafe(
+ http.StatusBadRequest,
+ err, text,
+ )
}
- return nil, gtserror.NewErrorInternalError(err)
+
+ // Append new filter keyword to filter and list of IDs.
+ filter.Keywords = append(filter.Keywords, filterKeyword)
+ filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID)
+
+ // Append database insert to funcs for later processing by caller.
+ deferred = append(deferred, func() gtserror.WithCode {
+ if err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); //
+ err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ const text = "duplicate keyword"
+ return gtserror.NewWithCode(http.StatusConflict, text)
+ }
+ err := gtserror.Newf("error inserting filter keyword: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
}
- apiFilter, errWithCode := p.apiFilter(ctx, filter)
- if errWithCode != nil {
- return nil, errWithCode
+ return deferred, nil
+}
+
+func (p *Processor) createFilterStatuses(ctx context.Context, filter *gtsmodel.Filter, form []apimodel.FilterStatusCreateRequest) ([]func() gtserror.WithCode, gtserror.WithCode) {
+ if len(form) == 0 {
+ // No statuses added.
+ return nil, nil
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ var deferred []func() gtserror.WithCode
+
+ // Create filter statuses in the database.
+ for _, request := range form {
+ // Check for valid request.
+ if request.StatusID == "" {
+ const text = "missing status"
+ return deferred, gtserror.NewWithCode(http.StatusBadRequest, text)
+ }
+
+ // Create new filter status for insert.
+ filterStatus := &gtsmodel.FilterStatus{
+ ID: id.NewULID(),
+ FilterID: filter.ID,
+ StatusID: request.StatusID,
+ }
+
+ // Append new filter status to filter and list of IDs.
+ filter.Statuses = append(filter.Statuses, filterStatus)
+ filter.StatusIDs = append(filter.StatusIDs, filterStatus.ID)
+
+ // Append database insert to funcs for later processing by caller.
+ deferred = append(deferred, func() gtserror.WithCode {
+ if err := p.state.DB.PutFilterStatus(ctx, filterStatus); //
+ err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ const text = "duplicate status"
+ return gtserror.NewWithCode(http.StatusConflict, text)
+ }
+ err := gtserror.Newf("error inserting filter status: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
+ }
- return apiFilter, nil
+ return deferred, nil
}
diff --git a/internal/processing/filters/v2/delete.go b/internal/processing/filters/v2/delete.go
index a312180b8..fdd6cca92 100644
--- a/internal/processing/filters/v2/delete.go
+++ b/internal/processing/filters/v2/delete.go
@@ -19,38 +19,33 @@ package v2
import (
"context"
- "fmt"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
-// Delete an existing filter and all its attached keywords and statuses for the given account.
+// Delete an existing filter and all its attached
+// keywords and statuses for the given account.
func (p *Processor) Delete(
ctx context.Context,
- account *gtsmodel.Account,
+ requester *gtsmodel.Account,
filterID string,
) gtserror.WithCode {
- // Get the filter for this keyword.
- filter, err := p.state.DB.GetFilterByID(ctx, filterID)
- if err != nil {
- return gtserror.NewErrorNotFound(err)
- }
- // Check that the account owns it.
- if filter.AccountID != account.ID {
- return gtserror.NewErrorNotFound(
- fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
- )
+ // Get the filter with given ID, also checking ownership.
+ filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
+ if errWithCode != nil {
+ return errWithCode
}
- // Delete the entire filter.
- if err := p.state.DB.DeleteFilterByID(ctx, filter.ID); err != nil {
+ // Delete filter from the database with all associated models.
+ if err := p.state.DB.DeleteFilter(ctx, filter); err != nil {
+ err := gtserror.Newf("error deleting filter: %w", err)
return gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
return nil
}
diff --git a/internal/processing/filters/v2/filters.go b/internal/processing/filters/v2/filters.go
index 85da4df6b..08725ccde 100644
--- a/internal/processing/filters/v2/filters.go
+++ b/internal/processing/filters/v2/filters.go
@@ -18,21 +18,24 @@
package v2
import (
- "github.com/superseriousbusiness/gotosocial/internal/processing/stream"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
+ // embedded common logic
+ c *common.Processor
+
state *state.State
converter *typeutils.Converter
- stream *stream.Processor
}
-func New(state *state.State, converter *typeutils.Converter, stream *stream.Processor) Processor {
+func New(state *state.State, converter *typeutils.Converter, common *common.Processor) Processor {
return Processor{
+ c: common,
+
state: state,
converter: converter,
- stream: stream,
}
}
diff --git a/internal/processing/filters/v2/get.go b/internal/processing/filters/v2/get.go
index 39b937eb2..4cdf9e8ee 100644
--- a/internal/processing/filters/v2/get.go
+++ b/internal/processing/filters/v2/get.go
@@ -19,56 +19,43 @@ package v2
import (
"context"
- "errors"
- "fmt"
"slices"
"strings"
- 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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Get looks up a filter by ID and returns it with keywords and statuses.
-func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, filterID string) (*apimodel.FilterV2, gtserror.WithCode) {
- filter, err := p.state.DB.GetFilterByID(ctx, filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
+func (p *Processor) Get(ctx context.Context, requester *gtsmodel.Account, filterID string) (*apimodel.FilterV2, gtserror.WithCode) {
+ filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
- )
- }
-
- return p.apiFilter(ctx, filter)
+ return typeutils.FilterToAPIFilterV2(filter), nil
}
// GetAll looks up all filters for the current account and returns them with keywords and statuses.
-func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*apimodel.FilterV2, gtserror.WithCode) {
- filters, err := p.state.DB.GetFiltersForAccountID(
- ctx,
- account.ID,
- )
+func (p *Processor) GetAll(ctx context.Context, requester *gtsmodel.Account) ([]*apimodel.FilterV2, gtserror.WithCode) {
+
+ // Get all filters belonging to this requester from the database.
+ filters, err := p.state.DB.GetFiltersByAccountID(ctx, requester.ID)
if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, nil
- }
+ err := gtserror.Newf("error getting account filters: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- apiFilters := make([]*apimodel.FilterV2, 0, len(filters))
- for _, filter := range filters {
- apiFilter, errWithCode := p.apiFilter(ctx, filter)
- if errWithCode != nil {
- return nil, errWithCode
- }
-
- apiFilters = append(apiFilters, apiFilter)
+ // Convert all these filters to frontend API models.
+ apiFilters := make([]*apimodel.FilterV2, len(filters))
+ if len(apiFilters) != len(filters) {
+ // bound check eliminiation compiler-hint
+ panic(gtserror.New("BCE"))
+ }
+ for i, filter := range filters {
+ apiFilter := typeutils.FilterToAPIFilterV2(filter)
+ apiFilters[i] = apiFilter
}
// Sort them by ID so that they're in a stable order.
diff --git a/internal/processing/filters/v2/keywordcreate.go b/internal/processing/filters/v2/keywordcreate.go
index 92d9e5dfd..7ad7c3bd9 100644
--- a/internal/processing/filters/v2/keywordcreate.go
+++ b/internal/processing/filters/v2/keywordcreate.go
@@ -20,51 +20,60 @@ package v2
import (
"context"
"errors"
- "fmt"
+ "net/http"
- 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"
+ 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/typeutils"
)
// KeywordCreate adds a filter keyword to an existing filter for the given account, using the provided parameters.
// These params should have already been normalized and validated by the time they reach this function.
-func (p *Processor) KeywordCreate(ctx context.Context, account *gtsmodel.Account, filterID string, form *apimodel.FilterKeywordCreateUpdateRequest) (*apimodel.FilterKeyword, gtserror.WithCode) {
- // Check that the filter is owned by the given account.
- filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
- )
+func (p *Processor) KeywordCreate(ctx context.Context, requester *gtsmodel.Account, filterID string, form *apimodel.FilterKeywordCreateUpdateRequest) (*apimodel.FilterKeyword, gtserror.WithCode) {
+
+ // Get the filter with given ID, also checking ownership.
+ filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
+ // Create new filter keyword model.
filterKeyword := &gtsmodel.FilterKeyword{
ID: id.NewULID(),
- AccountID: account.ID,
FilterID: filter.ID,
Keyword: form.Keyword,
WholeWord: form.WholeWord,
}
- if err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("duplicate keyword")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
+ // Insert the new filter keyword model into the database.
+ switch err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate keyword"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error inserting filter keyword: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Now update the filter it is attached to with new keyword.
+ filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID)
+ filter.Keywords = append(filter.Keywords, filterKeyword)
+
+ // Update the existing filter model in the database (only the needed col).
+ if err := p.state.DB.UpdateFilter(ctx, filter, "keywords"); err != nil {
+ err := gtserror.Newf("error updating filter: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
- return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
+ return typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword), nil
}
diff --git a/internal/processing/filters/v2/keyworddelete.go b/internal/processing/filters/v2/keyworddelete.go
index 024991109..5393ffd53 100644
--- a/internal/processing/filters/v2/keyworddelete.go
+++ b/internal/processing/filters/v2/keyworddelete.go
@@ -19,38 +19,43 @@ package v2
import (
"context"
- "fmt"
+ "slices"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// KeywordDelete deletes an existing filter keyword from a filter.
func (p *Processor) KeywordDelete(
ctx context.Context,
- account *gtsmodel.Account,
- filterID string,
+ requester *gtsmodel.Account,
+ filterKeywordID string,
) gtserror.WithCode {
- // Get the filter keyword.
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterID)
- if err != nil {
- return gtserror.NewErrorNotFound(err)
+ // Get filter keyword with given ID, also checking ownership to requester.
+ _, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return errWithCode
}
- // Check that the account owns it.
- if filterKeyword.AccountID != account.ID {
- return gtserror.NewErrorNotFound(
- fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID),
- )
+ // Delete this one filter keyword from the database, now ownership is confirmed.
+ if err := p.state.DB.DeleteFilterKeywordsByIDs(ctx, filterKeywordID); err != nil {
+ err := gtserror.Newf("error deleting filter keyword: %w", err)
+ return gtserror.NewErrorInternalError(err)
}
- // Delete the filter keyword.
- if err := p.state.DB.DeleteFilterKeywordByID(ctx, filterKeyword.ID); err != nil {
+ // Delete this filter keyword from the slice of IDs attached to filter.
+ filter.KeywordIDs = slices.DeleteFunc(filter.KeywordIDs, func(id string) bool {
+ return filterKeywordID == id
+ })
+
+ // Update filter in the database now the keyword has been unattached.
+ if err := p.state.DB.UpdateFilter(ctx, filter, "keywords"); err != nil {
+ err := gtserror.Newf("error updating filter: %w", err)
return gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
return nil
}
diff --git a/internal/processing/filters/v2/keywordget.go b/internal/processing/filters/v2/keywordget.go
index 5f5a63b26..3cf120ed8 100644
--- a/internal/processing/filters/v2/keywordget.go
+++ b/internal/processing/filters/v2/keywordget.go
@@ -20,63 +20,55 @@ package v2
import (
"context"
"errors"
- "fmt"
"slices"
"strings"
- 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"
+ 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/typeutils"
)
// KeywordGet looks up a filter keyword by ID.
-func (p *Processor) KeywordGet(ctx context.Context, account *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterKeyword, gtserror.WithCode) {
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterKeywordID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filterKeyword.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID),
- )
+func (p *Processor) KeywordGet(ctx context.Context, requester *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterKeyword, gtserror.WithCode) {
+ filterKeyword, _, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
-
- return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
+ return typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword), nil
}
// KeywordsGetForFilterID looks up all filter keywords for the given filter.
-func (p *Processor) KeywordsGetForFilterID(ctx context.Context, account *gtsmodel.Account, filterID string) ([]*apimodel.FilterKeyword, gtserror.WithCode) {
- // Check that the filter is owned by the given account.
- filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(nil)
- }
+func (p *Processor) KeywordsGetForFilterID(ctx context.Context, requester *gtsmodel.Account, filterID string) ([]*apimodel.FilterKeyword, gtserror.WithCode) {
- filterKeywords, err := p.state.DB.GetFilterKeywordsForFilterID(
- ctx,
- filter.ID,
+ // Get the filter with given ID (but
+ // without any sub-models attached).
+ filter, errWithCode := p.c.GetFilter(
+ gtscontext.SetBarebones(ctx),
+ requester,
+ filterID,
)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, nil
- }
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Fetch all associated filter keywords to the determined existent filter.
+ filterKeywords, err := p.state.DB.GetFilterKeywordsByIDs(ctx, filter.KeywordIDs)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter keywords: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- apiFilterKeywords := make([]*apimodel.FilterKeyword, 0, len(filterKeywords))
- for _, filterKeyword := range filterKeywords {
- apiFilterKeywords = append(apiFilterKeywords, p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword))
+ // Convert all of the filter keyword models from internal to frontend form.
+ apiFilterKeywords := make([]*apimodel.FilterKeyword, len(filterKeywords))
+ if len(apiFilterKeywords) != len(filterKeywords) {
+ // bound check eliminiation compiler-hint
+ panic(gtserror.New("BCE"))
+ }
+ for i, filterKeyword := range filterKeywords {
+ apiFilterKeywords[i] = typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword)
}
// Sort them by ID so that they're in a stable order.
diff --git a/internal/processing/filters/v2/keywordupdate.go b/internal/processing/filters/v2/keywordupdate.go
index 9492e7b3a..047b079db 100644
--- a/internal/processing/filters/v2/keywordupdate.go
+++ b/internal/processing/filters/v2/keywordupdate.go
@@ -20,50 +20,51 @@ package v2
import (
"context"
"errors"
- "fmt"
+ "net/http"
- 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"
+ 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/typeutils"
)
// KeywordUpdate updates an existing filter keyword for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
func (p *Processor) KeywordUpdate(
ctx context.Context,
- account *gtsmodel.Account,
+ requester *gtsmodel.Account,
filterKeywordID string,
form *apimodel.FilterKeywordCreateUpdateRequest,
) (*apimodel.FilterKeyword, gtserror.WithCode) {
- // Get the filter keyword by ID.
- filterKeyword, err := p.state.DB.GetFilterKeywordByID(gtscontext.SetBarebones(ctx), filterKeywordID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filterKeyword.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID),
- )
+
+ // Get the filter keyword with given ID, also checking ownership to requester.
+ filterKeyword, _, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
+ // Update the keyword model fields.
filterKeyword.Keyword = form.Keyword
filterKeyword.WholeWord = form.WholeWord
- if err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, "keyword", "whole_word"); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("duplicate keyword")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
+ // Update existing filter keyword model in the database, (only necessary cols).
+ switch err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, []string{
+ "keyword", "whole_word"}...); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate keyword"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error inserting filter keyword: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
- return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
+ return typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword), nil
}
diff --git a/internal/processing/filters/v2/statuscreate.go b/internal/processing/filters/v2/statuscreate.go
index 7d4469eef..2a3c3d74b 100644
--- a/internal/processing/filters/v2/statuscreate.go
+++ b/internal/processing/filters/v2/statuscreate.go
@@ -20,50 +20,59 @@ package v2
import (
"context"
"errors"
- "fmt"
+ "net/http"
- 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"
+ 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/typeutils"
)
// StatusCreate adds a filter status to an existing filter for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
-func (p *Processor) StatusCreate(ctx context.Context, account *gtsmodel.Account, filterID string, form *apimodel.FilterStatusCreateRequest) (*apimodel.FilterStatus, gtserror.WithCode) {
- // Check that the filter is owned by the given account.
- filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
- )
+func (p *Processor) StatusCreate(ctx context.Context, requester *gtsmodel.Account, filterID string, form *apimodel.FilterStatusCreateRequest) (*apimodel.FilterStatus, gtserror.WithCode) {
+
+ // Get the filter with given ID, also checking ownership.
+ filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
+ // Create new filter status model.
filterStatus := &gtsmodel.FilterStatus{
- ID: id.NewULID(),
- AccountID: account.ID,
- FilterID: filter.ID,
- StatusID: form.StatusID,
+ ID: id.NewULID(),
+ FilterID: filter.ID,
+ StatusID: form.StatusID,
}
- if err := p.state.DB.PutFilterStatus(ctx, filterStatus); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("duplicate status")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
+ // Insert the new filter status model into the database.
+ switch err := p.state.DB.PutFilterStatus(ctx, filterStatus); {
+ case err == nil:
+ // no issue
+
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate status"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
+
+ default:
+ err := gtserror.Newf("error inserting filter status: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Now update the filter it is attached to with new status.
+ filter.StatusIDs = append(filter.StatusIDs, filterStatus.ID)
+ filter.Statuses = append(filter.Statuses, filterStatus)
+
+ // Update the existing filter model in the database (only the needed col).
+ if err := p.state.DB.UpdateFilter(ctx, filter, "statuses"); err != nil {
+ err := gtserror.Newf("error updating filter: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
- return p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus), nil
+ return typeutils.FilterStatusToAPIFilterStatus(filterStatus), nil
}
diff --git a/internal/processing/filters/v2/statusdelete.go b/internal/processing/filters/v2/statusdelete.go
index 706ca691d..321dc88e9 100644
--- a/internal/processing/filters/v2/statusdelete.go
+++ b/internal/processing/filters/v2/statusdelete.go
@@ -19,38 +19,43 @@ package v2
import (
"context"
- "fmt"
+ "slices"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// StatusDelete deletes an existing filter status from a filter.
func (p *Processor) StatusDelete(
ctx context.Context,
- account *gtsmodel.Account,
- filterID string,
+ requester *gtsmodel.Account,
+ filterStatusID string,
) gtserror.WithCode {
- // Get the filter status.
- filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, filterID)
- if err != nil {
- return gtserror.NewErrorNotFound(err)
+ // Get filter status with given ID, also checking ownership to requester.
+ _, filter, errWithCode := p.c.GetFilterStatus(ctx, requester, filterStatusID)
+ if errWithCode != nil {
+ return errWithCode
}
- // Check that the account owns it.
- if filterStatus.AccountID != account.ID {
- return gtserror.NewErrorNotFound(
- fmt.Errorf("filter status %s doesn't belong to account %s", filterStatus.ID, account.ID),
- )
+ // Delete this one filter status from the database, now ownership is confirmed.
+ if err := p.state.DB.DeleteFilterStatusesByIDs(ctx, filterStatusID); err != nil {
+ err := gtserror.Newf("error deleting filter status: %w", err)
+ return gtserror.NewErrorInternalError(err)
}
- // Delete the filter status.
- if err := p.state.DB.DeleteFilterStatusByID(ctx, filterStatus.ID); err != nil {
+ // Delete this filter keyword from the slice of IDs attached to filter.
+ filter.StatusIDs = slices.DeleteFunc(filter.StatusIDs, func(id string) bool {
+ return filterStatusID == id
+ })
+
+ // Update filter in the database now the status has been unattached.
+ if err := p.state.DB.UpdateFilter(ctx, filter, "statuses"); err != nil {
+ err := gtserror.Newf("error updating filter: %w", err)
return gtserror.NewErrorInternalError(err)
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
return nil
}
diff --git a/internal/processing/filters/v2/statusget.go b/internal/processing/filters/v2/statusget.go
index 197a3872e..7aa51f830 100644
--- a/internal/processing/filters/v2/statusget.go
+++ b/internal/processing/filters/v2/statusget.go
@@ -20,63 +20,55 @@ package v2
import (
"context"
"errors"
- "fmt"
"slices"
"strings"
- 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"
+ 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/typeutils"
)
// StatusGet looks up a filter status by ID.
-func (p *Processor) StatusGet(ctx context.Context, account *gtsmodel.Account, filterStatusID string) (*apimodel.FilterStatus, gtserror.WithCode) {
- filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, filterStatusID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filterStatus.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter status %s doesn't belong to account %s", filterStatus.ID, account.ID),
- )
+func (p *Processor) StatusGet(ctx context.Context, requester *gtsmodel.Account, filterStatusID string) (*apimodel.FilterStatus, gtserror.WithCode) {
+ filterStatus, _, errWithCode := p.c.GetFilterStatus(ctx, requester, filterStatusID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
-
- return p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus), nil
+ return typeutils.FilterStatusToAPIFilterStatus(filterStatus), nil
}
// StatusesGetForFilterID looks up all filter statuses for the given filter.
-func (p *Processor) StatusesGetForFilterID(ctx context.Context, account *gtsmodel.Account, filterID string) ([]*apimodel.FilterStatus, gtserror.WithCode) {
- // Check that the filter is owned by the given account.
- filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(nil)
- }
+func (p *Processor) StatusesGetForFilterID(ctx context.Context, requester *gtsmodel.Account, filterID string) ([]*apimodel.FilterStatus, gtserror.WithCode) {
- filterStatuses, err := p.state.DB.GetFilterStatusesForFilterID(
- ctx,
- filter.ID,
+ // Get the filter with given ID (but
+ // without any sub-models attached).
+ filter, errWithCode := p.c.GetFilter(
+ gtscontext.SetBarebones(ctx),
+ requester,
+ filterID,
)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, nil
- }
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ // Fetch all associated filter statuses to the determined existent filter.
+ filterStatuses, err := p.state.DB.GetFilterStatusesByIDs(ctx, filter.StatusIDs)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter statuses: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- apiFilterStatuses := make([]*apimodel.FilterStatus, 0, len(filterStatuses))
- for _, filterStatus := range filterStatuses {
- apiFilterStatuses = append(apiFilterStatuses, p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus))
+ // Convert all of the filter status models from internal to frontend form.
+ apiFilterStatuses := make([]*apimodel.FilterStatus, len(filterStatuses))
+ if len(apiFilterStatuses) != len(filterStatuses) {
+ // bound check eliminiation compiler-hint
+ panic(gtserror.New("BCE"))
+ }
+ for i, filterStatus := range filterStatuses {
+ apiFilterStatuses[i] = typeutils.FilterStatusToAPIFilterStatus(filterStatus)
}
// Sort them by ID so that they're in a stable order.
diff --git a/internal/processing/filters/v2/update.go b/internal/processing/filters/v2/update.go
index d5b5cce01..f55f99bd5 100644
--- a/internal/processing/filters/v2/update.go
+++ b/internal/processing/filters/v2/update.go
@@ -20,250 +20,365 @@ package v2
import (
"context"
"errors"
- "fmt"
- 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/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "net/http"
+ "slices"
"time"
+
+ 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/processing/filters/common"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Update an existing filter for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
func (p *Processor) Update(
ctx context.Context,
- account *gtsmodel.Account,
+ requester *gtsmodel.Account,
filterID string,
form *apimodel.FilterUpdateRequestV2,
) (*apimodel.FilterV2, gtserror.WithCode) {
- var errWithCode gtserror.WithCode
-
- // Get the filter by ID, with existing keywords and statuses.
- filter, err := p.state.DB.GetFilterByID(ctx, filterID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
- if filter.AccountID != account.ID {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
- )
+ // Get the filter with given ID, also checking ownership.
+ filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
+ if errWithCode != nil {
+ return nil, errWithCode
}
- // Filter columns that we're going to update.
- filterColumns := []string{}
+ // Filter columns that
+ // we're going to update.
+ cols := make([]string, 0, 6)
- // Apply filter changes.
+ // Check for title change.
if form.Title != nil {
- filterColumns = append(filterColumns, "title")
+ cols = append(cols, "title")
filter.Title = *form.Title
}
+
+ // Check action type change.
if form.FilterAction != nil {
- filterColumns = append(filterColumns, "action")
+ cols = append(cols, "action")
+
+ // Parse filter action from form and set on filter, checking for validity.
filter.Action = typeutils.APIFilterActionToFilterAction(*form.FilterAction)
+ if filter.Action == 0 {
+ const text = "invalid filter action"
+ return nil, gtserror.NewWithCode(http.StatusBadRequest, text)
+ }
}
+
+ // Check expiry change.
if form.ExpiresIn != nil {
- expiresIn := *form.ExpiresIn
- filterColumns = append(filterColumns, "expires_at")
- if expiresIn == 0 {
- // Unset the expiration date.
- filter.ExpiresAt = time.Time{}
- } else {
- // Update the expiration date.
- filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(expiresIn))
+ cols = append(cols, "expires_at")
+ filter.ExpiresAt = time.Time{}
+
+ // Check form for valid
+ // expiry and set on filter.
+ if *form.ExpiresIn > 0 {
+ expiresIn := time.Duration(*form.ExpiresIn) * time.Second
+ filter.ExpiresAt = time.Now().Add(expiresIn)
}
}
+
+ // Check context change.
if form.Context != nil {
- filterColumns = append(filterColumns,
- "context_home",
- "context_notifications",
- "context_public",
- "context_thread",
- "context_account",
- )
- filter.ContextHome = util.Ptr(false)
- filter.ContextNotifications = util.Ptr(false)
- filter.ContextPublic = util.Ptr(false)
- filter.ContextThread = util.Ptr(false)
- filter.ContextAccount = util.Ptr(false)
- for _, context := range *form.Context {
- switch context {
- case apimodel.FilterContextHome:
- filter.ContextHome = util.Ptr(true)
- case apimodel.FilterContextNotifications:
- filter.ContextNotifications = util.Ptr(true)
- case apimodel.FilterContextPublic:
- filter.ContextPublic = util.Ptr(true)
- case apimodel.FilterContextThread:
- filter.ContextThread = util.Ptr(true)
- case apimodel.FilterContextAccount:
- filter.ContextAccount = util.Ptr(true)
- default:
- return nil, gtserror.NewErrorUnprocessableEntity(
- fmt.Errorf("unsupported filter context '%s'", context),
- )
- }
+ cols = append(cols, "contexts")
+
+ // Parse contexts filter applies in from incoming request form data.
+ filter.Contexts, errWithCode = common.FromAPIContexts(*form.Context)
+ if errWithCode != nil {
+ return nil, errWithCode
}
}
- filterKeywordColumns, deleteFilterKeywordIDs, errWithCode := applyKeywordChanges(filter, form.Keywords)
- if err != nil {
+ // Check for any changes to attached keywords on filter.
+ keywordQs, errWithCode := p.updateFilterKeywords(ctx,
+ filter, form.Keywords)
+ if errWithCode != nil {
return nil, errWithCode
+ } else if len(keywordQs.create) > 0 || len(keywordQs.delete) > 0 {
+
+ // Attached keywords have changed.
+ cols = append(cols, "keywords")
}
- deleteFilterStatusIDs, errWithCode := applyStatusChanges(filter, form.Statuses)
- if err != nil {
+ // Check for any changes to attached statuses on filter.
+ statusQs, errWithCode := p.updateFilterStatuses(ctx,
+ filter, form.Statuses)
+ if errWithCode != nil {
return nil, errWithCode
- }
+ } else if len(statusQs.create) > 0 || len(statusQs.delete) > 0 {
- if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, filterKeywordColumns, deleteFilterKeywordIDs, deleteFilterStatusIDs); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- err = errors.New("you already have a filter with this title")
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
- return nil, gtserror.NewErrorInternalError(err)
+ // Attached statuses have changed.
+ cols = append(cols, "statuses")
}
- apiFilter, errWithCode := p.apiFilter(ctx, filter)
+ // Perform all the deferred database queries.
+ errWithCode = performTxs(keywordQs, statusQs)
if errWithCode != nil {
return nil, errWithCode
}
- // Send a filters changed event.
- p.stream.FiltersChanged(ctx, account)
+ // Update the filter model in the database with determined cols.
+ switch err := p.state.DB.UpdateFilter(ctx, filter, cols...); {
+ case err == nil:
+ // no issue
- return apiFilter, nil
-}
+ case errors.Is(err, db.ErrAlreadyExists):
+ const text = "duplicate title"
+ return nil, gtserror.NewWithCode(http.StatusConflict, text)
-// applyKeywordChanges applies the provided changes to the filter's keywords in place,
-// and returns a list of lists of filter columns to update, and a list of filter keyword IDs to delete.
-func applyKeywordChanges(filter *gtsmodel.Filter, formKeywords []apimodel.FilterKeywordCreateUpdateDeleteRequest) ([][]string, []string, gtserror.WithCode) {
- if len(formKeywords) == 0 {
- // Detach currently existing keywords from the filter so we don't change them.
- filter.Keywords = nil
- return nil, nil, nil
+ default:
+ err := gtserror.Newf("error updating filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
}
- deleteFilterKeywordIDs := []string{}
- filterKeywordsByID := map[string]*gtsmodel.FilterKeyword{}
- filterKeywordColumnsByID := map[string][]string{}
- for _, filterKeyword := range filter.Keywords {
- filterKeywordsByID[filterKeyword.ID] = filterKeyword
+ // Handle filter change side-effects.
+ p.c.OnFilterChanged(ctx, requester)
+
+ // Return as converted frontend filter model.
+ return typeutils.FilterToAPIFilterV2(filter), nil
+}
+
+func (p *Processor) updateFilterKeywords(ctx context.Context, filter *gtsmodel.Filter, form []apimodel.FilterKeywordCreateUpdateDeleteRequest) (deferredQs, gtserror.WithCode) {
+ if len(form) == 0 {
+ // No keyword changes.
+ return deferredQs{}, nil
}
- for _, formKeyword := range formKeywords {
- if formKeyword.ID != nil {
- id := *formKeyword.ID
- filterKeyword, ok := filterKeywordsByID[id]
- if !ok {
- return nil, nil, gtserror.NewErrorNotFound(
- fmt.Errorf("couldn't find filter keyword '%s' to update or delete", id),
- )
+ var deferred deferredQs
+ for _, request := range form {
+ if request.ID != nil {
+ // Look by ID for keyword attached to filter.
+ idx := slices.IndexFunc(filter.Keywords,
+ func(f *gtsmodel.FilterKeyword) bool {
+ return f.ID == (*request.ID)
+ })
+ if idx == -1 {
+ const text = "filter keyword not found"
+ return deferred, gtserror.NewWithCode(http.StatusNotFound, text)
}
- // Process deletes.
- if *formKeyword.Destroy {
- delete(filterKeywordsByID, id)
- deleteFilterKeywordIDs = append(deleteFilterKeywordIDs, id)
+ // If this is a delete, update filter's id list.
+ if request.Destroy != nil && *request.Destroy {
+ filter.Keywords = slices.Delete(filter.Keywords, idx, idx+1)
+ filter.KeywordIDs = slices.Delete(filter.KeywordIDs, idx, idx+1)
+
+ // Append database delete to funcs for later processing by caller.
+ deferred.delete = append(deferred.delete, func() gtserror.WithCode {
+ if err := p.state.DB.DeleteFilterKeywordsByIDs(ctx, *request.ID); //
+ err != nil {
+ err := gtserror.Newf("error deleting filter keyword: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
continue
}
- // Process updates.
- columns := make([]string, 0, 2)
- if formKeyword.Keyword != nil {
- columns = append(columns, "keyword")
- filterKeyword.Keyword = *formKeyword.Keyword
+ // Get the filter keyword at index.
+ filterKeyword := filter.Keywords[idx]
+
+ // Filter keywords database
+ // columns we need to update.
+ cols := make([]string, 0, 2)
+
+ // Check for changes to keyword string.
+ if val := request.Keyword; val != nil {
+ cols = append(cols, "keyword")
+ filterKeyword.Keyword = *val
+ }
+
+ // Check for changes to wholeword flag.
+ if val := request.WholeWord; val != nil {
+ cols = append(cols, "whole_word")
+ filterKeyword.WholeWord = val
}
- if formKeyword.WholeWord != nil {
- columns = append(columns, "whole_word")
- filterKeyword.WholeWord = formKeyword.WholeWord
+
+ // Verify that this is valid regular expression.
+ if err := filterKeyword.Compile(); err != nil {
+ const text = "invalid regular expression"
+ err := gtserror.Newf("invalid regular expression: %w", err)
+ return deferred, gtserror.NewWithCodeSafe(
+ http.StatusBadRequest,
+ err, text,
+ )
+ }
+
+ if len(cols) > 0 {
+ // Append database update to funcs for later processing by caller.
+ deferred.update = append(deferred.update, func() gtserror.WithCode {
+ if err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, cols...); //
+ err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ const text = "duplicate keyword"
+ return gtserror.NewWithCode(http.StatusConflict, text)
+ }
+ err := gtserror.Newf("error updating filter keyword: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
}
- filterKeywordColumnsByID[id] = columns
+
continue
}
- // Process creates.
+ // Check for valid request.
+ if request.Keyword == nil {
+ const text = "missing keyword"
+ return deferred, gtserror.NewWithCode(http.StatusBadRequest, text)
+ }
+
+ // Create new filter keyword for insert.
filterKeyword := &gtsmodel.FilterKeyword{
ID: id.NewULID(),
- AccountID: filter.AccountID,
FilterID: filter.ID,
- Filter: filter,
- Keyword: *formKeyword.Keyword,
- WholeWord: util.Ptr(util.PtrOrValue(formKeyword.WholeWord, false)),
+ Keyword: *request.Keyword,
+ WholeWord: request.WholeWord,
}
- filterKeywordsByID[filterKeyword.ID] = filterKeyword
- // Don't need to set columns, as we're using all of them.
- }
- // Replace the filter's keywords list with our updated version.
- filterKeywordColumns := [][]string{}
- filter.Keywords = nil
- for id, filterKeyword := range filterKeywordsByID {
+ // Verify that this is valid regular expression.
+ if err := filterKeyword.Compile(); err != nil {
+ const text = "invalid regular expression"
+ err := gtserror.Newf("invalid regular expression: %w", err)
+ return deferred, gtserror.NewWithCodeSafe(
+ http.StatusBadRequest,
+ err, text,
+ )
+ }
+
+ // Append new filter keyword to filter and list of IDs.
filter.Keywords = append(filter.Keywords, filterKeyword)
- // Okay to use the nil slice zero value for entries being created instead of updated.
- filterKeywordColumns = append(filterKeywordColumns, filterKeywordColumnsByID[id])
+ filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID)
+
+ // Append database insert to funcs for later processing by caller.
+ deferred.create = append(deferred.create, func() gtserror.WithCode {
+ if err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); //
+ err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ const text = "duplicate keyword"
+ return gtserror.NewWithCode(http.StatusConflict, text)
+ }
+ err := gtserror.Newf("error inserting filter keyword: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
}
- return filterKeywordColumns, deleteFilterKeywordIDs, nil
+ return deferred, nil
}
-// applyKeywordChanges applies the provided changes to the filter's keywords in place,
-// and returns a list of filter status IDs to delete.
-func applyStatusChanges(filter *gtsmodel.Filter, formStatuses []apimodel.FilterStatusCreateDeleteRequest) ([]string, gtserror.WithCode) {
- if len(formStatuses) == 0 {
- // Detach currently existing statuses from the filter so we don't change them.
- filter.Statuses = nil
- return nil, nil
- }
-
- deleteFilterStatusIDs := []string{}
- filterStatusesByID := map[string]*gtsmodel.FilterStatus{}
- for _, filterStatus := range filter.Statuses {
- filterStatusesByID[filterStatus.ID] = filterStatus
+func (p *Processor) updateFilterStatuses(ctx context.Context, filter *gtsmodel.Filter, form []apimodel.FilterStatusCreateDeleteRequest) (deferredQs, gtserror.WithCode) {
+ if len(form) == 0 {
+ // No keyword changes.
+ return deferredQs{}, nil
}
- for _, formStatus := range formStatuses {
- if formStatus.ID != nil {
- id := *formStatus.ID
- _, ok := filterStatusesByID[id]
- if !ok {
- return nil, gtserror.NewErrorNotFound(
- fmt.Errorf("couldn't find filter status '%s' to delete", id),
- )
+ var deferred deferredQs
+ for _, request := range form {
+ if request.ID != nil {
+ // Look by ID for status attached to filter.
+ idx := slices.IndexFunc(filter.Statuses,
+ func(f *gtsmodel.FilterStatus) bool {
+ return f.ID == *request.ID
+ })
+ if idx == -1 {
+ const text = "filter status not found"
+ return deferred, gtserror.NewWithCode(http.StatusNotFound, text)
}
- // Process deletes.
- if *formStatus.Destroy {
- delete(filterStatusesByID, id)
- deleteFilterStatusIDs = append(deleteFilterStatusIDs, id)
- continue
- }
+ // If this is a delete, update filter's id list.
+ if request.Destroy != nil && *request.Destroy {
+ filter.Statuses = slices.Delete(filter.Statuses, idx, idx+1)
+ filter.StatusIDs = slices.Delete(filter.StatusIDs, idx, idx+1)
- // Filter statuses don't have updates.
+ // Append database delete to funcs for later processing by caller.
+ deferred.delete = append(deferred.delete, func() gtserror.WithCode {
+ if err := p.state.DB.DeleteFilterStatusesByIDs(ctx, *request.ID); //
+ err != nil {
+ err := gtserror.Newf("error deleting filter status: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
+ }
continue
}
- // Process creates.
+ // Check for valid request.
+ if request.StatusID == nil {
+ const text = "missing status"
+ return deferred, gtserror.NewWithCode(http.StatusBadRequest, text)
+ }
+
+ // Create new filter status for insert.
filterStatus := &gtsmodel.FilterStatus{
- ID: id.NewULID(),
- AccountID: filter.AccountID,
- FilterID: filter.ID,
- Filter: filter,
- StatusID: *formStatus.StatusID,
+ ID: id.NewULID(),
+ FilterID: filter.ID,
+ StatusID: *request.StatusID,
}
- filterStatusesByID[filterStatus.ID] = filterStatus
- }
- // Replace the filter's keywords list with our updated version.
- filter.Statuses = nil
- for _, filterStatus := range filterStatusesByID {
+ // Append new filter status to filter and list of IDs.
filter.Statuses = append(filter.Statuses, filterStatus)
+ filter.StatusIDs = append(filter.StatusIDs, filterStatus.ID)
+
+ // Append database insert to funcs for later processing by caller.
+ deferred.create = append(deferred.create, func() gtserror.WithCode {
+ if err := p.state.DB.PutFilterStatus(ctx, filterStatus); //
+ err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ const text = "duplicate status"
+ return gtserror.NewWithCode(http.StatusConflict, text)
+ }
+ err := gtserror.Newf("error inserting filter status: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ return nil
+ })
+ }
+
+ return deferred, nil
+}
+
+// deferredQs stores selection of
+// deferred database queries.
+type deferredQs struct {
+ create []func() gtserror.WithCode
+ update []func() gtserror.WithCode
+ delete []func() gtserror.WithCode
+}
+
+// performTx performs the passed deferredQs functions,
+// prioritising create / update operations before deletes.
+func performTxs(queries ...deferredQs) gtserror.WithCode {
+
+ // Perform create / update
+ // operations before anything.
+ for _, q := range queries {
+ for _, create := range q.create {
+ if errWithCode := create(); errWithCode != nil {
+ return errWithCode
+ }
+ }
+ for _, update := range q.update {
+ if errWithCode := update(); errWithCode != nil {
+ return errWithCode
+ }
+ }
+ }
+
+ // Perform deletes last.
+ for _, q := range queries {
+ for _, delete := range q.delete {
+ if errWithCode := delete(); errWithCode != nil {
+ return errWithCode
+ }
+ }
}
- return deleteFilterStatusIDs, nil
+ return nil
}
diff --git a/internal/processing/followrequest_test.go b/internal/processing/followrequest_test.go
index db0419522..d5b7fdad2 100644
--- a/internal/processing/followrequest_test.go
+++ b/internal/processing/followrequest_test.go
@@ -18,17 +18,16 @@
package processing_test
import (
- "context"
"encoding/json"
"fmt"
"io"
"testing"
"time"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
// TODO: move this to the "internal/processing/account" pkg
@@ -53,11 +52,11 @@ func (suite *FollowRequestTestSuite) TestFollowRequestAccept() {
TargetAccountID: requestingAccount.ID,
}
- err := suite.db.Put(context.Background(), fr)
+ err := suite.db.Put(suite.T().Context(), fr)
suite.NoError(err)
relationship, errWithCode := suite.processor.Account().FollowRequestAccept(
- context.Background(),
+ suite.T().Context(),
requestingAccount,
targetAccount.ID,
)
@@ -139,11 +138,11 @@ func (suite *FollowRequestTestSuite) TestFollowRequestReject() {
TargetAccountID: requestingAccount.ID,
}
- err := suite.db.Put(context.Background(), fr)
+ err := suite.db.Put(suite.T().Context(), fr)
suite.NoError(err)
relationship, errWithCode := suite.processor.Account().FollowRequestReject(
- context.Background(),
+ suite.T().Context(),
requestingAccount,
targetAccount.ID,
)
diff --git a/internal/processing/instance.go b/internal/processing/instance.go
index 2f4c40416..e1a3785e9 100644
--- a/internal/processing/instance.go
+++ b/internal/processing/instance.go
@@ -19,19 +19,22 @@ package processing
import (
"context"
+ "errors"
"fmt"
- "sort"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/text"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/util"
- "github.com/superseriousbusiness/gotosocial/internal/validate"
+ "slices"
+ "strings"
+
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "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/log"
+ "code.superseriousbusiness.org/gotosocial/internal/text"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/internal/util/xslices"
+ "code.superseriousbusiness.org/gotosocial/internal/validate"
)
func (p *Processor) InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
@@ -62,70 +65,126 @@ func (p *Processor) InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gt
return ai, nil
}
-func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool, includeOpen bool, flat bool) (interface{}, gtserror.WithCode) {
- domains := []*apimodel.Domain{}
+func (p *Processor) InstancePeersGet(
+ ctx context.Context,
+ includeBlocked bool,
+ includeAllowed bool,
+ includeOpen bool,
+ flatten bool,
+ includeSeverity bool,
+) (any, gtserror.WithCode) {
+ var (
+ domainPerms []gtsmodel.DomainPermission
+ apiDomains []*apimodel.Domain
+ )
+
+ if includeBlocked {
+ blocks, err := p.state.DB.GetDomainBlocks(ctx)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting domain blocks: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
- if includeOpen {
- instances, err := p.state.DB.GetInstancePeers(ctx, false)
- if err != nil && err != db.ErrNoEntries {
- err = fmt.Errorf("error selecting instance peers: %s", err)
+ for _, block := range blocks {
+ domainPerms = append(domainPerms, block)
+ }
+
+ } else if includeAllowed {
+ allows, err := p.state.DB.GetDomainAllows(ctx)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting domain allows: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- for _, i := range instances {
- // Domain may be in Punycode,
- // de-punify it just in case.
- d, err := util.DePunify(i.Domain)
- if err != nil {
- log.Errorf(ctx, "couldn't depunify domain %s: %s", i.Domain, err)
- continue
- }
+ for _, allow := range allows {
+ domainPerms = append(domainPerms, allow)
+ }
+ }
- domains = append(domains, &apimodel.Domain{Domain: d})
+ for _, domainPerm := range domainPerms {
+ // Domain may be in Punycode,
+ // de-punify it just in case.
+ domain := domainPerm.GetDomain()
+ depunied, err := util.DePunify(domain)
+ if err != nil {
+ log.Errorf(ctx, "couldn't depunify domain %s: %v", domain, err)
+ continue
}
+
+ if util.PtrOrZero(domainPerm.GetObfuscate()) {
+ // Obfuscate the de-punified version.
+ depunied = obfuscate(depunied)
+ }
+
+ apiDomain := &apimodel.Domain{
+ Domain: depunied,
+ Comment: util.Ptr(domainPerm.GetPublicComment()),
+ }
+
+ if domainPerm.GetType() == gtsmodel.DomainPermissionBlock {
+ const severity = "suspend"
+ apiDomain.Severity = severity
+ suspendedAt := domainPerm.GetCreatedAt()
+ apiDomain.SuspendedAt = util.FormatISO8601(suspendedAt)
+ }
+
+ apiDomains = append(apiDomains, apiDomain)
}
- if includeSuspended {
- domainBlocks := []*gtsmodel.DomainBlock{}
- if err := p.state.DB.GetAll(ctx, &domainBlocks); err != nil && err != db.ErrNoEntries {
+ if includeOpen {
+ instances, err := p.state.DB.GetInstancePeers(ctx, false)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err = gtserror.Newf("db error getting instance peers: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- for _, domainBlock := range domainBlocks {
+ for _, instance := range instances {
// Domain may be in Punycode,
// de-punify it just in case.
- d, err := util.DePunify(domainBlock.Domain)
+ domain := instance.Domain
+ depunied, err := util.DePunify(domain)
if err != nil {
- log.Errorf(ctx, "couldn't depunify domain %s: %s", domainBlock.Domain, err)
+ log.Errorf(ctx, "couldn't depunify domain %s: %v", domain, err)
continue
}
- if *domainBlock.Obfuscate {
- // Obfuscate the de-punified version.
- d = obfuscate(d)
- }
-
- domains = append(domains, &apimodel.Domain{
- Domain: d,
- SuspendedAt: util.FormatISO8601(domainBlock.CreatedAt),
- PublicComment: domainBlock.PublicComment,
- })
+ apiDomains = append(
+ apiDomains,
+ &apimodel.Domain{
+ Domain: depunied,
+ },
+ )
}
}
- sort.Slice(domains, func(i, j int) bool {
- return domains[i].Domain < domains[j].Domain
- })
-
- if flat {
- flattened := []string{}
- for _, d := range domains {
- flattened = append(flattened, d.Domain)
- }
- return flattened, nil
+ // Sort a-z.
+ slices.SortFunc(
+ apiDomains,
+ func(a, b *apimodel.Domain) int {
+ return strings.Compare(a.Domain, b.Domain)
+ },
+ )
+
+ // Deduplicate.
+ apiDomains = xslices.DeduplicateFunc(
+ apiDomains,
+ func(v *apimodel.Domain) string {
+ return v.Domain
+ },
+ )
+
+ if flatten {
+ // Return just the domains.
+ return xslices.Gather(
+ []string{},
+ apiDomains,
+ func(v *apimodel.Domain) string {
+ return v.Domain
+ },
+ ), nil
}
- return domains, nil
+ return apiDomains, nil
}
func (p *Processor) InstanceGetRules(ctx context.Context) ([]apimodel.InstanceRule, gtserror.WithCode) {
@@ -165,7 +224,7 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
}
// Don't allow html in site title.
- instance.Title = text.SanitizeToPlaintext(title)
+ instance.Title = text.StripHTMLFromText(title)
columns = append(columns, "title")
}
@@ -235,7 +294,7 @@ func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- instance.CustomCSS = text.SanitizeToPlaintext(customCSS)
+ instance.CustomCSS = text.StripHTMLFromText(customCSS)
columns = append(columns, []string{"custom_css"}...)
}
diff --git a/internal/processing/interactionrequests/accept.go b/internal/processing/interactionrequests/accept.go
index ad86e50d1..7efd1f373 100644
--- a/internal/processing/interactionrequests/accept.go
+++ b/internal/processing/interactionrequests/accept.go
@@ -21,13 +21,13 @@ import (
"context"
"time"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- 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/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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// Accept accepts an interaction request with the given ID,
@@ -65,14 +65,16 @@ func (p *Processor) Accept(
defer unlock()
// Mark the request as accepted
- // and generate a URI for it.
+ // and generate URIs for it.
req.AcceptedAt = time.Now()
- req.URI = uris.GenerateURIForAccept(acct.Username, req.ID)
+ req.ResponseURI = uris.GenerateURIForAccept(acct.Username, req.ID)
+ req.AuthorizationURI = uris.GenerateURIForAuthorization(acct.Username, req.ID)
if err := p.state.DB.UpdateInteractionRequest(
ctx,
req,
"accepted_at",
- "uri",
+ "response_uri",
+ "authorization_uri",
); err != nil {
err := gtserror.Newf("db error updating interaction request: %w", err)
return nil, gtserror.NewErrorInternalError(err)
@@ -132,7 +134,7 @@ func (p *Processor) acceptLike(
// Update the Like.
req.Like.PendingApproval = util.Ptr(false)
req.Like.PreApproved = false
- req.Like.ApprovedByURI = req.URI
+ req.Like.ApprovedByURI = req.AuthorizationURI
if err := p.state.DB.UpdateStatusFave(
ctx,
req.Like,
@@ -173,7 +175,7 @@ func (p *Processor) acceptReply(
// Update the Reply.
req.Reply.PendingApproval = util.Ptr(false)
req.Reply.PreApproved = false
- req.Reply.ApprovedByURI = req.URI
+ req.Reply.ApprovedByURI = req.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
req.Reply,
@@ -214,7 +216,7 @@ func (p *Processor) acceptAnnounce(
// Update the Announce.
req.Announce.PendingApproval = util.Ptr(false)
req.Announce.PreApproved = false
- req.Announce.ApprovedByURI = req.URI
+ req.Announce.ApprovedByURI = req.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
req.Announce,
diff --git a/internal/processing/interactionrequests/accept_test.go b/internal/processing/interactionrequests/accept_test.go
index 75fb1e512..cb4212c24 100644
--- a/internal/processing/interactionrequests/accept_test.go
+++ b/internal/processing/interactionrequests/accept_test.go
@@ -18,13 +18,12 @@
package interactionrequests_test
import (
- "context"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/interactionrequests"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type AcceptTestSuite struct {
@@ -36,7 +35,7 @@ func (suite *AcceptTestSuite) TestAccept() {
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
state = testStructs.State
acct = suite.testAccounts["local_account_2"]
intReq = suite.testInteractionRequests["admin_account_reply_turtle"]
@@ -68,7 +67,7 @@ func (suite *AcceptTestSuite) TestAccept() {
suite.FailNow(err.Error())
}
suite.False(*dbStatus.PendingApproval)
- suite.Equal(dbReq.URI, dbStatus.ApprovedByURI)
+ suite.Equal(dbReq.AuthorizationURI, dbStatus.ApprovedByURI)
// Wait for a notification
// for interacting status.
diff --git a/internal/processing/interactionrequests/get.go b/internal/processing/interactionrequests/get.go
index 8f8a7f35d..397730855 100644
--- a/internal/processing/interactionrequests/get.go
+++ b/internal/processing/interactionrequests/get.go
@@ -23,13 +23,13 @@ import (
"net/url"
"strconv"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/paging"
+ 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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
)
// GetPage returns a page of interaction requests targeting
diff --git a/internal/processing/interactionrequests/interactionrequests.go b/internal/processing/interactionrequests/interactionrequests.go
index d56636233..435bbd42f 100644
--- a/internal/processing/interactionrequests/interactionrequests.go
+++ b/internal/processing/interactionrequests/interactionrequests.go
@@ -18,9 +18,9 @@
package interactionrequests
import (
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Processor wraps functionality for getting,
diff --git a/internal/processing/interactionrequests/interactionrequests_test.go b/internal/processing/interactionrequests/interactionrequests_test.go
index ce2aa351a..6459036fc 100644
--- a/internal/processing/interactionrequests/interactionrequests_test.go
+++ b/internal/processing/interactionrequests/interactionrequests_test.go
@@ -18,9 +18,9 @@
package interactionrequests_test
import (
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
const (
diff --git a/internal/processing/interactionrequests/reject.go b/internal/processing/interactionrequests/reject.go
index dcf07a03f..4db52e260 100644
--- a/internal/processing/interactionrequests/reject.go
+++ b/internal/processing/interactionrequests/reject.go
@@ -21,12 +21,12 @@ import (
"context"
"time"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- 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/messages"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ 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/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
)
// Reject rejects an interaction request with the given ID,
@@ -66,12 +66,12 @@ func (p *Processor) Reject(
// Mark the request as rejected
// and generate a URI for it.
req.RejectedAt = time.Now()
- req.URI = uris.GenerateURIForReject(acct.Username, req.ID)
+ req.ResponseURI = uris.GenerateURIForReject(acct.Username, req.ID)
if err := p.state.DB.UpdateInteractionRequest(
ctx,
req,
"rejected_at",
- "uri",
+ "response_uri",
); err != nil {
err := gtserror.Newf("db error updating interaction request: %w", err)
return nil, gtserror.NewErrorInternalError(err)
diff --git a/internal/processing/interactionrequests/reject_test.go b/internal/processing/interactionrequests/reject_test.go
index 6e4aac691..76e31b491 100644
--- a/internal/processing/interactionrequests/reject_test.go
+++ b/internal/processing/interactionrequests/reject_test.go
@@ -18,15 +18,14 @@
package interactionrequests_test
import (
- "context"
"errors"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/interactionrequests"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type RejectTestSuite struct {
@@ -38,7 +37,7 @@ func (suite *RejectTestSuite) TestReject() {
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
state = testStructs.State
acct = suite.testAccounts["local_account_2"]
intReq = suite.testInteractionRequests["admin_account_reply_turtle"]
diff --git a/internal/processing/list/create.go b/internal/processing/list/create.go
index dacd7909f..43bec321a 100644
--- a/internal/processing/list/create.go
+++ b/internal/processing/list/create.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/id"
+ 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"
)
// Create creates one a new list for the given account, using the provided parameters.
diff --git a/internal/processing/list/delete.go b/internal/processing/list/delete.go
index 327ac9d16..8039487dd 100644
--- a/internal/processing/list/delete.go
+++ b/internal/processing/list/delete.go
@@ -20,9 +20,9 @@ package list
import (
"context"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// Delete deletes one list for the given account.
diff --git a/internal/processing/list/get.go b/internal/processing/list/get.go
index b98678eef..aefa01027 100644
--- a/internal/processing/list/get.go
+++ b/internal/processing/list/get.go
@@ -21,13 +21,13 @@ import (
"context"
"errors"
- 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"
- "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/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
)
// Get returns the api model of one list with the given ID.
diff --git a/internal/processing/list/list.go b/internal/processing/list/list.go
index 0003816fb..453bf3a57 100644
--- a/internal/processing/list/list.go
+++ b/internal/processing/list/list.go
@@ -18,8 +18,8 @@
package list
import (
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/list/update.go b/internal/processing/list/update.go
index 408c334de..c64686344 100644
--- a/internal/processing/list/update.go
+++ b/internal/processing/list/update.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/gtscontext"
- "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/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// Update updates one list for the given account, using the provided parameters.
diff --git a/internal/processing/list/updateentries.go b/internal/processing/list/updateentries.go
index c15248f39..7cf6a3cd3 100644
--- a/internal/processing/list/updateentries.go
+++ b/internal/processing/list/updateentries.go
@@ -22,12 +22,12 @@ import (
"errors"
"fmt"
- "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/util"
+ "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/util"
)
// AddToList adds targetAccountIDs to the given list, if valid.
diff --git a/internal/processing/list/util.go b/internal/processing/list/util.go
index 74d148704..faed5479b 100644
--- a/internal/processing/list/util.go
+++ b/internal/processing/list/util.go
@@ -22,10 +22,10 @@ import (
"errors"
"fmt"
- 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"
)
// getList is a shortcut to get one list from the database and
diff --git a/internal/processing/markers/get.go b/internal/processing/markers/get.go
index 38e8b53dc..19c8feedc 100644
--- a/internal/processing/markers/get.go
+++ b/internal/processing/markers/get.go
@@ -22,11 +22,11 @@ import (
"errors"
"fmt"
- 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/typeutils"
+ 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/typeutils"
)
// Get returns an API model for the markers of the requested timelines.
diff --git a/internal/processing/markers/markers.go b/internal/processing/markers/markers.go
index 8817ed1a7..18624d44c 100644
--- a/internal/processing/markers/markers.go
+++ b/internal/processing/markers/markers.go
@@ -18,8 +18,8 @@
package markers
import (
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/markers/update.go b/internal/processing/markers/update.go
index 22fe65faf..21c1bc169 100644
--- a/internal/processing/markers/update.go
+++ b/internal/processing/markers/update.go
@@ -22,10 +22,10 @@ import (
"errors"
"fmt"
- 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"
)
// Update updates the given markers and returns an API model for them.
diff --git a/internal/processing/media/create.go b/internal/processing/media/create.go
index 5ea630618..aaccf4bde 100644
--- a/internal/processing/media/create.go
+++ b/internal/processing/media/create.go
@@ -23,13 +23,14 @@ import (
"fmt"
"io"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
"codeberg.org/gruf/go-iotools"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
)
// Create creates a new media attachment belonging to the given account, using the request form.
@@ -89,11 +90,6 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
return nil, errWithCode
}
- apiAttachment, err := p.converter.AttachmentToAPIAttachment(ctx, attachment)
- if err != nil {
- err := fmt.Errorf("error parsing media attachment to frontend type: %s", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return &apiAttachment, nil
+ a := typeutils.AttachmentToAPIAttachment(attachment)
+ return &a, nil
}
diff --git a/internal/processing/media/delete.go b/internal/processing/media/delete.go
index 32650fb2c..afb5af1f3 100644
--- a/internal/processing/media/delete.go
+++ b/internal/processing/media/delete.go
@@ -23,9 +23,9 @@ import (
"fmt"
"strings"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/storage"
)
// Delete deletes the media attachment with the given ID, including all files pertaining to that attachment.
diff --git a/internal/processing/media/getemoji.go b/internal/processing/media/getemoji.go
index 06712756a..d80886979 100644
--- a/internal/processing/media/getemoji.go
+++ b/internal/processing/media/getemoji.go
@@ -21,10 +21,10 @@ import (
"context"
"fmt"
- 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/log"
+ 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"
)
// GetCustomEmojis returns a list of all useable local custom emojis stored on this instance.
diff --git a/internal/processing/media/getemoji_test.go b/internal/processing/media/getemoji_test.go
index eeb1e5020..0e2c2d0cc 100644
--- a/internal/processing/media/getemoji_test.go
+++ b/internal/processing/media/getemoji_test.go
@@ -18,7 +18,6 @@
package media_test
import (
- "context"
"testing"
"github.com/stretchr/testify/suite"
@@ -29,7 +28,7 @@ type GetEmojiTestSuite struct {
}
func (suite *GetEmojiTestSuite) TestGetCustomEmojis() {
- emojis, err := suite.mediaProcessor.GetCustomEmojis(context.Background())
+ emojis, err := suite.mediaProcessor.GetCustomEmojis(suite.T().Context())
suite.NoError(err)
suite.Equal(1, len(emojis))
diff --git a/internal/processing/media/getfile.go b/internal/processing/media/getfile.go
index 11d8f7eb5..0aeac04b3 100644
--- a/internal/processing/media/getfile.go
+++ b/internal/processing/media/getfile.go
@@ -21,18 +21,21 @@ import (
"context"
"errors"
"fmt"
+ "io"
+ "net/http"
"net/url"
"strings"
"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/media"
- "github.com/superseriousbusiness/gotosocial/internal/regexes"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
+ 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/media"
+ "code.superseriousbusiness.org/gotosocial/internal/regexes"
+ "code.superseriousbusiness.org/gotosocial/internal/storage"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// GetFile retrieves a file from storage and streams it back
@@ -162,47 +165,94 @@ func (p *Processor) getAttachmentContent(
requestUser = requester.Username
}
- // Ensure that stored media is cached.
- // (this handles local media / recaches).
- attach, err = p.federator.RefreshMedia(
- ctx,
- requestUser,
- attach,
- media.AdditionalMediaInfo{},
- false,
- )
- if err != nil {
- err := gtserror.Newf("error recaching media: %w", err)
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- // Start preparing API content model.
- apiContent := &apimodel.Content{}
-
- // Retrieve appropriate
- // size file from storage.
+ // Start preparing API content model and other
+ // values depending on requested media size.
+ var content apimodel.Content
+ var mediaPath string
switch sizeStr {
+ // Original media size.
case media.SizeOriginal:
- apiContent.ContentType = attach.File.ContentType
- apiContent.ContentLength = int64(attach.File.FileSize)
- return p.getContent(ctx,
- attach.File.Path,
- apiContent,
- )
+ content.ContentType = attach.File.ContentType
+ content.ContentLength = int64(attach.File.FileSize)
+ mediaPath = attach.File.Path
+ // Thumbnail media size.
case media.SizeSmall:
- apiContent.ContentType = attach.Thumbnail.ContentType
- apiContent.ContentLength = int64(attach.Thumbnail.FileSize)
- return p.getContent(ctx,
- attach.Thumbnail.Path,
- apiContent,
- )
+ content.ContentType = attach.Thumbnail.ContentType
+ content.ContentLength = int64(attach.Thumbnail.FileSize)
+ mediaPath = attach.Thumbnail.Path
default:
- const text = "invalid media attachment size"
- return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ const text = "invalid media size"
+ return nil, gtserror.NewErrorBadRequest(
+ errors.New(text),
+ text,
+ )
+ }
+
+ // Attachment file
+ // stream from storage.
+ var rc io.ReadCloser
+
+ // Check media is meant
+ // to be cached locally.
+ if *attach.Cached {
+
+ // Check storage for media at determined path.
+ rc, err = p.state.Storage.GetStream(ctx, mediaPath)
+ if err != nil && !storage.IsNotFound(err) {
+ err := gtserror.Newf("storage error getting media %s: %w", attach.URL, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ if rc == nil {
+ // This is local media without
+ // a cached attachment, unfulfillable!
+ if attach.IsLocal() {
+ return nil, gtserror.NewfWithCode(http.StatusNotFound,
+ "local media file not found: %s", attach.URL)
+ }
+
+ // Whether the cached flag was set or
+ // not, we know it isn't in storage.
+ attach.Cached = util.Ptr(false)
+
+ // Attempt to recache this remote media.
+ attach, err = p.federator.RefreshMedia(ctx,
+ requestUser,
+ attach,
+ media.AdditionalMediaInfo{},
+ false,
+ )
+ if err != nil {
+ err := gtserror.Newf("error recaching media %s: %w", attach.URL, err)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ // Check storage for media at determined path.
+ rc, err = p.state.Storage.GetStream(ctx, mediaPath)
+ if err != nil && !storage.IsNotFound(err) {
+ err := gtserror.Newf("storage error getting media %s: %w", attach.URL, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ } else if rc == nil {
+ return nil, gtserror.NewfWithCode(http.StatusNotFound,
+ "remote media file not found: %s", attach.URL)
+ }
+ }
+
+ // If running on S3 storage with proxying disabled,
+ // just fetch a pre-signed URL instead of the content.
+ if url := p.state.Storage.URL(ctx, mediaPath); url != nil {
+ _ = rc.Close() // close storage stream
+ content.URL = url
+ return &content, nil
}
+
+ // Return with stream.
+ content.Content = rc
+ return &content, nil
}
func (p *Processor) getEmojiContent(
@@ -242,82 +292,92 @@ func (p *Processor) getEmojiContent(
return nil, gtserror.NewErrorNotFound(errors.New(text), text)
}
- // Ensure that stored emoji is cached.
- // (this handles local emoji / recaches).
- emoji, err = p.federator.RecacheEmoji(
- ctx,
- emoji,
- )
- if err != nil {
- err := gtserror.Newf("error recaching emoji: %w", err)
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- // Start preparing API content model.
- apiContent := &apimodel.Content{}
-
- // Retrieve appropriate
- // size file from storage.
+ // Start preparing API content model and other
+ // values depending on requested media size.
+ var content apimodel.Content
+ var emojiPath string
switch sizeStr {
+ // Original emoji image.
case media.SizeOriginal:
- apiContent.ContentType = emoji.ImageContentType
- apiContent.ContentLength = int64(emoji.ImageFileSize)
- return p.getContent(ctx,
- emoji.ImagePath,
- apiContent,
- )
+ content.ContentType = emoji.ImageContentType
+ content.ContentLength = int64(emoji.ImageFileSize)
+ emojiPath = emoji.ImagePath
+ // Static emoji image.
case media.SizeStatic:
- apiContent.ContentType = emoji.ImageStaticContentType
- apiContent.ContentLength = int64(emoji.ImageStaticFileSize)
- return p.getContent(ctx,
- emoji.ImageStaticPath,
- apiContent,
- )
+ content.ContentType = emoji.ImageStaticContentType
+ content.ContentLength = int64(emoji.ImageStaticFileSize)
+ emojiPath = emoji.ImageStaticPath
default:
- const text = "invalid media attachment size"
- return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ const text = "invalid emoji size"
+ return nil, gtserror.NewErrorBadRequest(
+ errors.New(text),
+ text,
+ )
}
-}
-// getContent performs the final file fetching of
-// stored content at path in storage. This is
-// populated in the apimodel.Content{} and returned.
-// (note: this also handles un-proxied S3 storage).
-func (p *Processor) getContent(
- ctx context.Context,
- path string,
- content *apimodel.Content,
-) (
- *apimodel.Content,
- gtserror.WithCode,
-) {
- // If running on S3 storage with proxying disabled then
- // just fetch pre-signed URL instead of the content.
- if url := p.state.Storage.URL(ctx, path); url != nil {
- content.URL = url
- return content, nil
- }
+ // Emoji image file
+ // stream from storage.
+ var rc io.ReadCloser
- // Fetch file stream for the stored media at path.
- rc, err := p.state.Storage.GetStream(ctx, path)
- if err != nil && !storage.IsNotFound(err) {
- err := gtserror.Newf("error getting file %s from storage: %w", path, err)
- return nil, gtserror.NewErrorInternalError(err)
+ // Check emoji is meant
+ // to be cached locally.
+ if *emoji.Cached {
+
+ // Check storage for emoji at determined image path.
+ rc, err = p.state.Storage.GetStream(ctx, emojiPath)
+ if err != nil && !storage.IsNotFound(err) {
+ err := gtserror.Newf("storage error getting emoji %s: %w", emoji.URI, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
}
- // Ensure found.
if rc == nil {
- err := gtserror.Newf("file not found at %s", path)
- const text = "file not found"
- return nil, gtserror.NewErrorNotFound(err, text)
+ // This is a local emoji without
+ // a cached image, unfulfillable!
+ if emoji.IsLocal() {
+ return nil, gtserror.NewfWithCode(http.StatusNotFound,
+ "local emoji image not found: %s", emoji.URI)
+ }
+
+ // Whether the cached flag was set or
+ // not, we know it isn't in storage.
+ emoji.Cached = util.Ptr(false)
+
+ // Attempt to recache this remote emoji.
+ emoji, err = p.federator.RecacheEmoji(ctx,
+ emoji,
+ false,
+ )
+ if err != nil {
+ err := gtserror.Newf("error recaching emoji %s: %w", emoji.URI, err)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ // Check storage for emoji at determined image path.
+ rc, err = p.state.Storage.GetStream(ctx, emojiPath)
+ if err != nil && !storage.IsNotFound(err) {
+ err := gtserror.Newf("storage error getting emoji %s after recache: %w", emoji.URI, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ } else if rc == nil {
+ return nil, gtserror.NewfWithCode(http.StatusNotFound,
+ "remote emoji image not found: %s", emoji.URI)
+ }
+ }
+
+ // If running on S3 storage with proxying disabled,
+ // just fetch a pre-signed URL instead of the content.
+ if url := p.state.Storage.URL(ctx, emojiPath); url != nil {
+ _ = rc.Close() // close storage stream
+ content.URL = url
+ return &content, nil
}
// Return with stream.
content.Content = rc
- return content, nil
+ return &content, nil
}
// handles serving Content for "unknown" file
diff --git a/internal/processing/media/getfile_test.go b/internal/processing/media/getfile_test.go
index 34f5d99a2..9c2ffd589 100644
--- a/internal/processing/media/getfile_test.go
+++ b/internal/processing/media/getfile_test.go
@@ -18,17 +18,16 @@
package media_test
import (
- "context"
"io"
"path"
"testing"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/util"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type GetFileTestSuite struct {
@@ -36,7 +35,7 @@ type GetFileTestSuite struct {
}
func (suite *GetFileTestSuite) TestGetRemoteFileCached() {
- ctx := context.Background()
+ ctx := suite.T().Context()
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
fileName := path.Base(testAttachment.File.Path)
@@ -64,7 +63,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileCached() {
}
func (suite *GetFileTestSuite) TestGetRemoteFileUncached() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// uncache the file from local
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
@@ -116,7 +115,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileUncached() {
}
func (suite *GetFileTestSuite) TestGetRemoteFileUncachedInterrupted() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// uncache the file from local
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
@@ -163,7 +162,7 @@ func (suite *GetFileTestSuite) TestGetRemoteFileUncachedInterrupted() {
}
func (suite *GetFileTestSuite) TestGetRemoteFileThumbnailUncached() {
- ctx := context.Background()
+ ctx := suite.T().Context()
testAttachment := suite.testAttachments["remote_account_1_status_1_attachment_1"]
// fetch the existing thumbnail bytes from storage first
diff --git a/internal/processing/media/getmedia.go b/internal/processing/media/getmedia.go
index 8f5b9d740..22e05cab3 100644
--- a/internal/processing/media/getmedia.go
+++ b/internal/processing/media/getmedia.go
@@ -22,10 +22,11 @@ import (
"errors"
"fmt"
- 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"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
@@ -42,10 +43,6 @@ func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, mediaAtt
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
}
- a, err := p.converter.AttachmentToAPIAttachment(ctx, attachment)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
- }
-
+ a := typeutils.AttachmentToAPIAttachment(attachment)
return &a, nil
}
diff --git a/internal/processing/media/media.go b/internal/processing/media/media.go
index 76ed68f5a..4967e4d11 100644
--- a/internal/processing/media/media.go
+++ b/internal/processing/media/media.go
@@ -18,12 +18,12 @@
package media
import (
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/transport"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/media/media_test.go b/internal/processing/media/media_test.go
index 2930733c4..01506cc6f 100644
--- a/internal/processing/media/media_test.go
+++ b/internal/processing/media/media_test.go
@@ -18,19 +18,21 @@
package media_test
import (
+ "code.superseriousbusiness.org/gotosocial/internal/admin"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "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/processing/common"
+ mediaprocessing "code.superseriousbusiness.org/gotosocial/internal/processing/media"
+ "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/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- mediaprocessing "github.com/superseriousbusiness/gotosocial/internal/processing/media"
- "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 MediaStandardTestSuite struct {
@@ -45,7 +47,6 @@ type MediaStandardTestSuite 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
@@ -59,7 +60,6 @@ type MediaStandardTestSuite struct {
func (suite *MediaStandardTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
@@ -84,8 +84,10 @@ func (suite *MediaStandardTestSuite) SetupTest() {
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
federator := testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
- filter := visibility.NewFilter(&suite.state)
- common := common.New(&suite.state, suite.mediaManager, suite.tc, federator, filter)
+ visFilter := visibility.NewFilter(&suite.state)
+ muteFilter := mutes.NewFilter(&suite.state)
+ statusFilter := status.NewFilter(&suite.state)
+ common := common.New(&suite.state, suite.mediaManager, suite.tc, federator, visFilter, muteFilter, statusFilter)
suite.mediaProcessor = mediaprocessing.New(&common, &suite.state, suite.tc, federator, suite.mediaManager, suite.transportController)
testrig.StandardDBSetup(suite.db, nil)
diff --git a/internal/processing/media/profile.go b/internal/processing/media/profile.go
index 5c266e372..38c2893e1 100644
--- a/internal/processing/media/profile.go
+++ b/internal/processing/media/profile.go
@@ -21,9 +21,9 @@ import (
"context"
"fmt"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// DeleteAvatar deletes the account's avatar, if one exists, and returns the updated account.
diff --git a/internal/processing/media/unattach.go b/internal/processing/media/unattach.go
index ddf2dda20..55d793647 100644
--- a/internal/processing/media/unattach.go
+++ b/internal/processing/media/unattach.go
@@ -22,10 +22,11 @@ import (
"errors"
"fmt"
- 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"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Unattach unattaches the media attachment with the given ID from any statuses it was attached to, making it available
@@ -49,10 +50,6 @@ func (p *Processor) Unattach(ctx context.Context, account *gtsmodel.Account, med
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error updating attachment: %s", err))
}
- a, err := p.converter.AttachmentToAPIAttachment(ctx, attachment)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
- }
-
+ a := typeutils.AttachmentToAPIAttachment(attachment)
return &a, nil
}
diff --git a/internal/processing/media/unattach_test.go b/internal/processing/media/unattach_test.go
index 02d2c7077..582e24b9a 100644
--- a/internal/processing/media/unattach_test.go
+++ b/internal/processing/media/unattach_test.go
@@ -18,7 +18,6 @@
package media_test
import (
- "context"
"testing"
"github.com/stretchr/testify/suite"
@@ -29,7 +28,7 @@ type UnattachTestSuite struct {
}
func (suite *UnattachTestSuite) TestUnattachMedia() {
- ctx := context.Background()
+ ctx := suite.T().Context()
testAttachment := suite.testAttachments["admin_account_status_1_attachment_1"]
testAccount := suite.testAccounts["admin_account"]
diff --git a/internal/processing/media/update.go b/internal/processing/media/update.go
index c8592395f..cccc27534 100644
--- a/internal/processing/media/update.go
+++ b/internal/processing/media/update.go
@@ -22,13 +22,14 @@ import (
"errors"
"fmt"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/text"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+ "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/text"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Update updates a media attachment with the given id, using the provided form parameters.
@@ -77,17 +78,13 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, media
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating media: %s", err))
}
- a, err := p.converter.AttachmentToAPIAttachment(ctx, attachment)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
- }
-
+ a := typeutils.AttachmentToAPIAttachment(attachment)
return &a, nil
}
// processDescription will sanitize and valid description against server configuration.
func processDescription(description string) (string, gtserror.WithCode) {
- description = text.SanitizeToPlaintext(description)
+ description = text.StripHTMLFromText(description)
chars := len([]rune(description))
if min := config.GetMediaDescriptionMinChars(); chars < min {
diff --git a/internal/processing/oauth.go b/internal/processing/oauth.go
index ae511e949..573c0d53a 100644
--- a/internal/processing/oauth.go
+++ b/internal/processing/oauth.go
@@ -18,10 +18,11 @@
package processing
import (
+ "context"
"net/http"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/oauth2/v4"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/oauth2/v4"
)
func (p *Processor) OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtserror.WithCode {
@@ -38,3 +39,17 @@ func (p *Processor) OAuthValidateBearerToken(r *http.Request) (oauth2.TokenInfo,
// todo: some kind of metrics stuff here
return p.oauthServer.ValidationBearerToken(r)
}
+
+func (p *Processor) OAuthRevokeAccessToken(
+ ctx context.Context,
+ clientID string,
+ clientSecret string,
+ accessToken string,
+) gtserror.WithCode {
+ return p.oauthServer.RevokeAccessToken(
+ ctx,
+ clientID,
+ clientSecret,
+ accessToken,
+ )
+}
diff --git a/internal/processing/parsemention.go b/internal/processing/parsemention.go
index 035f057b8..7a75cb9bc 100644
--- a/internal/processing/parsemention.go
+++ b/internal/processing/parsemention.go
@@ -19,15 +19,17 @@ package processing
import (
"context"
+ "errors"
"fmt"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// GetParseMentionFunc returns a new ParseMentionFunc using the provided state and federator.
@@ -100,7 +102,29 @@ func GetParseMentionFunc(state *state.State, federator *federation.Federator) gt
}
}
- // Return mention with useful populated fields,
+ // Check if the mention was
+ // in the database already.
+ if statusID != "" {
+ mention, err := state.DB.GetMentionByTargetAcctStatus(ctx, targetAcct.ID, statusID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return nil, fmt.Errorf(
+ "db error checking for existing mention: %w",
+ err,
+ )
+ }
+
+ if mention != nil {
+ // We had it, return this rather
+ // than creating a new one.
+ mention.NameString = namestring
+ mention.OriginAccountURI = originAcct.URI
+ mention.TargetAccountURI = targetAcct.URI
+ mention.TargetAccountURL = targetAcct.URL
+ return mention, nil
+ }
+ }
+
+ // Return new mention with useful populated fields,
// but *don't* store it in the database; that's
// up to the calling function to do, if they want.
return &gtsmodel.Mention{
@@ -114,6 +138,10 @@ func GetParseMentionFunc(state *state.State, federator *federation.Federator) gt
TargetAccountURL: targetAcct.URL,
TargetAccount: targetAcct,
NameString: namestring,
+
+ // Mention wasn't
+ // stored in the db.
+ IsNew: true,
}, nil
}
}
diff --git a/internal/processing/parsemention_test.go b/internal/processing/parsemention_test.go
index f97133969..2eeedc2ea 100644
--- a/internal/processing/parsemention_test.go
+++ b/internal/processing/parsemention_test.go
@@ -18,14 +18,13 @@
package processing_test
import (
- "context"
"errors"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/processing"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/processing"
)
type ParseMentionTestSuite struct {
@@ -34,7 +33,7 @@ type ParseMentionTestSuite struct {
func (suite *ParseMentionTestSuite) TestParseMentionFunc() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
parseMention = processing.GetParseMentionFunc(&suite.state, suite.federator)
originAcctID = suite.testAccounts["local_account_1"].ID
statusID = id.NewULID()
diff --git a/internal/processing/polls/expiry.go b/internal/processing/polls/expiry.go
index d02a05f0d..5ce893111 100644
--- a/internal/processing/polls/expiry.go
+++ b/internal/processing/polls/expiry.go
@@ -21,12 +21,12 @@ import (
"context"
"time"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "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"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "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"
)
func (p *Processor) ScheduleAll(ctx context.Context) error {
diff --git a/internal/processing/polls/get.go b/internal/processing/polls/get.go
index 42fecbd43..fa4e235f7 100644
--- a/internal/processing/polls/get.go
+++ b/internal/processing/polls/get.go
@@ -20,9 +20,9 @@ package polls
import (
"context"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
func (p *Processor) PollGet(ctx context.Context, requester *gtsmodel.Account, pollID string) (*apimodel.Poll, gtserror.WithCode) {
diff --git a/internal/processing/polls/poll.go b/internal/processing/polls/poll.go
index fe8fc71c5..c51bb0e96 100644
--- a/internal/processing/polls/poll.go
+++ b/internal/processing/polls/poll.go
@@ -20,12 +20,12 @@ package polls
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/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "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/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/polls/poll_test.go b/internal/processing/polls/poll_test.go
index bf6ae4aad..2fd46c5ee 100644
--- a/internal/processing/polls/poll_test.go
+++ b/internal/processing/polls/poll_test.go
@@ -23,24 +23,27 @@ import (
"net/http"
"testing"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/polls"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/processing/polls"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type PollTestSuite struct {
suite.Suite
- state state.State
- filter *visibility.Filter
- polls polls.Processor
+ state state.State
+ visFilter *visibility.Filter
+ muteFilter *mutes.Filter
+ polls polls.Processor
testAccounts map[string]*gtsmodel.Account
testPolls map[string]*gtsmodel.Poll
@@ -56,8 +59,10 @@ func (suite *PollTestSuite) SetupTest() {
controller := testrig.NewTestTransportController(&suite.state, nil)
mediaMgr := media.NewManager(&suite.state)
federator := testrig.NewTestFederator(&suite.state, controller, mediaMgr)
- suite.filter = visibility.NewFilter(&suite.state)
- common := common.New(&suite.state, mediaMgr, converter, federator, suite.filter)
+ suite.visFilter = visibility.NewFilter(&suite.state)
+ suite.muteFilter = mutes.NewFilter(&suite.state)
+ statusFilter := status.NewFilter(&suite.state)
+ common := common.New(&suite.state, mediaMgr, converter, federator, suite.visFilter, suite.muteFilter, statusFilter)
suite.polls = polls.New(&common, &suite.state, converter)
}
@@ -68,7 +73,7 @@ func (suite *PollTestSuite) TearDownTest() {
func (suite *PollTestSuite) TestPollGet() {
// Create a new context for this test.
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// Perform test for all requester + poll combos.
@@ -88,7 +93,7 @@ func (suite *PollTestSuite) testPollGet(ctx context.Context, requester *gtsmodel
var check func(*apimodel.Poll, gtserror.WithCode) bool
switch {
- case !pollIsVisible(suite.filter, ctx, requester, poll):
+ case !pollIsVisible(suite.visFilter, ctx, requester, poll):
// Poll should not be visible to requester, this should
// return an error code 404 (to prevent info leak).
check = func(poll *apimodel.Poll, err gtserror.WithCode) bool {
@@ -111,7 +116,7 @@ func (suite *PollTestSuite) testPollGet(ctx context.Context, requester *gtsmodel
func (suite *PollTestSuite) TestPollVote() {
// Create a new context for this test.
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// randomChoices generates random vote choices in poll.
@@ -188,7 +193,7 @@ func (suite *PollTestSuite) testPollVote(ctx context.Context, requester *gtsmode
return poll == nil && err.Code() == http.StatusUnprocessableEntity
}
- case !pollIsVisible(suite.filter, ctx, requester, poll):
+ case !pollIsVisible(suite.visFilter, ctx, requester, poll):
// Poll should not be visible to requester, this should
// return an error code 404 (to prevent info leak).
check = func(poll *apimodel.Poll, err gtserror.WithCode) bool {
diff --git a/internal/processing/polls/vote.go b/internal/processing/polls/vote.go
index 6585793cd..14777d9df 100644
--- a/internal/processing/polls/vote.go
+++ b/internal/processing/polls/vote.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/id"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
+ "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/messages"
)
func (p *Processor) PollVote(ctx context.Context, requester *gtsmodel.Account, pollID string, choices []int) (*apimodel.Poll, gtserror.WithCode) {
diff --git a/internal/processing/preferences.go b/internal/processing/preferences.go
index fb445ec5b..ebcb0e6b6 100644
--- a/internal/processing/preferences.go
+++ b/internal/processing/preferences.go
@@ -20,9 +20,9 @@ package processing
import (
"context"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
func (p *Processor) PreferencesGet(ctx context.Context, accountID string) (*apimodel.Preferences, gtserror.WithCode) {
diff --git a/internal/processing/preferences_test.go b/internal/processing/preferences_test.go
index be88b5edf..daaaef5c6 100644
--- a/internal/processing/preferences_test.go
+++ b/internal/processing/preferences_test.go
@@ -18,12 +18,11 @@
package processing_test
import (
- "context"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
type PreferencesTestSuite struct {
@@ -31,7 +30,7 @@ type PreferencesTestSuite struct {
}
func (suite *PreferencesTestSuite) TestPreferencesGet() {
- ctx := context.Background()
+ ctx := suite.T().Context()
tests := []struct {
act *gtsmodel.Account
prefs *model.Preferences
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index 0bba23089..a5cea5da4 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -18,41 +18,45 @@
package processing
import (
- "github.com/superseriousbusiness/gotosocial/internal/cleaner"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- mm "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/processing/account"
- "github.com/superseriousbusiness/gotosocial/internal/processing/admin"
- "github.com/superseriousbusiness/gotosocial/internal/processing/advancedmigrations"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/processing/conversations"
- "github.com/superseriousbusiness/gotosocial/internal/processing/fedi"
- filtersv1 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v1"
- filtersv2 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v2"
- "github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
- "github.com/superseriousbusiness/gotosocial/internal/processing/list"
- "github.com/superseriousbusiness/gotosocial/internal/processing/markers"
- "github.com/superseriousbusiness/gotosocial/internal/processing/media"
- "github.com/superseriousbusiness/gotosocial/internal/processing/polls"
- "github.com/superseriousbusiness/gotosocial/internal/processing/push"
- "github.com/superseriousbusiness/gotosocial/internal/processing/report"
- "github.com/superseriousbusiness/gotosocial/internal/processing/search"
- "github.com/superseriousbusiness/gotosocial/internal/processing/status"
- "github.com/superseriousbusiness/gotosocial/internal/processing/stream"
- "github.com/superseriousbusiness/gotosocial/internal/processing/tags"
- "github.com/superseriousbusiness/gotosocial/internal/processing/timeline"
- "github.com/superseriousbusiness/gotosocial/internal/processing/user"
- "github.com/superseriousbusiness/gotosocial/internal/processing/workers"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/subscriptions"
- "github.com/superseriousbusiness/gotosocial/internal/text"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/webpush"
+ "code.superseriousbusiness.org/gotosocial/internal/cleaner"
+ "code.superseriousbusiness.org/gotosocial/internal/email"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/interaction"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/mutes"
+ statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ mm "code.superseriousbusiness.org/gotosocial/internal/media"
+ "code.superseriousbusiness.org/gotosocial/internal/oauth"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/account"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/admin"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/advancedmigrations"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/application"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/conversations"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/fedi"
+ filterCommon "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
+ filtersv1 "code.superseriousbusiness.org/gotosocial/internal/processing/filters/v1"
+ filtersv2 "code.superseriousbusiness.org/gotosocial/internal/processing/filters/v2"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/interactionrequests"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/list"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/markers"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/media"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/polls"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/push"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/report"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/search"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/status"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/tags"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/timeline"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/user"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/workers"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/subscriptions"
+ "code.superseriousbusiness.org/gotosocial/internal/text"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/webpush"
)
// Processor groups together processing functions and
@@ -81,6 +85,7 @@ type Processor struct {
account account.Processor
admin admin.Processor
advancedmigrations advancedmigrations.Processor
+ application application.Processor
conversations conversations.Processor
fedi fedi.Processor
filtersv1 filtersv1.Processor
@@ -113,6 +118,10 @@ func (p *Processor) AdvancedMigrations() *advancedmigrations.Processor {
return &p.advancedmigrations
}
+func (p *Processor) Application() *application.Processor {
+ return &p.application
+}
+
func (p *Processor) Conversations() *conversations.Processor {
return &p.conversations
}
@@ -185,7 +194,8 @@ func (p *Processor) Workers() *workers.Processor {
return &p.workers
}
-// NewProcessor returns a new Processor.
+// NewProcessor returns
+// a new Processor.
func NewProcessor(
cleaner *cleaner.Cleaner,
subscriptions *subscriptions.Subscriptions,
@@ -197,7 +207,9 @@ func NewProcessor(
emailSender email.Sender,
webPushSender webpush.Sender,
visFilter *visibility.Filter,
+ muteFilter *mutes.Filter,
intFilter *interaction.Filter,
+ statusFilter *statusfilter.Filter,
) *Processor {
parseMentionFunc := GetParseMentionFunc(state, federator)
processor := &Processor{
@@ -212,19 +224,21 @@ func NewProcessor(
//
// Start with sub processors that will
// be required by the workers processor.
- common := common.New(state, mediaManager, converter, federator, visFilter)
- processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc)
+ common := common.New(state, mediaManager, converter, federator, visFilter, muteFilter, statusFilter)
+ processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, statusFilter, parseMentionFunc)
processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController())
processor.stream = stream.New(state, oauthServer)
+ filterCommon := filterCommon.New(state, &processor.stream)
// Instantiate the rest of the sub
// processors + pin them to this struct.
- processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc)
+ processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, statusFilter, parseMentionFunc)
processor.admin = admin.New(&common, state, cleaner, subscriptions, federator, converter, mediaManager, federator.TransportController(), emailSender)
- processor.conversations = conversations.New(state, converter, visFilter)
+ processor.application = application.New(state, converter)
+ processor.conversations = conversations.New(state, converter, visFilter, muteFilter, statusFilter)
processor.fedi = fedi.New(state, &common, converter, federator, visFilter)
- processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
- processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
+ processor.filtersv1 = filtersv1.New(state, converter, filterCommon)
+ processor.filtersv2 = filtersv2.New(state, converter, filterCommon)
processor.interactionRequests = interactionrequests.New(&common, state, converter)
processor.list = list.New(state, converter)
processor.markers = markers.New(state, converter)
@@ -232,7 +246,7 @@ func NewProcessor(
processor.push = push.New(state, converter)
processor.report = report.New(state, converter)
processor.tags = tags.New(state, converter)
- processor.timeline = timeline.New(state, converter, visFilter)
+ processor.timeline = timeline.New(state, converter, visFilter, muteFilter, statusFilter)
processor.search = search.New(state, federator, converter, visFilter)
processor.status = status.New(state, &common, &processor.polls, &processor.interactionRequests, federator, converter, visFilter, intFilter, parseMentionFunc)
processor.user = user.New(state, converter, oauthServer, emailSender)
@@ -249,6 +263,8 @@ func NewProcessor(
federator,
converter,
visFilter,
+ muteFilter,
+ statusFilter,
emailSender,
webPushSender,
&processor.account,
diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go
index 84ab9ef48..3c564b929 100644
--- a/internal/processing/processor_test.go
+++ b/internal/processing/processor_test.go
@@ -20,25 +20,28 @@ package processing_test
import (
"context"
+ "code.superseriousbusiness.org/gotosocial/internal/admin"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+ "code.superseriousbusiness.org/gotosocial/internal/cleaner"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/email"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/interaction"
+ "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/oauth"
+ "code.superseriousbusiness.org/gotosocial/internal/processing"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/storage"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/subscriptions"
+ "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/cleaner"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/processing"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/storage"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
- "github.com/superseriousbusiness/gotosocial/internal/subscriptions"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type ProcessingStandardTestSuite struct {
@@ -57,7 +60,6 @@ type ProcessingStandardTestSuite 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
@@ -66,7 +68,7 @@ type ProcessingStandardTestSuite struct {
testStatuses map[string]*gtsmodel.Status
testTags map[string]*gtsmodel.Tag
testMentions map[string]*gtsmodel.Mention
- testAutheds map[string]*oauth.Auth
+ testAutheds map[string]*apiutil.Auth
testBlocks map[string]*gtsmodel.Block
testActivities map[string]testrig.ActivityWithSignature
testLists map[string]*gtsmodel.List
@@ -76,7 +78,6 @@ type ProcessingStandardTestSuite struct {
func (suite *ProcessingStandardTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
@@ -85,7 +86,7 @@ func (suite *ProcessingStandardTestSuite) SetupSuite() {
suite.testStatuses = testrig.NewTestStatuses()
suite.testTags = testrig.NewTestTags()
suite.testMentions = testrig.NewTestMentions()
- suite.testAutheds = map[string]*oauth.Auth{
+ suite.testAutheds = map[string]*apiutil.Auth{
"local_account_1": {
Application: suite.testApplications["local_account_1"],
User: suite.testUsers["local_account_1"],
@@ -110,12 +111,6 @@ func (suite *ProcessingStandardTestSuite) SetupTest() {
suite.state.Storage = suite.storage
suite.typeconverter = typeutils.NewConverter(&suite.state)
- testrig.StartTimelines(
- &suite.state,
- visibility.NewFilter(&suite.state),
- suite.typeconverter,
- )
-
suite.httpClient = testrig.NewMockHTTPClient(nil, "../../testrig/media")
suite.httpClient.TestRemotePeople = testrig.NewTestFediPeople()
suite.httpClient.TestRemoteStatuses = testrig.NewTestFediStatuses()
@@ -123,7 +118,7 @@ func (suite *ProcessingStandardTestSuite) SetupTest() {
suite.transportController = testrig.NewTestTransportController(&suite.state, suite.httpClient)
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
+ suite.oauthServer = testrig.NewTestOauthServer(&suite.state)
suite.emailSender = testrig.NewEmailSender("../../web/template/", nil)
suite.processor = processing.NewProcessor(
@@ -137,7 +132,9 @@ func (suite *ProcessingStandardTestSuite) SetupTest() {
suite.emailSender,
testrig.NewNoopWebPushSender(),
visibility.NewFilter(&suite.state),
+ mutes.NewFilter(&suite.state),
interaction.NewFilter(&suite.state),
+ status.NewFilter(&suite.state),
)
testrig.StartWorkers(&suite.state, suite.processor.Workers())
diff --git a/internal/processing/push/create.go b/internal/processing/push/create.go
index dc15ccf12..3beeab6cc 100644
--- a/internal/processing/push/create.go
+++ b/internal/processing/push/create.go
@@ -20,11 +20,11 @@ package push
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"
- "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/id"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// CreateOrReplace creates a Web Push subscription for the given access token,
diff --git a/internal/processing/push/delete.go b/internal/processing/push/delete.go
index 6f5c61444..e5369f56a 100644
--- a/internal/processing/push/delete.go
+++ b/internal/processing/push/delete.go
@@ -20,7 +20,7 @@ package push
import (
"context"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
)
// Delete deletes the Web Push subscription for the given access token, if there is one.
diff --git a/internal/processing/push/get.go b/internal/processing/push/get.go
index 542f08862..df0e80dc3 100644
--- a/internal/processing/push/get.go
+++ b/internal/processing/push/get.go
@@ -21,9 +21,9 @@ import (
"context"
"errors"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
)
// Get returns the Web Push subscription for the given access token.
diff --git a/internal/processing/push/push.go b/internal/processing/push/push.go
index f46280386..24a2254ac 100644
--- a/internal/processing/push/push.go
+++ b/internal/processing/push/push.go
@@ -20,11 +20,11 @@ package push
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/state"
- "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/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/push/update.go b/internal/processing/push/update.go
index 94529455a..bbd9e3943 100644
--- a/internal/processing/push/update.go
+++ b/internal/processing/push/update.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/typeutils"
+ 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/typeutils"
)
// Update updates the Web Push subscription for the given access token.
diff --git a/internal/processing/report/create.go b/internal/processing/report/create.go
index dd31a8798..734670e17 100644
--- a/internal/processing/report/create.go
+++ b/internal/processing/report/create.go
@@ -22,14 +22,14 @@ 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/messages"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
+ "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/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
)
// Create creates one user report / flag, using the provided form parameters.
diff --git a/internal/processing/report/get.go b/internal/processing/report/get.go
index 2e3c1b2dc..136f09470 100644
--- a/internal/processing/report/get.go
+++ b/internal/processing/report/get.go
@@ -24,12 +24,12 @@ import (
"net/url"
"strconv"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "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"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
)
// Get returns the user view of a moderation report, with the given id.
diff --git a/internal/processing/report/report.go b/internal/processing/report/report.go
index c871172bb..cdc2dc3e0 100644
--- a/internal/processing/report/report.go
+++ b/internal/processing/report/report.go
@@ -18,8 +18,8 @@
package report
import (
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/search/accounts.go b/internal/processing/search/accounts.go
index 7201d0688..af84abc31 100644
--- a/internal/processing/search/accounts.go
+++ b/internal/processing/search/accounts.go
@@ -22,14 +22,14 @@ import (
"errors"
"strings"
- "codeberg.org/gruf/go-kv"
- 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/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/util"
+ "codeberg.org/gruf/go-kv/v2"
)
// Accounts does a partial search for accounts that
diff --git a/internal/processing/search/get.go b/internal/processing/search/get.go
index d1462cf53..64aefd23c 100644
--- a/internal/processing/search/get.go
+++ b/internal/processing/search/get.go
@@ -25,16 +25,16 @@ import (
"net/url"
"strings"
- "codeberg.org/gruf/go-kv"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "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/text"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "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/text"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "codeberg.org/gruf/go-kv/v2"
)
const (
@@ -490,7 +490,7 @@ func (p *Processor) byURI(
if includeAccounts(queryType) {
// Check if URI points to an account.
- foundAccount, err := p.accountByURI(ctx, requestingAccount, uri, resolve)
+ foundAccounts, err := p.accountsByURI(ctx, requestingAccount, uri, resolve)
if err != nil {
// Check for semi-expected error types.
// On one of these, we can continue.
@@ -508,7 +508,9 @@ func (p *Processor) byURI(
} else {
// Hit! Return early since it's extremely unlikely
// a status and an account will have the same URL.
- appendAccount(foundAccount)
+ for _, foundAccount := range foundAccounts {
+ appendAccount(foundAccount)
+ }
return nil
}
}
@@ -522,7 +524,7 @@ func (p *Processor) byURI(
switch {
case gtserror.IsUnretrievable(err),
gtserror.IsWrongType(err),
- gtserror.NotPermitted(err):
+ gtserror.IsNotPermitted(err):
log.Debugf(ctx,
"semi-expected error type looking up %s as status: %v",
uri, err,
@@ -544,35 +546,42 @@ func (p *Processor) byURI(
return nil
}
-// accountByURI looks for one account with the given URI.
+// accountsByURI looks for one account with the given URI/ID,
+// then if nothing is found, multiple accounts with the given URL.
+//
// If resolve is false, it will only look in the database.
// If resolve is true, it will try to resolve the account
// from remote using the URI, if necessary.
//
// Will return either a hit, ErrNotRetrievable, ErrWrongType,
// or a real error that the caller should handle.
-func (p *Processor) accountByURI(
+func (p *Processor) accountsByURI(
ctx context.Context,
requestingAccount *gtsmodel.Account,
uri *url.URL,
resolve bool,
-) (*gtsmodel.Account, error) {
+) ([]*gtsmodel.Account, error) {
if resolve {
// We're allowed to resolve, leave the
// rest up to the dereferencer functions.
+ //
+ // Allow dereferencing by URL and not just URI;
+ // there are many cases where someone might
+ // paste a URL into the search bar.
account, _, err := p.federator.GetAccountByURI(
gtscontext.SetFastFail(ctx),
requestingAccount.Username,
uri,
+ true,
)
- return account, err
+ return []*gtsmodel.Account{account}, err
}
// We're not allowed to resolve; search database only.
uriStr := uri.String() // stringify uri just once
- // Search by ActivityPub URI.
+ // Search for single acct by ActivityPub URI.
account, err := p.state.DB.GetAccountByURI(ctx, uriStr)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = gtserror.Newf("error checking database for account using URI %s: %w", uriStr, err)
@@ -581,22 +590,22 @@ func (p *Processor) accountByURI(
if account != nil {
// We got a hit! No need to continue.
- return account, nil
+ return []*gtsmodel.Account{account}, nil
}
- // No hit yet. Fallback to try by URL.
- account, err = p.state.DB.GetAccountByURL(ctx, uriStr)
+ // No hit yet. Fallback to look for any accounts with URL.
+ accounts, err := p.state.DB.GetAccountsByURL(ctx, uriStr)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = gtserror.Newf("error checking database for account using URL %s: %w", uriStr, err)
+ err = gtserror.Newf("error checking database for accounts using URL %s: %w", uriStr, err)
return nil, err
}
- if account != nil {
- // We got a hit! No need to continue.
- return account, nil
+ if len(accounts) != 0 {
+ // We got hits! No need to continue.
+ return accounts, nil
}
- err = fmt.Errorf("account %s could not be retrieved locally and we cannot resolve", uriStr)
+ err = fmt.Errorf("account(s) %s could not be retrieved locally and we cannot resolve", uriStr)
return nil, gtserror.SetUnretrievable(err)
}
diff --git a/internal/processing/search/lookup.go b/internal/processing/search/lookup.go
index f5c131841..39fddad77 100644
--- a/internal/processing/search/lookup.go
+++ b/internal/processing/search/lookup.go
@@ -23,12 +23,12 @@ import (
"fmt"
"strings"
- "codeberg.org/gruf/go-kv"
- 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"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ 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"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "codeberg.org/gruf/go-kv/v2"
)
// Lookup does a quick, non-resolving search for accounts that
diff --git a/internal/processing/search/search.go b/internal/processing/search/search.go
index 18008647c..0543b4db4 100644
--- a/internal/processing/search/search.go
+++ b/internal/processing/search/search.go
@@ -18,10 +18,10 @@
package search
import (
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/search/util.go b/internal/processing/search/util.go
index e2947ea16..7d52204c3 100644
--- a/internal/processing/search/util.go
+++ b/internal/processing/search/util.go
@@ -20,11 +20,11 @@ package search
import (
"context"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- 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"
+ 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"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// return true if given queryType should include accounts.
@@ -114,7 +114,7 @@ func (p *Processor) packageStatuses(
continue
}
- apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil, nil)
+ apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount)
if err != nil {
log.Debugf(ctx, "skipping status %s because it couldn't be converted to its api representation: %s", status.ID, err)
continue
@@ -129,42 +129,22 @@ func (p *Processor) packageStatuses(
// packageHashtags is a util function that just
// converts the given hashtags into an apimodel
// hashtag slice, or errors appropriately.
-func (p *Processor) packageHashtags(
- ctx context.Context,
- requestingAccount *gtsmodel.Account,
- tags []*gtsmodel.Tag,
- v1 bool,
-) ([]any, gtserror.WithCode) {
- apiTags := make([]any, 0, len(tags))
-
- var rangeF func(*gtsmodel.Tag)
+func packageHashtags(tags []*gtsmodel.Tag, v1 bool) []any {
+ apiTags := make([]any, len(tags))
+ if len(apiTags) != len(tags) {
+ panic(gtserror.New("bound check elimination"))
+ }
if v1 {
- // If API version 1, just provide slice of tag names.
- rangeF = func(tag *gtsmodel.Tag) {
- apiTags = append(apiTags, tag.Name)
+ for i, tag := range tags {
+ apiTags[i] = tag.Name
}
} else {
- // If API not version 1, provide slice of full tags.
- rangeF = func(tag *gtsmodel.Tag) {
- apiTag, err := p.converter.TagToAPITag(ctx, tag, true, nil)
- if err != nil {
- log.Debugf(
- ctx,
- "skipping tag %s because it couldn't be converted to its api representation: %s",
- tag.Name, err,
- )
- return
- }
-
- apiTags = append(apiTags, &apiTag)
+ for i, tag := range tags {
+ apiTag := typeutils.TagToAPITag(tag, true, nil)
+ apiTags[i] = apiTag
}
}
-
- for _, tag := range tags {
- rangeF(tag)
- }
-
- return apiTags, nil
+ return apiTags
}
// packageSearchResult wraps up the given accounts
@@ -198,10 +178,7 @@ func (p *Processor) packageSearchResult(
return nil, errWithCode
}
- apiTags, errWithCode := p.packageHashtags(ctx, requestingAccount, tags, v1)
- if errWithCode != nil {
- return nil, errWithCode
- }
+ apiTags := packageHashtags(tags, v1)
return &apimodel.SearchResult{
Accounts: apiAccounts,
diff --git a/internal/processing/status/bookmark.go b/internal/processing/status/bookmark.go
index ae61696a8..7996a9518 100644
--- a/internal/processing/status/bookmark.go
+++ b/internal/processing/status/bookmark.go
@@ -21,12 +21,12 @@ import (
"context"
"errors"
- 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"
+ 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"
)
// BookmarkCreate adds a bookmark for the requestingAccount, targeting the given status (no-op if bookmark already exists).
diff --git a/internal/processing/status/bookmark_test.go b/internal/processing/status/bookmark_test.go
index f5ea60fe6..ad441ca0c 100644
--- a/internal/processing/status/bookmark_test.go
+++ b/internal/processing/status/bookmark_test.go
@@ -18,7 +18,6 @@
package status_test
import (
- "context"
"testing"
"github.com/stretchr/testify/suite"
@@ -29,7 +28,7 @@ type StatusBookmarkTestSuite struct {
}
func (suite *StatusBookmarkTestSuite) TestBookmark() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// bookmark a status
bookmarkingAccount1 := suite.testAccounts["local_account_1"]
@@ -43,7 +42,7 @@ func (suite *StatusBookmarkTestSuite) TestBookmark() {
}
func (suite *StatusBookmarkTestSuite) TestUnbookmark() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// bookmark a status
bookmarkingAccount1 := suite.testAccounts["local_account_1"]
diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go
index 0e09a8e7b..4a97706ab 100644
--- a/internal/processing/status/boost.go
+++ b/internal/processing/status/boost.go
@@ -22,13 +22,13 @@ 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/messages"
- "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/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// BoostCreate processes the boost/reblog of target
@@ -96,7 +96,7 @@ func (p *Processor) BoostCreate(
// Derive pendingApproval status.
var pendingApproval bool
switch {
- case policyResult.WithApproval():
+ case policyResult.ManualApproval():
// We're allowed to do
// this pending approval.
pendingApproval = true
@@ -117,7 +117,7 @@ func (p *Processor) BoostCreate(
boost.PreApproved = true
}
- case policyResult.Permitted():
+ case policyResult.AutomaticApproval():
// We're permitted to do this
// based on another kind of match.
pendingApproval = false
@@ -130,15 +130,6 @@ func (p *Processor) BoostCreate(
return nil, gtserror.NewErrorInternalError(err)
}
- // Process side effects asynchronously.
- p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
- APObjectType: ap.ActivityAnnounce,
- APActivityType: ap.ActivityCreate,
- GTSModel: boost,
- Origin: requester,
- Target: target.Account,
- })
-
// If the boost target status replies to a status
// that we own, and has a pending interaction
// request, use the boost as an implicit accept.
@@ -156,6 +147,16 @@ func (p *Processor) BoostCreate(
target.PendingApproval = util.Ptr(false)
}
+ // Queue remaining boost side effects
+ // (send out boost, update timeline, etc).
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ APObjectType: ap.ActivityAnnounce,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: boost,
+ Origin: requester,
+ Target: target.Account,
+ })
+
return p.c.GetAPIStatus(ctx, requester, boost)
}
diff --git a/internal/processing/status/boost_test.go b/internal/processing/status/boost_test.go
index 0249e2040..96971c05e 100644
--- a/internal/processing/status/boost_test.go
+++ b/internal/processing/status/boost_test.go
@@ -18,7 +18,6 @@
package status_test
import (
- "context"
"testing"
"github.com/stretchr/testify/suite"
@@ -29,7 +28,7 @@ type StatusBoostTestSuite struct {
}
func (suite *StatusBoostTestSuite) TestBoostOfBoost() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// first boost a status, no big deal
boostingAccount1 := suite.testAccounts["local_account_1"]
diff --git a/internal/processing/status/common.go b/internal/processing/status/common.go
index 3f2b7b6cb..ca17ab80e 100644
--- a/internal/processing/status/common.go
+++ b/internal/processing/status/common.go
@@ -23,15 +23,16 @@ import (
"fmt"
"time"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "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/text"
- "github.com/superseriousbusiness/gotosocial/internal/util/xslices"
- "github.com/superseriousbusiness/gotosocial/internal/validate"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "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/id"
+ "code.superseriousbusiness.org/gotosocial/internal/text"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util/xslices"
+ "code.superseriousbusiness.org/gotosocial/internal/validate"
)
// validateStatusContent will validate the common
@@ -106,11 +107,39 @@ type statusContent struct {
Tags []*gtsmodel.Tag
}
+// Returns the final content type to use when creating or editing a status.
+func processContentType(
+ requestContentType apimodel.StatusContentType,
+ existingStatus *gtsmodel.Status,
+ accountDefaultContentType string,
+) gtsmodel.StatusContentType {
+ switch {
+ // Content type set in the request, return the new value.
+ case requestContentType != "":
+ return typeutils.APIContentTypeToContentType(requestContentType)
+
+ // No content type in the request, return the existing
+ // status's current content type if we know of one.
+ case existingStatus != nil && existingStatus.ContentType != 0:
+ return existingStatus.ContentType
+
+ // We aren't editing an existing status, or if we are
+ // it's an old one that doesn't have a saved content
+ // type. Use the user's default content type setting.
+ case accountDefaultContentType != "":
+ return typeutils.APIContentTypeToContentType(apimodel.StatusContentType(accountDefaultContentType))
+
+ // uhh.. Fall back to global default.
+ default:
+ return gtsmodel.StatusContentTypeDefault
+ }
+}
+
func (p *Processor) processContent(
ctx context.Context,
author *gtsmodel.Account,
statusID string,
- contentType string,
+ contentType gtsmodel.StatusContentType,
content string,
contentWarning string,
language string,
@@ -142,25 +171,25 @@ func (p *Processor) processContent(
)
}
- // format is the currently set text formatting
- // function, according to the provided content-type.
- var format text.FormatFunc
-
- if contentType == "" {
- // If content type wasn't specified, use
- // the author's preferred content-type.
- contentType = author.Settings.StatusContentType
- }
+ var (
+ // format is the currently set text formatting
+ // function, according to the provided content-type.
+ format text.FormatFunc
+ // formatCW is like format, but for content warning.
+ formatCW text.FormatFunc
+ )
switch contentType {
// Format status according to text/plain.
- case "", string(apimodel.StatusContentTypePlain):
+ case gtsmodel.StatusContentTypePlain:
format = p.formatter.FromPlain
+ formatCW = p.formatter.FromPlainBasic
// Format status according to text/markdown.
- case string(apimodel.StatusContentTypeMarkdown):
+ case gtsmodel.StatusContentTypeMarkdown:
format = p.formatter.FromMarkdown
+ formatCW = p.formatter.FromMarkdownBasic
// Unknown.
default:
@@ -192,26 +221,23 @@ func (p *Processor) processContent(
status.Emojis = contentRes.Emojis
status.Tags = contentRes.Tags
- // From here-on-out just use emoji-only
- // plain-text formatting as the FormatFunc.
- format = p.formatter.FromPlainEmojiOnly
-
// Sanitize content warning and format.
- warning := text.SanitizeToPlaintext(contentWarning)
- warningRes := formatInput(format, warning)
+ cwRes := formatInput(formatCW, contentWarning)
// Gather results of the formatted.
- status.ContentWarning = warningRes.HTML
- status.Emojis = append(status.Emojis, warningRes.Emojis...)
+ status.ContentWarning = cwRes.HTML
+ status.Emojis = append(status.Emojis, cwRes.Emojis...)
if poll != nil {
// Pre-allocate slice of poll options of expected length.
status.PollOptions = make([]string, len(poll.Options))
for i, option := range poll.Options {
- // Sanitize each poll option and format.
- option = text.SanitizeToPlaintext(option)
- optionRes := formatInput(format, option)
+ // Strip each poll option and format.
+ //
+ // For polls just use basic formatting.
+ option = text.StripHTMLFromText(option)
+ optionRes := formatInput(p.formatter.FromPlainBasic, option)
// Gather results of the formatted.
status.PollOptions[i] = optionRes.HTML
@@ -256,6 +282,7 @@ func (p *Processor) processMedia(
authorID string,
statusID string,
mediaIDs []string,
+ scheduledStatusID *string,
) (
[]*gtsmodel.MediaAttachment,
gtserror.WithCode,
@@ -289,7 +316,7 @@ func (p *Processor) processMedia(
// Check media isn't already attached to another status.
if (media.StatusID != "" && media.StatusID != statusID) ||
- (media.ScheduledStatusID != "" && media.ScheduledStatusID != statusID) {
+ (media.ScheduledStatusID != "" && (media.ScheduledStatusID != statusID && (scheduledStatusID == nil || media.ScheduledStatusID != *scheduledStatusID))) {
text := fmt.Sprintf("media already attached to status: %s", id)
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go
index 47806a64b..f153b2e3a 100644
--- a/internal/processing/status/context.go
+++ b/internal/processing/status/context.go
@@ -23,11 +23,9 @@ import (
"slices"
"strings"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// internalThreadContext is like
@@ -277,40 +275,8 @@ func (p *Processor) ContextGet(
requester *gtsmodel.Account,
targetStatusID string,
) (*apimodel.ThreadContext, gtserror.WithCode) {
- // Retrieve filters as they affect
- // what should be shown to requester.
- filters, err := p.state.DB.GetFiltersForAccountID(
- ctx, // Populate filters.
- requester.ID,
- )
- if err != nil {
- err = gtserror.Newf(
- "couldn't retrieve filters for account %s: %w",
- requester.ID, err,
- )
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // Retrieve mutes as they affect
- // what should be shown to requester.
- mutes, err := p.state.DB.GetAccountMutes(
- // No need to populate mutes,
- // IDs are enough here.
- gtscontext.SetBarebones(ctx),
- requester.ID,
- nil, // No paging - get all.
- )
- if err != nil {
- err = gtserror.Newf(
- "couldn't retrieve mutes for account %s: %w",
- requester.ID, err,
- )
- return nil, gtserror.NewErrorInternalError(err)
- }
-
// Retrieve the full thread context.
- threadContext, errWithCode := p.contextGet(
- ctx,
+ threadContext, errWithCode := p.contextGet(ctx,
requester,
targetStatusID,
)
@@ -324,18 +290,14 @@ func (p *Processor) ContextGet(
apiContext.Ancestors = p.c.GetVisibleAPIStatuses(ctx,
requester,
threadContext.ancestors,
- statusfilter.FilterContextThread,
- filters,
- mutes,
+ gtsmodel.FilterContextThread,
)
// Convert and filter the thread context descendants
apiContext.Descendants = p.c.GetVisibleAPIStatuses(ctx,
requester,
threadContext.descendants,
- statusfilter.FilterContextThread,
- filters,
- mutes,
+ gtsmodel.FilterContextThread,
)
return &apiContext, nil
@@ -352,8 +314,8 @@ func (p *Processor) WebContextGet(
targetStatusID string,
) (*apimodel.WebThreadContext, gtserror.WithCode) {
// Retrieve the internal thread context.
- iCtx, errWithCode := p.contextGet(
- ctx,
+ iCtx, errWithCode := p.contextGet(ctx,
+
nil, // No authed requester.
targetStatusID,
)
diff --git a/internal/processing/status/context_test.go b/internal/processing/status/context_test.go
index aba58e776..5d7e40b71 100644
--- a/internal/processing/status/context_test.go
+++ b/internal/processing/status/context_test.go
@@ -20,9 +20,9 @@ package status_test
import (
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/status"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/processing/status"
)
type topoSortTestSuite struct {
diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go
index 727c12084..57338708c 100644
--- a/internal/processing/status/create.go
+++ b/internal/processing/status/create.go
@@ -22,19 +22,19 @@ import (
"errors"
"time"
- "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/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/log"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "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/config"
+ "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/log"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
@@ -44,10 +44,8 @@ func (p *Processor) Create(
requester *gtsmodel.Account,
application *gtsmodel.Application,
form *apimodel.StatusCreateRequest,
-) (
- *apimodel.Status,
- gtserror.WithCode,
-) {
+ scheduledStatusID *string,
+) (any, gtserror.WithCode) {
// Validate incoming form status content.
if errWithCode := validateStatusContent(
form.Status,
@@ -66,11 +64,14 @@ func (p *Processor) Create(
// Generate new ID for status.
statusID := id.NewULID()
+ // Process incoming content type.
+ contentType := processContentType(form.ContentType, nil, requester.Settings.StatusContentType)
+
// Process incoming status content fields.
content, errWithCode := p.processContent(ctx,
requester,
statusID,
- string(form.ContentType),
+ contentType,
form.Status,
form.SpoilerText,
form.Language,
@@ -80,16 +81,6 @@ func (p *Processor) Create(
return nil, errWithCode
}
- // Process incoming status attachments.
- media, errWithCode := p.processMedia(ctx,
- requester.ID,
- statusID,
- form.MediaIDs,
- )
- if errWithCode != nil {
- return nil, errWithCode
- }
-
// Generate necessary URIs for username, to build status URIs.
accountURIs := uris.GenerateURIsForAccount(requester.Username)
@@ -102,16 +93,27 @@ func (p *Processor) Create(
// Handle backfilled/scheduled statuses.
backfill := false
- if form.ScheduledAt != nil {
- scheduledAt := *form.ScheduledAt
-
- // Statuses may only be scheduled
- // a minimum time into the future.
- if now.Before(scheduledAt) {
- const errText = "scheduled statuses are not yet supported"
- return nil, gtserror.NewErrorNotImplemented(gtserror.New(errText), errText)
+
+ switch {
+ case form.ScheduledAt == nil:
+ // No scheduling/backfilling
+ break
+ case form.ScheduledAt.Sub(now) >= 5*time.Minute:
+ // Statuses may only be scheduled a minimum time into the future.
+ scheduledStatus, errWithCode := p.processScheduledStatus(ctx, statusID, form, requester, application)
+
+ if errWithCode != nil {
+ return nil, errWithCode
}
+ return scheduledStatus, nil
+
+ case now.Before(*form.ScheduledAt):
+ // Invalid future scheduled status
+ const errText = "scheduled_at must be at least 5 minutes in the future"
+ return nil, gtserror.NewErrorUnprocessableEntity(gtserror.New(errText), errText)
+
+ default:
// If not scheduled into the future, this status is being backfilled.
if !config.GetInstanceAllowBackdatingStatuses() {
const errText = "backdating statuses has been disabled on this instance"
@@ -124,7 +126,7 @@ func (p *Processor) Create(
// this would also cause issues with time.Time.IsZero() checks
// that normally signify an absent optional time,
// but this check covers both cases.
- if scheduledAt.Compare(time.UnixMilli(0)) <= 0 {
+ if form.ScheduledAt.Compare(time.UnixMilli(0)) <= 0 {
const errText = "statuses can't be backdated to or before the UNIX epoch"
return nil, gtserror.NewErrorNotAcceptable(gtserror.New(errText), errText)
}
@@ -135,7 +137,7 @@ func (p *Processor) Create(
backfill = true
// Update to backfill date.
- createdAt = scheduledAt
+ createdAt = *form.ScheduledAt
// Generate an appropriate, (and unique!), ID for the creation time.
if statusID, err = p.backfilledStatusID(ctx, createdAt); err != nil {
@@ -143,6 +145,17 @@ func (p *Processor) Create(
}
}
+ // Process incoming status attachments.
+ media, errWithCode := p.processMedia(ctx,
+ requester.ID,
+ statusID,
+ form.MediaIDs,
+ scheduledStatusID,
+ )
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
status := &gtsmodel.Status{
ID: statusID,
URI: accountURIs.StatusesURI + "/" + statusID,
@@ -163,6 +176,7 @@ func (p *Processor) Create(
Content: content.Content,
ContentWarning: content.ContentWarning,
Text: form.Status, // raw
+ ContentType: contentType,
// Set gathered mentions.
MentionIDs: content.MentionIDs,
@@ -185,6 +199,13 @@ func (p *Processor) Create(
PendingApproval: util.Ptr(false),
}
+ // Only store ContentWarningText if the parsed
+ // result is different from the given SpoilerText,
+ // otherwise skip to avoid duplicating db columns.
+ if content.ContentWarning != form.SpoilerText {
+ status.ContentWarningText = form.SpoilerText
+ }
+
if backfill {
// Ensure backfilled status contains no
// mentions to anyone other than author.
@@ -206,14 +227,11 @@ func (p *Processor) Create(
return nil, errWithCode
}
- if errWithCode := p.processThreadID(ctx, status); errWithCode != nil {
+ // Process the incoming created status visibility.
+ if errWithCode := processVisibility(form, requester.Settings.Privacy, status); errWithCode != nil {
return nil, errWithCode
}
- if err := p.processVisibility(ctx, form, requester.Settings.Privacy, status); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
// Process policy AFTER visibility as it relies
// on status.Visibility and form.Visibility being set.
if errWithCode := processInteractionPolicy(form, requester.Settings, status); errWithCode != nil {
@@ -267,21 +285,6 @@ func (p *Processor) Create(
}
}
- var model any = status
- if backfill {
- // We specifically wrap backfilled statuses in
- // a different type to signal to worker process.
- model = &gtsmodel.BackfillStatus{Status: status}
- }
-
- // Send it to the client API worker for async side-effects.
- p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
- APObjectType: ap.ObjectNote,
- APActivityType: ap.ActivityCreate,
- GTSModel: model,
- Origin: requester,
- })
-
// If the new status replies to a status that
// replies to us, use our reply as an implicit
// accept of any pending interaction.
@@ -299,6 +302,22 @@ func (p *Processor) Create(
status.InReplyTo.PendingApproval = util.Ptr(false)
}
+ var model any = status
+ if backfill {
+ // We specifically wrap backfilled statuses in
+ // a different type to signal to worker process.
+ model = &gtsmodel.BackfillStatus{Status: status}
+ }
+
+ // Queue remaining create side effects
+ // (send out status, update timeline, etc).
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: model,
+ Origin: requester,
+ })
+
return p.c.GetAPIStatus(ctx, requester, status)
}
@@ -396,7 +415,7 @@ func (p *Processor) processInReplyTo(
// Derive pendingApproval status.
var pendingApproval bool
switch {
- case policyResult.WithApproval():
+ case policyResult.ManualApproval():
// We're allowed to do
// this pending approval.
pendingApproval = true
@@ -417,7 +436,7 @@ func (p *Processor) processInReplyTo(
status.PreApproved = true
}
- case policyResult.Permitted():
+ case policyResult.AutomaticApproval():
// We're permitted to do this
// based on another kind of match.
pendingApproval = false
@@ -434,68 +453,36 @@ func (p *Processor) processInReplyTo(
return nil
}
-func (p *Processor) processThreadID(ctx context.Context, status *gtsmodel.Status) gtserror.WithCode {
- // Status takes the thread ID of
- // whatever it replies to, if set.
- //
- // Might not be set if status is local
- // and replies to a remote status that
- // doesn't have a thread ID yet.
- //
- // If so, we can just thread from this
- // status onwards instead, since this
- // is where the relevant part of the
- // thread starts, from the perspective
- // of our instance at least.
- if status.InReplyTo != nil &&
- status.InReplyTo.ThreadID != "" {
- // Just inherit threadID from parent.
- status.ThreadID = status.InReplyTo.ThreadID
- return nil
- }
-
- // Mark new thread (or threaded
- // subsection) starting from here.
- threadID := id.NewULID()
- if err := p.state.DB.PutThread(
- ctx,
- &gtsmodel.Thread{
- ID: threadID,
- },
- ); err != nil {
- err := gtserror.Newf("error inserting new thread in db: %w", err)
- return gtserror.NewErrorInternalError(err)
- }
-
- // Future replies to this status
- // (if any) will inherit this thread ID.
- status.ThreadID = threadID
-
- return nil
-}
-
-func (p *Processor) processVisibility(
- ctx context.Context,
+func processVisibility(
form *apimodel.StatusCreateRequest,
accountDefaultVis gtsmodel.Visibility,
status *gtsmodel.Status,
-) error {
+) gtserror.WithCode {
switch {
// Visibility set on form, use that.
case form.Visibility != "":
- status.Visibility = typeutils.APIVisToVis(form.Visibility)
+ visibility := typeutils.APIVisToVis(form.Visibility)
+
+ if visibility == 0 {
+ const errText = "invalid visibility"
+ err := gtserror.New(errText)
+ errWithCode := gtserror.NewErrorUnprocessableEntity(err, err.Error())
+ return errWithCode
+ }
+
+ status.Visibility = visibility
// Fall back to account default, set
// this back on the form for later use.
case accountDefaultVis != 0:
status.Visibility = accountDefaultVis
- form.Visibility = p.converter.VisToAPIVis(ctx, accountDefaultVis)
+ form.Visibility = typeutils.VisToAPIVis(accountDefaultVis)
// What? Fall back to global default, set
// this back on the form for later use.
default:
status.Visibility = gtsmodel.VisibilityDefault
- form.Visibility = p.converter.VisToAPIVis(ctx, gtsmodel.VisibilityDefault)
+ form.Visibility = typeutils.VisToAPIVis(gtsmodel.VisibilityDefault)
}
// Set federated according to "local_only" field,
@@ -569,3 +556,103 @@ func processInteractionPolicy(
// setting it explicitly to save space.
return nil
}
+
+func (p *Processor) processScheduledStatus(
+ ctx context.Context,
+ statusID string,
+ form *apimodel.StatusCreateRequest,
+ requester *gtsmodel.Account,
+ application *gtsmodel.Application,
+) (*apimodel.ScheduledStatus, gtserror.WithCode) {
+ // Validate scheduled status against server configuration
+ // (max scheduled statuses limit).
+ if errWithCode := p.validateScheduledStatusLimits(ctx, requester.ID, form.ScheduledAt, nil); errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ media, errWithCode := p.processMedia(ctx,
+ requester.ID,
+ statusID,
+ form.MediaIDs,
+ nil,
+ )
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+ status := &gtsmodel.ScheduledStatus{
+ ID: statusID,
+ Account: requester,
+ AccountID: requester.ID,
+ Application: application,
+ ApplicationID: application.ID,
+ ScheduledAt: *form.ScheduledAt,
+ Text: form.Status,
+ MediaIDs: form.MediaIDs,
+ MediaAttachments: media,
+ Sensitive: &form.Sensitive,
+ SpoilerText: form.SpoilerText,
+ InReplyToID: form.InReplyToID,
+ Language: form.Language,
+ LocalOnly: form.LocalOnly,
+ ContentType: string(form.ContentType),
+ }
+
+ if form.Poll != nil {
+ status.Poll = gtsmodel.ScheduledStatusPoll{
+ Options: form.Poll.Options,
+ ExpiresIn: form.Poll.ExpiresIn,
+ Multiple: &form.Poll.Multiple,
+ HideTotals: &form.Poll.HideTotals,
+ }
+ }
+
+ accountDefaultVisibility := requester.Settings.Privacy
+
+ switch {
+ case form.Visibility != "":
+ status.Visibility = typeutils.APIVisToVis(form.Visibility)
+
+ case accountDefaultVisibility != 0:
+ status.Visibility = accountDefaultVisibility
+ form.Visibility = typeutils.VisToAPIVis(accountDefaultVisibility)
+
+ default:
+ status.Visibility = gtsmodel.VisibilityDefault
+ form.Visibility = typeutils.VisToAPIVis(gtsmodel.VisibilityDefault)
+ }
+
+ if form.InteractionPolicy != nil {
+ interactionPolicy, err := typeutils.APIInteractionPolicyToInteractionPolicy(form.InteractionPolicy, form.Visibility)
+
+ if err != nil {
+ err := gtserror.Newf("error converting interaction policy: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ status.InteractionPolicy = interactionPolicy
+ }
+
+ // Insert this newly prepared status into the database.
+ if err := p.state.DB.PutScheduledStatus(ctx, status); err != nil {
+ err := gtserror.Newf("error inserting status in db: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Schedule the newly inserted status for publishing.
+ if err := p.ScheduledStatusesSchedulePublication(ctx, status.ID); err != nil {
+ err := gtserror.Newf("error scheduling status publish: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ apiScheduledStatus, err := p.converter.ScheduledStatusToAPIScheduledStatus(
+ ctx,
+ status,
+ )
+
+ if err != nil {
+ err := gtserror.Newf("error converting: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiScheduledStatus, nil
+}
diff --git a/internal/processing/status/create_test.go b/internal/processing/status/create_test.go
index 16cefcebf..646a26978 100644
--- a/internal/processing/status/create_test.go
+++ b/internal/processing/status/create_test.go
@@ -18,15 +18,15 @@
package status_test
import (
- "context"
+ "net/http"
"testing"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/util"
)
type StatusCreateTestSuite struct {
@@ -34,7 +34,7 @@ type StatusCreateTestSuite struct {
}
func (suite *StatusCreateTestSuite) TestProcessContentWarningWithQuotationMarks() {
- ctx := context.Background()
+ ctx := suite.T().Context()
creatingAccount := suite.testAccounts["local_account_1"]
creatingApplication := suite.testApplications["application_1"]
@@ -53,34 +53,8 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithQuotationMarks(
ContentType: apimodel.StatusContentTypePlain,
}
- apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm)
- suite.NoError(err)
- suite.NotNil(apiStatus)
-
- suite.Equal("\"test\"", apiStatus.SpoilerText)
-}
-
-func (suite *StatusCreateTestSuite) TestProcessContentWarningWithHTMLEscapedQuotationMarks() {
- ctx := context.Background()
-
- creatingAccount := suite.testAccounts["local_account_1"]
- creatingApplication := suite.testApplications["application_1"]
-
- statusCreateForm := &apimodel.StatusCreateRequest{
- Status: "poopoo peepee",
- MediaIDs: []string{},
- Poll: nil,
- InReplyToID: "",
- Sensitive: false,
- SpoilerText: "&#34test&#34", // the html-escaped quotation marks should appear as normal quotation marks in the finished text
- Visibility: apimodel.VisibilityPublic,
- LocalOnly: util.Ptr(false),
- ScheduledAt: nil,
- Language: "en",
- ContentType: apimodel.StatusContentTypePlain,
- }
-
- apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm)
+ apiStatusAny, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm, nil)
+ apiStatus := apiStatusAny.(*apimodel.Status)
suite.NoError(err)
suite.NotNil(apiStatus)
@@ -88,7 +62,7 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithHTMLEscapedQuot
}
func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithUnderscoreEmoji() {
- ctx := context.Background()
+ ctx := suite.T().Context()
// update the shortcode of the rainbow emoji to surround it in underscores
if err := suite.db.UpdateWhere(ctx, []db.Where{{Key: "shortcode", Value: "rainbow"}}, "shortcode", "_rainbow_", &gtsmodel.Emoji{}); err != nil {
@@ -111,7 +85,8 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithUnderscoreEmoji
ContentType: apimodel.StatusContentTypeMarkdown,
}
- apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm)
+ apiStatusAny, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm, nil)
+ apiStatus := apiStatusAny.(*apimodel.Status)
suite.NoError(err)
suite.NotNil(apiStatus)
@@ -120,7 +95,7 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithUnderscoreEmoji
}
func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithSpoilerTextEmoji() {
- ctx := context.Background()
+ ctx := suite.T().Context()
creatingAccount := suite.testAccounts["local_account_1"]
creatingApplication := suite.testApplications["application_1"]
@@ -138,7 +113,8 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithSpoilerTextEmoj
ContentType: apimodel.StatusContentTypeMarkdown,
}
- apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm)
+ apiStatusAny, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm, nil)
+ apiStatus := apiStatusAny.(*apimodel.Status)
suite.NoError(err)
suite.NotNil(apiStatus)
@@ -148,7 +124,7 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithSpoilerTextEmoj
}
func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() {
- ctx := context.Background()
+ ctx := suite.T().Context()
config.SetMediaDescriptionMinChars(100)
@@ -169,13 +145,13 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() {
ContentType: apimodel.StatusContentTypePlain,
}
- apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm)
+ apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm, nil)
suite.EqualError(err, "media description less than min chars (100)")
suite.Nil(apiStatus)
}
func (suite *StatusCreateTestSuite) TestProcessLanguageWithScriptPart() {
- ctx := context.Background()
+ ctx := suite.T().Context()
creatingAccount := suite.testAccounts["local_account_1"]
creatingApplication := suite.testApplications["application_1"]
@@ -194,7 +170,8 @@ func (suite *StatusCreateTestSuite) TestProcessLanguageWithScriptPart() {
ContentType: apimodel.StatusContentTypePlain,
}
- apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm)
+ apiStatusAny, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm, nil)
+ apiStatus := apiStatusAny.(*apimodel.Status)
suite.NoError(err)
suite.NotNil(apiStatus)
@@ -202,7 +179,7 @@ func (suite *StatusCreateTestSuite) TestProcessLanguageWithScriptPart() {
}
func (suite *StatusCreateTestSuite) TestProcessReplyToUnthreadedRemoteStatus() {
- ctx := context.Background()
+ ctx := suite.T().Context()
creatingAccount := suite.testAccounts["local_account_1"]
creatingApplication := suite.testApplications["application_1"]
@@ -224,7 +201,8 @@ func (suite *StatusCreateTestSuite) TestProcessReplyToUnthreadedRemoteStatus() {
ContentType: apimodel.StatusContentTypePlain,
}
- apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm)
+ apiStatusAny, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm, nil)
+ apiStatus := apiStatusAny.(*apimodel.Status)
suite.NoError(err)
suite.NotNil(apiStatus)
@@ -238,6 +216,62 @@ func (suite *StatusCreateTestSuite) TestProcessReplyToUnthreadedRemoteStatus() {
suite.NotEmpty(dbStatus.ThreadID)
}
+func (suite *StatusCreateTestSuite) TestProcessNoContentTypeUsesDefault() {
+ ctx := suite.T().Context()
+ creatingAccount := suite.testAccounts["local_account_1"]
+ creatingApplication := suite.testApplications["application_1"]
+
+ statusCreateForm := &apimodel.StatusCreateRequest{
+ Status: "poopoo peepee",
+ SpoilerText: "",
+ MediaIDs: []string{},
+ Poll: nil,
+ InReplyToID: "",
+ Sensitive: false,
+ Visibility: apimodel.VisibilityPublic,
+ LocalOnly: util.Ptr(false),
+ ScheduledAt: nil,
+ Language: "en",
+ ContentType: "",
+ }
+
+ apiStatusAny, errWithCode := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm, nil)
+ apiStatus := apiStatusAny.(*apimodel.Status)
+ suite.NoError(errWithCode)
+ suite.NotNil(apiStatus)
+
+ suite.Equal("<p>poopoo peepee</p>", apiStatus.Content)
+
+ // the test accounts don't have settings, so we're comparing to
+ // the global default value instead of the requester's default
+ suite.Equal(apimodel.StatusContentTypeDefault, apiStatus.ContentType)
+}
+
+func (suite *StatusCreateTestSuite) TestProcessInvalidVisibility() {
+ ctx := suite.T().Context()
+ creatingAccount := suite.testAccounts["local_account_1"]
+ creatingApplication := suite.testApplications["application_1"]
+
+ statusCreateForm := &apimodel.StatusCreateRequest{
+ Status: "my tests content is boring",
+ SpoilerText: "",
+ MediaIDs: []string{},
+ Poll: nil,
+ InReplyToID: "",
+ Sensitive: false,
+ Visibility: "local",
+ LocalOnly: util.Ptr(false),
+ ScheduledAt: nil,
+ Language: "en",
+ ContentType: apimodel.StatusContentTypePlain,
+ }
+
+ apiStatus, errWithCode := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm, nil)
+ suite.Nil(apiStatus)
+ suite.Equal(http.StatusUnprocessableEntity, errWithCode.Code())
+ suite.Equal("Unprocessable Entity: processVisibility: invalid visibility", errWithCode.Safe())
+}
+
func TestStatusCreateTestSuite(t *testing.T) {
suite.Run(t, new(StatusCreateTestSuite))
}
diff --git a/internal/processing/status/delete.go b/internal/processing/status/delete.go
index 700909f44..4a3c32f4a 100644
--- a/internal/processing/status/delete.go
+++ b/internal/processing/status/delete.go
@@ -22,11 +22,11 @@ import (
"errors"
"fmt"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- 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/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ 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/messages"
)
// Delete processes the delete of a given status, returning the deleted status if the delete goes through.
@@ -50,6 +50,13 @@ func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Acco
return nil, errWithCode
}
+ // Replace content warning with raw
+ // version if it's available, to make
+ // delete + redraft work nicer.
+ if targetStatus.ContentWarningText != "" {
+ apiStatus.SpoilerText = targetStatus.ContentWarningText
+ }
+
// Process delete side effects.
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
diff --git a/internal/processing/status/edit.go b/internal/processing/status/edit.go
index 95665074e..2a2321604 100644
--- a/internal/processing/status/edit.go
+++ b/internal/processing/status/edit.go
@@ -24,16 +24,16 @@ import (
"slices"
"time"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
- "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/util/xslices"
+ "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/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/util/xslices"
)
// Edit ...
@@ -84,11 +84,18 @@ func (p *Processor) Edit(
return nil, errWithCode
}
+ // Process incoming content type.
+ contentType := processContentType(
+ form.ContentType,
+ status,
+ requester.Settings.StatusContentType,
+ )
+
// Process incoming status edit content fields.
content, errWithCode := p.processContent(ctx,
requester,
statusID,
- string(form.ContentType),
+ contentType,
form.Status,
form.SpoilerText,
form.Language,
@@ -103,6 +110,7 @@ func (p *Processor) Edit(
requester.ID,
statusID,
form.MediaIDs,
+ nil,
)
if errWithCode != nil {
return nil, errWithCode
@@ -256,6 +264,7 @@ func (p *Processor) Edit(
edit.Content = status.Content
edit.ContentWarning = status.ContentWarning
edit.Text = status.Text
+ edit.ContentType = status.ContentType
edit.Language = status.Language
edit.Sensitive = status.Sensitive
edit.StatusID = status.ID
@@ -297,13 +306,21 @@ func (p *Processor) Edit(
// update the other necessary status fields.
status.Content = content.Content
status.ContentWarning = content.ContentWarning
- status.Text = form.Status
+ status.Text = form.Status // raw
+ status.ContentType = contentType
status.Language = content.Language
status.Sensitive = &form.Sensitive
status.AttachmentIDs = form.MediaIDs
status.Attachments = media
status.EditedAt = now
+ // Only store ContentWarningText if the parsed
+ // result is different from the given SpoilerText,
+ // otherwise skip to avoid duplicating db columns.
+ if content.ContentWarning != form.SpoilerText {
+ status.ContentWarningText = form.SpoilerText
+ }
+
if poll != nil {
// Set relevent fields for latest with poll.
status.ActivityStreamsType = ap.ActivityQuestion
@@ -358,13 +375,13 @@ func (p *Processor) HistoryGet(ctx context.Context, requester *gtsmodel.Account,
return nil, gtserror.NewErrorInternalError(err)
}
- edits, err := p.converter.StatusToAPIEdits(ctx, target)
+ editHistory, err := p.converter.StatusToEditHistory(ctx, target)
if err != nil {
err := gtserror.Newf("error converting status edits: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- return edits, nil
+ return editHistory, nil
}
func (p *Processor) processMediaEdits(
diff --git a/internal/processing/status/edit_test.go b/internal/processing/status/edit_test.go
index 36ebf2765..87fb73f67 100644
--- a/internal/processing/status/edit_test.go
+++ b/internal/processing/status/edit_test.go
@@ -23,11 +23,12 @@ import (
"testing"
"time"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/internal/util/xslices"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/util"
- "github.com/superseriousbusiness/gotosocial/internal/util/xslices"
)
type StatusEditTestSuite struct {
@@ -36,7 +37,7 @@ type StatusEditTestSuite struct {
func (suite *StatusEditTestSuite) TestSimpleEdit() {
// Create cancellable context to use for test.
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// Get a local account to use as test requester.
@@ -90,6 +91,142 @@ func (suite *StatusEditTestSuite) TestSimpleEdit() {
previousEdit := latestStatus.Edits[len(latestStatus.Edits)-1]
suite.Equal(status.Content, previousEdit.Content)
suite.Equal(status.Text, previousEdit.Text)
+ suite.Equal(status.ContentType, previousEdit.ContentType)
+ suite.Equal(status.ContentWarning, previousEdit.ContentWarning)
+ suite.Equal(*status.Sensitive, *previousEdit.Sensitive)
+ suite.Equal(status.Language, previousEdit.Language)
+ suite.Equal(status.UpdatedAt(), previousEdit.CreatedAt)
+}
+
+func (suite *StatusEditTestSuite) TestEditChangeContentType() {
+ // Create cancellable context to use for test.
+ ctx, cncl := context.WithCancel(suite.T().Context())
+ defer cncl()
+
+ // Get a local account to use as test requester.
+ requester := suite.testAccounts["local_account_1"]
+ requester, _ = suite.state.DB.GetAccountByID(ctx, requester.ID)
+
+ // Get requester's existing plain text status to perform an edit on.
+ status := suite.testStatuses["local_account_1_status_6"]
+ status, _ = suite.state.DB.GetStatusByID(ctx, status.ID)
+
+ // Prepare edit with a Markdown body.
+ form := &apimodel.StatusEditRequest{
+ Status: "ooh the status is *fancy* now!",
+ ContentType: apimodel.StatusContentTypeMarkdown,
+ SpoilerText: "shhhhh",
+ Sensitive: true,
+ Language: "fr", // hoh hoh hoh
+ MediaIDs: nil,
+ MediaAttributes: nil,
+ Poll: nil,
+ }
+
+ // Pass the prepared form to the status processor to perform the edit.
+ apiStatus, errWithCode := suite.status.Edit(ctx, requester, status.ID, form)
+ suite.NotNil(apiStatus)
+ suite.NoError(errWithCode)
+
+ // Check response against input form data.
+ suite.Equal(form.Status, apiStatus.Text)
+ suite.Equal(form.ContentType, apiStatus.ContentType)
+ suite.Equal(form.SpoilerText, apiStatus.SpoilerText)
+ suite.Equal(form.Sensitive, apiStatus.Sensitive)
+ suite.Equal(form.Language, *apiStatus.Language)
+ suite.NotEqual(util.FormatISO8601(status.EditedAt), *apiStatus.EditedAt)
+
+ // Fetched the latest version of edited status from the database.
+ latestStatus, err := suite.state.DB.GetStatusByID(ctx, status.ID)
+ suite.NoError(err)
+
+ // Check latest status against input form data.
+ suite.Equal(form.Status, latestStatus.Text)
+ suite.Equal(typeutils.APIContentTypeToContentType(form.ContentType), latestStatus.ContentType)
+ suite.Equal(form.SpoilerText, latestStatus.ContentWarning)
+ suite.Equal(form.Sensitive, *latestStatus.Sensitive)
+ suite.Equal(form.Language, latestStatus.Language)
+ suite.Equal(len(status.EditIDs)+1, len(latestStatus.EditIDs))
+ suite.NotEqual(status.UpdatedAt(), latestStatus.UpdatedAt())
+
+ // Populate all historical edits for this status.
+ err = suite.state.DB.PopulateStatusEdits(ctx, latestStatus)
+ suite.NoError(err)
+
+ // Check previous status edit matches original status content.
+ previousEdit := latestStatus.Edits[len(latestStatus.Edits)-1]
+ suite.Equal(status.Content, previousEdit.Content)
+ suite.Equal(status.Text, previousEdit.Text)
+ suite.Equal(status.ContentType, previousEdit.ContentType)
+ suite.Equal(status.ContentWarning, previousEdit.ContentWarning)
+ suite.Equal(*status.Sensitive, *previousEdit.Sensitive)
+ suite.Equal(status.Language, previousEdit.Language)
+ suite.Equal(status.UpdatedAt(), previousEdit.CreatedAt)
+}
+
+func (suite *StatusEditTestSuite) TestEditOnStatusWithNoContentType() {
+ // Create cancellable context to use for test.
+ ctx, cncl := context.WithCancel(suite.T().Context())
+ defer cncl()
+
+ // Get a local account to use as test requester.
+ requester := suite.testAccounts["local_account_1"]
+ requester, _ = suite.state.DB.GetAccountByID(ctx, requester.ID)
+
+ // Get requester's existing status, which has no
+ // stored content type, to perform an edit on.
+ status := suite.testStatuses["local_account_1_status_2"]
+ status, _ = suite.state.DB.GetStatusByID(ctx, status.ID)
+
+ // Prepare edit without setting a new content type.
+ form := &apimodel.StatusEditRequest{
+ Status: "how will this text be parsed? it is a mystery",
+ SpoilerText: "shhhhh",
+ Sensitive: true,
+ Language: "fr", // hoh hoh hoh
+ MediaIDs: nil,
+ MediaAttributes: nil,
+ Poll: nil,
+ }
+
+ // Pass the prepared form to the status processor to perform the edit.
+ apiStatus, errWithCode := suite.status.Edit(ctx, requester, status.ID, form)
+ suite.NotNil(apiStatus)
+ suite.NoError(errWithCode)
+
+ // Check response against input form data.
+ suite.Equal(form.Status, apiStatus.Text)
+ suite.NotEqual(util.FormatISO8601(status.EditedAt), *apiStatus.EditedAt)
+
+ // Check response against requester's default content type setting
+ // (the test accounts don't actually have settings on them, so
+ // instead we check that the global default content type is used)
+ suite.Equal(apimodel.StatusContentTypeDefault, apiStatus.ContentType)
+
+ // Fetched the latest version of edited status from the database.
+ latestStatus, err := suite.state.DB.GetStatusByID(ctx, status.ID)
+ suite.NoError(err)
+
+ // Check latest status against input form data
+ suite.Equal(form.Status, latestStatus.Text)
+ suite.Equal(form.Sensitive, *latestStatus.Sensitive)
+ suite.Equal(form.Language, latestStatus.Language)
+ suite.Equal(len(status.EditIDs)+1, len(latestStatus.EditIDs))
+ suite.NotEqual(status.UpdatedAt(), latestStatus.UpdatedAt())
+
+ // Check latest status against requester's default content
+ // type (again, actually just checking for the global default)
+ suite.Equal(gtsmodel.StatusContentTypeDefault, latestStatus.ContentType)
+
+ // Populate all historical edits for this status.
+ err = suite.state.DB.PopulateStatusEdits(ctx, latestStatus)
+ suite.NoError(err)
+
+ // Check previous status edit matches original status content.
+ previousEdit := latestStatus.Edits[len(latestStatus.Edits)-1]
+ suite.Equal(status.Content, previousEdit.Content)
+ suite.Equal(status.Text, previousEdit.Text)
+ suite.Equal(status.ContentType, previousEdit.ContentType)
suite.Equal(status.ContentWarning, previousEdit.ContentWarning)
suite.Equal(*status.Sensitive, *previousEdit.Sensitive)
suite.Equal(status.Language, previousEdit.Language)
@@ -98,7 +235,7 @@ func (suite *StatusEditTestSuite) TestSimpleEdit() {
func (suite *StatusEditTestSuite) TestEditAddPoll() {
// Create cancellable context to use for test.
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// Get a local account to use as test requester.
@@ -176,7 +313,7 @@ func (suite *StatusEditTestSuite) TestEditAddPoll() {
func (suite *StatusEditTestSuite) TestEditAddPollNoExpiry() {
// Create cancellable context to use for test.
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// Get a local account to use as test requester.
@@ -254,7 +391,7 @@ func (suite *StatusEditTestSuite) TestEditAddPollNoExpiry() {
func (suite *StatusEditTestSuite) TestEditMediaDescription() {
// Create cancellable context to use for test.
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// Get a local account to use as test requester.
@@ -350,7 +487,7 @@ func (suite *StatusEditTestSuite) TestEditMediaDescription() {
func (suite *StatusEditTestSuite) TestEditAddMedia() {
// Create cancellable context to use for test.
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// Get a local account to use as test requester.
@@ -425,7 +562,7 @@ func (suite *StatusEditTestSuite) TestEditAddMedia() {
func (suite *StatusEditTestSuite) TestEditRemoveMedia() {
// Create cancellable context to use for test.
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// Get a local account to use as test requester.
@@ -491,7 +628,7 @@ func (suite *StatusEditTestSuite) TestEditRemoveMedia() {
func (suite *StatusEditTestSuite) TestEditOthersStatus1() {
// Create cancellable context to use for test.
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// Get a local account to use as test requester.
@@ -516,7 +653,7 @@ func (suite *StatusEditTestSuite) TestEditOthersStatus1() {
func (suite *StatusEditTestSuite) TestEditOthersStatus2() {
// Create cancellable context to use for test.
- ctx, cncl := context.WithCancel(context.Background())
+ ctx, cncl := context.WithCancel(suite.T().Context())
defer cncl()
// Get a local account to use as test requester.
diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go
index defc59af0..983f12b6a 100644
--- a/internal/processing/status/fave.go
+++ b/internal/processing/status/fave.go
@@ -22,16 +22,16 @@ 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/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/uris"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
func (p *Processor) getFaveableStatus(
@@ -65,7 +65,7 @@ func (p *Processor) getFaveableStatus(
fave, err := p.state.DB.GetStatusFave(ctx, requester.ID, target.ID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err)
+ err = gtserror.Newf("error checking existing fave: %w", err)
return nil, nil, gtserror.NewErrorInternalError(err)
}
@@ -112,7 +112,7 @@ func (p *Processor) FaveCreate(
)
switch {
- case policyResult.WithApproval():
+ case policyResult.ManualApproval():
// We're allowed to do
// this pending approval.
pendingApproval = true
@@ -133,7 +133,7 @@ func (p *Processor) FaveCreate(
preApproved = true
}
- case policyResult.Permitted():
+ case policyResult.AutomaticApproval():
// We're permitted to do this
// based on another kind of match.
pendingApproval = false
@@ -160,15 +160,6 @@ func (p *Processor) FaveCreate(
return nil, gtserror.NewErrorInternalError(err)
}
- // Process new status fave side effects.
- p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
- APObjectType: ap.ActivityLike,
- APActivityType: ap.ActivityCreate,
- GTSModel: gtsFave,
- Origin: requester,
- Target: status.Account,
- })
-
// If the fave target status replies to a status
// that we own, and has a pending interaction
// request, use the fave as an implicit accept.
@@ -186,6 +177,16 @@ func (p *Processor) FaveCreate(
status.PendingApproval = util.Ptr(false)
}
+ // Queue remaining fave side effects
+ // (send out fave, update timeline, etc).
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ APObjectType: ap.ActivityLike,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: gtsFave,
+ Origin: requester,
+ Target: status.Account,
+ })
+
return p.c.GetAPIStatus(ctx, requester, status)
}
diff --git a/internal/processing/status/get.go b/internal/processing/status/get.go
index 812f01683..edcaa07fa 100644
--- a/internal/processing/status/get.go
+++ b/internal/processing/status/get.go
@@ -21,9 +21,10 @@ import (
"context"
"errors"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// Get gets the given status, taking account of privacy settings and blocks etc.
@@ -52,9 +53,21 @@ func (p *Processor) SourceGet(ctx context.Context, requester *gtsmodel.Account,
"target status not found",
)
}
+
+ // Try to use unparsed content
+ // warning text if available,
+ // fall back to parsed cw html.
+ var spoilerText string
+ if status.ContentWarningText != "" {
+ spoilerText = status.ContentWarningText
+ } else {
+ spoilerText = status.ContentWarning
+ }
+
return &apimodel.StatusSource{
ID: status.ID,
Text: status.Text,
- SpoilerText: status.ContentWarning,
+ SpoilerText: spoilerText,
+ ContentType: typeutils.ContentTypeToAPIContentType(status.ContentType),
}, nil
}
diff --git a/internal/processing/status/mute.go b/internal/processing/status/mute.go
index 8888b59d4..638b7cfb2 100644
--- a/internal/processing/status/mute.go
+++ b/internal/processing/status/mute.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/id"
+ 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"
)
// getMuteableStatus fetches targetStatusID status and
diff --git a/internal/processing/status/pin.go b/internal/processing/status/pin.go
index 3f8435e52..dad11d6b2 100644
--- a/internal/processing/status/pin.go
+++ b/internal/processing/status/pin.go
@@ -23,9 +23,9 @@ import (
"fmt"
"time"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
const allowedPinnedCount = 10
diff --git a/internal/processing/status/scheduledstatus.go b/internal/processing/status/scheduledstatus.go
new file mode 100644
index 000000000..d0ec6898c
--- /dev/null
+++ b/internal/processing/status/scheduledstatus.go
@@ -0,0 +1,357 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package status
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "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/paging"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+)
+
+// ScheduledStatusesGetPage returns a page of scheduled statuses authored
+// by the requester.
+func (p *Processor) ScheduledStatusesGetPage(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ page *paging.Page,
+) (*apimodel.PageableResponse, gtserror.WithCode) {
+ scheduledStatuses, err := p.state.DB.GetScheduledStatusesForAcct(
+ ctx,
+ requester.ID,
+ page,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting scheduled statuses: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ count := len(scheduledStatuses)
+ if count == 0 {
+ return paging.EmptyResponse(), nil
+ }
+
+ var (
+ // Get the lowest and highest
+ // ID values, used for paging.
+ lo = scheduledStatuses[count-1].ID
+ hi = scheduledStatuses[0].ID
+
+ // Best-guess items length.
+ items = make([]interface{}, 0, count)
+ )
+
+ for _, scheduledStatus := range scheduledStatuses {
+ apiScheduledStatus, err := p.converter.ScheduledStatusToAPIScheduledStatus(
+ ctx, scheduledStatus,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error converting scheduled status to api scheduled status: %v", err)
+ continue
+ }
+
+ // Append scheduledStatus to return items.
+ items = append(items, apiScheduledStatus)
+ }
+
+ return paging.PackageResponse(paging.ResponseParams{
+ Items: items,
+ Path: "/api/v1/scheduled_statuses",
+ Next: page.Next(lo, hi),
+ Prev: page.Prev(lo, hi),
+ }), nil
+}
+
+// ScheduledStatusesGetOne returns one scheduled
+// status with the given ID.
+func (p *Processor) ScheduledStatusesGetOne(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (*apimodel.ScheduledStatus, gtserror.WithCode) {
+ scheduledStatus, err := p.state.DB.GetScheduledStatusByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting scheduled status: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if scheduledStatus == nil {
+ err := gtserror.New("scheduled status not found")
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ if scheduledStatus.AccountID != requester.ID {
+ err := gtserror.Newf(
+ "scheduled status %s is not authored by account %s",
+ scheduledStatus.ID, requester.ID,
+ )
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ apiScheduledStatus, err := p.converter.ScheduledStatusToAPIScheduledStatus(
+ ctx, scheduledStatus,
+ )
+ if err != nil {
+ err := gtserror.Newf("error converting scheduled status to api scheduled status: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiScheduledStatus, nil
+}
+
+func (p *Processor) ScheduledStatusesScheduleAll(ctx context.Context) error {
+ // Fetch all pending statuses from the database (barebones models are enough).
+ statuses, err := p.state.DB.GetAllScheduledStatuses(gtscontext.SetBarebones(ctx))
+ if err != nil {
+ return gtserror.Newf("error getting scheduled statuses from db: %w", err)
+ }
+
+ var errs gtserror.MultiError
+
+ for _, status := range statuses {
+ // Schedule publication of each of the statuses and catch any errors.
+ if err := p.ScheduledStatusesSchedulePublication(ctx, status.ID); err != nil {
+ errs.Append(err)
+ }
+ }
+
+ return errs.Combine()
+}
+
+func (p *Processor) ScheduledStatusesSchedulePublication(ctx context.Context, statusID string) gtserror.WithCode {
+ status, err := p.state.DB.GetScheduledStatusByID(ctx, statusID)
+
+ if err != nil {
+ return gtserror.NewErrorNotFound(gtserror.Newf("failed to get scheduled status %s", statusID))
+ }
+
+ // Add the given status to the scheduler.
+ ok := p.state.Workers.Scheduler.AddOnce(
+ status.ID,
+ status.ScheduledAt,
+ p.onPublish(status.ID),
+ )
+
+ if !ok {
+ // Failed to add the status to the scheduler, either it was
+ // starting / stopping or there already exists a task for status.
+ return gtserror.NewErrorInternalError(gtserror.Newf("failed adding status %s to scheduler", status.ID))
+ }
+
+ atStr := status.ScheduledAt.Local().Format("Jan _2 2006 15:04:05")
+ log.Infof(ctx, "scheduled status publication for %s at '%s'", status.ID, atStr)
+ return nil
+}
+
+// onPublish returns a callback function to be used by the scheduler on the scheduled date.
+func (p *Processor) onPublish(statusID string) func(context.Context, time.Time) {
+ return func(ctx context.Context, now time.Time) {
+ // Get the latest version of status from database.
+ status, err := p.state.DB.GetScheduledStatusByID(ctx, statusID)
+ if err != nil {
+ log.Errorf(ctx, "error getting status %s from db: %v", statusID, err)
+ return
+ }
+
+ request := &apimodel.StatusCreateRequest{
+ Status: status.Text,
+ MediaIDs: status.MediaIDs,
+ Poll: nil,
+ InReplyToID: status.InReplyToID,
+ Sensitive: *status.Sensitive,
+ SpoilerText: status.SpoilerText,
+ Visibility: typeutils.VisToAPIVis(status.Visibility),
+ Language: status.Language,
+ }
+
+ if status.Poll.Options != nil && len(status.Poll.Options) > 1 {
+ request.Poll = &apimodel.PollRequest{
+ Options: status.Poll.Options,
+ ExpiresIn: status.Poll.ExpiresIn,
+ Multiple: *status.Poll.Multiple,
+ HideTotals: *status.Poll.HideTotals,
+ }
+ }
+
+ _, errWithCode := p.Create(ctx, status.Account, status.Application, request, &statusID)
+
+ if errWithCode != nil {
+ log.Errorf(ctx, "could not publish scheduled status: %v", errWithCode.Unwrap())
+ return
+ }
+
+ err = p.state.DB.DeleteScheduledStatusByID(ctx, statusID)
+
+ if err != nil {
+ log.Error(ctx, err)
+ }
+ }
+}
+
+// Update scheduled status schedule data
+func (p *Processor) ScheduledStatusesUpdate(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+ scheduledAt *time.Time,
+) (*apimodel.ScheduledStatus, gtserror.WithCode) {
+ scheduledStatus, err := p.state.DB.GetScheduledStatusByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting scheduled status: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if scheduledStatus == nil {
+ err := gtserror.New("scheduled status not found")
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ if scheduledStatus.AccountID != requester.ID {
+ err := gtserror.Newf(
+ "scheduled status %s is not authored by account %s",
+ scheduledStatus.ID, requester.ID,
+ )
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ if errWithCode := p.validateScheduledStatusLimits(ctx, requester.ID, scheduledAt, &scheduledStatus.ScheduledAt); errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ scheduledStatus.ScheduledAt = *scheduledAt
+ err = p.state.DB.UpdateScheduledStatusScheduledDate(ctx, scheduledStatus, scheduledAt)
+
+ if err != nil {
+ err := gtserror.Newf("db error getting scheduled status: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ ok := p.state.Workers.Scheduler.Cancel(id)
+
+ if !ok {
+ err := gtserror.Newf("failed to cancel scheduled status")
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ err = p.ScheduledStatusesSchedulePublication(ctx, id)
+
+ if err != nil {
+ err := gtserror.Newf("error scheduling status: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ apiScheduledStatus, err := p.converter.ScheduledStatusToAPIScheduledStatus(
+ ctx, scheduledStatus,
+ )
+ if err != nil {
+ err := gtserror.Newf("error converting scheduled status to api req: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiScheduledStatus, nil
+}
+
+// Cancel a scheduled status
+func (p *Processor) ScheduledStatusesDelete(ctx context.Context, requester *gtsmodel.Account, id string) gtserror.WithCode {
+ scheduledStatus, err := p.state.DB.GetScheduledStatusByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting scheduled status: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if scheduledStatus == nil {
+ err := gtserror.New("scheduled status not found")
+ return gtserror.NewErrorNotFound(err)
+ }
+
+ if scheduledStatus.AccountID != requester.ID {
+ err := gtserror.Newf(
+ "scheduled status %s is not authored by account %s",
+ scheduledStatus.ID, requester.ID,
+ )
+ return gtserror.NewErrorNotFound(err)
+ }
+
+ ok := p.state.Workers.Scheduler.Cancel(id)
+
+ if !ok {
+ err := gtserror.Newf("failed to cancel scheduled status")
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ err = p.state.DB.DeleteScheduledStatusByID(ctx, id)
+
+ if err != nil {
+ err := gtserror.Newf("db error deleting scheduled status: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ return nil
+}
+
+func (p *Processor) validateScheduledStatusLimits(ctx context.Context, acctID string, scheduledAt *time.Time, prevScheduledAt *time.Time) gtserror.WithCode {
+ // Skip check when the scheduled status already exists and the day stays the same
+ if prevScheduledAt != nil {
+ y1, m1, d1 := scheduledAt.Date()
+ y2, m2, d2 := prevScheduledAt.Date()
+
+ if y1 == y2 && m1 == m2 && d1 == d2 {
+ return nil
+ }
+ }
+
+ scheduledDaily, err := p.state.DB.GetScheduledStatusesCountForAcct(ctx, acctID, scheduledAt)
+
+ if err != nil {
+ err := gtserror.Newf("error getting scheduled statuses count for day: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if max := config.GetScheduledStatusesMaxDaily(); scheduledDaily >= max {
+ err := gtserror.Newf("scheduled statuses count for day is at the limit (%d)", max)
+ return gtserror.NewErrorUnprocessableEntity(err)
+ }
+
+ // Skip total check when editing an existing scheduled status
+ if prevScheduledAt != nil {
+ return nil
+ }
+
+ scheduledTotal, err := p.state.DB.GetScheduledStatusesCountForAcct(ctx, acctID, nil)
+
+ if err != nil {
+ err := gtserror.Newf("error getting total scheduled statuses count: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if max := config.GetScheduledStatusesMaxTotal(); scheduledTotal >= max {
+ err := gtserror.Newf("total scheduled statuses count is at the limit (%d)", max)
+ return gtserror.NewErrorUnprocessableEntity(err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/status/scheduledstatus_test.go b/internal/processing/status/scheduledstatus_test.go
new file mode 100644
index 000000000..d53b1ec70
--- /dev/null
+++ b/internal/processing/status/scheduledstatus_test.go
@@ -0,0 +1,69 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package status_test
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/testrig"
+ "github.com/stretchr/testify/suite"
+)
+
+type ScheduledStatusTestSuite struct {
+ StatusStandardTestSuite
+}
+
+func (suite *ScheduledStatusTestSuite) TestUpdate() {
+ ctx := suite.T().Context()
+
+ account1 := suite.testAccounts["local_account_1"]
+ scheduledStatus1 := suite.testScheduledStatuses["scheduled_status_1"]
+ newScheduledAt := testrig.TimeMustParse("2080-07-02T21:37:00+02:00")
+
+ suite.state.Workers.Scheduler.AddOnce(scheduledStatus1.ID, scheduledStatus1.ScheduledAt, func(ctx context.Context, t time.Time) {})
+
+ // update scheduled status publication date
+ scheduledStatus2, err := suite.status.ScheduledStatusesUpdate(ctx, account1, scheduledStatus1.ID, util.Ptr(newScheduledAt))
+ suite.NoError(err)
+ suite.NotNil(scheduledStatus2)
+ suite.Equal(scheduledStatus2.ScheduledAt, util.FormatISO8601(newScheduledAt))
+ // should be rescheduled
+ suite.Equal(suite.state.Workers.Scheduler.Cancel(scheduledStatus1.ID), true)
+}
+
+func (suite *ScheduledStatusTestSuite) TestDelete() {
+ ctx := suite.T().Context()
+
+ account1 := suite.testAccounts["local_account_1"]
+ scheduledStatus1 := suite.testScheduledStatuses["scheduled_status_1"]
+
+ suite.state.Workers.Scheduler.AddOnce(scheduledStatus1.ID, scheduledStatus1.ScheduledAt, func(ctx context.Context, t time.Time) {})
+
+ // delete scheduled status
+ err := suite.status.ScheduledStatusesDelete(ctx, account1, scheduledStatus1.ID)
+ suite.NoError(err)
+ // should be already cancelled
+ suite.Equal(suite.state.Workers.Scheduler.Cancel(scheduledStatus1.ID), false)
+}
+
+func TestScheduledStatusTestSuite(t *testing.T) {
+ suite.Run(t, new(ScheduledStatusTestSuite))
+}
diff --git a/internal/processing/status/status.go b/internal/processing/status/status.go
index 26dfd0d7a..b911c03d7 100644
--- a/internal/processing/status/status.go
+++ b/internal/processing/status/status.go
@@ -18,16 +18,16 @@
package status
import (
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
- "github.com/superseriousbusiness/gotosocial/internal/processing/polls"
- "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/interaction"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/interactionrequests"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/polls"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/text"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/status/status_test.go b/internal/processing/status/status_test.go
index 74aef7188..18d20d67c 100644
--- a/internal/processing/status/status_test.go
+++ b/internal/processing/status/status_test.go
@@ -18,24 +18,26 @@
package status_test
import (
+ "code.superseriousbusiness.org/gotosocial/internal/admin"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/interaction"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/mutes"
+ statusfilter "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"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/interactionrequests"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/polls"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/status"
+ "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/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
- "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"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
- "github.com/superseriousbusiness/gotosocial/internal/processing/polls"
- "github.com/superseriousbusiness/gotosocial/internal/processing/status"
- "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 StatusStandardTestSuite struct {
@@ -49,15 +51,15 @@ type StatusStandardTestSuite struct {
federator *federation.Federator
// 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
- testAttachments map[string]*gtsmodel.MediaAttachment
- testStatuses map[string]*gtsmodel.Status
- testTags map[string]*gtsmodel.Tag
- testMentions map[string]*gtsmodel.Mention
+ testTokens map[string]*gtsmodel.Token
+ testApplications map[string]*gtsmodel.Application
+ testUsers map[string]*gtsmodel.User
+ testAccounts map[string]*gtsmodel.Account
+ testAttachments map[string]*gtsmodel.MediaAttachment
+ testStatuses map[string]*gtsmodel.Status
+ testTags map[string]*gtsmodel.Tag
+ testMentions map[string]*gtsmodel.Mention
+ testScheduledStatuses map[string]*gtsmodel.ScheduledStatus
// module being tested
status status.Processor
@@ -65,7 +67,6 @@ type StatusStandardTestSuite struct {
func (suite *StatusStandardTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
@@ -73,6 +74,7 @@ func (suite *StatusStandardTestSuite) SetupSuite() {
suite.testStatuses = testrig.NewTestStatuses()
suite.testTags = testrig.NewTestTags()
suite.testMentions = testrig.NewTestMentions()
+ suite.testScheduledStatuses = testrig.NewTestScheduledStatuses()
}
func (suite *StatusStandardTestSuite) SetupTest() {
@@ -94,14 +96,11 @@ func (suite *StatusStandardTestSuite) SetupTest() {
suite.federator = testrig.NewTestFederator(&suite.state, suite.tc, suite.mediaManager)
visFilter := visibility.NewFilter(&suite.state)
+ muteFilter := mutes.NewFilter(&suite.state)
intFilter := interaction.NewFilter(&suite.state)
- testrig.StartTimelines(
- &suite.state,
- visFilter,
- suite.typeConverter,
- )
+ statusFilter := statusfilter.NewFilter(&suite.state)
- common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter)
+ common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter, muteFilter, statusFilter)
polls := polls.New(&common, &suite.state, suite.typeConverter)
intReqs := interactionrequests.New(&common, &suite.state, suite.typeConverter)
diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go
index 99cff7c56..fcaf37d82 100644
--- a/internal/processing/status/util.go
+++ b/internal/processing/status/util.go
@@ -21,10 +21,10 @@ import (
"context"
"errors"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
func (p *Processor) implicitlyAccept(
diff --git a/internal/processing/stream/authorize.go b/internal/processing/stream/authorize.go
index 0baea29f1..44463d5c6 100644
--- a/internal/processing/stream/authorize.go
+++ b/internal/processing/stream/authorize.go
@@ -19,11 +19,15 @@ package stream
import (
"context"
+ "errors"
"fmt"
+ "slices"
+ "strings"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// Authorize returns an oauth2 token info in response to an access token query from the streaming API
@@ -58,5 +62,22 @@ func (p *Processor) Authorize(ctx context.Context, accessToken string) (*gtsmode
return nil, gtserror.NewErrorInternalError(err)
}
+ // Ensure read scope.
+ //
+ // TODO: make this more granular
+ // depending on stream type.
+ hasScopes := strings.Split(ti.GetScope(), " ")
+ scopeOK := slices.ContainsFunc(
+ hasScopes,
+ func(hasScope string) bool {
+ return apiutil.Scope(hasScope).Permits(apiutil.ScopeRead)
+ },
+ )
+
+ if !scopeOK {
+ const errText = "token has insufficient scope permission"
+ return nil, gtserror.NewErrorForbidden(errors.New(errText), errText)
+ }
+
return acct, nil
}
diff --git a/internal/processing/stream/authorize_test.go b/internal/processing/stream/authorize_test.go
index cb91d5b30..7124888d9 100644
--- a/internal/processing/stream/authorize_test.go
+++ b/internal/processing/stream/authorize_test.go
@@ -18,11 +18,10 @@
package stream_test
import (
- "context"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/db"
)
type AuthorizeTestSuite struct {
@@ -30,15 +29,15 @@ type AuthorizeTestSuite struct {
}
func (suite *AuthorizeTestSuite) TestAuthorize() {
- account1, err := suite.streamProcessor.Authorize(context.Background(), suite.testTokens["local_account_1"].Access)
+ account1, err := suite.streamProcessor.Authorize(suite.T().Context(), suite.testTokens["local_account_1"].Access)
suite.NoError(err)
suite.Equal(suite.testAccounts["local_account_1"].ID, account1.ID)
- account2, err := suite.streamProcessor.Authorize(context.Background(), suite.testTokens["local_account_2"].Access)
+ account2, err := suite.streamProcessor.Authorize(suite.T().Context(), suite.testTokens["local_account_2"].Access)
suite.NoError(err)
suite.Equal(suite.testAccounts["local_account_2"].ID, account2.ID)
- noAccount, err := suite.streamProcessor.Authorize(context.Background(), "aaaaaaaaaaaaaaaaaaaaa!!")
+ noAccount, err := suite.streamProcessor.Authorize(suite.T().Context(), "aaaaaaaaaaaaaaaaaaaaa!!")
suite.EqualError(err, "could not load access token: "+db.ErrNoEntries.Error())
suite.Nil(noAccount)
}
diff --git a/internal/processing/stream/conversation.go b/internal/processing/stream/conversation.go
index a0236c459..b5394ee17 100644
--- a/internal/processing/stream/conversation.go
+++ b/internal/processing/stream/conversation.go
@@ -21,10 +21,10 @@ import (
"context"
"encoding/json"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
"codeberg.org/gruf/go-byteutil"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
)
// Conversation streams the given conversation to any open, appropriate streams belonging to the given account.
diff --git a/internal/processing/stream/delete.go b/internal/processing/stream/delete.go
index 1c61b98d3..fc763f910 100644
--- a/internal/processing/stream/delete.go
+++ b/internal/processing/stream/delete.go
@@ -20,7 +20,7 @@ package stream
import (
"context"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
)
// Delete streams the delete of the given statusID to *ALL* open streams.
diff --git a/internal/processing/stream/filterschanged.go b/internal/processing/stream/filterschanged.go
index b98506b9f..dff22c64b 100644
--- a/internal/processing/stream/filterschanged.go
+++ b/internal/processing/stream/filterschanged.go
@@ -20,8 +20,8 @@ package stream
import (
"context"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
)
// FiltersChanged streams a filters changed event to any open, appropriate streams belonging to the given account.
diff --git a/internal/processing/stream/notification.go b/internal/processing/stream/notification.go
index a16da11e6..ef3acdc79 100644
--- a/internal/processing/stream/notification.go
+++ b/internal/processing/stream/notification.go
@@ -21,11 +21,11 @@ import (
"context"
"encoding/json"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
"codeberg.org/gruf/go-byteutil"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
)
// Notify streams the given notification to any open, appropriate streams belonging to the given account.
diff --git a/internal/processing/stream/notification_test.go b/internal/processing/stream/notification_test.go
index 2ede28079..70fc45397 100644
--- a/internal/processing/stream/notification_test.go
+++ b/internal/processing/stream/notification_test.go
@@ -19,13 +19,12 @@ package stream_test
import (
"bytes"
- "context"
"encoding/json"
"testing"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
type NotificationTestSuite struct {
@@ -35,11 +34,11 @@ type NotificationTestSuite struct {
func (suite *NotificationTestSuite) TestStreamNotification() {
account := suite.testAccounts["local_account_1"]
- openStream, errWithCode := suite.streamProcessor.Open(context.Background(), account, "user")
+ openStream, errWithCode := suite.streamProcessor.Open(suite.T().Context(), account, "user")
suite.NoError(errWithCode)
followAccount := suite.testAccounts["remote_account_1"]
- followAccountAPIModel, err := typeutils.NewConverter(&suite.state).AccountToAPIAccountPublic(context.Background(), followAccount)
+ followAccountAPIModel, err := typeutils.NewConverter(&suite.state).AccountToAPIAccountPublic(suite.T().Context(), followAccount)
suite.NoError(err)
notification := &apimodel.Notification{
@@ -49,9 +48,9 @@ func (suite *NotificationTestSuite) TestStreamNotification() {
Account: followAccountAPIModel,
}
- suite.streamProcessor.Notify(context.Background(), account, notification)
+ suite.streamProcessor.Notify(suite.T().Context(), account, notification)
- msg, ok := openStream.Recv(context.Background())
+ msg, ok := openStream.Recv(suite.T().Context())
suite.True(ok)
dst := new(bytes.Buffer)
diff --git a/internal/processing/stream/open.go b/internal/processing/stream/open.go
index 2f2bbd4a3..899e26896 100644
--- a/internal/processing/stream/open.go
+++ b/internal/processing/stream/open.go
@@ -20,11 +20,11 @@ package stream
import (
"context"
- "codeberg.org/gruf/go-kv"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
+ "codeberg.org/gruf/go-kv/v2"
)
// Open returns a new Stream for the given account, which will contain a channel for passing messages back to the caller.
diff --git a/internal/processing/stream/open_test.go b/internal/processing/stream/open_test.go
index 21ef69154..cc08a17a7 100644
--- a/internal/processing/stream/open_test.go
+++ b/internal/processing/stream/open_test.go
@@ -18,7 +18,6 @@
package stream_test
import (
- "context"
"testing"
"github.com/stretchr/testify/suite"
@@ -31,7 +30,7 @@ type OpenStreamTestSuite struct {
func (suite *OpenStreamTestSuite) TestOpenStream() {
account := suite.testAccounts["local_account_1"]
- _, errWithCode := suite.streamProcessor.Open(context.Background(), account, "user")
+ _, errWithCode := suite.streamProcessor.Open(suite.T().Context(), account, "user")
suite.NoError(errWithCode)
}
diff --git a/internal/processing/stream/statusupdate.go b/internal/processing/stream/statusupdate.go
index bd4658873..2f1ca598b 100644
--- a/internal/processing/stream/statusupdate.go
+++ b/internal/processing/stream/statusupdate.go
@@ -21,11 +21,11 @@ import (
"context"
"encoding/json"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
"codeberg.org/gruf/go-byteutil"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
)
// StatusUpdate streams the given edited status to any open, appropriate streams belonging to the given account.
diff --git a/internal/processing/stream/statusupdate_test.go b/internal/processing/stream/statusupdate_test.go
index 180538c60..a82348c31 100644
--- a/internal/processing/stream/statusupdate_test.go
+++ b/internal/processing/stream/statusupdate_test.go
@@ -19,14 +19,12 @@ package stream_test
import (
"bytes"
- "context"
"encoding/json"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
"github.com/stretchr/testify/suite"
- statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
type StatusUpdateTestSuite struct {
@@ -36,16 +34,16 @@ type StatusUpdateTestSuite struct {
func (suite *StatusUpdateTestSuite) TestStreamNotification() {
account := suite.testAccounts["local_account_1"]
- openStream, errWithCode := suite.streamProcessor.Open(context.Background(), account, "user")
+ openStream, errWithCode := suite.streamProcessor.Open(suite.T().Context(), account, "user")
suite.NoError(errWithCode)
editedStatus := suite.testStatuses["remote_account_1_status_1"]
- apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(context.Background(), editedStatus, account, statusfilter.FilterContextNotifications, nil, nil)
+ apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(suite.T().Context(), editedStatus, account)
suite.NoError(err)
- suite.streamProcessor.StatusUpdate(context.Background(), account, apiStatus, stream.TimelineHome)
+ suite.streamProcessor.StatusUpdate(suite.T().Context(), account, apiStatus, stream.TimelineHome)
- msg, ok := openStream.Recv(context.Background())
+ msg, ok := openStream.Recv(suite.T().Context())
suite.True(ok)
dst := new(bytes.Buffer)
@@ -71,7 +69,7 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() {
"muted": false,
"bookmarked": false,
"pinned": false,
- "content": "dark souls status bot: \"thoughts of dog\"",
+ "content": "\u003cp\u003edark souls status bot: \"thoughts of dog\"\u003c/p\u003e",
"reblog": null,
"account": {
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
@@ -135,6 +133,11 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() {
"poll": null,
"interaction_policy": {
"can_favourite": {
+ "automatic_approval": [
+ "public",
+ "me"
+ ],
+ "manual_approval": [],
"always": [
"public",
"me"
@@ -142,6 +145,11 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() {
"with_approval": []
},
"can_reply": {
+ "automatic_approval": [
+ "public",
+ "me"
+ ],
+ "manual_approval": [],
"always": [
"public",
"me"
@@ -149,6 +157,11 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() {
"with_approval": []
},
"can_reblog": {
+ "automatic_approval": [
+ "public",
+ "me"
+ ],
+ "manual_approval": [],
"always": [
"public",
"me"
diff --git a/internal/processing/stream/stream.go b/internal/processing/stream/stream.go
index 0b7285b58..19a82a8a2 100644
--- a/internal/processing/stream/stream.go
+++ b/internal/processing/stream/stream.go
@@ -18,9 +18,9 @@
package stream
import (
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/oauth"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
)
type Processor struct {
diff --git a/internal/processing/stream/stream_test.go b/internal/processing/stream/stream_test.go
index 96ea65b0f..47c85af1f 100644
--- a/internal/processing/stream/stream_test.go
+++ b/internal/processing/stream/stream_test.go
@@ -18,14 +18,14 @@
package stream_test
import (
+ "code.superseriousbusiness.org/gotosocial/internal/admin"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/oauth"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "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/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/processing/stream"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type StreamTestSuite struct {
@@ -52,7 +52,7 @@ func (suite *StreamTestSuite) SetupTest() {
suite.db = testrig.NewTestDB(&suite.state)
suite.state.DB = suite.db
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
+ suite.oauthServer = testrig.NewTestOauthServer(&suite.state)
suite.streamProcessor = stream.New(&suite.state, suite.oauthServer)
testrig.StandardDBSetup(suite.db, suite.testAccounts)
diff --git a/internal/processing/stream/update.go b/internal/processing/stream/update.go
index a84763d51..f2b064c28 100644
--- a/internal/processing/stream/update.go
+++ b/internal/processing/stream/update.go
@@ -21,11 +21,11 @@ import (
"context"
"encoding/json"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
"codeberg.org/gruf/go-byteutil"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
)
// Update streams the given update to any open, appropriate streams belonging to the given account.
diff --git a/internal/processing/tags/follow.go b/internal/processing/tags/follow.go
index f840f4bb7..879a1d9e8 100644
--- a/internal/processing/tags/follow.go
+++ b/internal/processing/tags/follow.go
@@ -21,11 +21,13 @@ 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/id"
+ 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/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// Follow follows the tag with the given name as the given account.
@@ -63,5 +65,6 @@ func (p *Processor) Follow(
)
}
- return p.apiTag(ctx, tag, true)
+ apiTag := typeutils.TagToAPITag(tag, true, util.Ptr(true))
+ return &apiTag, nil
}
diff --git a/internal/processing/tags/followed.go b/internal/processing/tags/followed.go
index b9c450653..958960623 100644
--- a/internal/processing/tags/followed.go
+++ b/internal/processing/tags/followed.go
@@ -21,12 +21,12 @@ 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/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/paging"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// Followed gets the user's list of followed tags.
@@ -53,14 +53,10 @@ func (p *Processor) Followed(
lo := tags[count-1].ID
hi := tags[0].ID
- items := make([]interface{}, 0, count)
following := util.Ptr(true)
+ items := make([]interface{}, 0, count)
for _, tag := range tags {
- apiTag, err := p.converter.TagToAPITag(ctx, tag, true, following)
- if err != nil {
- log.Errorf(ctx, "error converting tag %s to API representation: %v", tag.ID, err)
- continue
- }
+ apiTag := typeutils.TagToAPITag(tag, true, following)
items = append(items, apiTag)
}
diff --git a/internal/processing/tags/followedtags.go b/internal/processing/tags/followedtags.go
index c9093a6c6..c78e0cc23 100644
--- a/internal/processing/tags/followedtags.go
+++ b/internal/processing/tags/followedtags.go
@@ -18,13 +18,8 @@
package tags
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/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
@@ -38,16 +33,3 @@ func New(state *state.State, converter *typeutils.Converter) Processor {
converter: converter,
}
}
-
-// apiTag is a shortcut to return the API version of the given tag,
-// or return an appropriate error if conversion fails.
-func (p *Processor) apiTag(ctx context.Context, tag *gtsmodel.Tag, following bool) (*apimodel.Tag, gtserror.WithCode) {
- apiTag, err := p.converter.TagToAPITag(ctx, tag, true, &following)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(
- gtserror.Newf("error converting tag %s to API representation: %w", tag.Name, err),
- )
- }
-
- return &apiTag, nil
-}
diff --git a/internal/processing/tags/get.go b/internal/processing/tags/get.go
index c8fa66137..6c515ee1a 100644
--- a/internal/processing/tags/get.go
+++ b/internal/processing/tags/get.go
@@ -21,10 +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"
+ 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/typeutils"
)
// Get gets the tag with the given name, including whether it's followed by the given account.
@@ -53,5 +54,6 @@ func (p *Processor) Get(
)
}
- return p.apiTag(ctx, tag, following)
+ apiTag := typeutils.TagToAPITag(tag, true, &following)
+ return &apiTag, nil
}
diff --git a/internal/processing/tags/unfollow.go b/internal/processing/tags/unfollow.go
index fb844cd9f..3d15d68c2 100644
--- a/internal/processing/tags/unfollow.go
+++ b/internal/processing/tags/unfollow.go
@@ -21,10 +21,12 @@ 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"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// Unfollow unfollows the tag with the given name as the given account.
@@ -54,5 +56,6 @@ func (p *Processor) Unfollow(
)
}
- return p.apiTag(ctx, tag, false)
+ apiTag := typeutils.TagToAPITag(tag, true, util.Ptr(false))
+ return &apiTag, nil
}
diff --git a/internal/processing/timeline/common.go b/internal/processing/timeline/common.go
deleted file mode 100644
index 6d29d81d6..000000000
--- a/internal/processing/timeline/common.go
+++ /dev/null
@@ -1,71 +0,0 @@
-// 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 timeline
-
-import (
- "context"
-
- "github.com/superseriousbusiness/gotosocial/internal/timeline"
-)
-
-// SkipInsert returns a function that satisifes SkipInsertFunction.
-func SkipInsert() timeline.SkipInsertFunction {
- // Gap to allow between a status or boost of status,
- // and reinsertion of a new boost of that status.
- // This is useful to avoid a heavily boosted status
- // showing up way too often in a user's timeline.
- const boostReinsertionDepth = 50
-
- return func(
- ctx context.Context,
- newItemID string,
- newItemAccountID string,
- newItemBoostOfID string,
- newItemBoostOfAccountID string,
- nextItemID string,
- nextItemAccountID string,
- nextItemBoostOfID string,
- nextItemBoostOfAccountID string,
- depth int,
- ) (bool, error) {
- if newItemID == nextItemID {
- // Don't insert duplicates.
- return true, nil
- }
-
- if newItemBoostOfID != "" {
- if newItemBoostOfID == nextItemBoostOfID &&
- depth < boostReinsertionDepth {
- // Don't insert boosts of items
- // we've seen boosted recently.
- return true, nil
- }
-
- if newItemBoostOfID == nextItemID &&
- depth < boostReinsertionDepth {
- // Don't insert boosts of items when
- // we've seen the original recently.
- return true, nil
- }
- }
-
- // Proceed with insertion
- // (that's what she said!).
- return false, nil
- }
-}
diff --git a/internal/processing/timeline/faved.go b/internal/processing/timeline/faved.go
index bb7f03fff..9218af9c8 100644
--- a/internal/processing/timeline/faved.go
+++ b/internal/processing/timeline/faved.go
@@ -22,16 +22,16 @@ 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/log"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ 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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
-func (p *Processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
+// FavedTimelineGet ...
+func (p *Processor) FavedTimelineGet(ctx context.Context, authed *apiutil.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
statuses, nextMaxID, prevMinID, err := p.state.DB.GetFavedTimeline(ctx, authed.Account.ID, maxID, minID, limit)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("FavedTimelineGet: db error getting statuses: %w", err)
@@ -55,7 +55,7 @@ func (p *Processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, ma
continue
}
- apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, statusfilter.FilterContextNone, nil, nil)
+ apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account)
if err != nil {
log.Errorf(ctx, "error convering to api status: %v", err)
continue
diff --git a/internal/processing/timeline/home.go b/internal/processing/timeline/home.go
index 215000933..3089f52fc 100644
--- a/internal/processing/timeline/home.go
+++ b/internal/processing/timeline/home.go
@@ -19,132 +19,97 @@ package timeline
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/filter/usermute"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/timeline"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-// HomeTimelineGrab returns a function that satisfies GrabFunction for home timelines.
-func HomeTimelineGrab(state *state.State) timeline.GrabFunction {
- return func(ctx context.Context, accountID string, maxID string, sinceID string, minID string, limit int) ([]timeline.Timelineable, bool, error) {
- statuses, err := state.DB.GetHomeTimeline(ctx, accountID, maxID, sinceID, minID, limit, false)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = gtserror.Newf("error getting statuses from db: %w", err)
- return nil, false, err
- }
-
- count := len(statuses)
- if count == 0 {
- // We just don't have enough statuses
- // left in the db so return stop = true.
- return nil, true, nil
- }
-
- items := make([]timeline.Timelineable, count)
- for i, s := range statuses {
- items[i] = s
- }
-
- return items, false, nil
- }
-}
-
-// HomeTimelineFilter returns a function that satisfies FilterFunction for home timelines.
-func HomeTimelineFilter(state *state.State, visFilter *visibility.Filter) timeline.FilterFunction {
- return func(ctx context.Context, accountID string, item timeline.Timelineable) (shouldIndex bool, err error) {
- status, ok := item.(*gtsmodel.Status)
- if !ok {
- err = gtserror.New("could not convert item to *gtsmodel.Status")
- return false, err
- }
-
- requestingAccount, err := state.DB.GetAccountByID(ctx, accountID)
- if err != nil {
- err = gtserror.Newf("error getting account with id %s: %w", accountID, err)
- return false, err
- }
+ "net/url"
- timelineable, err := visFilter.StatusHomeTimelineable(ctx, requestingAccount, status)
- if err != nil {
- err = gtserror.Newf("error checking hometimelineability of status %s for account %s: %w", status.ID, accountID, err)
- return false, err
- }
-
- return timelineable, nil
- }
-}
-
-// HomeTimelineStatusPrepare returns a function that satisfies PrepareFunction for home timelines.
-func HomeTimelineStatusPrepare(state *state.State, converter *typeutils.Converter) timeline.PrepareFunction {
- return func(ctx context.Context, accountID string, itemID string) (timeline.Preparable, error) {
- status, err := state.DB.GetStatusByID(ctx, itemID)
- if err != nil {
- err = gtserror.Newf("error getting status with id %s: %w", itemID, err)
- return nil, err
- }
-
- requestingAccount, err := state.DB.GetAccountByID(ctx, accountID)
- if err != nil {
- err = gtserror.Newf("error getting account with id %s: %w", accountID, err)
- return nil, err
- }
-
- filters, err := 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, err
- }
+ 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"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
+)
- mutes, err := state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), requestingAccount.ID, nil)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", requestingAccount.ID, err)
- return nil, err
+// HomeTimelineGet gets a pageable timeline of statuses
+// in the home timeline of the requesting account.
+func (p *Processor) HomeTimelineGet(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ page *paging.Page,
+ local bool,
+) (
+ *apimodel.PageableResponse,
+ gtserror.WithCode,
+) {
+
+ var pageQuery url.Values
+ var postFilter func(*gtsmodel.Status) bool
+ if local {
+ // Set local = true query.
+ pageQuery = localOnlyTrue
+ postFilter = func(s *gtsmodel.Status) bool {
+ return !*s.Local
}
- compiledMutes := usermute.NewCompiledUserMuteList(mutes)
-
- return converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextHome, filters, compiledMutes)
+ } else {
+ // Set local = false query.
+ pageQuery = localOnlyFalse
+ postFilter = nil
}
-}
-
-func (p *Processor) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) {
- statuses, err := p.state.Timelines.Home.GetTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = gtserror.Newf("error getting statuses: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- count := len(statuses)
- if count == 0 {
- return util.EmptyPageableResponse(), nil
- }
-
- var (
- items = make([]interface{}, count)
- nextMaxIDValue = statuses[count-1].GetID()
- prevMinIDValue = statuses[0].GetID()
+ return p.getStatusTimeline(ctx,
+
+ // Auth'd
+ // account.
+ requester,
+
+ // Keyed-by-account-ID, home timeline cache.
+ p.state.Caches.Timelines.Home.MustGet(requester.ID),
+
+ // Current
+ // page.
+ page,
+
+ // Home timeline endpoint.
+ "/api/v1/timelines/home",
+
+ // Set local-only timeline
+ // page query flag, (this map
+ // later gets copied before
+ // any further usage).
+ pageQuery,
+
+ // Status filter context.
+ gtsmodel.FilterContextHome,
+
+ // Database load function.
+ func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
+ return p.state.DB.GetHomeTimeline(ctx, requester.ID, pg)
+ },
+
+ // Filtering function,
+ // i.e. filter before caching.
+ func(s *gtsmodel.Status) bool {
+
+ // Check the visibility of passed status to requesting user.
+ ok, err := p.visFilter.StatusHomeTimelineable(ctx, requester, s)
+ if err != nil {
+ log.Errorf(ctx, "error checking status %s visibility: %v", s.URI, err)
+ return true // default assume not visible
+ } else if !ok {
+ return true
+ }
+
+ // Check if status been muted by requester from timelines.
+ muted, err := p.muteFilter.StatusMuted(ctx, requester, s)
+ if err != nil {
+ log.Errorf(ctx, "error checking status %s mutes: %v", s.URI, err)
+ return true // default assume muted
+ } else if muted {
+ return true
+ }
+
+ return false
+ },
+
+ // Post filtering funtion,
+ // i.e. filter after caching.
+ postFilter,
)
-
- for i := range statuses {
- items[i] = statuses[i]
- }
-
- return util.PackagePageableResponse(util.PageableResponseParams{
- Items: items,
- Path: "/api/v1/timelines/home",
- NextMaxIDValue: nextMaxIDValue,
- PrevMinIDValue: prevMinIDValue,
- Limit: limit,
- })
}
diff --git a/internal/processing/timeline/home_test.go b/internal/processing/timeline/home_test.go
index c73c209a3..2d0c912f8 100644
--- a/internal/processing/timeline/home_test.go
+++ b/internal/processing/timeline/home_test.go
@@ -18,53 +18,28 @@
package timeline_test
import (
- "context"
"testing"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- tlprocessor "github.com/superseriousbusiness/gotosocial/internal/processing/timeline"
- "github.com/superseriousbusiness/gotosocial/internal/timeline"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/util"
)
type HomeTestSuite struct {
TimelineStandardTestSuite
}
-func (suite *HomeTestSuite) SetupTest() {
- suite.TimelineStandardTestSuite.SetupTest()
-
- suite.state.Timelines.Home = timeline.NewManager(
- tlprocessor.HomeTimelineGrab(&suite.state),
- tlprocessor.HomeTimelineFilter(&suite.state, visibility.NewFilter(&suite.state)),
- tlprocessor.HomeTimelineStatusPrepare(&suite.state, typeutils.NewConverter(&suite.state)),
- tlprocessor.SkipInsert(),
- )
- if err := suite.state.Timelines.Home.Start(); err != nil {
- suite.FailNow(err.Error())
- }
-}
-
func (suite *HomeTestSuite) TearDownTest() {
- if err := suite.state.Timelines.Home.Stop(); err != nil {
- suite.FailNow(err.Error())
- }
-
suite.TimelineStandardTestSuite.TearDownTest()
}
// A timeline containing a status hidden due to filtering should return other statuses with no error.
func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
requester = suite.testAccounts["local_account_1"]
- authed = &oauth.Auth{Account: requester}
maxID = ""
sinceID = ""
minID = "01F8MHAAY43M6RJ473VQFCVH36" // 1 before filteredStatus
@@ -73,35 +48,32 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() {
filteredStatus = suite.testStatuses["admin_account_status_2"]
filteredStatusFound = false
filterID = id.NewULID()
- filter = &gtsmodel.Filter{
+ filterStatusID = id.NewULID()
+ filterStatus = &gtsmodel.FilterStatus{
+ ID: filterStatusID,
+ FilterID: filterID,
+ StatusID: filteredStatus.ID,
+ }
+ filter = &gtsmodel.Filter{
ID: filterID,
AccountID: requester.ID,
Title: "timeline filtering test",
Action: gtsmodel.FilterActionHide,
- Statuses: []*gtsmodel.FilterStatus{
- {
- ID: id.NewULID(),
- AccountID: requester.ID,
- FilterID: filterID,
- StatusID: filteredStatus.ID,
- },
- },
- ContextHome: util.Ptr(true),
- ContextNotifications: util.Ptr(false),
- ContextPublic: util.Ptr(false),
- ContextThread: util.Ptr(false),
- ContextAccount: util.Ptr(false),
+ Statuses: []*gtsmodel.FilterStatus{filterStatus},
+ StatusIDs: []string{filterStatusID},
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome),
}
)
// Fetch the timeline to make sure the status we're going to filter is in that section of it.
resp, errWithCode := suite.timeline.HomeTimelineGet(
ctx,
- authed,
- maxID,
- sinceID,
- minID,
- limit,
+ requester,
+ &paging.Page{
+ Min: paging.EitherMinID(minID, sinceID),
+ Max: paging.MaxID(maxID),
+ Limit: limit,
+ },
local,
)
suite.NoError(errWithCode)
@@ -114,8 +86,12 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() {
if !filteredStatusFound {
suite.FailNow("precondition failed: status we would filter isn't present in unfiltered timeline")
}
- // Prune the timeline to drop cached prepared statuses, a side effect of this precondition check.
- if _, err := suite.state.Timelines.Home.Prune(ctx, requester.ID, 0, 0); err != nil {
+
+ // Clear the timeline to drop all cached statuses.
+ suite.state.Caches.Timelines.Home.Clear(requester.ID)
+
+ // Create the filter status associated with the main filter.
+ if err := suite.db.PutFilterStatus(ctx, filterStatus); err != nil {
suite.FailNow(err.Error())
}
@@ -127,11 +103,12 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() {
// Fetch the timeline again with the filter in place.
resp, errWithCode = suite.timeline.HomeTimelineGet(
ctx,
- authed,
- maxID,
- sinceID,
- minID,
- limit,
+ requester,
+ &paging.Page{
+ Min: paging.EitherMinID(minID, sinceID),
+ Max: paging.MaxID(maxID),
+ Limit: limit,
+ },
local,
)
diff --git a/internal/processing/timeline/list.go b/internal/processing/timeline/list.go
index a7f5e9d71..265cd5ca2 100644
--- a/internal/processing/timeline/list.go
+++ b/internal/processing/timeline/list.go
@@ -21,156 +21,106 @@ 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/filter/usermute"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/timeline"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "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/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
)
-// ListTimelineGrab returns a function that satisfies GrabFunction for list timelines.
-func ListTimelineGrab(state *state.State) timeline.GrabFunction {
- return func(ctx context.Context, listID string, maxID string, sinceID string, minID string, limit int) ([]timeline.Timelineable, bool, error) {
- statuses, err := state.DB.GetListTimeline(ctx, listID, maxID, sinceID, minID, limit)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = gtserror.Newf("error getting statuses from db: %w", err)
- return nil, false, err
- }
-
- count := len(statuses)
- if count == 0 {
- // We just don't have enough statuses
- // left in the db so return stop = true.
- return nil, true, nil
- }
-
- items := make([]timeline.Timelineable, count)
- for i, s := range statuses {
- items[i] = s
- }
-
- return items, false, nil
- }
-}
-
-// ListTimelineFilter returns a function that satisfies FilterFunction for list timelines.
-func ListTimelineFilter(state *state.State, visFilter *visibility.Filter) timeline.FilterFunction {
- return func(ctx context.Context, listID string, item timeline.Timelineable) (shouldIndex bool, err error) {
- status, ok := item.(*gtsmodel.Status)
- if !ok {
- err = gtserror.New("could not convert item to *gtsmodel.Status")
- return false, err
- }
-
- list, err := state.DB.GetListByID(ctx, listID)
- if err != nil {
- err = gtserror.Newf("error getting list with id %s: %w", listID, err)
- return false, err
- }
-
- requestingAccount, err := state.DB.GetAccountByID(ctx, list.AccountID)
- if err != nil {
- err = gtserror.Newf("error getting account with id %s: %w", list.AccountID, err)
- return false, err
- }
-
- timelineable, err := visFilter.StatusHomeTimelineable(ctx, requestingAccount, status)
- if err != nil {
- err = gtserror.Newf("error checking hometimelineability of status %s for account %s: %w", status.ID, list.AccountID, err)
- return false, err
- }
-
- return timelineable, nil
- }
-}
-
-// ListTimelineStatusPrepare returns a function that satisfies PrepareFunction for list timelines.
-func ListTimelineStatusPrepare(state *state.State, converter *typeutils.Converter) timeline.PrepareFunction {
- return func(ctx context.Context, listID string, itemID string) (timeline.Preparable, error) {
- status, err := state.DB.GetStatusByID(ctx, itemID)
- if err != nil {
- err = gtserror.Newf("error getting status with id %s: %w", itemID, err)
- return nil, err
- }
-
- list, err := state.DB.GetListByID(ctx, listID)
- if err != nil {
- err = gtserror.Newf("error getting list with id %s: %w", listID, err)
- return nil, err
- }
-
- requestingAccount, err := state.DB.GetAccountByID(ctx, list.AccountID)
- if err != nil {
- err = gtserror.Newf("error getting account with id %s: %w", list.AccountID, err)
- return nil, err
- }
-
- filters, err := 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, err
- }
-
- mutes, err := state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), requestingAccount.ID, nil)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", requestingAccount.ID, err)
- return nil, err
- }
- compiledMutes := usermute.NewCompiledUserMuteList(mutes)
-
- return converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextHome, filters, compiledMutes)
- }
-}
-
-func (p *Processor) ListTimelineGet(ctx context.Context, authed *oauth.Auth, listID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
- // Ensure list exists + is owned by this account.
- list, err := p.state.DB.GetListByID(ctx, listID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if list.AccountID != authed.Account.ID {
- err = gtserror.Newf("list with id %s does not belong to account %s", list.ID, authed.Account.ID)
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- statuses, err := p.state.Timelines.List.GetTimeline(ctx, listID, maxID, sinceID, minID, limit, false)
+// ListTimelineGet gets a pageable timeline of statuses
+// in the list timeline of ID by the requesting account.
+func (p *Processor) ListTimelineGet(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ listID string,
+ page *paging.Page,
+) (
+ *apimodel.PageableResponse,
+ gtserror.WithCode,
+) {
+ // Fetch the requested list with ID.
+ list, err := p.state.DB.GetListByID(
+ gtscontext.SetBarebones(ctx),
+ listID,
+ )
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = gtserror.Newf("error getting statuses: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- count := len(statuses)
- if count == 0 {
- return util.EmptyPageableResponse(), nil
+ // Check exists.
+ if list == nil {
+ const text = "list not found"
+ return nil, gtserror.NewErrorNotFound(
+ errors.New(text),
+ text,
+ )
}
- var (
- items = make([]interface{}, count)
- nextMaxIDValue = statuses[count-1].GetID()
- prevMinIDValue = statuses[0].GetID()
- )
-
- for i := range statuses {
- items[i] = statuses[i]
+ // Check list owned by auth'd account.
+ if list.AccountID != requester.ID {
+ err := gtserror.New("list does not belong to account")
+ return nil, gtserror.NewErrorNotFound(err)
}
- return util.PackagePageableResponse(util.PageableResponseParams{
- Items: items,
- Path: "/api/v1/timelines/list/" + listID,
- NextMaxIDValue: nextMaxIDValue,
- PrevMinIDValue: prevMinIDValue,
- Limit: limit,
- })
+ // Fetch status timeline for list.
+ return p.getStatusTimeline(ctx,
+
+ // Auth'd
+ // account.
+ requester,
+
+ // Keyed-by-list-ID, list timeline cache.
+ p.state.Caches.Timelines.List.MustGet(listID),
+
+ // Current
+ // page.
+ page,
+
+ // List timeline ID's endpoint.
+ "/api/v1/timelines/list/"+listID,
+
+ // No page
+ // query.
+ nil,
+
+ // Status filter context.
+ gtsmodel.FilterContextHome,
+
+ // Database load function.
+ func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
+ return p.state.DB.GetListTimeline(ctx, listID, pg)
+ },
+
+ // Filtering function,
+ // i.e. filter before caching.
+ func(s *gtsmodel.Status) bool {
+
+ // Check the visibility of passed status to requesting user.
+ ok, err := p.visFilter.StatusHomeTimelineable(ctx, requester, s)
+ if err != nil {
+ log.Errorf(ctx, "error checking status %s visibility: %v", s.URI, err)
+ return true // default assume not visible
+ } else if !ok {
+ return true
+ }
+
+ // Check if status been muted by requester from timelines.
+ muted, err := p.muteFilter.StatusMuted(ctx, requester, s)
+ if err != nil {
+ log.Errorf(ctx, "error checking status %s mutes: %v", s.URI, err)
+ return true // default assume muted
+ } else if muted {
+ return true
+ }
+
+ return false
+ },
+
+ // Post filtering funtion,
+ // i.e. filter after caching.
+ nil,
+ )
}
diff --git a/internal/processing/timeline/notification.go b/internal/processing/timeline/notification.go
index 09636e7eb..7b98970c2 100644
--- a/internal/processing/timeline/notification.go
+++ b/internal/processing/timeline/notification.go
@@ -21,31 +21,29 @@ import (
"context"
"errors"
"fmt"
+ "net/http"
"net/url"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/filter/status"
- "github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
- "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/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/paging"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ 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/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"
)
+// NotificationsGet ...
func (p *Processor) NotificationsGet(
ctx context.Context,
- authed *oauth.Auth,
+ requester *gtsmodel.Account,
page *paging.Page,
types []gtsmodel.NotificationType,
excludeTypes []gtsmodel.NotificationType,
) (*apimodel.PageableResponse, gtserror.WithCode) {
- notifs, err := p.state.DB.GetAccountNotifications(
- ctx,
- authed.Account.ID,
+ notifs, err := p.state.DB.GetAccountNotifications(ctx,
+ requester.ID,
page,
types,
excludeTypes,
@@ -60,19 +58,6 @@ func (p *Processor) NotificationsGet(
return util.EmptyPageableResponse(), nil
}
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, authed.Account.ID)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve filters for account %s: %w", authed.Account.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), authed.Account.ID, nil)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", authed.Account.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
- compiledMutes := usermute.NewCompiledUserMuteList(mutes)
-
var (
items = make([]interface{}, 0, count)
@@ -83,7 +68,7 @@ func (p *Processor) NotificationsGet(
)
for _, n := range notifs {
- visible, err := p.notifVisible(ctx, n, authed.Account)
+ visible, err := p.notifVisible(ctx, n, requester)
if err != nil {
log.Debugf(ctx, "skipping notification %s because of an error checking notification visibility: %v", n.ID, err)
continue
@@ -93,14 +78,66 @@ func (p *Processor) NotificationsGet(
continue
}
- item, err := p.converter.NotificationToAPINotification(ctx, n, filters, compiledMutes)
+ // Check whether notification origin account is muted.
+ muted, err := p.muteFilter.AccountNotificationsMuted(ctx,
+ requester,
+ n.OriginAccount,
+ )
if err != nil {
- if !errors.Is(err, status.ErrHideStatus) {
- log.Debugf(ctx, "skipping notification %s because it couldn't be converted to its api representation: %s", n.ID, err)
+ log.Errorf(ctx, "error checking account mute: %v", err)
+ continue
+ }
+
+ if muted {
+ continue
+ }
+
+ var filtered []apimodel.FilterResult
+
+ if n.Status != nil {
+ var hide bool
+
+ // Check whether notification status is muted by requester.
+ muted, err = p.muteFilter.StatusNotificationsMuted(ctx,
+ requester,
+ n.Status,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error checking status mute: %v", err)
+ continue
}
+
+ if muted {
+ continue
+ }
+
+ // Check whether notification status is filtered by requester in notifs.
+ filtered, hide, err = p.statusFilter.StatusFilterResultsInContext(ctx,
+ requester,
+ n.Status,
+ gtsmodel.FilterContextNotifications,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error checking status filtering: %v", err)
+ continue
+ }
+
+ if hide {
+ continue
+ }
+ }
+
+ item, err := p.converter.NotificationToAPINotification(ctx, n)
+ if err != nil {
continue
}
+ if item.Status != nil {
+ // Set filter results on status,
+ // in case any were set above.
+ item.Status.Filtered = filtered
+ }
+
items = append(items, item)
}
@@ -124,47 +161,38 @@ func (p *Processor) NotificationsGet(
func (p *Processor) NotificationGet(ctx context.Context, account *gtsmodel.Account, targetNotifID string) (*apimodel.Notification, gtserror.WithCode) {
notif, err := p.state.DB.GetNotificationByID(ctx, targetNotifID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- // Real error.
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting from db: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- if notifTargetAccountID := notif.TargetAccountID; notifTargetAccountID != account.ID {
- err = fmt.Errorf("account %s does not have permission to view notification belong to account %s", account.ID, notifTargetAccountID)
- return nil, gtserror.NewErrorNotFound(err)
+ if notif == nil {
+ const text = "notification not found"
+ return nil, gtserror.NewErrorNotFound(
+ errors.New(text),
+ text,
+ )
}
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, account.ID)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve filters for account %s: %w", account.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
+ if notif.TargetAccountID != account.ID {
+ err := gtserror.New("requester does not match notification target")
+ return nil, gtserror.NewErrorNotFound(err)
}
- mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), account.ID, nil)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", account.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
- compiledMutes := usermute.NewCompiledUserMuteList(mutes)
+ // NOTE: we specifically don't do any filtering
+ // or mute checking for a notification directly
+ // fetched by ID. only from timelines etc.
- apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif, filters, compiledMutes)
+ apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif)
if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- // Real error.
- return nil, gtserror.NewErrorInternalError(err)
+ err := gtserror.Newf("error converting to api model: %w", err)
+ return nil, gtserror.WrapWithCode(http.StatusInternalServerError, err)
}
return apiNotif, nil
}
-func (p *Processor) NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode {
+func (p *Processor) NotificationsClear(ctx context.Context, authed *apiutil.Auth) gtserror.WithCode {
// Delete all notifications of all types that target the authorized account.
if err := p.state.DB.DeleteNotifications(ctx, nil, authed.Account.ID, ""); err != nil && !errors.Is(err, db.ErrNoEntries) {
return gtserror.NewErrorInternalError(err)
diff --git a/internal/processing/timeline/public.go b/internal/processing/timeline/public.go
index dc00688e3..d724bfaa1 100644
--- a/internal/processing/timeline/public.go
+++ b/internal/processing/timeline/public.go
@@ -19,152 +19,166 @@ package timeline
import (
"context"
- "errors"
- "strconv"
-
- 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/filter/usermute"
- "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/util"
+
+ 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"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
)
+// PublicTimelineGet gets a pageable timeline of public statuses
+// for the given requesting account. It ensures that each status
+// in timeline is visible to the account before returning it.
+//
+// The local argument limits this to local-only statuses.
func (p *Processor) PublicTimelineGet(
ctx context.Context,
requester *gtsmodel.Account,
- maxID string,
- sinceID string,
- minID string,
- limit int,
+ page *paging.Page,
local bool,
-) (*apimodel.PageableResponse, gtserror.WithCode) {
- const maxAttempts = 3
- var (
- nextMaxIDValue string
- prevMinIDValue string
- items = make([]any, 0, limit)
- )
-
- var filters []*gtsmodel.Filter
- var compiledMutes *usermute.CompiledUserMuteList
- if requester != nil {
- var err error
- filters, err = p.state.DB.GetFiltersForAccountID(ctx, requester.ID)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requester.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), requester.ID, nil)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", requester.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
- compiledMutes = usermute.NewCompiledUserMuteList(mutes)
+) (
+ *apimodel.PageableResponse,
+ gtserror.WithCode,
+) {
+ if local {
+ return p.localTimelineGet(ctx, requester, page)
}
+ return p.publicTimelineGet(ctx, requester, page)
+}
+
+func (p *Processor) publicTimelineGet(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ page *paging.Page,
+) (
+ *apimodel.PageableResponse,
+ gtserror.WithCode,
+) {
+ return p.getStatusTimeline(ctx,
+
+ // Auth acconut,
+ // can be nil.
+ requester,
+
+ // No cache.
+ nil,
+
+ // Current
+ // page.
+ page,
+
+ // Public timeline endpoint.
+ "/api/v1/timelines/public",
+
+ // Set local-only timeline
+ // page query flag, (this map
+ // later gets copied before
+ // any further usage).
+ localOnlyFalse,
+
+ // Status filter context.
+ gtsmodel.FilterContextPublic,
+
+ // Database load function.
+ func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
+ return p.state.DB.GetPublicTimeline(ctx, pg)
+ },
- // Try a few times to select appropriate public
- // statuses from the db, paging up or down to
- // reattempt if nothing suitable is found.
-outer:
- for attempts := 1; ; attempts++ {
- // Select slightly more than the limit to try to avoid situations where
- // we filter out all the entries, and have to make another db call.
- // It's cheaper to select more in 1 query than it is to do multiple queries.
- statuses, err := p.state.DB.GetPublicTimeline(ctx, maxID, sinceID, minID, limit+5, local)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = gtserror.Newf("db error getting statuses: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- count := len(statuses)
- if count == 0 {
- // Nothing relevant (left) in the db.
- return util.EmptyPageableResponse(), nil
- }
-
- // Page up from first status in slice
- // (ie., one with the highest ID).
- prevMinIDValue = statuses[0].ID
-
- inner:
- for _, s := range statuses {
- // Push back the next page down ID to
- // this status, regardless of whether
- // we end up filtering it out or not.
- nextMaxIDValue = s.ID
-
- timelineable, err := p.visFilter.StatusPublicTimelineable(ctx, requester, s)
+ // Pre-filtering function,
+ // i.e. filter before caching.
+ func(s *gtsmodel.Status) bool {
+
+ // Check the visibility of passed status to requesting user.
+ ok, err := p.visFilter.StatusPublicTimelineable(ctx, requester, s)
if err != nil {
- log.Errorf(ctx, "error checking status visibility: %v", err)
- continue inner
+ log.Errorf(ctx, "error checking status %s visibility: %v", s.URI, err)
+ return true // default assume not visible
+ } else if !ok {
+ return true
}
- if !timelineable {
- continue inner
+ // Check if status been muted by requester from timelines.
+ muted, err := p.muteFilter.StatusMuted(ctx, requester, s)
+ if err != nil {
+ log.Errorf(ctx, "error checking status %s mutes: %v", s.URI, err)
+ return true // default assume muted
+ } else if muted {
+ return true
}
- apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requester, statusfilter.FilterContextPublic, filters, compiledMutes)
- if errors.Is(err, statusfilter.ErrHideStatus) {
- continue
- }
+ return false
+ },
+
+ // Post filtering funtion,
+ // i.e. filter after caching.
+ nil,
+ )
+}
+
+func (p *Processor) localTimelineGet(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ page *paging.Page,
+) (
+ *apimodel.PageableResponse,
+ gtserror.WithCode,
+) {
+ return p.getStatusTimeline(ctx,
+
+ // Auth acconut,
+ // can be nil.
+ requester,
+
+ // No cache.
+ nil,
+
+ // Current
+ // page.
+ page,
+
+ // Public timeline endpoint.
+ "/api/v1/timelines/public",
+
+ // Set local-only timeline
+ // page query flag, (this map
+ // later gets copied before
+ // any further usage).
+ localOnlyTrue,
+
+ // Status filter context.
+ gtsmodel.FilterContextPublic,
+
+ // Database load function.
+ func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
+ return p.state.DB.GetLocalTimeline(ctx, pg)
+ },
+
+ // Filtering function,
+ // i.e. filter before caching.
+ func(s *gtsmodel.Status) bool {
+
+ // Check the visibility of passed status to requesting user.
+ ok, err := p.visFilter.StatusPublicTimelineable(ctx, requester, s)
if err != nil {
- log.Errorf(ctx, "error converting to api status: %v", err)
- continue inner
+ log.Errorf(ctx, "error checking status %s visibility: %v", s.URI, err)
+ } else if !ok {
+ return true
}
- // Looks good, add this.
- items = append(items, apiStatus)
-
- // We called the db with a little
- // more than the desired limit.
- //
- // Ensure we don't return more
- // than the caller asked for.
- if len(items) == limit {
- break outer
+ // Check if status been muted by requester from timelines.
+ muted, err := p.muteFilter.StatusMuted(ctx, requester, s)
+ if err != nil {
+ log.Errorf(ctx, "error checking status %s mutes: %v", s.URI, err)
+ } else if muted {
+ return true
}
- }
-
- if len(items) != 0 {
- // We've got some items left after
- // filtering, happily break + return.
- break
- }
-
- if attempts >= maxAttempts {
- // We reached our attempts limit.
- // Be nice + warn about it.
- log.Warn(ctx, "reached max attempts to find items in public timeline")
- break
- }
-
- // We filtered out all items before we
- // found anything we could return, but
- // we still have attempts left to try
- // fetching again. Set paging params
- // and allow loop to continue.
- if minID != "" {
- // Paging up.
- minID = prevMinIDValue
- } else {
- // Paging down.
- maxID = nextMaxIDValue
- }
- }
- return util.PackagePageableResponse(util.PageableResponseParams{
- Items: items,
- Path: "/api/v1/timelines/public",
- NextMaxIDValue: nextMaxIDValue,
- PrevMinIDValue: prevMinIDValue,
- Limit: limit,
- ExtraQueryParams: []string{
- "local=" + strconv.FormatBool(local),
+ return false
},
- })
+
+ // Post filtering funtion,
+ // i.e. filter after caching.
+ nil,
+ )
}
diff --git a/internal/processing/timeline/public_test.go b/internal/processing/timeline/public_test.go
index ab8e33429..341df999e 100644
--- a/internal/processing/timeline/public_test.go
+++ b/internal/processing/timeline/public_test.go
@@ -18,14 +18,13 @@
package timeline_test
import (
- "context"
"testing"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/util"
)
type PublicTestSuite struct {
@@ -34,7 +33,7 @@ type PublicTestSuite struct {
func (suite *PublicTestSuite) TestPublicTimelineGet() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
requester = suite.testAccounts["local_account_1"]
maxID = ""
sinceID = ""
@@ -46,10 +45,11 @@ func (suite *PublicTestSuite) TestPublicTimelineGet() {
resp, errWithCode := suite.timeline.PublicTimelineGet(
ctx,
requester,
- maxID,
- sinceID,
- minID,
- limit,
+ &paging.Page{
+ Min: paging.EitherMinID(minID, sinceID),
+ Max: paging.MaxID(maxID),
+ Limit: limit,
+ },
local,
)
@@ -64,10 +64,10 @@ func (suite *PublicTestSuite) TestPublicTimelineGet() {
func (suite *PublicTestSuite) TestPublicTimelineGetNotEmpty() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
requester = suite.testAccounts["local_account_1"]
// Select 1 *just above* a status we know should
- // not be in the public timeline -- a public
+ // not be in the public timeline -- an unlisted
// reply to one of admin's statuses.
maxID = "01HE7XJ1CG84TBKH5V9XKBVGF6"
sinceID = ""
@@ -79,10 +79,11 @@ func (suite *PublicTestSuite) TestPublicTimelineGetNotEmpty() {
resp, errWithCode := suite.timeline.PublicTimelineGet(
ctx,
requester,
- maxID,
- sinceID,
- minID,
- limit,
+ &paging.Page{
+ Min: paging.EitherMinID(minID, sinceID),
+ Max: paging.MaxID(maxID),
+ Limit: limit,
+ },
local,
)
@@ -90,15 +91,15 @@ func (suite *PublicTestSuite) TestPublicTimelineGetNotEmpty() {
// some other statuses were filtered out.
suite.NoError(errWithCode)
suite.Len(resp.Items, 1)
- suite.Equal(`<http://localhost:8080/api/v1/timelines/public?limit=1&max_id=01F8MHCP5P2NWYQ416SBA0XSEV&local=false>; rel="next", <http://localhost:8080/api/v1/timelines/public?limit=1&min_id=01HE7XJ1CG84TBKH5V9XKBVGF5&local=false>; rel="prev"`, resp.LinkHeader)
- suite.Equal(`http://localhost:8080/api/v1/timelines/public?limit=1&max_id=01F8MHCP5P2NWYQ416SBA0XSEV&local=false`, resp.NextLink)
- suite.Equal(`http://localhost:8080/api/v1/timelines/public?limit=1&min_id=01HE7XJ1CG84TBKH5V9XKBVGF5&local=false`, resp.PrevLink)
+ suite.Equal("<http://localhost:8080/api/v1/timelines/public?limit=1&local=false&max_id=01F8MHCP5P2NWYQ416SBA0XSEV>; rel=\"next\", <http://localhost:8080/api/v1/timelines/public?limit=1&local=false&min_id=01F8MHCP5P2NWYQ416SBA0XSEV>; rel=\"prev\"", resp.LinkHeader)
+ suite.Equal("http://localhost:8080/api/v1/timelines/public?limit=1&local=false&max_id=01F8MHCP5P2NWYQ416SBA0XSEV", resp.NextLink)
+ suite.Equal("http://localhost:8080/api/v1/timelines/public?limit=1&local=false&min_id=01F8MHCP5P2NWYQ416SBA0XSEV", resp.PrevLink)
}
// A timeline containing a status hidden due to filtering should return other statuses with no error.
func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
requester = suite.testAccounts["local_account_1"]
maxID = ""
sinceID = ""
@@ -108,24 +109,20 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
filteredStatus = suite.testStatuses["admin_account_status_2"]
filteredStatusFound = false
filterID = id.NewULID()
- filter = &gtsmodel.Filter{
+ filterStatusID = id.NewULID()
+ filterStatus = &gtsmodel.FilterStatus{
+ ID: filterStatusID,
+ FilterID: filterID,
+ StatusID: filteredStatus.ID,
+ }
+ filter = &gtsmodel.Filter{
ID: filterID,
AccountID: requester.ID,
Title: "timeline filtering test",
Action: gtsmodel.FilterActionHide,
- Statuses: []*gtsmodel.FilterStatus{
- {
- ID: id.NewULID(),
- AccountID: requester.ID,
- FilterID: filterID,
- StatusID: filteredStatus.ID,
- },
- },
- ContextHome: util.Ptr(false),
- ContextNotifications: util.Ptr(false),
- ContextPublic: util.Ptr(true),
- ContextThread: util.Ptr(false),
- ContextAccount: util.Ptr(false),
+ Statuses: []*gtsmodel.FilterStatus{filterStatus},
+ StatusIDs: []string{filterStatusID},
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextPublic),
}
)
@@ -133,10 +130,11 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
resp, errWithCode := suite.timeline.PublicTimelineGet(
ctx,
requester,
- maxID,
- sinceID,
- minID,
- limit,
+ &paging.Page{
+ Min: paging.EitherMinID(minID, sinceID),
+ Max: paging.MaxID(maxID),
+ Limit: limit,
+ },
local,
)
suite.NoError(errWithCode)
@@ -149,8 +147,11 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
if !filteredStatusFound {
suite.FailNow("precondition failed: status we would filter isn't present in unfiltered timeline")
}
- // The public timeline has no prepared status cache and doesn't need to be pruned,
- // as in the home timeline version of this test.
+
+ // Create the filter status associated with the main filter.
+ if err := suite.db.PutFilterStatus(ctx, filterStatus); err != nil {
+ suite.FailNow(err.Error())
+ }
// Create a filter to hide one status on the timeline.
if err := suite.db.PutFilter(ctx, filter); err != nil {
@@ -161,10 +162,11 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
resp, errWithCode = suite.timeline.PublicTimelineGet(
ctx,
requester,
- maxID,
- sinceID,
- minID,
- limit,
+ &paging.Page{
+ Min: paging.EitherMinID(minID, sinceID),
+ Max: paging.MaxID(maxID),
+ Limit: limit,
+ },
local,
)
diff --git a/internal/processing/timeline/tag.go b/internal/processing/timeline/tag.go
index 811d0bb33..995f9f8cc 100644
--- a/internal/processing/timeline/tag.go
+++ b/internal/processing/timeline/tag.go
@@ -20,18 +20,15 @@ package timeline
import (
"context"
"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/filter/usermute"
- "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/text"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "net/http"
+
+ 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/text"
)
// TagTimelineGet gets a pageable timeline for the given
@@ -40,37 +37,90 @@ import (
// to requestingAcct before returning it.
func (p *Processor) TagTimelineGet(
ctx context.Context,
- requestingAcct *gtsmodel.Account,
+ requester *gtsmodel.Account,
tagName string,
maxID string,
sinceID string,
minID string,
limit int,
) (*apimodel.PageableResponse, gtserror.WithCode) {
+
+ // Fetch the requested tag with name.
tag, errWithCode := p.getTag(ctx, tagName)
if errWithCode != nil {
return nil, errWithCode
}
+ // Check for a useable returned tag for endpoint.
if tag == nil || !*tag.Useable || !*tag.Listable {
+
// Obey mastodon API by returning 404 for this.
- err := fmt.Errorf("tag was not found, or not useable/listable on this instance")
- return nil, gtserror.NewErrorNotFound(err, err.Error())
+ const text = "tag was not found, or not useable/listable on this instance"
+ return nil, gtserror.NewWithCode(http.StatusNotFound, text)
}
- statuses, err := p.state.DB.GetTagTimeline(ctx, tag.ID, maxID, sinceID, minID, limit)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = gtserror.Newf("db error getting statuses: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
+ // Fetch status timeline for tag.
+ return p.getStatusTimeline(ctx,
+
+ // Auth'd
+ // account.
+ requester,
+
+ // No
+ // cache.
+ nil,
- return p.packageTagResponse(
- ctx,
- requestingAcct,
- statuses,
- limit,
- // Use API URL for tag.
+ // Current
+ // page.
+ &paging.Page{
+ Min: paging.EitherMinID(minID, sinceID),
+ Max: paging.MaxID(maxID),
+ Limit: limit,
+ },
+
+ // Tag timeline name's endpoint.
"/api/v1/timelines/tag/"+tagName,
+
+ // No page
+ // query.
+ nil,
+
+ // Status filter context.
+ gtsmodel.FilterContextPublic,
+
+ // Database load function.
+ func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
+ return p.state.DB.GetTagTimeline(ctx, tag.ID, pg)
+ },
+
+ // Filtering function,
+ // i.e. filter before caching.
+ func(s *gtsmodel.Status) bool {
+
+ // Check the visibility of passed status to requesting user.
+ ok, err := p.visFilter.StatusPublicTimelineable(ctx, requester, s)
+ if err != nil {
+ log.Errorf(ctx, "error checking status %s visibility: %v", s.URI, err)
+ return true // default assume not visible
+ } else if !ok {
+ return true
+ }
+
+ // Check if status been muted by requester from timelines.
+ muted, err := p.muteFilter.StatusMuted(ctx, requester, s)
+ if err != nil {
+ log.Errorf(ctx, "error checking status %s mutes: %v", s.URI, err)
+ return true // default assume muted
+ } else if muted {
+ return true
+ }
+
+ return false
+ },
+
+ // Post filtering funtion,
+ // i.e. filter after caching.
+ nil,
)
}
@@ -92,69 +142,3 @@ func (p *Processor) getTag(ctx context.Context, tagName string) (*gtsmodel.Tag,
return tag, nil
}
-
-func (p *Processor) packageTagResponse(
- ctx context.Context,
- requestingAcct *gtsmodel.Account,
- statuses []*gtsmodel.Status,
- limit int,
- requestPath string,
-) (*apimodel.PageableResponse, gtserror.WithCode) {
- count := len(statuses)
- if count == 0 {
- return util.EmptyPageableResponse(), nil
- }
-
- var (
- items = make([]interface{}, 0, count)
-
- // Set next + prev values before filtering and API
- // converting, so caller can still page properly.
- nextMaxIDValue = statuses[count-1].ID
- prevMinIDValue = statuses[0].ID
- )
-
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAcct.ID)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAcct.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), requestingAcct.ID, nil)
- if err != nil {
- err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", requestingAcct.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
- compiledMutes := usermute.NewCompiledUserMuteList(mutes)
-
- for _, s := range statuses {
- timelineable, err := p.visFilter.StatusTagTimelineable(ctx, requestingAcct, s)
- if err != nil {
- log.Errorf(ctx, "error checking status visibility: %v", err)
- continue
- }
-
- if !timelineable {
- continue
- }
-
- apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requestingAcct, statusfilter.FilterContextPublic, filters, compiledMutes)
- if errors.Is(err, statusfilter.ErrHideStatus) {
- continue
- }
- if err != nil {
- log.Errorf(ctx, "error converting to api status: %v", err)
- continue
- }
-
- items = append(items, apiStatus)
- }
-
- return util.PackagePageableResponse(util.PageableResponseParams{
- Items: items,
- Path: requestPath,
- NextMaxIDValue: nextMaxIDValue,
- PrevMinIDValue: prevMinIDValue,
- Limit: limit,
- })
-}
diff --git a/internal/processing/timeline/timeline.go b/internal/processing/timeline/timeline.go
index 5966fe864..06580b3c7 100644
--- a/internal/processing/timeline/timeline.go
+++ b/internal/processing/timeline/timeline.go
@@ -18,21 +18,151 @@
package timeline
import (
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "context"
+ "net/http"
+ "net/url"
+
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ timelinepkg "code.superseriousbusiness.org/gotosocial/internal/cache/timeline"
+ "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/paging"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util/xslices"
+)
+
+var (
+ // pre-prepared URL values to be passed in to
+ // paging response forms. The paging package always
+ // copies values before any modifications so it's
+ // safe to only use a single map variable for these.
+ localOnlyTrue = url.Values{"local": {"true"}}
+ localOnlyFalse = url.Values{"local": {"false"}}
)
type Processor struct {
- state *state.State
- converter *typeutils.Converter
- visFilter *visibility.Filter
+ state *state.State
+ converter *typeutils.Converter
+ visFilter *visibility.Filter
+ muteFilter *mutes.Filter
+ statusFilter *status.Filter
}
-func New(state *state.State, converter *typeutils.Converter, visFilter *visibility.Filter) Processor {
+func New(
+ state *state.State,
+ converter *typeutils.Converter,
+ visFilter *visibility.Filter,
+ muteFilter *mutes.Filter,
+ statusFilter *status.Filter,
+) Processor {
return Processor{
- state: state,
- converter: converter,
- visFilter: visFilter,
+ state: state,
+ converter: converter,
+ visFilter: visFilter,
+ muteFilter: muteFilter,
+ statusFilter: statusFilter,
+ }
+}
+
+func (p *Processor) getStatusTimeline(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ timeline *timelinepkg.StatusTimeline,
+ page *paging.Page,
+ pagePath string,
+ pageQuery url.Values,
+ filterCtx gtsmodel.FilterContext,
+ loadPage func(*paging.Page) (statuses []*gtsmodel.Status, err error),
+ filter func(*gtsmodel.Status) (delete bool),
+ postFilter func(*gtsmodel.Status) (remove bool),
+) (
+ *apimodel.PageableResponse,
+ gtserror.WithCode,
+) {
+ var err error
+
+ // Ensure we have valid
+ // input paging cursor.
+ id.ValidatePage(page)
+
+ // Load status page via timeline cache, also
+ // getting lo, hi values for next, prev pages.
+ //
+ // NOTE: this safely handles the case of a nil
+ // input timeline, i.e. uncached timeline type.
+ apiStatuses, lo, hi, err := timeline.Load(ctx,
+
+ // Status page
+ // to load.
+ page,
+
+ // Caller provided database
+ // status page loading function.
+ loadPage,
+
+ // Status load function for cached timeline entries.
+ func(ids []string) ([]*gtsmodel.Status, error) {
+ return p.state.DB.GetStatusesByIDs(ctx, ids)
+ },
+
+ // Call provided status
+ // filtering function.
+ filter,
+
+ // Frontend API model preparation function.
+ func(status *gtsmodel.Status) (*apimodel.Status, error) {
+
+ // Check if status needs filtering OUTSIDE of caching stage.
+ // TODO: this will be moved to separate postFilter hook when
+ // all filtering has been removed from the type converter.
+ if postFilter != nil && postFilter(status) {
+ return nil, nil
+ }
+
+ // Check whether this status is filtered by requester in this context.
+ filters, hide, err := p.statusFilter.StatusFilterResultsInContext(ctx,
+ requester,
+ status,
+ filterCtx,
+ )
+ if err != nil {
+ return nil, err
+ } else if hide {
+ return nil, nil
+ }
+
+ // Finally, pass status to get converted to API model.
+ apiStatus, err := p.converter.StatusToAPIStatus(ctx,
+ status,
+ requester,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ // Set any filters on status.
+ apiStatus.Filtered = filters
+
+ return apiStatus, nil
+ },
+ )
+
+ if err != nil {
+ err := gtserror.Newf("error loading timeline: %w", err)
+ return nil, gtserror.WrapWithCode(http.StatusInternalServerError, err)
}
+
+ // Package returned API statuses as pageable response.
+ return paging.PackageResponse(paging.ResponseParams{
+ Items: xslices.ToAny(apiStatuses),
+ Path: pagePath,
+ Next: page.Next(lo, hi),
+ Prev: page.Prev(lo, hi),
+ Query: pageQuery,
+ }), nil
}
diff --git a/internal/processing/timeline/timeline_test.go b/internal/processing/timeline/timeline_test.go
index 8ff6be5d1..ce7817df6 100644
--- a/internal/processing/timeline/timeline_test.go
+++ b/internal/processing/timeline/timeline_test.go
@@ -18,15 +18,17 @@
package timeline_test
import (
+ "code.superseriousbusiness.org/gotosocial/internal/admin"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "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/processing/timeline"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "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/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/processing/timeline"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type TimelineStandardTestSuite struct {
@@ -62,6 +64,8 @@ func (suite *TimelineStandardTestSuite) SetupTest() {
&suite.state,
typeutils.NewConverter(&suite.state),
visibility.NewFilter(&suite.state),
+ mutes.NewFilter(&suite.state),
+ status.NewFilter(&suite.state),
)
testrig.StandardDBSetup(suite.db, suite.testAccounts)
diff --git a/internal/processing/user/create.go b/internal/processing/user/create.go
index f878d8320..0b2451b4c 100644
--- a/internal/processing/user/create.go
+++ b/internal/processing/user/create.go
@@ -22,14 +22,14 @@ import (
"fmt"
"time"
- "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/messages"
- "github.com/superseriousbusiness/gotosocial/internal/text"
- "github.com/superseriousbusiness/oauth2/v4"
+ "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/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/text"
+ "code.superseriousbusiness.org/oauth2/v4"
)
// Create processes the given form for creating a new user+account.
@@ -122,7 +122,7 @@ func (p *Processor) Create(
Username: form.Username,
Email: form.Email,
Password: form.Password,
- Reason: text.SanitizeToPlaintext(reason),
+ Reason: text.StripHTMLFromText(reason),
SignUpIP: form.IP,
Locale: form.Locale,
AppID: app.ID,
diff --git a/internal/processing/user/create_test.go b/internal/processing/user/create_test.go
index 9781a4214..9babbdfd5 100644
--- a/internal/processing/user/create_test.go
+++ b/internal/processing/user/create_test.go
@@ -18,12 +18,11 @@
package user_test
import (
- "context"
"net"
"testing"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
)
type CreateTestSuite struct {
@@ -32,7 +31,7 @@ type CreateTestSuite struct {
func (suite *CreateTestSuite) TestCreateOK() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
app = suite.testApps["application_1"]
appToken = suite.testTokens["local_account_1_client_application_token"]
form = &apimodel.AccountCreateRequest{
diff --git a/internal/processing/user/delete.go b/internal/processing/user/delete.go
index 9783010ef..8ab70b278 100644
--- a/internal/processing/user/delete.go
+++ b/internal/processing/user/delete.go
@@ -20,10 +20,10 @@ package user
import (
"context"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
)
// DeleteSelf is like Account.Delete, but specifically
diff --git a/internal/processing/user/email.go b/internal/processing/user/email.go
index ea9dbb64c..7339b282d 100644
--- a/internal/processing/user/email.go
+++ b/internal/processing/user/email.go
@@ -23,13 +23,14 @@ import (
"fmt"
"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/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/validate"
+ "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/validate"
+ "codeberg.org/gruf/go-byteutil"
"golang.org/x/crypto/bcrypt"
)
@@ -41,7 +42,10 @@ func (p *Processor) EmailChange(
newEmail string,
) (*apimodel.User, gtserror.WithCode) {
// Ensure provided password is correct.
- if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil {
+ if err := bcrypt.CompareHashAndPassword(
+ byteutil.S2B(user.EncryptedPassword),
+ byteutil.S2B(password),
+ ); err != nil {
err := gtserror.Newf("%w", err)
return nil, gtserror.NewErrorUnauthorized(err, "password was incorrect")
}
diff --git a/internal/processing/user/email_test.go b/internal/processing/user/email_test.go
index 23d448a84..e68fe21d5 100644
--- a/internal/processing/user/email_test.go
+++ b/internal/processing/user/email_test.go
@@ -18,7 +18,6 @@
package user_test
import (
- "context"
"testing"
"time"
@@ -30,7 +29,7 @@ type EmailConfirmTestSuite struct {
}
func (suite *EmailConfirmTestSuite) TestConfirmEmail() {
- ctx := context.Background()
+ ctx := suite.T().Context()
user := suite.testUsers["local_account_1"]
@@ -58,7 +57,7 @@ func (suite *EmailConfirmTestSuite) TestConfirmEmail() {
}
func (suite *EmailConfirmTestSuite) TestConfirmEmailOldToken() {
- ctx := context.Background()
+ ctx := suite.T().Context()
user := suite.testUsers["local_account_1"]
diff --git a/internal/processing/user/get.go b/internal/processing/user/get.go
index 9b19189a8..cbc8c6631 100644
--- a/internal/processing/user/get.go
+++ b/internal/processing/user/get.go
@@ -20,9 +20,9 @@ package user
import (
"context"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "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/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// Get returns the API model of the given user.
diff --git a/internal/processing/user/password.go b/internal/processing/user/password.go
index 68bc8ddb5..bcde46b3f 100644
--- a/internal/processing/user/password.go
+++ b/internal/processing/user/password.go
@@ -20,16 +20,20 @@ package user
import (
"context"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/validate"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/validate"
+ "codeberg.org/gruf/go-byteutil"
"golang.org/x/crypto/bcrypt"
)
// PasswordChange processes a password change request for the given user.
func (p *Processor) PasswordChange(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode {
// Ensure provided oldPassword is the correct current password.
- if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(oldPassword)); err != nil {
+ if err := bcrypt.CompareHashAndPassword(
+ byteutil.S2B(user.EncryptedPassword),
+ byteutil.S2B(oldPassword),
+ ); err != nil {
err := gtserror.Newf("%w", err)
return gtserror.NewErrorUnauthorized(err, "old password was incorrect")
}
@@ -48,7 +52,7 @@ func (p *Processor) PasswordChange(ctx context.Context, user *gtsmodel.User, old
// Hash the new password.
encryptedPassword, err := bcrypt.GenerateFromPassword(
- []byte(newPassword),
+ byteutil.S2B(newPassword),
bcrypt.DefaultCost,
)
if err != nil {
diff --git a/internal/processing/user/password_test.go b/internal/processing/user/password_test.go
index ee30558c6..dfe249058 100644
--- a/internal/processing/user/password_test.go
+++ b/internal/processing/user/password_test.go
@@ -18,12 +18,12 @@
package user_test
import (
- "context"
"net/http"
"testing"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "codeberg.org/gruf/go-byteutil"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"golang.org/x/crypto/bcrypt"
)
@@ -34,55 +34,67 @@ type ChangePasswordTestSuite struct {
func (suite *ChangePasswordTestSuite) TestChangePasswordOK() {
user := suite.testUsers["local_account_1"]
- errWithCode := suite.user.PasswordChange(context.Background(), user, "password", "verygoodnewpassword")
+ errWithCode := suite.user.PasswordChange(suite.T().Context(), user, "password", "verygoodnewpassword")
suite.NoError(errWithCode)
- err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte("verygoodnewpassword"))
+ err := bcrypt.CompareHashAndPassword(
+ byteutil.S2B(user.EncryptedPassword),
+ byteutil.S2B("verygoodnewpassword"),
+ )
suite.NoError(err)
// get user from the db again
dbUser := &gtsmodel.User{}
- err = suite.db.GetByID(context.Background(), user.ID, dbUser)
+ err = suite.db.GetByID(suite.T().Context(), user.ID, dbUser)
suite.NoError(err)
// check the password has changed
- err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("verygoodnewpassword"))
+ err = bcrypt.CompareHashAndPassword(
+ byteutil.S2B(dbUser.EncryptedPassword),
+ byteutil.S2B("verygoodnewpassword"),
+ )
suite.NoError(err)
}
func (suite *ChangePasswordTestSuite) TestChangePasswordIncorrectOld() {
user := suite.testUsers["local_account_1"]
- errWithCode := suite.user.PasswordChange(context.Background(), user, "ooooopsydoooopsy", "verygoodnewpassword")
+ errWithCode := suite.user.PasswordChange(suite.T().Context(), user, "ooooopsydoooopsy", "verygoodnewpassword")
suite.EqualError(errWithCode, "PasswordChange: crypto/bcrypt: hashedPassword is not the hash of the given password")
suite.Equal(http.StatusUnauthorized, errWithCode.Code())
suite.Equal("Unauthorized: old password was incorrect", errWithCode.Safe())
// get user from the db again
dbUser := &gtsmodel.User{}
- err := suite.db.GetByID(context.Background(), user.ID, dbUser)
+ err := suite.db.GetByID(suite.T().Context(), user.ID, dbUser)
suite.NoError(err)
// check the password has not changed
- err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password"))
+ err = bcrypt.CompareHashAndPassword(
+ byteutil.S2B(dbUser.EncryptedPassword),
+ byteutil.S2B("password"),
+ )
suite.NoError(err)
}
func (suite *ChangePasswordTestSuite) TestChangePasswordWeakNew() {
user := suite.testUsers["local_account_1"]
- errWithCode := suite.user.PasswordChange(context.Background(), user, "password", "1234")
+ errWithCode := suite.user.PasswordChange(suite.T().Context(), user, "password", "1234")
suite.EqualError(errWithCode, "password is only 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password")
suite.Equal(http.StatusBadRequest, errWithCode.Code())
suite.Equal("Bad Request: password is only 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password", errWithCode.Safe())
// get user from the db again
dbUser := &gtsmodel.User{}
- err := suite.db.GetByID(context.Background(), user.ID, dbUser)
+ err := suite.db.GetByID(suite.T().Context(), user.ID, dbUser)
suite.NoError(err)
// check the password has not changed
- err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password"))
+ err = bcrypt.CompareHashAndPassword(
+ byteutil.S2B(dbUser.EncryptedPassword),
+ byteutil.S2B("password"),
+ )
suite.NoError(err)
}
diff --git a/internal/processing/user/twofactor.go b/internal/processing/user/twofactor.go
new file mode 100644
index 000000000..cbd986b23
--- /dev/null
+++ b/internal/processing/user/twofactor.go
@@ -0,0 +1,285 @@
+// 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 user
+
+import (
+ "bytes"
+ "context"
+ "crypto/rand"
+ "encoding/base32"
+ "errors"
+ "image/png"
+ "io"
+ "net/url"
+ "sort"
+ "strings"
+ "time"
+
+ 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/state"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "codeberg.org/gruf/go-byteutil"
+ "github.com/pquerna/otp"
+ "github.com/pquerna/otp/totp"
+ "golang.org/x/crypto/bcrypt"
+)
+
+var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding)
+
+func (p *Processor) TwoFactorQRCodePngGet(
+ ctx context.Context,
+ user *gtsmodel.User,
+) (*apimodel.Content, gtserror.WithCode) {
+ // Get the 2FA url for this user.
+ totpURI, errWithCode := p.TwoFactorQRCodeURIGet(ctx, user)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ key, err := otp.NewKeyFromURL(totpURI.String())
+ if err != nil {
+ err := gtserror.Newf("error creating totp key from url: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Spawn a QR code image from the key.
+ qr, err := key.Image(256, 256)
+ if err != nil {
+ err := gtserror.Newf("error creating qr image from key: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Blat the key into a buffer.
+ buf := new(bytes.Buffer)
+ if err := png.Encode(buf, qr); err != nil {
+ err := gtserror.Newf("error encoding qr image to png: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Return it as our nice content model.
+ return &apimodel.Content{
+ ContentType: "image/png",
+ ContentLength: int64(buf.Len()),
+ Content: io.NopCloser(buf),
+ }, nil
+}
+
+// TwoFactorQRCodeURIGet will generate a new
+// 2 factor auth secret for user, and return a
+// URI of expected format for generating a QR code
+// or inputting into a password manager.
+//
+// This may be called multiple times without error
+// UNTIL the moment the user has finalized enabling
+// 2FA. i.e. when user.TwoFactorEnabled() == true.
+// Until this point, the URI may be requested for
+// both QR code generation, and requesting the URI,
+// but once 2FA is confirmed enabled it is not safe
+// to re-share the agreed-upon secret.
+func (p *Processor) TwoFactorQRCodeURIGet(
+ ctx context.Context,
+ user *gtsmodel.User,
+) (*url.URL, gtserror.WithCode) {
+ if user.TwoFactorEnabled() {
+ const errText = "2fa already enabled; not sharing secret again"
+ return nil, gtserror.NewErrorConflict(errors.New(errText), errText)
+ }
+
+ // Only generate new 2FA secret
+ // if not already been generated
+ // during this enabling process.
+ if user.TwoFactorSecret == "" {
+
+ // 32 bytes should be plenty entropy.
+ secret := make([]byte, 32)
+ if _, err := io.ReadFull(rand.Reader, secret); err != nil {
+ err := gtserror.Newf("error generating new secret: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Set + store the secret.
+ user.TwoFactorSecret = b32NoPadding.EncodeToString(secret)
+ if err := p.state.DB.UpdateUser(ctx, user, "two_factor_secret"); err != nil {
+ err := gtserror.Newf("db error updating user: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ // see: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
+ issuer := config.GetHost() + " - GoToSocial"
+ return &url.URL{
+ Scheme: "otpauth",
+ Host: "totp",
+ Path: "/" + issuer + ":" + user.Email,
+ RawQuery: encodeQuery(url.Values{
+ "secret": {user.TwoFactorSecret},
+ "issuer": {issuer},
+ "period": {"30"}, // 30 seconds totp validity.
+ "digits": {"6"}, // 6-digit totp.
+ "algorithm": {"SHA1"},
+ }),
+ }, nil
+}
+
+// TwoFactorEnable will enable 2 factor auth for
+// account, using given TOTP code to validate the
+// user's 2fa secret before continuing.
+func (p *Processor) TwoFactorEnable(
+ ctx context.Context,
+ user *gtsmodel.User,
+ code string,
+) ([]string, gtserror.WithCode) {
+ if user.TwoFactorEnabled() {
+ const errText = "2fa already enabled; disable it first then try again"
+ return nil, gtserror.NewErrorConflict(errors.New(errText), errText)
+ }
+
+ if user.TwoFactorSecret == "" {
+ const errText = "no 2fa secret stored; first read qr code / totp secret"
+ return nil, gtserror.NewErrorForbidden(errors.New(errText), errText)
+ }
+
+ // Try validating the provided code and give
+ // a helpful error message if it doesn't work.
+ if !totp.Validate(code, user.TwoFactorSecret) {
+ const errText = "invalid code provided, you may have been too late, try again; " +
+ "if it keeps not working, pester your admin to check that the server clock is correct"
+ return nil, gtserror.NewErrorForbidden(errors.New(errText), errText)
+ }
+
+ // Valid code was provided so we
+ // should turn 2fa on for this user.
+ user.TwoFactorEnabledAt = time.Now()
+
+ // Create recovery codes in cleartext
+ // to show to the user ONCE ONLY.
+ backupsClearText := make([]string, 8)
+ for i := 0; i < 8; i++ {
+ backupsClearText[i] = util.MustGenerateSecret()
+ }
+
+ // Store only the bcrypt-encrypted
+ // versions of the recovery codes.
+ user.TwoFactorBackups = make([]string, 8)
+ for i, backup := range backupsClearText {
+ encryptedBackup, err := bcrypt.GenerateFromPassword(
+ byteutil.S2B(backup),
+ bcrypt.DefaultCost,
+ )
+ if err != nil {
+ err := gtserror.Newf("error encrypting backup codes: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ user.TwoFactorBackups[i] = string(encryptedBackup)
+ }
+
+ // Update user in the database.
+ if err := p.state.DB.UpdateUser(ctx,
+ user,
+ "two_factor_enabled_at",
+ "two_factor_backups",
+ ); err != nil {
+ err := gtserror.Newf("db error updating user: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return backupsClearText, nil
+}
+
+// TwoFactorDisable: see TwoFactorDisable().
+func (p *Processor) TwoFactorDisable(
+ ctx context.Context,
+ user *gtsmodel.User,
+ password string,
+) gtserror.WithCode {
+ // Ensure provided password is correct.
+ if err := bcrypt.CompareHashAndPassword(
+ byteutil.S2B(user.EncryptedPassword),
+ byteutil.S2B(password),
+ ); err != nil {
+ const errText = "incorrect password"
+ return gtserror.NewErrorUnauthorized(errors.New(errText), errText)
+ }
+
+ // Disable 2 factor auth for this account.
+ return TwoFactorDisable(ctx, p.state, user)
+}
+
+// TwoFactorDisable disables 2 factor auth
+// for given user account. Note this should
+// be gated with password authentication if
+// accessed via web.
+func TwoFactorDisable(
+ ctx context.Context,
+ state *state.State,
+ user *gtsmodel.User,
+) gtserror.WithCode {
+ if !user.TwoFactorEnabled() {
+ const errText = "2fa already disabled"
+ return gtserror.NewErrorConflict(errors.New(errText), errText)
+ }
+
+ // Clear 2FA fields on user account.
+ user.TwoFactorEnabledAt = time.Time{}
+ user.TwoFactorSecret = ""
+ user.TwoFactorBackups = nil
+ if err := state.DB.UpdateUser(ctx,
+ user,
+ "two_factor_enabled_at",
+ "two_factor_secret",
+ "two_factor_backups",
+ ); err != nil {
+ err := gtserror.Newf("db error updating user: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ return nil
+}
+
+// encodeQuery is a copy-paste of url.Values.Encode, except it uses
+// %20 instead of + to encode spaces. This is necessary to correctly
+// render spaces in some authenticator apps, like Google Authenticator.
+//
+// [Note: this func and the above comment are both taken
+// directly from github.com/pquerna/otp/internal/encode.go.]
+func encodeQuery(v url.Values) string {
+ var buf strings.Builder
+ keys := make([]string, 0, len(v))
+ for k := range v {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ vs := v[k]
+ // Changed from url.QueryEscape.
+ keyEscaped := url.PathEscape(k)
+ for _, v := range vs {
+ if buf.Len() > 0 {
+ buf.WriteByte('&')
+ }
+ buf.WriteString(keyEscaped)
+ buf.WriteByte('=')
+ // Changed from url.QueryEscape.
+ buf.WriteString(url.PathEscape(v))
+ }
+ }
+ return buf.String()
+}
diff --git a/internal/processing/user/user.go b/internal/processing/user/user.go
index 5efb89061..412935fba 100644
--- a/internal/processing/user/user.go
+++ b/internal/processing/user/user.go
@@ -18,10 +18,10 @@
package user
import (
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/email"
+ "code.superseriousbusiness.org/gotosocial/internal/oauth"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
type Processor struct {
diff --git a/internal/processing/user/user_test.go b/internal/processing/user/user_test.go
index 72fd22117..9db26a725 100644
--- a/internal/processing/user/user_test.go
+++ b/internal/processing/user/user_test.go
@@ -18,16 +18,16 @@
package user_test
import (
+ "code.superseriousbusiness.org/gotosocial/internal/admin"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/email"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/oauth"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/user"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "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/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/processing/user"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type UserStandardTestSuite struct {
@@ -54,7 +54,7 @@ func (suite *UserStandardTestSuite) SetupTest() {
suite.db = testrig.NewTestDB(&suite.state)
suite.state.DB = suite.db
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
- suite.oauthServer = testrig.NewTestOauthServer(suite.state.DB)
+ suite.oauthServer = testrig.NewTestOauthServer(&suite.state)
suite.sentEmails = make(map[string]string)
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
@@ -62,7 +62,7 @@ func (suite *UserStandardTestSuite) SetupTest() {
suite.testTokens = testrig.NewTestTokens()
suite.testUsers = testrig.NewTestUsers()
- suite.user = user.New(&suite.state, typeutils.NewConverter(&suite.state), testrig.NewTestOauthServer(suite.db), suite.emailSender)
+ suite.user = user.New(&suite.state, typeutils.NewConverter(&suite.state), testrig.NewTestOauthServer(&suite.state), suite.emailSender)
testrig.StandardDBSetup(suite.db, nil)
}
diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go
index d6dec6691..7459f0114 100644
--- a/internal/processing/workers/federate.go
+++ b/internal/processing/workers/federate.go
@@ -21,15 +21,15 @@ import (
"context"
"net/url"
- "github.com/superseriousbusiness/activity/streams"
- "github.com/superseriousbusiness/activity/streams/vocab"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "code.superseriousbusiness.org/activity/streams"
+ "code.superseriousbusiness.org/activity/streams/vocab"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/federation"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// federate wraps functions for federating
@@ -115,7 +115,7 @@ func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account)
// Address the delete CC public.
deleteCC := streams.NewActivityStreamsCcProperty()
- deleteCC.AppendIRI(ap.PublicURI())
+ deleteCC.AppendIRI(ap.PublicIRI())
delete.SetActivityStreamsCc(deleteCC)
// Send the Delete via the Actor's outbox.
@@ -491,12 +491,7 @@ func (f *federate) UndoAnnounce(ctx context.Context, boost *gtsmodel.Status) err
}
// Recreate the ActivityStreams Announce.
- asAnnounce, err := f.converter.BoostToAS(
- ctx,
- boost,
- boost.Account,
- boost.BoostOfAccount,
- )
+ asAnnounce, err := f.converter.BoostToAS(ctx, boost)
if err != nil {
return gtserror.Newf("error converting boost to AS: %w", err)
}
@@ -767,12 +762,7 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
}
// Create the ActivityStreams Announce.
- announce, err := f.converter.BoostToAS(
- ctx,
- boost,
- boost.Account,
- boost.BoostOfAccount,
- )
+ announce, err := f.converter.BoostToAS(ctx, boost)
if err != nil {
return gtserror.Newf("error converting boost to AS: %w", err)
}
@@ -1104,7 +1094,7 @@ func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) e
ap.AppendTo(move, followersIRI)
// Address the move CC public.
- ap.AppendCc(move, ap.PublicURI())
+ ap.AppendCc(move, ap.PublicIRI())
// Send the Move via the Actor's outbox.
if _, err := f.FederatingActor().Send(
diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go
index 28a2b37b9..992f6d9e8 100644
--- a/internal/processing/workers/fromclientapi.go
+++ b/internal/processing/workers/fromclientapi.go
@@ -22,21 +22,21 @@ import (
"errors"
"time"
- "codeberg.org/gruf/go-kv"
- "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/id"
- "github.com/superseriousbusiness/gotosocial/internal/log"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "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/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "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/id"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "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/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "codeberg.org/gruf/go-kv/v2"
)
// clientAPI wraps processing functions
@@ -287,7 +287,7 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
// and/or notify the account that's being
// interacted with (if it's local): they can
// approve or deny the interaction later.
- if err := p.utils.requestReply(ctx, status); err != nil {
+ if err := p.utils.impoliteReplyRequest(ctx, status); err != nil {
return gtserror.Newf("error pending reply: %w", err)
}
@@ -310,19 +310,22 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
// URI attached.
// Store an already-accepted interaction request.
- id := id.NewULID()
+ requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
- ID: id,
- StatusID: status.InReplyToID,
- TargetAccountID: status.InReplyToAccountID,
- TargetAccount: status.InReplyToAccount,
- InteractingAccountID: status.AccountID,
- InteractingAccount: status.Account,
- InteractionURI: status.URI,
- InteractionType: gtsmodel.InteractionLike,
- Reply: status,
- URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
- AcceptedAt: time.Now(),
+ ID: requestID,
+ TargetStatusID: status.InReplyToID,
+ TargetAccountID: status.InReplyToAccountID,
+ TargetAccount: status.InReplyToAccount,
+ InteractingAccountID: status.AccountID,
+ InteractingAccount: status.Account,
+ InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix),
+ InteractionURI: status.URI,
+ InteractionType: gtsmodel.InteractionReply,
+ Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests.
+ Reply: status,
+ ResponseURI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, requestID),
+ AuthorizationURI: uris.GenerateURIForAuthorization(status.InReplyToAccount.Username, requestID),
+ AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@@ -331,7 +334,7 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
// Mark the status as now approved.
status.PendingApproval = util.Ptr(false)
status.PreApproved = false
- status.ApprovedByURI = approval.URI
+ status.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
status,
@@ -348,11 +351,6 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
// Don't return, just continue as normal.
}
- // Update stats for the actor account.
- if err := p.utils.incrementStatusesCount(ctx, cMsg.Origin, status); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
// We specifically do not timeline
// or notify for backfilled statuses,
// as these are more for archival than
@@ -371,7 +369,7 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+ p.surface.invalidateStatusFromTimelines(status.InReplyToID)
}
return nil
@@ -413,7 +411,7 @@ func (p *clientAPI) CreatePollVote(ctx context.Context, cMsg *messages.FromClien
}
// Interaction counts changed on the source status, uncache from timelines.
- p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
+ p.surface.invalidateStatusFromTimelines(vote.Poll.StatusID)
return nil
}
@@ -448,11 +446,6 @@ func (p *clientAPI) CreateFollowReq(ctx context.Context, cMsg *messages.FromClie
})
}
- // Update stats for the target account.
- if err := p.utils.incrementFollowRequestsCount(ctx, cMsg.Target); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
if err := p.surface.notifyFollowRequest(ctx, followRequest); err != nil {
log.Errorf(ctx, "error notifying follow request: %v", err)
}
@@ -494,7 +487,7 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// and/or notify the account that's being
// interacted with (if it's local): they can
// approve or deny the interaction later.
- if err := p.utils.requestFave(ctx, fave); err != nil {
+ if err := p.utils.impoliteFaveRequest(ctx, fave); err != nil {
return gtserror.Newf("error pending fave: %w", err)
}
@@ -517,19 +510,22 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// URI attached.
// Store an already-accepted interaction request.
- id := id.NewULID()
+ requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
- ID: id,
- StatusID: fave.StatusID,
- TargetAccountID: fave.TargetAccountID,
- TargetAccount: fave.TargetAccount,
- InteractingAccountID: fave.AccountID,
- InteractingAccount: fave.Account,
- InteractionURI: fave.URI,
- InteractionType: gtsmodel.InteractionLike,
- Like: fave,
- URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id),
- AcceptedAt: time.Now(),
+ ID: requestID,
+ TargetStatusID: fave.StatusID,
+ TargetAccountID: fave.TargetAccountID,
+ TargetAccount: fave.TargetAccount,
+ InteractingAccountID: fave.AccountID,
+ InteractingAccount: fave.Account,
+ InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix),
+ InteractionURI: fave.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests.
+ Like: fave,
+ ResponseURI: uris.GenerateURIForAccept(fave.TargetAccount.Username, requestID),
+ AuthorizationURI: uris.GenerateURIForAuthorization(fave.TargetAccount.Username, requestID),
+ AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@@ -538,7 +534,7 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// Mark the fave itself as now approved.
fave.PendingApproval = util.Ptr(false)
fave.PreApproved = false
- fave.ApprovedByURI = approval.URI
+ fave.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatusFave(
ctx,
fave,
@@ -565,7 +561,7 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, fave.StatusID)
+ p.surface.invalidateStatusFromTimelines(fave.StatusID)
return nil
}
@@ -589,7 +585,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// and/or notify the account that's being
// interacted with (if it's local): they can
// approve or deny the interaction later.
- if err := p.utils.requestAnnounce(ctx, boost); err != nil {
+ if err := p.utils.impoliteAnnounceRequest(ctx, boost); err != nil {
return gtserror.Newf("error pending boost: %w", err)
}
@@ -612,19 +608,22 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// URI attached.
// Store an already-accepted interaction request.
- id := id.NewULID()
+ requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
- ID: id,
- StatusID: boost.BoostOfID,
- TargetAccountID: boost.BoostOfAccountID,
- TargetAccount: boost.BoostOfAccount,
- InteractingAccountID: boost.AccountID,
- InteractingAccount: boost.Account,
- InteractionURI: boost.URI,
- InteractionType: gtsmodel.InteractionLike,
- Announce: boost,
- URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
- AcceptedAt: time.Now(),
+ ID: requestID,
+ TargetStatusID: boost.BoostOfID,
+ TargetAccountID: boost.BoostOfAccountID,
+ TargetAccount: boost.BoostOfAccount,
+ InteractingAccountID: boost.AccountID,
+ InteractingAccount: boost.Account,
+ InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(boost.URI, gtsmodel.AnnounceRequestSuffix),
+ InteractionURI: boost.URI,
+ InteractionType: gtsmodel.InteractionAnnounce,
+ Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests.
+ Announce: boost,
+ ResponseURI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, requestID),
+ AuthorizationURI: uris.GenerateURIForAuthorization(boost.BoostOfAccount.Username, requestID),
+ AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@@ -633,7 +632,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// Mark the boost itself as now approved.
boost.PendingApproval = util.Ptr(false)
boost.PreApproved = false
- boost.ApprovedByURI = approval.URI
+ boost.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
boost,
@@ -650,11 +649,6 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// Don't return, just continue as normal.
}
- // Update stats for the actor account.
- if err := p.utils.incrementStatusesCount(ctx, cMsg.Origin, boost); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
// Timeline and notify the boost wrapper status.
if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
@@ -671,7 +665,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
+ p.surface.invalidateStatusFromTimelines(boost.BoostOfID)
return nil
}
@@ -682,22 +676,20 @@ func (p *clientAPI) CreateBlock(ctx context.Context, cMsg *messages.FromClientAP
return gtserror.Newf("%T not parseable as *gtsmodel.Block", cMsg.GTSModel)
}
- // Remove blockee's statuses from blocker's timeline.
- if err := p.state.Timelines.Home.WipeItemsFromAccountID(
- ctx,
- block.AccountID,
- block.TargetAccountID,
- ); err != nil {
- return gtserror.Newf("error wiping timeline items for block: %w", err)
+ if block.Account.IsLocal() {
+ // Remove posts by target from origin's timelines.
+ p.surface.removeRelationshipFromTimelines(ctx,
+ block.AccountID,
+ block.TargetAccountID,
+ )
}
- // Remove blocker's statuses from blockee's timeline.
- if err := p.state.Timelines.Home.WipeItemsFromAccountID(
- ctx,
- block.TargetAccountID,
- block.AccountID,
- ); err != nil {
- return gtserror.Newf("error wiping timeline items for block: %w", err)
+ if block.TargetAccount.IsLocal() {
+ // Remove posts by origin from target's timelines.
+ p.surface.removeRelationshipFromTimelines(ctx,
+ block.TargetAccountID,
+ block.AccountID,
+ )
}
// TODO: same with notifications?
@@ -731,13 +723,47 @@ func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg *messages.FromClientA
}
}
+ // Notify any *new* mentions added
+ // to this status by the editor.
+ for _, mention := range status.Mentions {
+ // Check if we've seen
+ // this mention already.
+ if !mention.IsNew {
+ // Already seen
+ // it, skip.
+ continue
+ }
+
+ // Haven't seen this mention
+ // yet, notify it if necessary.
+ mention.Status = status
+ if err := p.surface.notifyMention(ctx, mention); err != nil {
+ log.Errorf(ctx, "error notifying mention: %v", err)
+ }
+ }
+
+ if len(status.EditIDs) > 0 {
+ // Ensure edits are fully populated for this status before anything.
+ if err := p.surface.State.DB.PopulateStatusEdits(ctx, status); err != nil {
+ log.Error(ctx, "error populating updated status edits: %v")
+
+ // Then send notifications of a status edit
+ // to any local interactors of the status.
+ } else if err := p.surface.notifyStatusEdit(ctx,
+ status,
+ status.Edits[len(status.Edits)-1], // latest
+ ); err != nil {
+ log.Errorf(ctx, "error notifying status edit: %v", err)
+ }
+ }
+
// Push message that the status has been edited to streams.
if err := p.surface.timelineStatusUpdate(ctx, status); err != nil {
log.Errorf(ctx, "error streaming status edit: %v", err)
}
// Status representation has changed, invalidate from timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.ID)
+ p.surface.invalidateStatusFromTimelines(status.ID)
return nil
}
@@ -752,6 +778,9 @@ func (p *clientAPI) UpdateAccount(ctx context.Context, cMsg *messages.FromClient
log.Errorf(ctx, "error federating account update: %v", err)
}
+ // Account representation has changed, invalidate from timelines.
+ p.surface.invalidateTimelineEntriesByAccount(account.ID)
+
return nil
}
@@ -796,20 +825,6 @@ func (p *clientAPI) AcceptFollow(ctx context.Context, cMsg *messages.FromClientA
return gtserror.Newf("%T not parseable as *gtsmodel.Follow", cMsg.GTSModel)
}
- // Update stats for the target account.
- if err := p.utils.decrementFollowRequestsCount(ctx, cMsg.Target); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
- if err := p.utils.incrementFollowersCount(ctx, cMsg.Target); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
- // Update stats for the origin account.
- if err := p.utils.incrementFollowingCount(ctx, cMsg.Origin); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
if err := p.surface.notifyFollow(ctx, follow); err != nil {
log.Errorf(ctx, "error notifying follow: %v", err)
}
@@ -827,13 +842,7 @@ func (p *clientAPI) RejectFollowRequest(ctx context.Context, cMsg *messages.From
return gtserror.Newf("%T not parseable as *gtsmodel.FollowRequest", cMsg.GTSModel)
}
- // Update stats for the target account.
- if err := p.utils.decrementFollowRequestsCount(ctx, cMsg.Target); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
- if err := p.federate.RejectFollow(
- ctx,
+ if err := p.federate.RejectFollow(ctx,
p.converter.FollowRequestToFollow(ctx, followReq),
); err != nil {
log.Errorf(ctx, "error federating follow reject: %v", err)
@@ -848,14 +857,20 @@ func (p *clientAPI) UndoFollow(ctx context.Context, cMsg *messages.FromClientAPI
return gtserror.Newf("%T not parseable as *gtsmodel.Follow", cMsg.GTSModel)
}
- // Update stats for the origin account.
- if err := p.utils.decrementFollowingCount(ctx, cMsg.Origin); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
+ if follow.Account.IsLocal() {
+ // Remove posts by target from origin's timelines.
+ p.surface.removeRelationshipFromTimelines(ctx,
+ follow.AccountID,
+ follow.TargetAccountID,
+ )
}
- // Update stats for the target account.
- if err := p.utils.decrementFollowersCount(ctx, cMsg.Target); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
+ if follow.TargetAccount.IsLocal() {
+ // Remove posts by origin from target's timelines.
+ p.surface.removeRelationshipFromTimelines(ctx,
+ follow.TargetAccountID,
+ follow.AccountID,
+ )
}
if err := p.federate.UndoFollow(ctx, follow); err != nil {
@@ -890,7 +905,7 @@ func (p *clientAPI) UndoFave(ctx context.Context, cMsg *messages.FromClientAPI)
// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, statusFave.StatusID)
+ p.surface.invalidateStatusFromTimelines(statusFave.StatusID)
return nil
}
@@ -905,14 +920,8 @@ func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg *messages.FromClientA
return gtserror.Newf("db error deleting status: %w", err)
}
- // Update stats for the origin account.
- if err := p.utils.decrementStatusesCount(ctx, cMsg.Origin, status); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
- if err := p.surface.deleteStatusFromTimelines(ctx, status.ID); err != nil {
- log.Errorf(ctx, "error removing timelined status: %v", err)
- }
+ // Delete the boost wrapper status from timelines.
+ p.surface.deleteStatusFromTimelines(ctx, status.ID)
if err := p.federate.UndoAnnounce(ctx, status); err != nil {
log.Errorf(ctx, "error federating announce undo: %v", err)
@@ -920,7 +929,7 @@ func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg *messages.FromClientA
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.BoostOfID)
+ p.surface.invalidateStatusFromTimelines(status.BoostOfID)
return nil
}
@@ -971,11 +980,6 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error wiping status: %v", err)
}
- // Update stats for the origin account.
- if err := p.utils.decrementStatusesCount(ctx, cMsg.Origin, status); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
if err := p.federate.DeleteStatus(ctx, status); err != nil {
log.Errorf(ctx, "error federating status delete: %v", err)
}
@@ -983,7 +987,7 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg *messages.FromClientA
if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+ p.surface.invalidateStatusFromTimelines(status.InReplyToID)
}
return nil
@@ -1026,11 +1030,30 @@ func (p *clientAPI) DeleteAccountOrUser(ctx context.Context, cMsg *messages.From
p.state.Workers.Federator.Queue.Delete("Receiving.ID", account.ID)
p.state.Workers.Federator.Queue.Delete("TargetURI", account.URI)
+ // Remove any entries authored by account from timelines.
+ p.surface.removeTimelineEntriesByAccount(account.ID)
+
+ // Remove any of their cached timelines.
+ p.state.Caches.Timelines.Home.Delete(account.ID)
+
+ // Get the IDs of all the lists owned by the given account ID.
+ listIDs, err := p.state.DB.GetListIDsByAccountID(ctx, account.ID)
+ if err != nil {
+ log.Errorf(ctx, "error getting lists for account %s: %v", account.ID, err)
+ }
+
+ // Remove list timelines of account.
+ for _, listID := range listIDs {
+ p.state.Caches.Timelines.List.Delete(listID)
+ }
+
+ // Federate out a delete activity targeting account to remote servers.
if err := p.federate.DeleteAccount(ctx, cMsg.Target); err != nil {
log.Errorf(ctx, "error federating account delete: %v", err)
}
- if err := p.account.Delete(ctx, cMsg.Target, originID); err != nil {
+ // And finally, perform the actual account deletion synchronously.
+ if err := p.account.Delete(ctx, account, originID); err != nil {
log.Errorf(ctx, "error deleting account: %v", err)
}
@@ -1169,7 +1192,7 @@ func (p *clientAPI) AcceptLike(ctx context.Context, cMsg *messages.FromClientAPI
// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, req.Like.StatusID)
+ p.surface.invalidateStatusFromTimelines(req.Like.StatusID)
return nil
}
@@ -1180,15 +1203,7 @@ func (p *clientAPI) AcceptReply(ctx context.Context, cMsg *messages.FromClientAP
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
}
- var (
- interactingAcct = req.InteractingAccount
- reply = req.Reply
- )
-
- // Update stats for the reply author account.
- if err := p.utils.incrementStatusesCount(ctx, interactingAcct, reply); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
+ reply := req.Reply
// Timeline the reply + notify relevant accounts.
if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
@@ -1202,7 +1217,7 @@ func (p *clientAPI) AcceptReply(ctx context.Context, cMsg *messages.FromClientAP
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, reply.InReplyToID)
+ p.surface.invalidateStatusFromTimelines(reply.InReplyToID)
return nil
}
@@ -1213,15 +1228,7 @@ func (p *clientAPI) AcceptAnnounce(ctx context.Context, cMsg *messages.FromClien
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
}
- var (
- interactingAcct = req.InteractingAccount
- boost = req.Announce
- )
-
- // Update stats for the boost author account.
- if err := p.utils.incrementStatusesCount(ctx, interactingAcct, boost); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
+ boost := req.Announce
// Timeline and notify the announce.
if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
@@ -1240,7 +1247,7 @@ func (p *clientAPI) AcceptAnnounce(ctx context.Context, cMsg *messages.FromClien
// Interaction counts changed on the original status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
+ p.surface.invalidateStatusFromTimelines(boost.BoostOfID)
return nil
}
diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go
index 1d70eb96c..5967d4d34 100644
--- a/internal/processing/workers/fromclientapi_test.go
+++ b/internal/processing/workers/fromclientapi_test.go
@@ -24,19 +24,18 @@ import (
"testing"
"time"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/util"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type FromClientAPITestSuite struct {
@@ -89,6 +88,7 @@ func (suite *FromClientAPITestSuite) newStatus(
OriginAccountID: account.ID,
OriginAccountURI: account.URI,
TargetAccountID: replyToStatus.AccountID,
+ IsNew: true,
}
if err := state.DB.PutMention(ctx, mention); err != nil {
@@ -117,6 +117,7 @@ func (suite *FromClientAPITestSuite) newStatus(
TargetAccountID: mentionedAccount.ID,
TargetAccount: mentionedAccount,
Silent: util.Ptr(false),
+ IsNew: true,
}
newStatus.Mentions = append(newStatus.Mentions, newMention)
@@ -156,7 +157,7 @@ func (suite *FromClientAPITestSuite) checkStreamed(
) {
// Set a 5s timeout on context.
- ctx := context.Background()
+ ctx := suite.T().Context()
ctx, cncl := context.WithTimeout(ctx, time.Second*5)
defer cncl()
@@ -211,9 +212,6 @@ func (suite *FromClientAPITestSuite) statusJSON(
ctx,
status,
requestingAccount,
- statusfilter.FilterContextNone,
- nil,
- nil,
)
if err != nil {
suite.FailNow(err.Error())
@@ -237,8 +235,6 @@ func (suite *FromClientAPITestSuite) conversationJSON(
ctx,
conversation,
requestingAccount,
- nil,
- nil,
)
if err != nil {
suite.FailNow(err.Error())
@@ -257,7 +253,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
@@ -266,9 +262,10 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
receivingAccount,
[]string{testList.ID},
)
- homeStream = streams[stream.TimelineHome]
- listStream = streams[stream.TimelineList+":"+testList.ID]
- notifStream = streams[stream.TimelineNotifications]
+ publicStream = streams[stream.TimelinePublic]
+ homeStream = streams[stream.TimelineHome]
+ listStream = streams[stream.TimelineList+":"+testList.ID]
+ notifStream = streams[stream.TimelineNotifications]
// Admin account posts a new top-level status.
status = suite.newStatus(
@@ -314,6 +311,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
receivingAccount,
)
+ // Check message in public stream.
+ suite.checkStreamed(
+ publicStream,
+ true,
+ statusJSON,
+ stream.EventTypeUpdate,
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -346,7 +351,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
suite.FailNow("timed out waiting for new status notification")
}
- apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil, nil)
+ apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif)
if err != nil {
suite.FailNow(err.Error())
}
@@ -374,7 +379,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBackfilledStatusWithNotifi
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
@@ -383,9 +388,10 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBackfilledStatusWithNotifi
receivingAccount,
[]string{testList.ID},
)
- homeStream = streams[stream.TimelineHome]
- listStream = streams[stream.TimelineList+":"+testList.ID]
- notifStream = streams[stream.TimelineNotifications]
+ publicStream = streams[stream.TimelinePublic]
+ homeStream = streams[stream.TimelineHome]
+ listStream = streams[stream.TimelineList+":"+testList.ID]
+ notifStream = streams[stream.TimelineNotifications]
// Admin account posts a new top-level status.
status = suite.newStatus(
@@ -424,6 +430,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBackfilledStatusWithNotifi
suite.FailNow(err.Error())
}
+ // There should be no message in public stream.
+ suite.checkStreamed(
+ publicStream,
+ false,
+ "",
+ "",
+ )
+
// There should be no message in the home stream.
suite.checkStreamed(
homeStream,
@@ -473,7 +487,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBackfilledStatusWithRemote
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["local_account_1"]
receivingAccount = suite.testAccounts["remote_account_1"]
@@ -529,11 +543,12 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
+ publicStream = streams[stream.TimelinePublic]
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
@@ -575,6 +590,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
receivingAccount,
)
+ // Check message *not* in public stream.
+ suite.checkStreamed(
+ publicStream,
+ false,
+ "",
+ "",
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -600,7 +623,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
@@ -664,7 +687,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
@@ -732,10 +755,11 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
*testList = *suite.testLists["local_account_1_list_1"]
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
+ publicStream = streams[stream.TimelinePublic]
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
@@ -782,6 +806,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
receivingAccount,
)
+ // Check message *not* in public stream.
+ suite.checkStreamed(
+ publicStream,
+ false,
+ "",
+ "",
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -811,10 +843,11 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
*testList = *suite.testLists["local_account_1_list_1"]
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
+ publicStream = streams[stream.TimelinePublic]
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
@@ -867,6 +900,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
receivingAccount,
)
+ // Check message *not* in public stream.
+ suite.checkStreamed(
+ publicStream,
+ false,
+ "",
+ "",
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -896,10 +937,11 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
*testList = *suite.testLists["local_account_1_list_1"]
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
+ publicStream = streams[stream.TimelinePublic]
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
@@ -946,6 +988,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
receivingAccount,
)
+ // Check message *not* in public stream.
+ suite.checkStreamed(
+ publicStream,
+ false,
+ "",
+ "",
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -971,11 +1021,12 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
+ publicStream = streams[stream.TimelinePublic]
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
@@ -1013,6 +1064,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
receivingAccount,
)
+ // Check message *not* in public stream.
+ suite.checkStreamed(
+ publicStream,
+ false,
+ "",
+ "",
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -1038,11 +1097,12 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
+ publicStream = streams[stream.TimelinePublic]
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
@@ -1082,6 +1142,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
suite.FailNow(err.Error())
}
+ // Check message *not* in public stream.
+ suite.checkStreamed(
+ publicStream,
+ false,
+ "",
+ "",
+ )
+
// Check message NOT in home stream.
suite.checkStreamed(
homeStream,
@@ -1105,7 +1173,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichBeginsConversat
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["local_account_2"]
receivingAccount = suite.testAccounts["local_account_1"]
streams = suite.openStreams(ctx,
@@ -1194,7 +1262,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichShouldNotCreate
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["local_account_2"]
receivingAccount = suite.testAccounts["local_account_1"]
streams = suite.openStreams(ctx,
@@ -1267,7 +1335,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtag(
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_2"]
streams = suite.openStreams(ctx,
@@ -1344,7 +1412,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtagA
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["remote_account_1"]
receivingAccount = suite.testAccounts["local_account_2"]
streams = suite.openStreams(ctx,
@@ -1428,7 +1496,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtag()
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["remote_account_2"]
boostingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_2"]
@@ -1534,7 +1602,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAn
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["remote_account_1"]
boostingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_2"]
@@ -1647,7 +1715,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAn
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
boostingAccount = suite.testAccounts["remote_account_1"]
receivingAccount = suite.testAccounts["local_account_2"]
@@ -1758,7 +1826,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["local_account_2"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
@@ -1767,8 +1835,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
receivingAccount,
[]string{testList.ID},
)
- homeStream = streams[stream.TimelineHome]
- listStream = streams[stream.TimelineList+":"+testList.ID]
+ publicStream = streams[stream.TimelinePublic]
+ homeStream = streams[stream.TimelineHome]
+ listStream = streams[stream.TimelineList+":"+testList.ID]
// postingAccount posts a new public status not mentioning anyone.
status = suite.newStatus(
@@ -1806,6 +1875,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
suite.FailNow(err.Error())
}
+ // Check status in public stream.
+ suite.checkStreamed(
+ publicStream,
+ true,
+ "",
+ stream.EventTypeUpdate,
+ )
+
// Check status in list stream.
suite.checkStreamed(
listStream,
@@ -1834,7 +1911,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["local_account_2"]
receivingAccount = suite.testAccounts["local_account_1"]
testInclusiveList = suite.testLists["local_account_1_list_1"]
@@ -1861,6 +1938,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
testExclusiveList.ID,
},
)
+ publicStream = streams[stream.TimelinePublic]
homeStream = streams[stream.TimelineHome]
inclusiveListStream = streams[stream.TimelineList+":"+testInclusiveList.ID]
exclusiveListStream = streams[stream.TimelineList+":"+testExclusiveList.ID]
@@ -1915,6 +1993,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
suite.FailNow(err.Error())
}
+ // Check status in public stream.
+ suite.checkStreamed(
+ publicStream,
+ true,
+ "",
+ stream.EventTypeUpdate,
+ )
+
// Check status in inclusive list stream.
suite.checkStreamed(
inclusiveListStream,
@@ -1951,7 +2037,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["local_account_2"]
receivingAccount = suite.testAccounts["local_account_1"]
testFollow = suite.testFollows["local_account_1_local_account_2"]
@@ -1961,9 +2047,10 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
receivingAccount,
[]string{testList.ID},
)
- homeStream = streams[stream.TimelineHome]
- listStream = streams[stream.TimelineList+":"+testList.ID]
- notifStream = streams[stream.TimelineNotifications]
+ publicStream = streams[stream.TimelinePublic]
+ homeStream = streams[stream.TimelineHome]
+ listStream = streams[stream.TimelineList+":"+testList.ID]
+ notifStream = streams[stream.TimelineNotifications]
// postingAccount posts a new public status not mentioning anyone.
status = suite.newStatus(
@@ -2009,6 +2096,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
suite.FailNow(err.Error())
}
+ // Check status in public stream.
+ suite.checkStreamed(
+ publicStream,
+ true,
+ "",
+ stream.EventTypeUpdate,
+ )
+
// Check status in list stream.
suite.checkStreamed(
listStream,
@@ -2033,7 +2128,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
suite.FailNow("timed out waiting for new status notification")
}
- apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil, nil)
+ apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif)
if err != nil {
suite.FailNow(err.Error())
}
@@ -2078,7 +2173,7 @@ func (suite *FromClientAPITestSuite) TestProcessUpdateStatusWithFollowedHashtag(
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_2"]
streams = suite.openStreams(ctx,
@@ -2147,12 +2242,102 @@ func (suite *FromClientAPITestSuite) TestProcessUpdateStatusWithFollowedHashtag(
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
}
+// Test that when someone edits a status that's been interacted with,
+// the interacter gets a notification that the status has been edited.
+func (suite *FromClientAPITestSuite) TestProcessUpdateStatusInteractedWith() {
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
+
+ var (
+ ctx = suite.T().Context()
+ postingAccount = suite.testAccounts["local_account_1"]
+ receivingAccount = suite.testAccounts["admin_account"]
+ streams = suite.openStreams(ctx,
+ testStructs.Processor,
+ receivingAccount,
+ nil,
+ )
+ notifStream = streams[stream.TimelineNotifications]
+ )
+
+ // Copy the test status.
+ //
+ // This is one that the receiving account
+ // has interacted with (by replying).
+ testStatus := new(gtsmodel.Status)
+ *testStatus = *suite.testStatuses["local_account_1_status_1"]
+
+ // Create + store an edit.
+ edit := &gtsmodel.StatusEdit{
+ // Just set the ID + status ID, other
+ // fields don't matter for this test.
+ ID: "01JTR74W15VS6A6MK15N5JVJ55",
+ StatusID: testStatus.ID,
+ }
+
+ if err := testStructs.State.DB.PutStatusEdit(ctx, edit); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Set edit on status as
+ // it would be for real.
+ testStatus.EditIDs = []string{edit.ID}
+ testStatus.Edits = []*gtsmodel.StatusEdit{edit}
+
+ // Update the status.
+ if err := testStructs.Processor.Workers().ProcessFromClientAPI(
+ ctx,
+ &messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityUpdate,
+ GTSModel: testStatus,
+ Origin: postingAccount,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Wait for a notification to appear for the status.
+ var notif *gtsmodel.Notification
+ if !testrig.WaitFor(func() bool {
+ var err error
+ notif, err = testStructs.State.DB.GetNotification(
+ ctx,
+ gtsmodel.NotificationUpdate,
+ receivingAccount.ID,
+ postingAccount.ID,
+ edit.ID,
+ )
+ return err == nil
+ }) {
+ suite.FailNow("timed out waiting for edited status notification")
+ }
+
+ apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ notifJSON, err := json.Marshal(apiNotif)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Check notif in stream.
+ suite.checkStreamed(
+ notifStream,
+ true,
+ string(notifJSON),
+ stream.EventTypeNotification,
+ )
+}
+
func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
defer testrig.TearDownTestStructs(testStructs)
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
deletingAccount = suite.testAccounts["local_account_1"]
receivingAccount = suite.testAccounts["local_account_2"]
deletedStatus = suite.testStatuses["local_account_1_status_1"]
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
index 2e513449b..b64b8bbec 100644
--- a/internal/processing/workers/fromfediapi.go
+++ b/internal/processing/workers/fromfediapi.go
@@ -23,22 +23,22 @@ import (
"net/url"
"time"
- "codeberg.org/gruf/go-kv"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
-
- "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/processing/account"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/federation/dereferencing"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
+ "codeberg.org/gruf/go-kv/v2"
+
+ "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/processing/account"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// fediAPI wraps processing functions
@@ -88,6 +88,10 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
case ap.ObjectNote:
return p.fediAPI.CreateStatus(ctx, fMsg)
+ // REQUEST TO REPLY TO A STATUS
+ case ap.ActivityReplyRequest:
+ return p.fediAPI.CreateReplyRequest(ctx, fMsg)
+
// CREATE FOLLOW (request)
case ap.ActivityFollow:
return p.fediAPI.CreateFollowReq(ctx, fMsg)
@@ -96,10 +100,18 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
case ap.ActivityLike:
return p.fediAPI.CreateLike(ctx, fMsg)
+ // REQUEST TO LIKE A STATUS
+ case ap.ActivityLikeRequest:
+ return p.fediAPI.CreateLikeRequest(ctx, fMsg)
+
// CREATE ANNOUNCE/BOOST
case ap.ActivityAnnounce:
return p.fediAPI.CreateAnnounce(ctx, fMsg)
+ // REQUEST TO BOOST A STATUS
+ case ap.ActivityAnnounceRequest:
+ return p.fediAPI.CreateAnnounceRequest(ctx, fMsg)
+
// CREATE BLOCK
case ap.ActivityBlock:
return p.fediAPI.CreateBlock(ctx, fMsg)
@@ -146,11 +158,15 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
case ap.ObjectNote:
return p.fediAPI.AcceptReply(ctx, fMsg)
+ // ACCEPT (pending) POLITE REPLY REQUEST
+ case ap.ActivityReplyRequest:
+ return p.fediAPI.AcceptPoliteReplyRequest(ctx, fMsg)
+
// ACCEPT (pending) ANNOUNCE
case ap.ActivityAnnounce:
return p.fediAPI.AcceptAnnounce(ctx, fMsg)
- // ACCEPT (remote) REPLY or ANNOUNCE
+ // ACCEPT (remote) IMPOLITE REPLY or ANNOUNCE
case ap.ObjectUnknown:
return p.fediAPI.AcceptRemoteStatus(ctx, fMsg)
}
@@ -197,15 +213,31 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
// UNDO SOMETHING
case ap.ActivityUndo:
+ switch fMsg.APObjectType {
+ // UNDO FOLLOW
+ case ap.ActivityFollow:
+ return p.fediAPI.UndoFollow(ctx, fMsg)
+
+ // UNDO BLOCK
+ case ap.ActivityBlock:
+ return p.fediAPI.UndoBlock(ctx, fMsg)
+
// UNDO ANNOUNCE
- if fMsg.APObjectType == ap.ActivityAnnounce {
+ case ap.ActivityAnnounce:
return p.fediAPI.UndoAnnounce(ctx, fMsg)
+
+ // UNDO LIKE
+ case ap.ActivityLike:
+ return p.fediAPI.UndoFave(ctx, fMsg)
}
}
return gtserror.Newf("unhandled: %s %s", fMsg.APActivityType, fMsg.APObjectType)
}
+// CreateStatus handles the creation of a status/post sent as a Create message.
+// It is also capable of handling impolite reply requests to local + remote statuses,
+// ie., replies sent directly without doing the ReplyRequest process first.
func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI) error {
var (
status *gtsmodel.Status
@@ -278,7 +310,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// preapproved, then just notify the account
// that's being interacted with: they can
// approve or deny the interaction later.
- if err := p.utils.requestReply(ctx, status); err != nil {
+ if err := p.utils.impoliteReplyRequest(ctx, status); err != nil {
return gtserror.Newf("error pending reply: %w", err)
}
@@ -293,20 +325,24 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// collection. Do the Accept immediately and
// then process everything else as normal.
- // Store an already-accepted interaction request.
- id := id.NewULID()
+ // Store an already-accepted
+ // impolite interaction request.
+ requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
- ID: id,
- StatusID: status.InReplyToID,
- TargetAccountID: status.InReplyToAccountID,
- TargetAccount: status.InReplyToAccount,
- InteractingAccountID: status.AccountID,
- InteractingAccount: status.Account,
- InteractionURI: status.URI,
- InteractionType: gtsmodel.InteractionLike,
- Reply: status,
- URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
- AcceptedAt: time.Now(),
+ ID: requestID,
+ TargetStatusID: status.InReplyToID,
+ TargetAccountID: status.InReplyToAccountID,
+ TargetAccount: status.InReplyToAccount,
+ InteractingAccountID: status.AccountID,
+ InteractingAccount: status.Account,
+ InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix),
+ InteractionURI: status.URI,
+ InteractionType: gtsmodel.InteractionReply,
+ Polite: util.Ptr(false),
+ Reply: status,
+ ResponseURI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, requestID),
+ AuthorizationURI: uris.GenerateURIForAuthorization(status.InReplyToAccount.Username, requestID),
+ AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@@ -315,7 +351,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// Mark the status as now approved.
status.PendingApproval = util.Ptr(false)
status.PreApproved = false
- status.ApprovedByURI = approval.URI
+ status.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
status,
@@ -333,11 +369,6 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// Don't return, just continue as normal.
}
- // Update stats for the remote account.
- if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, status); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
@@ -346,12 +377,119 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// Interaction counts changed on the replied status; uncache the
// prepared version from all timelines. The status dereferencer
// functions will ensure necessary ancestors exist before this point.
- p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+ p.surface.invalidateStatusFromTimelines(status.InReplyToID)
}
return nil
}
+// CreateReplyRequest handles a polite ReplyRequest.
+// This is distinct from CreateStatus, which is capable
+// of handling both "normal" top-level status creation,
+// in addition to *impolite* reply requests.
+func (p *fediAPI) CreateReplyRequest(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ // Extract the ap model Statusable
+ // set by the federating db.
+ statusable, ok := fMsg.APObject.(ap.Statusable)
+ if !ok {
+ return gtserror.Newf("cannot cast %T -> ap.Statusable", fMsg.APObject)
+ }
+
+ // Call RefreshStatus to parse and process the
+ // statusable. This will also check permissions.
+ replyURI := ap.GetJSONLDId(statusable).String()
+ reply, _, err := p.federate.RefreshStatus(ctx,
+ fMsg.Receiving.Username,
+ &gtsmodel.Status{
+ URI: replyURI,
+ Local: util.Ptr(false),
+ },
+ statusable,
+ // Force refresh within 5min window.
+ dereferencing.Fresh,
+ )
+
+ switch {
+ case err == nil:
+ // All fine.
+
+ case gtserror.IsNotPermitted(err):
+ // Reply is straight up not permitted by
+ // the interaction policy of the status
+ // it's replying to. Nothing more to do.
+ log.Debugf(ctx,
+ "dropping unpermitted ReplyRequest with instrument %s",
+ replyURI,
+ )
+ return nil
+
+ default:
+ // There's some real error.
+ return gtserror.Newf(
+ "error processing ReplyRequest with instrument %s: %w",
+ replyURI, err,
+ )
+ }
+
+ // The reply is permitted. Check if we
+ // should send out an Accept immediately.
+ manualApproval := *reply.PendingApproval && !reply.PreApproved
+ if manualApproval {
+ // The reply requires manual approval.
+ //
+ // Just notify target account about
+ // the requested interaction.
+ if err := p.surface.notifyPendingReply(ctx, reply); err != nil {
+ return gtserror.Newf("error notifying pending reply: %w", err)
+ }
+
+ return nil
+ }
+
+ // The reply is automatically approved,
+ // handle side effects of this.
+ req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
+ }
+
+ // Mark the request as accepted.
+ req.AcceptedAt = time.Now()
+ req.ResponseURI = uris.GenerateURIForAccept(
+ req.TargetAccount.Username, req.ID,
+ )
+ req.AuthorizationURI = uris.GenerateURIForAuthorization(
+ req.TargetAccount.Username, req.ID,
+ )
+
+ // Update in the db.
+ if err := p.state.DB.UpdateInteractionRequest(
+ ctx,
+ req,
+ "accepted_at",
+ "response_uri",
+ "authorization_uri",
+ ); err != nil {
+ return gtserror.Newf("db error updating interaction request: %w", err)
+ }
+
+ // Send out the accept.
+ if err := p.federate.AcceptInteraction(ctx, req); err != nil {
+ log.Errorf(ctx, "error federating accept: %v", err)
+ }
+
+ // Timeline the reply + notify recipient(s).
+ if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
+ log.Errorf(ctx, "error timelining and notifying status: %v", err)
+ }
+
+ // Interaction counts changed on the replied status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(reply.InReplyToID)
+
+ return nil
+}
+
func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI) error {
// Cast poll vote type from the worker message.
vote, ok := fMsg.GTSModel.(*gtsmodel.PollVote)
@@ -393,7 +531,7 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
}
// Interaction counts changed, uncache from timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.ID)
+ p.surface.invalidateStatusFromTimelines(status.ID)
return nil
}
@@ -417,18 +555,18 @@ func (p *fediAPI) UpdatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
}
// Get the origin status.
- status := vote.Poll.Status
+ reply := vote.Poll.Status
- if *status.Local {
+ if *reply.Local {
// These were poll votes in a local status, we need to
// federate the updated status model with latest vote counts.
- if err := p.federate.UpdateStatus(ctx, status); err != nil {
+ if err := p.federate.UpdateStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error federating status update: %v", err)
}
}
// Interaction counts changed, uncache from timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.ID)
+ p.surface.invalidateStatusFromTimelines(reply.ID)
return nil
}
@@ -449,11 +587,6 @@ func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg *messages.FromFediAP
log.Errorf(ctx, "error notifying follow request: %v", err)
}
- // And update stats for the local account.
- if err := p.utils.incrementFollowRequestsCount(ctx, fMsg.Receiving); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
return nil
}
@@ -469,16 +602,6 @@ func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg *messages.FromFediAP
return gtserror.Newf("error accepting follow request: %w", err)
}
- // Update stats for the local account.
- if err := p.utils.incrementFollowersCount(ctx, fMsg.Receiving); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
- // Update stats for the remote account.
- if err := p.utils.incrementFollowingCount(ctx, fMsg.Requesting); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
if err := p.federate.AcceptFollow(ctx, follow); err != nil {
log.Errorf(ctx, "error federating follow request accept: %v", err)
}
@@ -490,6 +613,8 @@ func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg *messages.FromFediAP
return nil
}
+// CreateLike handles an impolite Like, ie., a Like sent directly.
+// This is different from the CreateLikeRequest function, which handles polite LikeRequests.
func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) error {
fave, ok := fMsg.GTSModel.(*gtsmodel.StatusFave)
if !ok {
@@ -512,7 +637,7 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
// preapproved, then just notify the account
// that's being interacted with: they can
// approve or deny the interaction later.
- if err := p.utils.requestFave(ctx, fave); err != nil {
+ if err := p.utils.impoliteFaveRequest(ctx, fave); err != nil {
return gtserror.Newf("error pending fave: %w", err)
}
@@ -527,20 +652,24 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
// collection. Do the Accept immediately and
// then process everything else as normal.
- // Store an already-accepted interaction request.
- id := id.NewULID()
+ // Store an already-accepted
+ // impolite interaction request.
+ requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
- ID: id,
- StatusID: fave.StatusID,
- TargetAccountID: fave.TargetAccountID,
- TargetAccount: fave.TargetAccount,
- InteractingAccountID: fave.AccountID,
- InteractingAccount: fave.Account,
- InteractionURI: fave.URI,
- InteractionType: gtsmodel.InteractionLike,
- Like: fave,
- URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id),
- AcceptedAt: time.Now(),
+ ID: requestID,
+ TargetStatusID: fave.StatusID,
+ TargetAccountID: fave.TargetAccountID,
+ TargetAccount: fave.TargetAccount,
+ InteractingAccountID: fave.AccountID,
+ InteractingAccount: fave.Account,
+ InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix),
+ InteractionURI: fave.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Polite: util.Ptr(false),
+ Like: fave,
+ ResponseURI: uris.GenerateURIForAccept(fave.TargetAccount.Username, requestID),
+ AuthorizationURI: uris.GenerateURIForAuthorization(fave.TargetAccount.Username, requestID),
+ AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@@ -549,7 +678,7 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
// Mark the fave itself as now approved.
fave.PendingApproval = util.Ptr(false)
fave.PreApproved = false
- fave.ApprovedByURI = approval.URI
+ fave.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatusFave(
ctx,
fave,
@@ -573,7 +702,88 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, fave.StatusID)
+ p.surface.invalidateStatusFromTimelines(fave.StatusID)
+
+ return nil
+}
+
+// CreateLikeRequest handles a polite LikeRequest, as
+// opposed to CreateLike, which handles *impolite* like
+// requests (ie., Likes sent directly).
+func (p *fediAPI) CreateLikeRequest(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
+ }
+
+ // At this point the not-yet-approved
+ // interaction request, and the pending
+ // fave, are both in the database.
+
+ if !req.Like.PreApproved {
+ // The fave is *not* pre-approved, and
+ // therefore requires manual approval.
+ //
+ // Just notify target account about
+ // the requested interaction.
+ if err := p.surface.notifyPendingFave(ctx, req.Like); err != nil {
+ return gtserror.Newf("error notifying pending like: %w", err)
+ }
+
+ return nil
+ }
+
+ // If it's pre-approved on the other hand
+ // we can handle everything immediately.
+
+ // Mark the request as accepted.
+ req.AcceptedAt = time.Now()
+ req.ResponseURI = uris.GenerateURIForAccept(
+ req.TargetAccount.Username, req.ID,
+ )
+ req.AuthorizationURI = uris.GenerateURIForAuthorization(
+ req.TargetAccount.Username, req.ID,
+ )
+
+ // Update in the db.
+ if err := p.state.DB.UpdateInteractionRequest(
+ ctx,
+ req,
+ "accepted_at",
+ "response_uri",
+ "authorization_uri",
+ ); err != nil {
+ return gtserror.Newf("db error updating interaction request: %w", err)
+ }
+
+ // Send out the accept.
+ if err := p.federate.AcceptInteraction(ctx, req); err != nil {
+ log.Errorf(ctx, "error federating accept: %v", err)
+ }
+
+ // Mark the fave as approved.
+ req.Like.PendingApproval = util.Ptr(false)
+ req.Like.ApprovedByURI = req.AuthorizationURI
+ req.Like.PreApproved = false
+
+ // Update in the db.
+ if err := p.state.DB.UpdateStatusFave(
+ ctx,
+ req.Like,
+ "pending_approval",
+ "approved_by_uri",
+ ); err != nil {
+ return gtserror.Newf("db error updating status fave: %w", err)
+ }
+
+ // Notify the faved account.
+ if err := p.surface.notifyFave(ctx, req.Like); err != nil {
+ log.Errorf(ctx, "error notifying fave: %v", err)
+ }
+
+ // Interaction counts changed on the faved status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(req.Like.StatusID)
return nil
}
@@ -597,7 +807,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
)
if err != nil {
if gtserror.IsUnretrievable(err) ||
- gtserror.NotPermitted(err) {
+ gtserror.IsNotPermitted(err) {
// Boosted status domain blocked, or
// otherwise not permitted, nothing to do.
log.Debugf(ctx, "skipping announce: %v", err)
@@ -619,7 +829,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// preapproved, then just notify the account
// that's being interacted with: they can
// approve or deny the interaction later.
- if err := p.utils.requestAnnounce(ctx, boost); err != nil {
+ if err := p.utils.impoliteAnnounceRequest(ctx, boost); err != nil {
return gtserror.Newf("error pending boost: %w", err)
}
@@ -634,20 +844,24 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// collection. Do the Accept immediately and
// then process everything else as normal.
- // Store an already-accepted interaction request.
- id := id.NewULID()
+ // Store an already-accepted
+ // impolite interaction request.
+ requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
- ID: id,
- StatusID: boost.BoostOfID,
- TargetAccountID: boost.BoostOfAccountID,
- TargetAccount: boost.BoostOfAccount,
- InteractingAccountID: boost.AccountID,
- InteractingAccount: boost.Account,
- InteractionURI: boost.URI,
- InteractionType: gtsmodel.InteractionLike,
- Announce: boost,
- URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
- AcceptedAt: time.Now(),
+ ID: requestID,
+ TargetStatusID: boost.BoostOfID,
+ TargetAccountID: boost.BoostOfAccountID,
+ TargetAccount: boost.BoostOfAccount,
+ InteractingAccountID: boost.AccountID,
+ InteractingAccount: boost.Account,
+ InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(boost.URI, gtsmodel.AnnounceRequestSuffix),
+ InteractionURI: boost.URI,
+ InteractionType: gtsmodel.InteractionAnnounce,
+ Polite: util.Ptr(false),
+ Announce: boost,
+ ResponseURI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, requestID),
+ AuthorizationURI: uris.GenerateURIForAuthorization(boost.BoostOfAccount.Username, requestID),
+ AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@@ -656,7 +870,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// Mark the boost itself as now approved.
boost.PendingApproval = util.Ptr(false)
boost.PreApproved = false
- boost.ApprovedByURI = approval.URI
+ boost.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
boost,
@@ -674,11 +888,6 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// Don't return, just continue as normal.
}
- // Update stats for the remote account.
- if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, boost); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
// Timeline and notify the announce.
if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
@@ -690,64 +899,135 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// Interaction counts changed on the original status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
+ p.surface.invalidateStatusFromTimelines(boost.BoostOfID)
return nil
}
-func (p *fediAPI) CreateBlock(ctx context.Context, fMsg *messages.FromFediAPI) error {
- block, ok := fMsg.GTSModel.(*gtsmodel.Block)
+func (p *fediAPI) CreateAnnounceRequest(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
- return gtserror.Newf("%T not parseable as *gtsmodel.Block", fMsg.GTSModel)
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
}
- // Remove each account's posts from the other's timelines.
+ // At this point the not-yet-handled interaction req
+ // is in the database, but the announce isn't yet.
//
- // First home timelines.
- if err := p.state.Timelines.Home.WipeItemsFromAccountID(
+ // We can check permissions for the announce *and*
+ // put it in the db (if acceptable) by doing Enrich.
+ boost, err := p.federate.EnrichAnnounce(
ctx,
- block.AccountID,
- block.TargetAccountID,
- ); err != nil {
- log.Errorf(ctx, "error wiping items from block -> target's home timeline: %v", err)
+ req.Announce,
+ fMsg.Receiving.Username,
+ )
+
+ switch {
+ case err == nil:
+ // All fine.
+
+ case gtserror.IsNotPermitted(err):
+ // Announce is straight up not permitted
+ // by the interaction policy of the status
+ // it's targeting. Nothing more to do.
+ log.Debugf(ctx,
+ "dropping unpermitted AnnounceRequest with instrument %s",
+ req.Announce.URI,
+ )
+ return nil
+
+ default:
+ // There's some real error.
+ return gtserror.Newf(
+ "error processing AnnounceRequest with instrument %s: %w",
+ req.Announce.URI, err,
+ )
}
- if err := p.state.Timelines.Home.WipeItemsFromAccountID(
- ctx,
- block.TargetAccountID,
- block.AccountID,
- ); err != nil {
- log.Errorf(ctx, "error wiping items from target -> block's home timeline: %v", err)
+ // The announce is permitted. Check if we
+ // should send out an Accept immediately.
+ manualApproval := *boost.PendingApproval && !boost.PreApproved
+ if manualApproval {
+ // The announce requires manual approval.
+ //
+ // Just notify target account about
+ // the requested interaction.
+ if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil {
+ return gtserror.Newf("error notifying pending announce: %w", err)
+ }
+
+ return nil
}
- // Now list timelines.
- if err := p.state.Timelines.List.WipeItemsFromAccountID(
+ // The announce is automatically approved,
+ // mark the request as accepted.
+ req.AcceptedAt = time.Now()
+ req.ResponseURI = uris.GenerateURIForAccept(
+ req.TargetAccount.Username, req.ID,
+ )
+ req.AuthorizationURI = uris.GenerateURIForAuthorization(
+ req.TargetAccount.Username, req.ID,
+ )
+
+ // Update in the db.
+ if err := p.state.DB.UpdateInteractionRequest(
ctx,
- block.AccountID,
- block.TargetAccountID,
+ req,
+ "accepted_at",
+ "response_uri",
+ "authorization_uri",
); err != nil {
- log.Errorf(ctx, "error wiping items from block -> target's list timeline(s): %v", err)
+ return gtserror.Newf("db error updating interaction request: %w", err)
}
- if err := p.state.Timelines.List.WipeItemsFromAccountID(
- ctx,
- block.TargetAccountID,
- block.AccountID,
- ); err != nil {
- log.Errorf(ctx, "error wiping items from target -> block's list timeline(s): %v", err)
+ // Send out the accept.
+ if err := p.federate.AcceptInteraction(ctx, req); err != nil {
+ log.Errorf(ctx, "error federating accept: %v", err)
+ }
+
+ // Timeline the boost + notify recipient(s).
+ if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
+ log.Errorf(ctx, "error timelining and notifying status: %v", err)
+ }
+
+ // Interaction counts changed on the boosted status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(boost.BoostOfID)
+
+ return nil
+}
+
+func (p *fediAPI) CreateBlock(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ block, ok := fMsg.GTSModel.(*gtsmodel.Block)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Block", fMsg.GTSModel)
+ }
+
+ if block.Account.IsLocal() {
+ // Remove posts by target from origin's timelines.
+ p.surface.removeRelationshipFromTimelines(ctx,
+ block.AccountID,
+ block.TargetAccountID,
+ )
+ }
+
+ if block.TargetAccount.IsLocal() {
+ // Remove posts by origin from target's timelines.
+ p.surface.removeRelationshipFromTimelines(ctx,
+ block.TargetAccountID,
+ block.AccountID,
+ )
}
// Remove any follows that existed between blocker + blockee.
- if err := p.state.DB.DeleteFollow(
- ctx,
+ // (note this handles removing any necessary list entries).
+ if err := p.state.DB.DeleteFollow(ctx,
block.AccountID,
block.TargetAccountID,
); err != nil {
log.Errorf(ctx, "error deleting follow from block -> target: %v", err)
}
- if err := p.state.DB.DeleteFollow(
- ctx,
+ if err := p.state.DB.DeleteFollow(ctx,
block.TargetAccountID,
block.AccountID,
); err != nil {
@@ -755,16 +1035,14 @@ func (p *fediAPI) CreateBlock(ctx context.Context, fMsg *messages.FromFediAPI) e
}
// Remove any follow requests that existed between blocker + blockee.
- if err := p.state.DB.DeleteFollowRequest(
- ctx,
+ if err := p.state.DB.DeleteFollowRequest(ctx,
block.AccountID,
block.TargetAccountID,
); err != nil {
log.Errorf(ctx, "error deleting follow request from block -> target: %v", err)
}
- if err := p.state.DB.DeleteFollowRequest(
- ctx,
+ if err := p.state.DB.DeleteFollowRequest(ctx,
block.TargetAccountID,
block.AccountID,
); err != nil {
@@ -821,24 +1099,13 @@ func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg *messages.FromFediAPI)
log.Errorf(ctx, "error refreshing account: %v", err)
}
+ // Account representation has changed, invalidate from timelines.
+ p.surface.invalidateTimelineEntriesByAccount(account.ID)
+
return nil
}
func (p *fediAPI) AcceptFollow(ctx context.Context, fMsg *messages.FromFediAPI) error {
- // Update stats for the remote account.
- if err := p.utils.decrementFollowRequestsCount(ctx, fMsg.Requesting); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
- if err := p.utils.incrementFollowersCount(ctx, fMsg.Requesting); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
- // Update stats for the local account.
- if err := p.utils.incrementFollowingCount(ctx, fMsg.Receiving); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
return nil
}
@@ -849,29 +1116,24 @@ func (p *fediAPI) AcceptLike(ctx context.Context, fMsg *messages.FromFediAPI) er
}
func (p *fediAPI) AcceptReply(ctx context.Context, fMsg *messages.FromFediAPI) error {
- status, ok := fMsg.GTSModel.(*gtsmodel.Status)
+ reply, ok := fMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
}
- // Update stats for the actor account.
- if err := p.utils.incrementStatusesCount(ctx, status.Account, status); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
// Timeline and notify the status.
- if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
+ if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
// Send out the reply again, fully this time.
- if err := p.federate.CreateStatus(ctx, status); err != nil {
+ if err := p.federate.CreateStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error federating announce: %v", err)
}
// Interaction counts changed on the replied-to status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+ p.surface.invalidateStatusFromTimelines(reply.InReplyToID)
return nil
}
@@ -900,9 +1162,9 @@ func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFed
// barebones status and insert it into the database,
// if indeed it's actually a status URI we can fetch.
//
- // This will also check whether the given AcceptIRI
+ // This will also check whether the given approvedByURI
// actually grants permission for this status.
- status, _, err := p.federate.RefreshStatus(ctx,
+ reply, _, err := p.federate.RefreshStatus(ctx,
fMsg.Receiving.Username,
bareStatus,
nil, nil,
@@ -913,20 +1175,65 @@ func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFed
// No error means it was indeed a remote status, and the
// given approvedByURI permitted it. Timeline and notify it.
- if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
+ if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
// Interaction counts changed on the interacted status;
// uncache the prepared version from all timelines.
- if status.InReplyToID != "" {
- p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+ if reply.InReplyToID != "" {
+ p.surface.invalidateStatusFromTimelines(reply.InReplyToID)
+ }
+
+ if reply.BoostOfID != "" {
+ p.surface.invalidateStatusFromTimelines(reply.BoostOfID)
+ }
+
+ return nil
+}
+
+func (p *fediAPI) AcceptPoliteReplyRequest(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ if util.IsNil(fMsg.GTSModel) {
+ // If the interaction request is nil, this
+ // must be an accept of a remote ReplyRequest
+ // not targeting one of our statuses.
+ //
+ // Just pass it to the AcceptRemoteStatus
+ // func to do dereferencing + side effects.
+ log.Debug(ctx, "accepting remote ReplyRequest for remote reply")
+ return p.AcceptRemoteStatus(ctx, fMsg)
+ }
+
+ // If the interaction request is not nil, this will
+ // be an accept of one of our replies to a remote.
+ //
+ // Since the int req + reply have already been updated
+ // in the federatingDB, we just need to do side effects.
+ intReq, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
+ }
+
+ // Ensure reply populated.
+ reply := intReq.Reply
+ if err := p.state.DB.PopulateStatus(ctx, reply); err != nil {
+ return gtserror.Newf("error populating status: %w", err)
+ }
+
+ // Timeline and notify the status.
+ if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
+ log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
- if status.BoostOfID != "" {
- p.surface.invalidateStatusFromTimelines(ctx, status.BoostOfID)
+ // Send out the reply with approval attached.
+ if err := p.federate.CreateStatus(ctx, reply); err != nil {
+ log.Errorf(ctx, "error federating announce: %v", err)
}
+ // Interaction counts changed on the replied-to status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(reply.InReplyToID)
+
return nil
}
@@ -936,11 +1243,6 @@ func (p *fediAPI) AcceptAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
}
- // Update stats for the actor account.
- if err := p.utils.incrementStatusesCount(ctx, boost.Account, boost); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
// Timeline and notify the boost wrapper status.
if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
@@ -953,7 +1255,7 @@ func (p *fediAPI) AcceptAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
+ p.surface.invalidateStatusFromTimelines(boost.BoostOfID)
return nil
}
@@ -998,13 +1300,47 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
}
}
+ // Notify any *new* mentions added
+ // to this status by the editor.
+ for _, mention := range status.Mentions {
+ // Check if we've seen
+ // this mention already.
+ if !mention.IsNew {
+ // Already seen
+ // it, skip.
+ continue
+ }
+
+ // Haven't seen this mention
+ // yet, notify it if necessary.
+ mention.Status = status
+ if err := p.surface.notifyMention(ctx, mention); err != nil {
+ log.Errorf(ctx, "error notifying mention: %v", err)
+ }
+ }
+
+ if len(status.EditIDs) > 0 {
+ // Ensure edits are fully populated for this status before anything.
+ if err := p.surface.State.DB.PopulateStatusEdits(ctx, status); err != nil {
+ log.Error(ctx, "error populating updated status edits: %v")
+
+ // Then send notifications of a status edit
+ // to any local interactors of the status.
+ } else if err := p.surface.notifyStatusEdit(ctx,
+ status,
+ status.Edits[len(status.Edits)-1], // latest
+ ); err != nil {
+ log.Errorf(ctx, "error notifying status edit: %v", err)
+ }
+ }
+
// Push message that the status has been edited to streams.
if err := p.surface.timelineStatusUpdate(ctx, status); err != nil {
log.Errorf(ctx, "error streaming status edit: %v", err)
}
- // Status representation was refetched, uncache from timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.ID)
+ // Status representation changed, uncache from timelines.
+ p.surface.invalidateStatusFromTimelines(status.ID)
return nil
}
@@ -1055,15 +1391,10 @@ func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg *messages.FromFediAPI)
log.Errorf(ctx, "error wiping status: %v", err)
}
- // Update stats for the remote account.
- if err := p.utils.decrementStatusesCount(ctx, fMsg.Requesting, status); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+ p.surface.invalidateStatusFromTimelines(status.InReplyToID)
}
return nil
@@ -1090,7 +1421,10 @@ func (p *fediAPI) DeleteAccount(ctx context.Context, fMsg *messages.FromFediAPI)
p.state.Workers.Federator.Queue.Delete("Requesting.ID", account.ID)
p.state.Workers.Federator.Queue.Delete("TargetURI", account.URI)
- // First perform the actual account deletion.
+ // Remove any entries authored by account from timelines.
+ p.surface.removeTimelineEntriesByAccount(account.ID)
+
+ // And finally, perform the actual account deletion synchronously.
if err := p.account.Delete(ctx, account, account.ID); err != nil {
log.Errorf(ctx, "error deleting account: %v", err)
}
@@ -1139,7 +1473,7 @@ func (p *fediAPI) RejectReply(ctx context.Context, fMsg *messages.FromFediAPI) e
// be in the database, we just need to do side effects.
// Get the rejected status.
- status, err := p.state.DB.GetStatusByURI(
+ reply, err := p.state.DB.GetStatusByURI(
gtscontext.SetBarebones(ctx),
req.InteractionURI,
)
@@ -1159,7 +1493,7 @@ func (p *fediAPI) RejectReply(ctx context.Context, fMsg *messages.FromFediAPI) e
// Perform the actual status deletion.
if err := p.utils.wipeStatus(
ctx,
- status,
+ reply,
deleteAttachments,
copyToSinBin,
); err != nil {
@@ -1208,6 +1542,42 @@ func (p *fediAPI) RejectAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
return nil
}
+func (p *fediAPI) UndoFollow(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ follow, ok := fMsg.GTSModel.(*gtsmodel.Follow)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Follow", fMsg.GTSModel)
+ }
+
+ if follow.Account.IsLocal() {
+ // Remove posts by target from origin's timelines.
+ p.surface.removeRelationshipFromTimelines(ctx,
+ follow.AccountID,
+ follow.TargetAccountID,
+ )
+ }
+
+ if follow.TargetAccount.IsLocal() {
+ // Remove posts by origin from target's timelines.
+ p.surface.removeRelationshipFromTimelines(ctx,
+ follow.TargetAccountID,
+ follow.AccountID,
+ )
+ }
+
+ return nil
+}
+
+func (p *fediAPI) UndoBlock(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ _, ok := fMsg.GTSModel.(*gtsmodel.Block)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Block", fMsg.GTSModel)
+ }
+
+ // TODO: any required changes
+
+ return nil
+}
+
func (p *fediAPI) UndoAnnounce(
ctx context.Context,
fMsg *messages.FromFediAPI,
@@ -1222,19 +1592,25 @@ func (p *fediAPI) UndoAnnounce(
return gtserror.Newf("db error deleting boost: %w", err)
}
- // Update statuses count for the requesting account.
- if err := p.utils.decrementStatusesCount(ctx, fMsg.Requesting, boost); err != nil {
- log.Errorf(ctx, "error updating account stats: %v", err)
- }
-
// Remove the boost wrapper from all timelines.
- if err := p.surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil {
- log.Errorf(ctx, "error removing timelined boost: %v", err)
- }
+ p.surface.deleteStatusFromTimelines(ctx, boost.ID)
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
+ p.surface.invalidateStatusFromTimelines(boost.BoostOfID)
+
+ return nil
+}
+
+func (p *fediAPI) UndoFave(ctx context.Context, fMsg *messages.FromFediAPI) error {
+ statusFave, ok := fMsg.GTSModel.(*gtsmodel.StatusFave)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", fMsg.GTSModel)
+ }
+
+ // Interaction counts changed on the faved status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(statusFave.StatusID)
return nil
}
diff --git a/internal/processing/workers/fromfediapi_move.go b/internal/processing/workers/fromfediapi_move.go
index d1e43c0c7..93e7b39a4 100644
--- a/internal/processing/workers/fromfediapi_move.go
+++ b/internal/processing/workers/fromfediapi_move.go
@@ -22,14 +22,14 @@ import (
"errors"
"time"
- "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/log"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
+ "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/log"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
)
// ShouldProcessMove checks whether we should attempt
@@ -269,9 +269,8 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg *messages.FromFediAPI) e
// try to send the same Move several times with
// different IDs (you never know), but we only
// want to process them based on origin + target.
- unlock := p.state.FedLocks.Lock(
- "move:" + originAcctURIStr + ":" + targetAcctURIStr,
- )
+ key := "move:" + originAcctURIStr + ":" + targetAcctURIStr
+ unlock := p.state.FedLocks.Lock(key)
defer unlock()
// Check if Move is rate limited based
@@ -303,10 +302,13 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg *messages.FromFediAPI) e
}
// Account to which the Move is taking place.
+ //
+ // Match by uri only.
targetAcct, targetAcctable, err := p.federate.GetAccountByURI(
ctx,
fMsg.Receiving.Username,
targetAcctURI,
+ false,
)
if err != nil {
return gtserror.Newf(
diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go
index f3e719890..790c78b70 100644
--- a/internal/processing/workers/fromfediapi_test.go
+++ b/internal/processing/workers/fromfediapi_test.go
@@ -18,6 +18,7 @@
package workers_test
import (
+ "bytes"
"context"
"encoding/json"
"errors"
@@ -26,16 +27,18 @@ import (
"testing"
"time"
+ "code.superseriousbusiness.org/activity/streams/vocab"
+ "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/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/messages"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "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/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
- "github.com/superseriousbusiness/gotosocial/internal/util"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type FromFediAPITestSuite struct {
@@ -62,7 +65,7 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
announceStatus.Account = boostingAccount
announceStatus.Visibility = boostedStatus.Visibility
- err := testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
+ err := testStructs.Processor.Workers().ProcessFromFediAPI(suite.T().Context(), &messages.FromFediAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: announceStatus,
@@ -81,7 +84,7 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
}
_, err = testStructs.State.DB.GetStatusByID(
- context.Background(),
+ suite.T().Context(),
announceStatus.ID,
)
return err == nil
@@ -97,12 +100,12 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
},
}
notif := &gtsmodel.Notification{}
- err = testStructs.State.DB.GetWhere(context.Background(), where, notif)
+ err = testStructs.State.DB.GetWhere(suite.T().Context(), where, notif)
suite.NoError(err)
suite.Equal(gtsmodel.NotificationReblog, notif.NotificationType)
suite.Equal(boostedStatus.AccountID, notif.TargetAccountID)
suite.Equal(announceStatus.AccountID, notif.OriginAccountID)
- suite.Equal(announceStatus.ID, notif.StatusID)
+ suite.Equal(announceStatus.ID, notif.StatusOrEditID)
suite.False(*notif.Read)
}
@@ -125,7 +128,7 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
replyingAccount.FetchedAt = time.Now()
replyingAccount.SuspendedAt = time.Time{}
replyingAccount.SuspensionOrigin = ""
- err := testStructs.State.DB.UpdateAccount(context.Background(),
+ err := testStructs.State.DB.UpdateAccount(suite.T().Context(),
replyingAccount,
"fetched_at",
"suspended_at",
@@ -139,11 +142,11 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI))
// Open a websocket stream to later test the streamed status reply.
- wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome)
+ wssStream, errWithCode := testStructs.Processor.Stream().Open(suite.T().Context(), repliedAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// Send the replied status off to the fedi worker to be further processed.
- err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
+ err = testStructs.Processor.Workers().ProcessFromFediAPI(suite.T().Context(), &messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APObject: replyingStatusable,
@@ -158,7 +161,7 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
// 1. status should be in the database
var replyingStatus *gtsmodel.Status
if !testrig.WaitFor(func() bool {
- replyingStatus, err = testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI)
+ replyingStatus, err = testStructs.State.DB.GetStatusByURI(suite.T().Context(), replyingURI)
return err == nil
}) {
suite.FailNow("timed out waiting for replying status to be in the database")
@@ -166,17 +169,17 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
// 2. a notification should exist for the mention
var notif gtsmodel.Notification
- err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{
+ err = testStructs.State.DB.GetWhere(suite.T().Context(), []db.Where{
{Key: "status_id", Value: replyingStatus.ID},
}, &notif)
suite.NoError(err)
suite.Equal(gtsmodel.NotificationMention, notif.NotificationType)
suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID)
suite.Equal(replyingStatus.AccountID, notif.OriginAccountID)
- suite.Equal(replyingStatus.ID, notif.StatusID)
+ suite.Equal(replyingStatus.ID, notif.StatusOrEditID)
suite.False(*notif.Read)
- ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, _ := context.WithTimeout(suite.T().Context(), time.Second*5)
msg, ok := wssStream.Recv(ctx)
suite.True(ok)
@@ -198,7 +201,7 @@ func (suite *FromFediAPITestSuite) TestProcessFave() {
favedStatus := suite.testStatuses["local_account_1_status_1"]
favingAccount := suite.testAccounts["remote_account_1"]
- wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), favedAccount, stream.TimelineNotifications)
+ wssStream, errWithCode := testStructs.Processor.Stream().Open(suite.T().Context(), favedAccount, stream.TimelineNotifications)
suite.NoError(errWithCode)
fave := &gtsmodel.StatusFave{
@@ -214,10 +217,10 @@ func (suite *FromFediAPITestSuite) TestProcessFave() {
URI: favingAccount.URI + "/faves/aaaaaaaaaaaa",
}
- err := testStructs.State.DB.Put(context.Background(), fave)
+ err := testStructs.State.DB.Put(suite.T().Context(), fave)
suite.NoError(err)
- err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
+ err = testStructs.Processor.Workers().ProcessFromFediAPI(suite.T().Context(), &messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
@@ -240,15 +243,15 @@ func (suite *FromFediAPITestSuite) TestProcessFave() {
}
notif := &gtsmodel.Notification{}
- err = testStructs.State.DB.GetWhere(context.Background(), where, notif)
+ err = testStructs.State.DB.GetWhere(suite.T().Context(), where, notif)
suite.NoError(err)
suite.Equal(gtsmodel.NotificationFavourite, notif.NotificationType)
suite.Equal(fave.TargetAccountID, notif.TargetAccountID)
suite.Equal(fave.AccountID, notif.OriginAccountID)
- suite.Equal(fave.StatusID, notif.StatusID)
+ suite.Equal(fave.StatusID, notif.StatusOrEditID)
suite.False(*notif.Read)
- ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, _ := context.WithTimeout(suite.T().Context(), time.Second*5)
msg, ok := wssStream.Recv(ctx)
suite.True(ok)
@@ -271,7 +274,7 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount(
favedStatus := suite.testStatuses["local_account_1_status_1"]
favingAccount := suite.testAccounts["remote_account_1"]
- wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), receivingAccount, stream.TimelineHome)
+ wssStream, errWithCode := testStructs.Processor.Stream().Open(suite.T().Context(), receivingAccount, stream.TimelineHome)
suite.NoError(errWithCode)
fave := &gtsmodel.StatusFave{
@@ -287,10 +290,10 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount(
URI: favingAccount.URI + "/faves/aaaaaaaaaaaa",
}
- err := testStructs.State.DB.Put(context.Background(), fave)
+ err := testStructs.State.DB.Put(suite.T().Context(), fave)
suite.NoError(err)
- err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
+ err = testStructs.Processor.Workers().ProcessFromFediAPI(suite.T().Context(), &messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
@@ -313,16 +316,16 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount(
}
notif := &gtsmodel.Notification{}
- err = testStructs.State.DB.GetWhere(context.Background(), where, notif)
+ err = testStructs.State.DB.GetWhere(suite.T().Context(), where, notif)
suite.NoError(err)
suite.Equal(gtsmodel.NotificationFavourite, notif.NotificationType)
suite.Equal(fave.TargetAccountID, notif.TargetAccountID)
suite.Equal(fave.AccountID, notif.OriginAccountID)
- suite.Equal(fave.StatusID, notif.StatusID)
+ suite.Equal(fave.StatusID, notif.StatusOrEditID)
suite.False(*notif.Read)
// 2. no notification should be streamed to the account that received the fave message, because they weren't the target
- ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, _ := context.WithTimeout(suite.T().Context(), time.Second*5)
_, ok := wssStream.Recv(ctx)
suite.False(ok)
}
@@ -331,7 +334,7 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
defer testrig.TearDownTestStructs(testStructs)
- ctx := context.Background()
+ ctx := suite.T().Context()
deletedAccount := &gtsmodel.Account{}
*deletedAccount = *suite.testAccounts["remote_account_1"]
@@ -425,14 +428,14 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
defer testrig.TearDownTestStructs(testStructs)
- ctx := context.Background()
+ ctx := suite.T().Context()
originAccount := suite.testAccounts["remote_account_1"]
// target is a locked account
targetAccount := suite.testAccounts["local_account_2"]
- wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome)
+ wssStream, errWithCode := testStructs.Processor.Stream().Open(suite.T().Context(), targetAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// put the follow request in the database as though it had passed through the federating db already
@@ -462,7 +465,7 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
suite.NoError(err)
ctx, _ = context.WithTimeout(ctx, time.Second*5)
- msg, ok := wssStream.Recv(context.Background())
+ msg, ok := wssStream.Recv(suite.T().Context())
suite.True(ok)
suite.Equal(stream.EventTypeNotification, msg.Event)
@@ -482,14 +485,14 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
defer testrig.TearDownTestStructs(testStructs)
- ctx := context.Background()
+ ctx := suite.T().Context()
originAccount := suite.testAccounts["remote_account_1"]
// target is an unlocked account
targetAccount := suite.testAccounts["local_account_1"]
- wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome)
+ wssStream, errWithCode := testStructs.Processor.Stream().Open(suite.T().Context(), targetAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// put the follow request in the database as though it had passed through the federating db already
@@ -565,7 +568,7 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
suite.Equal("Accept", accept.Type)
ctx, _ = context.WithTimeout(ctx, time.Second*5)
- msg, ok := wssStream.Recv(context.Background())
+ msg, ok := wssStream.Recv(suite.T().Context())
suite.True(ok)
suite.Equal(stream.EventTypeNotification, msg.Event)
@@ -583,7 +586,7 @@ func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
defer testrig.TearDownTestStructs(testStructs)
- ctx := context.Background()
+ ctx := suite.T().Context()
receivingAccount := suite.testAccounts["local_account_1"]
statusCreator := suite.testAccounts["remote_account_2"]
@@ -599,7 +602,7 @@ func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
suite.NoError(err)
// status should now be in the database, attributed to remote_account_2
- s, err := testStructs.State.DB.GetStatusByURI(context.Background(), "http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1")
+ s, err := testStructs.State.DB.GetStatusByURI(suite.T().Context(), "http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1")
suite.NoError(err)
suite.Equal(statusCreator.URI, s.AccountURI)
}
@@ -609,7 +612,7 @@ func (suite *FromFediAPITestSuite) TestMoveAccount() {
defer testrig.TearDownTestStructs(testStructs)
// We're gonna migrate foss_satan to our local admin account.
- ctx := context.Background()
+ ctx := suite.T().Context()
receivingAcct := suite.testAccounts["local_account_1"]
// Copy requesting and target accounts
@@ -683,7 +686,7 @@ func (suite *FromFediAPITestSuite) TestMoveAccount() {
func (suite *FromFediAPITestSuite) TestUndoAnnounce() {
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
testStructs = testrig.SetupTestStructs(rMediaPath, rTemplatePath)
requestingAcct = suite.testAccounts["remote_account_1"]
receivingAcct = suite.testAccounts["local_account_1"]
@@ -735,6 +738,169 @@ func (suite *FromFediAPITestSuite) TestUndoAnnounce() {
}
}
+func (suite *FromFediAPITestSuite) TestUpdateNote() {
+ var (
+ ctx = suite.T().Context()
+ testStructs = testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ requestingAcct = suite.testAccounts["remote_account_2"]
+ receivingAcct = suite.testAccounts["local_account_1"]
+ )
+ defer testrig.TearDownTestStructs(testStructs)
+
+ update := testrig.NewTestActivities(suite.testAccounts)["remote_account_2_status_1_update"]
+ statusable := update.Activity.GetActivityStreamsObject().At(0).GetActivityStreamsNote()
+ noteURI := ap.GetJSONLDId(statusable)
+
+ // Get the OG status.
+ status, err := testStructs.State.DB.GetStatusByURI(ctx, noteURI.String())
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Process the Update.
+ err = testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityUpdate,
+ GTSModel: status, // original status
+ APObject: (ap.Statusable)(statusable),
+ Receiving: receivingAcct,
+ Requesting: requestingAcct,
+ })
+ suite.NoError(err)
+
+ // Wait for side effects to trigger:
+ // zork should have a mention notif.
+ if !testrig.WaitFor(func() bool {
+ _, err := testStructs.State.DB.GetNotification(
+ gtscontext.SetBarebones(ctx),
+ gtsmodel.NotificationMention,
+ receivingAcct.ID,
+ requestingAcct.ID,
+ status.ID,
+ )
+ return err == nil
+ }) {
+ suite.FailNow("timed out waiting for mention notif")
+ }
+}
+
+func (suite *FromFediAPITestSuite) TestCreateReplyRequest() {
+ var (
+ ctx = suite.T().Context()
+ testStructs = testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ requesting = suite.testAccounts["remote_account_1"]
+ receiving = suite.testAccounts["admin_account"]
+ testStatus = suite.testStatuses["admin_account_status_1"]
+ intReqURI = "http://fossbros-anonymous.io/requests/87fb1478-ac46-406a-8463-96ce05645219"
+ intURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/87fb1478-ac46-406a-8463-96ce05645219"
+ jsonStr = `{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://gotosocial.org/ns",
+ {
+ "sensitive": "as:sensitive"
+ }
+ ],
+ "type": "ReplyRequest",
+ "id": "` + intReqURI + `",
+ "actor": "` + requesting.URI + `",
+ "object": "` + testStatus.URI + `",
+ "to": "` + receiving.URI + `",
+ "instrument": {
+ "attributedTo": "` + requesting.URI + `",
+ "cc": "` + requesting.FollowersURI + `",
+ "content": "\u003cp\u003ethis is a reply!\u003c/p\u003e",
+ "id": "` + intURI + `",
+ "inReplyTo": "` + testStatus.URI + `",
+ "tag": {
+ "href": "` + receiving.URI + `",
+ "name": "@` + receiving.Username + `@localhost:8080",
+ "type": "Mention"
+ },
+ "to": "https://www.w3.org/ns/activitystreams#Public",
+ "type": "Note"
+ }
+}`
+ )
+ defer testrig.TearDownTestStructs(testStructs)
+
+ suite.T().Logf("testing reply request:\n\n%s", jsonStr)
+
+ // Decode the reply request + embedded statusable.
+ t, err := ap.DecodeType(ctx, io.NopCloser(bytes.NewBufferString(jsonStr)))
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ replyReq := t.(vocab.GoToSocialReplyRequest)
+ statusable := replyReq.GetActivityStreamsInstrument().At(0).GetActivityStreamsNote().(ap.Statusable)
+
+ // Create a pending interaction request in the
+ // database, as though the reply req had already
+ // passed through the federatingdb function.
+ intReq := &gtsmodel.InteractionRequest{
+ ID: id.NewULID(),
+ TargetStatusID: testStatus.ID,
+ TargetStatus: testStatus,
+ TargetAccountID: receiving.ID,
+ TargetAccount: receiving,
+ InteractingAccountID: requesting.ID,
+ InteractingAccount: requesting,
+ InteractionRequestURI: intReqURI,
+ InteractionURI: ap.GetJSONLDId(statusable).String(),
+ InteractionType: gtsmodel.InteractionReply,
+ Polite: util.Ptr(true),
+ Reply: nil, // Not settable yet.
+ }
+ if err := testStructs.State.DB.PutInteractionRequest(ctx, intReq); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Process the message.
+ if err = testStructs.Processor.Workers().ProcessFromFediAPI(
+ ctx,
+ &messages.FromFediAPI{
+ APObjectType: ap.ActivityReplyRequest,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: intReq,
+ APObject: statusable,
+ Receiving: receiving,
+ Requesting: requesting,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // The interaction request should be accepted.
+ intReq, err = testStructs.State.DB.GetInteractionRequestByID(ctx, intReq.ID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.WithinDuration(time.Now(), intReq.AcceptedAt, 1*time.Minute)
+ suite.NotEmpty(intReq.AuthorizationURI)
+ suite.NotEmpty(intReq.ResponseURI)
+
+ // Federator should send out an Accept that looks something like:
+ //
+ // {
+ // "@context": [
+ // "https://gotosocial.org/ns",
+ // "https://www.w3.org/ns/activitystreams"
+ // ],
+ // "actor": "http://localhost:8080/users/admin",
+ // "id": "http://localhost:8080/users/admin/accepts/01K2CV90660VRPZM39R35NMSG9",
+ // "object": {
+ // "actor": "http://fossbros-anonymous.io/users/foss_satan",
+ // "id": "http://fossbros-anonymous.io/requests/87fb1478-ac46-406a-8463-96ce05645219",
+ // "instrument": "http://fossbros-anonymous.io/users/foss_satan/statuses/87fb1478-ac46-406a-8463-96ce05645219",
+ // "object": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
+ // "type": "ReplyRequest"
+ // },
+ // "result": "http://localhost:8080/users/admin/authorizations/01K2CV90660VRPZM39R35NMSG9",
+ // "to": "http://fossbros-anonymous.io/users/foss_satan",
+ // "type": "Accept"
+ // }
+}
+
func TestFromFederatorTestSuite(t *testing.T) {
suite.Run(t, &FromFediAPITestSuite{})
}
diff --git a/internal/processing/workers/surface.go b/internal/processing/workers/surface.go
index 4dc58c433..e0e441479 100644
--- a/internal/processing/workers/surface.go
+++ b/internal/processing/workers/surface.go
@@ -18,13 +18,15 @@
package workers
import (
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/processing/conversations"
- "github.com/superseriousbusiness/gotosocial/internal/processing/stream"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/webpush"
+ "code.superseriousbusiness.org/gotosocial/internal/email"
+ "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/processing/conversations"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/webpush"
)
// Surface wraps functions for 'surfacing' the result
@@ -38,6 +40,8 @@ type Surface struct {
Converter *typeutils.Converter
Stream *stream.Processor
VisFilter *visibility.Filter
+ MuteFilter *mutes.Filter
+ StatusFilter *status.Filter
EmailSender email.Sender
WebPushSender webpush.Sender
Conversations *conversations.Processor
diff --git a/internal/processing/workers/surfaceemail.go b/internal/processing/workers/surfaceemail.go
index 56f33eaf3..219f395ad 100644
--- a/internal/processing/workers/surfaceemail.go
+++ b/internal/processing/workers/surfaceemail.go
@@ -22,13 +22,13 @@ import (
"errors"
"time"
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/email"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/uris"
"github.com/google/uuid"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
)
// emailUserReportClosed emails the user who created the
diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go
index fdbd5e3c1..15ad79b26 100644
--- a/internal/processing/workers/surfacenotify.go
+++ b/internal/processing/workers/surfacenotify.go
@@ -22,14 +22,14 @@ import (
"errors"
"strings"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/filter/status"
- "github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
- "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/util"
+ 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/util"
+ "code.superseriousbusiness.org/gotosocial/internal/util/xslices"
)
// notifyPendingReply notifies the account replied-to
@@ -58,8 +58,7 @@ func (s *Surface) notifyPendingReply(
// Ensure thread not muted
// by replied-to account.
- muted, err := s.State.DB.IsThreadMutedByAccount(
- ctx,
+ muted, err := s.State.DB.IsThreadMutedByAccount(ctx,
status.ThreadID,
status.InReplyToAccountID,
)
@@ -80,7 +79,8 @@ func (s *Surface) notifyPendingReply(
gtsmodel.NotificationPendingReply,
status.InReplyToAccount,
status.Account,
- status.ID,
+ status,
+ nil,
); err != nil {
return gtserror.Newf("error notifying replied-to account %s: %w", status.InReplyToAccountID, err)
}
@@ -99,54 +99,75 @@ func (s *Surface) notifyMentions(
for _, mention := range status.Mentions {
// Set status on the mention (stops
- // the below function populating it).
+ // notifyMention having to populate it).
mention.Status = status
- // Beforehand, ensure the passed mention is fully populated.
- if err := s.State.DB.PopulateMention(ctx, mention); err != nil {
- errs.Appendf("error populating mention %s: %w", mention.ID, err)
- continue
+ // Do the thing.
+ if err := s.notifyMention(ctx, mention); err != nil {
+ errs = append(errs, err)
}
+ }
- if mention.TargetAccount.IsRemote() {
- // no need to notify
- // remote accounts.
- continue
- }
+ return errs.Combine()
+}
- // Ensure thread not muted
- // by mentioned account.
- muted, err := s.State.DB.IsThreadMutedByAccount(
- ctx,
- status.ThreadID,
- mention.TargetAccountID,
+// notifyMention notifies the target
+// of the given mention that they've
+// been mentioned in a status.
+func (s *Surface) notifyMention(
+ ctx context.Context,
+ mention *gtsmodel.Mention,
+) error {
+ // Beforehand, ensure the passed mention is fully populated.
+ if err := s.State.DB.PopulateMention(ctx, mention); err != nil {
+ return gtserror.Newf(
+ "error populating mention %s: %w",
+ mention.ID, err,
)
- if err != nil {
- errs.Appendf("error checking status thread mute %s: %w", status.ThreadID, err)
- continue
- }
+ }
- if muted {
- // This mentioned account
- // has muted the thread.
- // Don't pester them.
- continue
- }
+ if mention.TargetAccount.IsRemote() {
+ // no need to notify
+ // remote accounts.
+ return nil
+ }
- // notify mentioned
- // by status author.
- if err := s.Notify(ctx,
- gtsmodel.NotificationMention,
- mention.TargetAccount,
- mention.OriginAccount,
- mention.StatusID,
- ); err != nil {
- errs.Appendf("error notifying mention target %s: %w", mention.TargetAccountID, err)
- continue
- }
+ // Ensure thread not muted
+ // by mentioned account.
+ muted, err := s.State.DB.IsThreadMutedByAccount(ctx,
+ mention.Status.ThreadID,
+ mention.TargetAccountID,
+ )
+ if err != nil {
+ return gtserror.Newf(
+ "error checking status thread mute %s: %w",
+ mention.Status.ThreadID, err,
+ )
}
- return errs.Combine()
+ if muted {
+ // This mentioned account
+ // has muted the thread.
+ // Don't pester them.
+ return nil
+ }
+
+ // Notify mentioned
+ // by status author.
+ if err := s.Notify(ctx,
+ gtsmodel.NotificationMention,
+ mention.TargetAccount,
+ mention.OriginAccount,
+ mention.Status,
+ nil,
+ ); err != nil {
+ return gtserror.Newf(
+ "error notifying mention target %s: %w",
+ mention.TargetAccountID, err,
+ )
+ }
+
+ return nil
}
// notifyFollowRequest notifies the target of the given
@@ -171,7 +192,8 @@ func (s *Surface) notifyFollowRequest(
gtsmodel.NotificationFollowRequest,
followReq.TargetAccount,
followReq.Account,
- "",
+ nil,
+ nil,
); err != nil {
return gtserror.Newf("error notifying follow target %s: %w", followReq.TargetAccountID, err)
}
@@ -223,7 +245,8 @@ func (s *Surface) notifyFollow(
gtsmodel.NotificationFollow,
follow.TargetAccount,
follow.Account,
- "",
+ nil,
+ nil,
); err != nil {
return gtserror.Newf("error notifying follow target %s: %w", follow.TargetAccountID, err)
}
@@ -253,7 +276,8 @@ func (s *Surface) notifyFave(
gtsmodel.NotificationFavourite,
fave.TargetAccount,
fave.Account,
- fave.StatusID,
+ fave.Status,
+ nil,
); err != nil {
return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err)
}
@@ -284,7 +308,8 @@ func (s *Surface) notifyPendingFave(
gtsmodel.NotificationPendingFave,
fave.TargetAccount,
fave.Account,
- fave.StatusID,
+ fave.Status,
+ nil,
); err != nil {
return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err)
}
@@ -317,8 +342,7 @@ func (s *Surface) notifyableFave(
// Ensure favee hasn't
// muted the thread.
- muted, err := s.State.DB.IsThreadMutedByAccount(
- ctx,
+ muted, err := s.State.DB.IsThreadMutedByAccount(ctx,
fave.Status.ThreadID,
fave.TargetAccountID,
)
@@ -357,7 +381,8 @@ func (s *Surface) notifyAnnounce(
gtsmodel.NotificationReblog,
boost.BoostOfAccount,
boost.Account,
- boost.ID,
+ boost,
+ nil,
); err != nil {
return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err)
}
@@ -388,7 +413,8 @@ func (s *Surface) notifyPendingAnnounce(
gtsmodel.NotificationPendingReblog,
boost.BoostOfAccount,
boost.Account,
- boost.ID,
+ boost,
+ nil,
); err != nil {
return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err)
}
@@ -426,8 +452,7 @@ func (s *Surface) notifyableAnnounce(
// Ensure boostee hasn't
// muted the thread.
- muted, err := s.State.DB.IsThreadMutedByAccount(
- ctx,
+ muted, err := s.State.DB.IsThreadMutedByAccount(ctx,
status.BoostOf.ThreadID,
status.BoostOfAccountID,
)
@@ -466,7 +491,8 @@ func (s *Surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status)
gtsmodel.NotificationPoll,
status.Account,
status.Account,
- status.ID,
+ status,
+ nil,
); err != nil {
errs.Appendf("error notifying poll author: %w", err)
}
@@ -485,7 +511,8 @@ func (s *Surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status)
gtsmodel.NotificationPoll,
vote.Account,
status.Account,
- status.ID,
+ status,
+ nil,
); err != nil {
errs.Appendf("error notifying poll voter %s: %w", vote.AccountID, err)
continue
@@ -524,7 +551,8 @@ func (s *Surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro
gtsmodel.NotificationAdminSignup,
mod,
newUser.Account,
- "",
+ nil,
+ nil,
); err != nil {
errs.Appendf("error notifying moderator %s: %w", mod.ID, err)
continue
@@ -534,19 +562,68 @@ func (s *Surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro
return errs.Combine()
}
+func (s *Surface) notifyStatusEdit(
+ ctx context.Context,
+ status *gtsmodel.Status,
+ edit *gtsmodel.StatusEdit,
+) error {
+ // Get local-only interactions (we can't/don't notify remotes).
+ interactions, err := s.State.DB.GetStatusInteractions(ctx, status.ID, true)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return gtserror.Newf("db error getting status interactions: %w", err)
+ }
+
+ // Deduplicate interactions by account ID,
+ // we don't need to notify someone twice
+ // if they've both boosted *and* replied
+ // to an edited status, for example.
+ interactions = xslices.DeduplicateFunc(
+ interactions,
+ func(v gtsmodel.Interaction) string {
+ return v.GetAccount().ID
+ },
+ )
+
+ // Notify each account that's
+ // interacted with the status.
+ var errs gtserror.MultiError
+ for _, i := range interactions {
+ targetAcct := i.GetAccount()
+ if targetAcct.ID == status.AccountID {
+ // Don't notify an account
+ // if they've interacted
+ // with their *own* status.
+ continue
+ }
+
+ if err := s.Notify(ctx,
+ gtsmodel.NotificationUpdate,
+ targetAcct,
+ status.Account,
+ status,
+ edit,
+ ); err != nil {
+ errs.Appendf("error notifying status edit: %w", err)
+ continue
+ }
+ }
+
+ return errs.Combine()
+}
+
func getNotifyLockURI(
notificationType gtsmodel.NotificationType,
targetAccount *gtsmodel.Account,
originAccount *gtsmodel.Account,
- statusID string,
+ statusOrEditID string,
) string {
builder := strings.Builder{}
builder.WriteString("notification:?")
builder.WriteString("type=" + notificationType.String())
- builder.WriteString("&target=" + targetAccount.URI)
- builder.WriteString("&origin=" + originAccount.URI)
- if statusID != "" {
- builder.WriteString("&statusID=" + statusID)
+ builder.WriteString("&targetAcct=" + targetAccount.URI)
+ builder.WriteString("&originAcct=" + originAccount.URI)
+ if statusOrEditID != "" {
+ builder.WriteString("&statusOrEditID=" + statusOrEditID)
}
return builder.String()
}
@@ -561,28 +638,38 @@ func getNotifyLockURI(
// for non-local first.
//
// targetAccount and originAccount must be
-// set, but statusID can be an empty string.
+// set, but statusOrEditID can be empty.
func (s *Surface) Notify(
ctx context.Context,
notificationType gtsmodel.NotificationType,
targetAccount *gtsmodel.Account,
originAccount *gtsmodel.Account,
- statusID string,
+ status *gtsmodel.Status,
+ edit *gtsmodel.StatusEdit,
) error {
if targetAccount.IsRemote() {
// nothing to do.
return nil
}
+ // Get status / edit ID
+ // if either was provided.
+ // (prefer edit though!)
+ var statusOrEditID string
+ if edit != nil {
+ statusOrEditID = edit.ID
+ } else if status != nil {
+ statusOrEditID = status.ID
+ }
+
// We're doing state-y stuff so get a
// lock on this combo of notif params.
- lockURI := getNotifyLockURI(
+ unlock := s.State.ProcessingLocks.Lock(getNotifyLockURI(
notificationType,
targetAccount,
originAccount,
- statusID,
- )
- unlock := s.State.ProcessingLocks.Lock(lockURI)
+ statusOrEditID,
+ ))
// Wrap the unlock so we
// can do granular unlocking.
@@ -596,7 +683,7 @@ func (s *Surface) Notify(
notificationType,
targetAccount.ID,
originAccount.ID,
- statusID,
+ statusOrEditID,
); err == nil {
// Notification exists;
// nothing to do.
@@ -615,7 +702,7 @@ func (s *Surface) Notify(
TargetAccount: targetAccount,
OriginAccountID: originAccount.ID,
OriginAccount: originAccount,
- StatusID: statusID,
+ StatusOrEditID: statusOrEditID,
}
if err := s.State.DB.PutNotification(ctx, notif); err != nil {
@@ -626,29 +713,72 @@ func (s *Surface) Notify(
// with the state-y stuff.
unlock()
- // Stream notification to the user.
- filters, err := s.State.DB.GetFiltersForAccountID(ctx, targetAccount.ID)
+ // Check whether origin account is muted by target account.
+ muted, err := s.MuteFilter.AccountNotificationsMuted(ctx,
+ targetAccount,
+ originAccount,
+ )
if err != nil {
- return gtserror.Newf("couldn't retrieve filters for account %s: %w", targetAccount.ID, err)
+ return gtserror.Newf("error checking account mute: %w", err)
}
- mutes, err := s.State.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), targetAccount.ID, nil)
- if err != nil {
- return gtserror.Newf("couldn't retrieve mutes for account %s: %w", targetAccount.ID, err)
+ if muted {
+ // Don't notify.
+ return nil
}
- compiledMutes := usermute.NewCompiledUserMuteList(mutes)
- apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif, filters, compiledMutes)
- if err != nil {
- if errors.Is(err, status.ErrHideStatus) {
+ var filtered []apimodel.FilterResult
+
+ if status != nil {
+ // Check whether status is muted by the target account.
+ muted, err := s.MuteFilter.StatusNotificationsMuted(ctx,
+ targetAccount,
+ status,
+ )
+ if err != nil {
+ return gtserror.Newf("error checking status mute: %w", err)
+ }
+
+ if muted {
+ // Don't notify.
+ return nil
+ }
+
+ var hide bool
+
+ // Check whether notification status is filtered by requester in notifs.
+ filtered, hide, err = s.StatusFilter.StatusFilterResultsInContext(ctx,
+ targetAccount,
+ status,
+ gtsmodel.FilterContextNotifications,
+ )
+ if err != nil {
+ return gtserror.Newf("error checking status filtering: %w", err)
+ }
+
+ if hide {
+ // Don't notify.
return nil
}
+ }
+
+ // Convert notification to frontend API model for streaming / web push.
+ apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif)
+ if err != nil {
return gtserror.Newf("error converting notification to api representation: %w", err)
}
+
+ if apiNotif.Status != nil {
+ // Set filter results on status,
+ // in case any were set above.
+ apiNotif.Status.Filtered = filtered
+ }
+
+ // Stream notification to the user.
s.Stream.Notify(ctx, targetAccount, apiNotif)
// Send Web Push notification to the user.
- if err = s.WebPushSender.Send(ctx, notif, filters, compiledMutes); err != nil {
+ if err = s.WebPushSender.Send(ctx, notif, apiNotif); err != nil {
return gtserror.Newf("error sending Web Push notifications: %w", err)
}
diff --git a/internal/processing/workers/surfacenotify_test.go b/internal/processing/workers/surfacenotify_test.go
index 6444314e2..a5124a3af 100644
--- a/internal/processing/workers/surfacenotify_test.go
+++ b/internal/processing/workers/surfacenotify_test.go
@@ -18,17 +18,17 @@
package workers_test
import (
- "context"
"sync"
"testing"
"time"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/mutes"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/workers"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/processing/workers"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
type SurfaceNotifyTestSuite struct {
@@ -44,13 +44,14 @@ func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() {
Converter: testStructs.TypeConverter,
Stream: testStructs.Processor.Stream(),
VisFilter: visibility.NewFilter(testStructs.State),
+ MuteFilter: mutes.NewFilter(testStructs.State),
EmailSender: testStructs.EmailSender,
WebPushSender: testStructs.WebPushSender,
Conversations: testStructs.Processor.Conversations(),
}
var (
- ctx = context.Background()
+ ctx = suite.T().Context()
notificationType = gtsmodel.NotificationFollow
targetAccount = suite.testAccounts["local_account_1"]
originAccount = suite.testAccounts["local_account_2"]
@@ -75,7 +76,8 @@ func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() {
notificationType,
targetAccount,
originAccount,
- "",
+ nil,
+ nil,
); err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/processing/workers/surfacetimeline.go b/internal/processing/workers/surfacetimeline.go
index b071bd72e..0e30f54f7 100644
--- a/internal/processing/workers/surfacetimeline.go
+++ b/internal/processing/workers/surfacetimeline.go
@@ -19,17 +19,15 @@ package workers
import (
"context"
- "errors"
-
- statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
- "github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
- "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/stream"
- "github.com/superseriousbusiness/gotosocial/internal/timeline"
- "github.com/superseriousbusiness/gotosocial/internal/util"
+
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/cache/timeline"
+ "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/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
)
// timelineAndNotifyStatus inserts the given status into the HOME
@@ -40,6 +38,7 @@ import (
// the account, notifications for any local accounts that want
// to know when this account posts, and conversations containing the status.
func (s *Surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error {
+
// Ensure status fully populated; including account, mentions, etc.
if err := s.State.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status with id %s: %w", status.ID, err)
@@ -62,6 +61,11 @@ func (s *Surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.
})
}
+ // Stream the status for public timelines for all local users as update msg.
+ if err := s.timelineStatusForPublic(ctx, status, s.Stream.Update); err != nil {
+ return err
+ }
+
// Timeline the status for each local follower of this account. This will
// also handle notifying any followers with notify set to true on their follow.
homeTimelinedAccountIDs := s.timelineAndNotifyStatusForFollowers(ctx, status, follows)
@@ -71,16 +75,18 @@ func (s *Surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.
return gtserror.Newf("error timelining status %s for tag followers: %w", status.ID, err)
}
- // Notify each local account that's mentioned by this status.
+ // Notify each local account mentioned by status.
if err := s.notifyMentions(ctx, status); err != nil {
return gtserror.Newf("error notifying status mentions for status %s: %w", status.ID, err)
}
- // Update any conversations containing this status, and send conversation notifications.
+ // Update any conversations containing this status, and get notifications for them.
notifications, err := s.Conversations.UpdateConversationsForStatus(ctx, status)
if err != nil {
return gtserror.Newf("error updating conversations for status %s: %w", status.ID, err)
}
+
+ // Stream these conversation notfications.
for _, notification := range notifications {
s.Stream.Conversation(ctx, notification.AccountID, notification.Conversation)
}
@@ -88,6 +94,95 @@ func (s *Surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.
return nil
}
+// timelineStatusForPublic timelines the given status
+// to LOCAL and PUBLIC (i.e. federated) timelines.
+func (s *Surface) timelineStatusForPublic(
+ ctx context.Context,
+ status *gtsmodel.Status,
+ streamFn func(context.Context, *gtsmodel.Account, *apimodel.Status, string),
+) error {
+ // Nil check function
+ // outside main loop.
+ if streamFn == nil {
+ panic("nil func")
+ }
+
+ if status.Visibility != gtsmodel.VisibilityPublic ||
+ status.BoostOfID != "" {
+ // Fast code path, if it's not "public"
+ // or a boost, don't public timeline it.
+ return nil
+ }
+
+ // Get a list of all our local users.
+ users, err := s.State.DB.GetAllUsers(ctx)
+ if err != nil {
+ return gtserror.Newf("error getting local users: %v", err)
+ }
+
+ // Iterate our list of users.
+ isLocal := status.IsLocal()
+ for _, user := range users {
+
+ // Check whether this status should be visible this user on public timelines.
+ visible, err := s.VisFilter.StatusPublicTimelineable(ctx, user.Account, status)
+ if err != nil {
+ log.Errorf(ctx, "error checking status %s visibility: %v", status.URI, err)
+ continue
+ }
+
+ if !visible {
+ continue
+ }
+
+ // Check whether this status is muted in any form by this user.
+ muted, err := s.MuteFilter.StatusMuted(ctx, user.Account, status)
+ if err != nil {
+ log.Errorf(ctx, "error checking status %s mutes: %v", status.URI, err)
+ continue
+ }
+
+ if muted {
+ continue
+ }
+
+ // Get status-filter results for this status in context by this user.
+ filtered, hidden, err := s.StatusFilter.StatusFilterResultsInContext(ctx,
+ user.Account,
+ status,
+ gtsmodel.FilterContextPublic,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error getting status %s filter results: %v", status.URI, err)
+ continue
+ }
+
+ if hidden {
+ continue
+ }
+
+ // Now all checks / filters are passed, convert status to frontend model.
+ apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, user.Account)
+ if err != nil {
+ log.Errorf(ctx, "error converting status %s: %v", status.URI, err)
+ continue
+ }
+
+ // Set API model filter results.
+ apiStatus.Filtered = filtered
+
+ if isLocal {
+ // This is local status, send it to local timeline stream.
+ streamFn(ctx, user.Account, apiStatus, stream.TimelineLocal)
+ }
+
+ // For public timeline stream, send all local / remote statuses.
+ streamFn(ctx, user.Account, apiStatus, stream.TimelinePublic)
+ }
+
+ return nil
+}
+
// timelineAndNotifyStatusForFollowers iterates through the given
// slice of followers of the account that posted the given status,
// adding the status to list timelines + home timelines of each
@@ -119,11 +214,12 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
// if something is hometimelineable according to this filter,
// it's also eligible to appear in exclusive lists,
// even if it ultimately doesn't appear on the home timeline.
- timelineable, err := s.VisFilter.StatusHomeTimelineable(
- ctx, follow.Account, status,
+ timelineable, err := s.VisFilter.StatusHomeTimelineable(ctx,
+ follow.Account,
+ status,
)
if err != nil {
- log.Errorf(ctx, "error checking status home visibility for follow: %v", err)
+ log.Errorf(ctx, "error checking status home visibility: %v", err)
continue
}
@@ -132,11 +228,18 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
continue
}
- // Get relevant filters and mutes for this follow's account.
- // (note the origin account of the follow is receiver of status).
- filters, mutes, err := s.getFiltersAndMutes(ctx, follow.AccountID)
+ // Check if the status is muted by this follower.
+ muted, err := s.MuteFilter.StatusMuted(ctx,
+ follow.Account,
+ status,
+ )
if err != nil {
- log.Error(ctx, err)
+ log.Errorf(ctx, "error checking status mute: %v", err)
+ continue
+ }
+
+ if muted {
+ // Nothing to do.
continue
}
@@ -144,8 +247,6 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
listTimelined, exclusive, err := s.listTimelineStatusForFollow(ctx,
status,
follow,
- filters,
- mutes,
)
if err != nil {
log.Errorf(ctx, "error list timelining status: %v", err)
@@ -161,21 +262,14 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
// Add status to home timeline for owner of
// this follow (origin account), if applicable.
- homeTimelined, err = s.timelineStatus(ctx,
- s.State.Timelines.Home.IngestOne,
- follow.AccountID, // home timelines are keyed by account ID
+ if homeTimelined = s.timelineStatus(ctx,
+ s.State.Caches.Timelines.Home.MustGet(follow.AccountID),
follow.Account,
status,
stream.TimelineHome,
- filters,
- mutes,
- )
- if err != nil {
- log.Errorf(ctx, "error home timelining status: %v", err)
- continue
- }
+ gtsmodel.FilterContextHome,
+ ); homeTimelined {
- if homeTimelined {
// If hometimelined, add to list of returned account IDs.
homeTimelinedAccountIDs = append(homeTimelinedAccountIDs, follow.AccountID)
}
@@ -210,7 +304,8 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
gtsmodel.NotificationStatus,
follow.Account,
status.Account,
- status.ID,
+ status,
+ nil,
); err != nil {
log.Errorf(ctx, "error notifying status for account: %v", err)
continue
@@ -230,8 +325,6 @@ func (s *Surface) listTimelineStatusForFollow(
ctx context.Context,
status *gtsmodel.Status,
follow *gtsmodel.Follow,
- filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
) (timelined bool, exclusive bool, err error) {
// Get all lists that contain this given follow.
@@ -261,22 +354,14 @@ func (s *Surface) listTimelineStatusForFollow(
exclusive = exclusive || *list.Exclusive
// At this point we are certain this status
- // should be included in the timeline of the
- // list that this list entry belongs to.
- listTimelined, err := s.timelineStatus(
- ctx,
- s.State.Timelines.List.IngestOne,
- list.ID, // list timelines are keyed by list ID
+ // should be included in timeline of this list.
+ listTimelined := s.timelineStatus(ctx,
+ s.State.Caches.Timelines.List.MustGet(list.ID),
follow.Account,
status,
stream.TimelineList+":"+list.ID, // key streamType to this specific list
- filters,
- mutes,
+ gtsmodel.FilterContextHome,
)
- if err != nil {
- log.Errorf(ctx, "error adding status to list timeline: %v", err)
- continue
- }
// Update flag based on if timelined.
timelined = timelined || listTimelined
@@ -285,22 +370,6 @@ func (s *Surface) listTimelineStatusForFollow(
return timelined, exclusive, nil
}
-// getFiltersAndMutes returns an account's filters and mutes.
-func (s *Surface) getFiltersAndMutes(ctx context.Context, accountID string) ([]*gtsmodel.Filter, *usermute.CompiledUserMuteList, error) {
- filters, err := s.State.DB.GetFiltersForAccountID(ctx, accountID)
- if err != nil {
- return nil, nil, gtserror.Newf("couldn't retrieve filters for account %s: %w", accountID, err)
- }
-
- mutes, err := s.State.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), accountID, nil)
- if err != nil {
- return nil, nil, gtserror.Newf("couldn't retrieve mutes for account %s: %w", accountID, err)
- }
-
- compiledMutes := usermute.NewCompiledUserMuteList(mutes)
- return filters, compiledMutes, err
-}
-
// listEligible checks if the given status is eligible
// for inclusion in the list that that the given listEntry
// belongs to, based on the replies policy of the list.
@@ -367,53 +436,56 @@ func (s *Surface) listEligible(
}
}
-// timelineStatus uses the provided ingest function to put the given
-// status in a timeline with the given ID, if it's timelineable.
-//
-// If the status was inserted into the timeline, true will be returned
-// + it will also be streamed to the user using the given streamType.
+// timelineStatus will insert the given status into the given timeline, if it's
+// timelineable. if the status was inserted into the timeline, true will be returned.
func (s *Surface) timelineStatus(
ctx context.Context,
- ingest func(context.Context, string, timeline.Timelineable) (bool, error),
- timelineID string,
+ timeline *timeline.StatusTimeline,
account *gtsmodel.Account,
status *gtsmodel.Status,
streamType string,
- filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
-) (bool, error) {
+ filterCtx gtsmodel.FilterContext,
+) bool {
+ // Check whether status is filtered in this context by timeline account.
+ filtered, hide, err := s.StatusFilter.StatusFilterResultsInContext(ctx,
+ account,
+ status,
+ filterCtx,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error filtering status %s: %v", status.URI, err)
+ }
- // Ingest status into given timeline using provided function.
- if inserted, err := ingest(ctx, timelineID, status); err != nil &&
- !errors.Is(err, statusfilter.ErrHideStatus) {
- err := gtserror.Newf("error ingesting status %s: %w", status.ID, err)
- return false, err
- } else if !inserted {
- // Nothing more to do.
- return false, nil
+ if hide {
+ // Don't even show to
+ // timeline account.
+ return false
}
- // Convert updated database model to frontend model.
- apiStatus, err := s.Converter.StatusToAPIStatus(ctx,
+ // Attempt to convert status to frontend API representation,
+ // this will check whether status is filtered / muted.
+ apiModel, err := s.Converter.StatusToAPIStatus(ctx,
status,
account,
- statusfilter.FilterContextHome,
- filters,
- mutes,
)
- if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) {
- err := gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
- return true, err
+ if err != nil {
+ log.Error(ctx, "error converting status %s to frontend: %v", status.URI, err)
+ } else {
+
+ // Attach any filter results.
+ apiModel.Filtered = filtered
}
- if apiStatus != nil {
- // The status was inserted so stream it to the user.
- s.Stream.Update(ctx, account, apiStatus, streamType)
- return true, nil
+ // Insert status to timeline cache regardless of
+ // if API model was succesfully prepared or not.
+ repeatBoost := timeline.InsertOne(status, apiModel)
+
+ if !repeatBoost {
+ // Only stream if not repeated boost of recent status.
+ s.Stream.Update(ctx, account, apiModel, streamType)
}
- // Status was hidden.
- return false, nil
+ return true
}
// timelineAndNotifyStatusForTagFollowers inserts the status into the
@@ -435,32 +507,17 @@ func (s *Surface) timelineAndNotifyStatusForTagFollowers(
status = status.BoostOf
}
+ var errs gtserror.MultiError
+
// Insert the status into the home timeline of each tag follower.
- errs := gtserror.MultiError{}
for _, tagFollowerAccount := range tagFollowerAccounts {
- filters, mutes, err := s.getFiltersAndMutes(ctx, tagFollowerAccount.ID)
- if err != nil {
- errs.Append(err)
- continue
- }
-
- if _, err := s.timelineStatus(
- ctx,
- s.State.Timelines.Home.IngestOne,
- tagFollowerAccount.ID, // home timelines are keyed by account ID
+ _ = s.timelineStatus(ctx,
+ s.State.Caches.Timelines.Home.MustGet(tagFollowerAccount.ID),
tagFollowerAccount,
status,
stream.TimelineHome,
- filters,
- mutes,
- ); err != nil {
- errs.Appendf(
- "error inserting status %s into home timeline for account %s: %w",
- status.ID,
- tagFollowerAccount.ID,
- err,
- )
- }
+ gtsmodel.FilterContextHome,
+ )
}
return errs.Combine()
@@ -550,39 +607,6 @@ func (s *Surface) tagFollowersForStatus(
return visibleTagFollowerAccounts, errs.Combine()
}
-// deleteStatusFromTimelines completely removes the given status from all timelines.
-// It will also stream deletion of the status to all open streams.
-func (s *Surface) deleteStatusFromTimelines(ctx context.Context, statusID string) error {
- if err := s.State.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil {
- return err
- }
- if err := s.State.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil {
- return err
- }
- s.Stream.Delete(ctx, statusID)
- return nil
-}
-
-// invalidateStatusFromTimelines does cache invalidation on the given status by
-// unpreparing it from all timelines, forcing it to be prepared again (with updated
-// stats, boost counts, etc) next time it's fetched by the timeline owner. This goes
-// both for the status itself, and for any boosts of the status.
-func (s *Surface) invalidateStatusFromTimelines(ctx context.Context, statusID string) {
- if err := s.State.Timelines.Home.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
- log.
- WithContext(ctx).
- WithField("statusID", statusID).
- Errorf("error unpreparing status from home timelines: %v", err)
- }
-
- if err := s.State.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
- log.
- WithContext(ctx).
- WithField("statusID", statusID).
- Errorf("error unpreparing status from list timelines: %v", err)
- }
-}
-
// timelineStatusUpdate looks up HOME and LIST timelines of accounts
// that follow the the status author or tags and pushes edit messages into any
// active streams.
@@ -612,6 +636,11 @@ func (s *Surface) timelineStatusUpdate(ctx context.Context, status *gtsmodel.Sta
})
}
+ // Stream the status update for public timelines for all of our local users.
+ if err := s.timelineStatusForPublic(ctx, status, s.Stream.StatusUpdate); err != nil {
+ return err
+ }
+
// Push updated status to streams for each local follower of this account.
homeTimelinedAccountIDs := s.timelineStatusUpdateForFollowers(ctx, status, follows)
@@ -660,20 +689,10 @@ func (s *Surface) timelineStatusUpdateForFollowers(
continue
}
- // Get relevant filters and mutes for this follow's account.
- // (note the origin account of the follow is receiver of status).
- filters, mutes, err := s.getFiltersAndMutes(ctx, follow.AccountID)
- if err != nil {
- log.Error(ctx, err)
- continue
- }
-
// Add status to relevant lists for this follow, if applicable.
_, exclusive, err := s.listTimelineStatusUpdateForFollow(ctx,
status,
follow,
- filters,
- mutes,
)
if err != nil {
log.Errorf(ctx, "error list timelining status: %v", err)
@@ -693,8 +712,6 @@ func (s *Surface) timelineStatusUpdateForFollowers(
follow.Account,
status,
stream.TimelineHome,
- filters,
- mutes,
)
if err != nil {
log.Errorf(ctx, "error home timelining status: %v", err)
@@ -719,8 +736,6 @@ func (s *Surface) listTimelineStatusUpdateForFollow(
ctx context.Context,
status *gtsmodel.Status,
follow *gtsmodel.Follow,
- filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
) (bool, bool, error) {
// Get all lists that contain this given follow.
@@ -759,8 +774,6 @@ func (s *Surface) listTimelineStatusUpdateForFollow(
follow.Account,
status,
stream.TimelineList+":"+list.ID, // key streamType to this specific list
- filters,
- mutes,
)
if err != nil {
log.Errorf(ctx, "error adding status to list timeline: %v", err)
@@ -783,31 +796,35 @@ func (s *Surface) timelineStreamStatusUpdate(
account *gtsmodel.Account,
status *gtsmodel.Status,
streamType string,
- filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
) (bool, error) {
+ // Check whether status is filtered in this context by timeline account.
+ filtered, hide, err := s.StatusFilter.StatusFilterResultsInContext(ctx,
+ account,
+ status,
+ gtsmodel.FilterContextHome,
+ )
+ if err != nil {
+ return false, gtserror.Newf("error filtering status: %w", err)
+ }
+
+ if hide {
+ // Don't even show to
+ // timeline account.
+ return false, nil
+ }
// Convert updated database model to frontend model.
apiStatus, err := s.Converter.StatusToAPIStatus(ctx,
status,
account,
- statusfilter.FilterContextHome,
- filters,
- mutes,
)
-
- switch {
- case err == nil:
- // no issue.
-
- case errors.Is(err, statusfilter.ErrHideStatus):
- // Don't put this status in the stream.
- return false, nil
-
- default:
+ if err != nil {
return false, gtserror.Newf("error converting status: %w", err)
}
+ // Attach any filter results.
+ apiStatus.Filtered = filtered
+
// The status was updated so stream it to the user.
s.Stream.StatusUpdate(ctx, account, apiStatus, streamType)
@@ -835,19 +852,11 @@ func (s *Surface) timelineStatusUpdateForTagFollowers(
// Stream the update to the home timeline of each tag follower.
errs := gtserror.MultiError{}
for _, tagFollowerAccount := range tagFollowerAccounts {
- filters, mutes, err := s.getFiltersAndMutes(ctx, tagFollowerAccount.ID)
- if err != nil {
- errs.Append(err)
- continue
- }
-
if _, err := s.timelineStreamStatusUpdate(
ctx,
tagFollowerAccount,
status,
stream.TimelineHome,
- filters,
- mutes,
); err != nil {
errs.Appendf(
"error updating status %s on home timeline for account %s: %w",
@@ -859,3 +868,53 @@ func (s *Surface) timelineStatusUpdateForTagFollowers(
}
return errs.Combine()
}
+
+// deleteStatusFromTimelines completely removes the given status from all timelines.
+// It will also stream deletion of the status to all open streams.
+func (s *Surface) deleteStatusFromTimelines(ctx context.Context, statusID string) {
+ s.State.Caches.Timelines.Home.RemoveByStatusIDs(statusID)
+ s.State.Caches.Timelines.List.RemoveByStatusIDs(statusID)
+ s.Stream.Delete(ctx, statusID)
+}
+
+// invalidateStatusFromTimelines does cache invalidation on the given status by
+// unpreparing it from all timelines, forcing it to be prepared again (with updated
+// stats, boost counts, etc) next time it's fetched by the timeline owner. This goes
+// both for the status itself, and for any boosts of the status.
+func (s *Surface) invalidateStatusFromTimelines(statusID string) {
+ s.State.Caches.Timelines.Home.UnprepareByStatusIDs(statusID)
+ s.State.Caches.Timelines.List.UnprepareByStatusIDs(statusID)
+}
+
+// removeTimelineEntriesByAccount removes all cached timeline entries authored by account ID.
+func (s *Surface) removeTimelineEntriesByAccount(accountID string) {
+ s.State.Caches.Timelines.Home.RemoveByAccountIDs(accountID)
+ s.State.Caches.Timelines.List.RemoveByAccountIDs(accountID)
+}
+
+// removeTimelineEntriesByAccount invalidates all cached timeline entries authored by account ID.
+func (s *Surface) invalidateTimelineEntriesByAccount(accountID string) {
+ s.State.Caches.Timelines.Home.UnprepareByAccountIDs(accountID)
+ s.State.Caches.Timelines.List.UnprepareByAccountIDs(accountID)
+}
+
+func (s *Surface) removeRelationshipFromTimelines(ctx context.Context, timelineAccountID string, targetAccountID string) {
+ // Remove all statuses by target account
+ // from given account's home timeline.
+ s.State.Caches.Timelines.Home.
+ MustGet(timelineAccountID).
+ RemoveByAccountIDs(targetAccountID)
+
+ // Get the IDs of all the lists owned by the given account ID.
+ listIDs, err := s.State.DB.GetListIDsByAccountID(ctx, timelineAccountID)
+ if err != nil {
+ log.Errorf(ctx, "error getting lists for account %s: %v", timelineAccountID, err)
+ }
+
+ for _, listID := range listIDs {
+ // Remove all statuses by target account
+ // from given account's list timelines.
+ s.State.Caches.Timelines.List.MustGet(listID).
+ RemoveByAccountIDs(targetAccountID)
+ }
+}
diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go
index b358dc951..0aa0febf0 100644
--- a/internal/processing/workers/util.go
+++ b/internal/processing/workers/util.go
@@ -21,17 +21,16 @@ import (
"context"
"errors"
- 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"
- "github.com/superseriousbusiness/gotosocial/internal/processing/account"
- "github.com/superseriousbusiness/gotosocial/internal/processing/media"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "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/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/account"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/media"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
)
// util provides util functions used by both
@@ -172,15 +171,11 @@ func (u *utils) wipeStatus(
}
// Remove the boost from any and all timelines.
- if err := u.surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil {
- errs.Appendf("error deleting boost from timelines: %w", err)
- }
+ u.surface.deleteStatusFromTimelines(ctx, boost.ID)
}
// Delete the status itself from any and all timelines.
- if err := u.surface.deleteStatusFromTimelines(ctx, status.ID); err != nil {
- errs.Appendf("error deleting status from timelines: %w", err)
- }
+ u.surface.deleteStatusFromTimelines(ctx, status.ID)
// Delete this status from any conversations it's part of.
if err := u.state.DB.DeleteStatusFromConversations(ctx, status.ID); err != nil {
@@ -289,250 +284,13 @@ func (u *utils) redirectFollowers(
return true
}
-func (u *utils) incrementStatusesCount(
- ctx context.Context,
- account *gtsmodel.Account,
- status *gtsmodel.Status,
-) error {
- // Lock on this account since we're changing stats.
- unlock := u.state.ProcessingLocks.Lock(account.URI)
- defer unlock()
-
- // Ensure account stats are populated.
- if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
- return gtserror.Newf("db error getting account stats: %w", err)
- }
-
- // Update status meta for account.
- *account.Stats.StatusesCount++
- account.Stats.LastStatusAt = status.CreatedAt
-
- // Update details in the database for stats.
- if err := u.state.DB.UpdateAccountStats(ctx,
- account.Stats,
- "statuses_count",
- "last_status_at",
- ); err != nil {
- return gtserror.Newf("db error updating account stats: %w", err)
- }
-
- return nil
-}
-
-func (u *utils) decrementStatusesCount(
- ctx context.Context,
- account *gtsmodel.Account,
- status *gtsmodel.Status,
-) error {
- // Lock on this account since we're changing stats.
- unlock := u.state.ProcessingLocks.Lock(account.URI)
- defer unlock()
-
- // Ensure account stats are populated.
- if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
- return gtserror.Newf("db error getting account stats: %w", err)
- }
-
- // Update status meta for account (safely checking for zero value).
- *account.Stats.StatusesCount = util.Decr(*account.Stats.StatusesCount)
-
- if !status.PinnedAt.IsZero() {
- // Update status pinned count for account (safely checking for zero value).
- *account.Stats.StatusesPinnedCount = util.Decr(*account.Stats.StatusesPinnedCount)
- }
-
- // Update details in the database for stats.
- if err := u.state.DB.UpdateAccountStats(ctx,
- account.Stats,
- "statuses_count",
- "statuses_pinned_count",
- ); err != nil {
- return gtserror.Newf("db error updating account stats: %w", err)
- }
-
- return nil
-}
-
-func (u *utils) incrementFollowersCount(
- ctx context.Context,
- account *gtsmodel.Account,
-) error {
- // Lock on this account since we're changing stats.
- unlock := u.state.ProcessingLocks.Lock(account.URI)
- defer unlock()
-
- // Ensure account stats are populated.
- if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
- return gtserror.Newf("db error getting account stats: %w", err)
- }
-
- // Update stats by incrementing followers
- // count by one and setting last posted.
- *account.Stats.FollowersCount++
- if err := u.state.DB.UpdateAccountStats(
- ctx,
- account.Stats,
- "followers_count",
- ); err != nil {
- return gtserror.Newf("db error updating account stats: %w", err)
- }
-
- return nil
-}
-
-func (u *utils) decrementFollowersCount(
- ctx context.Context,
- account *gtsmodel.Account,
-) error {
- // Lock on this account since we're changing stats.
- unlock := u.state.ProcessingLocks.Lock(account.URI)
- defer unlock()
-
- // Ensure account stats are populated.
- if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
- return gtserror.Newf("db error getting account stats: %w", err)
- }
-
- // Update stats by decrementing
- // followers count by one.
- //
- // Clamp to 0 to avoid funny business.
- *account.Stats.FollowersCount--
- if *account.Stats.FollowersCount < 0 {
- *account.Stats.FollowersCount = 0
- }
- if err := u.state.DB.UpdateAccountStats(
- ctx,
- account.Stats,
- "followers_count",
- ); err != nil {
- return gtserror.Newf("db error updating account stats: %w", err)
- }
-
- return nil
-}
-
-func (u *utils) incrementFollowingCount(
- ctx context.Context,
- account *gtsmodel.Account,
-) error {
- // Lock on this account since we're changing stats.
- unlock := u.state.ProcessingLocks.Lock(account.URI)
- defer unlock()
-
- // Ensure account stats are populated.
- if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
- return gtserror.Newf("db error getting account stats: %w", err)
- }
-
- // Update stats by incrementing
- // followers count by one.
- *account.Stats.FollowingCount++
- if err := u.state.DB.UpdateAccountStats(
- ctx,
- account.Stats,
- "following_count",
- ); err != nil {
- return gtserror.Newf("db error updating account stats: %w", err)
- }
-
- return nil
-}
-
-func (u *utils) decrementFollowingCount(
- ctx context.Context,
- account *gtsmodel.Account,
-) error {
- // Lock on this account since we're changing stats.
- unlock := u.state.ProcessingLocks.Lock(account.URI)
- defer unlock()
-
- // Ensure account stats are populated.
- if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
- return gtserror.Newf("db error getting account stats: %w", err)
- }
-
- // Update stats by decrementing
- // following count by one.
- //
- // Clamp to 0 to avoid funny business.
- *account.Stats.FollowingCount--
- if *account.Stats.FollowingCount < 0 {
- *account.Stats.FollowingCount = 0
- }
- if err := u.state.DB.UpdateAccountStats(
- ctx,
- account.Stats,
- "following_count",
- ); err != nil {
- return gtserror.Newf("db error updating account stats: %w", err)
- }
-
- return nil
-}
-
-func (u *utils) incrementFollowRequestsCount(
- ctx context.Context,
- account *gtsmodel.Account,
-) error {
- // Lock on this account since we're changing stats.
- unlock := u.state.ProcessingLocks.Lock(account.URI)
- defer unlock()
-
- // Ensure account stats are populated.
- if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
- return gtserror.Newf("db error getting account stats: %w", err)
- }
-
- // Update stats by incrementing
- // follow requests count by one.
- *account.Stats.FollowRequestsCount++
- if err := u.state.DB.UpdateAccountStats(
- ctx,
- account.Stats,
- "follow_requests_count",
- ); err != nil {
- return gtserror.Newf("db error updating account stats: %w", err)
- }
-
- return nil
-}
-
-func (u *utils) decrementFollowRequestsCount(
- ctx context.Context,
- account *gtsmodel.Account,
-) error {
- // Lock on this account since we're changing stats.
- unlock := u.state.ProcessingLocks.Lock(account.URI)
- defer unlock()
-
- // Ensure account stats are populated.
- if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
- return gtserror.Newf("db error getting account stats: %w", err)
- }
-
- // Update stats by decrementing
- // follow requests count by one.
- //
- // Clamp to 0 to avoid funny business.
- *account.Stats.FollowRequestsCount--
- if *account.Stats.FollowRequestsCount < 0 {
- *account.Stats.FollowRequestsCount = 0
- }
- if err := u.state.DB.UpdateAccountStats(
- ctx,
- account.Stats,
- "follow_requests_count",
- ); err != nil {
- return gtserror.Newf("db error updating account stats: %w", err)
- }
-
- return nil
-}
-
-// requestFave stores an interaction request
+// impoliteFaveRequest stores an interaction request
// for the given fave, and notifies the interactee.
-func (u *utils) requestFave(
+//
+// It should be used only when an actor has sent a Like
+// directly in response to a post that requires approval
+// for it, instead of sending a LikeRequest.
+func (u *utils) impoliteFaveRequest(
ctx context.Context,
fave *gtsmodel.StatusFave,
) error {
@@ -559,13 +317,13 @@ func (u *utils) requestFave(
return nil
}
- // Create + store new interaction request.
- req = typeutils.StatusFaveToInteractionRequest(fave)
+ // Create + store new impolite interaction request.
+ req = typeutils.StatusFaveToImpoliteInteractionRequest(fave)
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
return gtserror.Newf("db error storing interaction request: %w", err)
}
- // Notify *local* account of pending announce.
+ // Notify *local* account of pending fave.
if err := u.surface.notifyPendingFave(ctx, fave); err != nil {
return gtserror.Newf("error notifying pending fave: %w", err)
}
@@ -573,9 +331,13 @@ func (u *utils) requestFave(
return nil
}
-// requestReply stores an interaction request
+// impoliteReplyRequest stores an interaction request
// for the given reply, and notifies the interactee.
-func (u *utils) requestReply(
+//
+// It should be used only when an actor has sent a reply
+// directly in response to a post that requires approval
+// for it, instead of sending a ReplyRequest.
+func (u *utils) impoliteReplyRequest(
ctx context.Context,
reply *gtsmodel.Status,
) error {
@@ -602,8 +364,8 @@ func (u *utils) requestReply(
return nil
}
- // Create + store interaction request.
- req = typeutils.StatusToInteractionRequest(reply)
+ // Create + store impolite interaction request.
+ req = typeutils.StatusToImpoliteInteractionRequest(reply)
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
return gtserror.Newf("db error storing interaction request: %w", err)
}
@@ -616,9 +378,13 @@ func (u *utils) requestReply(
return nil
}
-// requestAnnounce stores an interaction request
+// impoliteAnnounceRequest stores an interaction request
// for the given announce, and notifies the interactee.
-func (u *utils) requestAnnounce(
+//
+// It should be used only when an actor has sent an Announce
+// directly in response to a post that requires approval
+// for it, instead of sending an AnnounceRequest.
+func (u *utils) impoliteAnnounceRequest(
ctx context.Context,
boost *gtsmodel.Status,
) error {
@@ -645,8 +411,8 @@ func (u *utils) requestAnnounce(
return nil
}
- // Create + store interaction request.
- req = typeutils.StatusToInteractionRequest(boost)
+ // Create + store impolite interaction request.
+ req = typeutils.StatusToImpoliteInteractionRequest(boost)
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
return gtserror.Newf("db error storing interaction request: %w", err)
}
diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go
index 9f37f554e..67e928db0 100644
--- a/internal/processing/workers/workers.go
+++ b/internal/processing/workers/workers.go
@@ -18,18 +18,20 @@
package workers
import (
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
- "github.com/superseriousbusiness/gotosocial/internal/processing/account"
- "github.com/superseriousbusiness/gotosocial/internal/processing/common"
- "github.com/superseriousbusiness/gotosocial/internal/processing/conversations"
- "github.com/superseriousbusiness/gotosocial/internal/processing/media"
- "github.com/superseriousbusiness/gotosocial/internal/processing/stream"
- "github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/webpush"
- "github.com/superseriousbusiness/gotosocial/internal/workers"
+ "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/processing/account"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/common"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/conversations"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/media"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/stream"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
+ "code.superseriousbusiness.org/gotosocial/internal/webpush"
+ "code.superseriousbusiness.org/gotosocial/internal/workers"
)
type Processor struct {
@@ -44,6 +46,8 @@ func New(
federator *federation.Federator,
converter *typeutils.Converter,
visFilter *visibility.Filter,
+ muteFilter *mutes.Filter,
+ statusFilter *status.Filter,
emailSender email.Sender,
webPushSender webpush.Sender,
account *account.Processor,
@@ -66,6 +70,8 @@ func New(
Converter: converter,
Stream: stream,
VisFilter: visFilter,
+ MuteFilter: muteFilter,
+ StatusFilter: statusFilter,
EmailSender: emailSender,
WebPushSender: webPushSender,
Conversations: conversations,
diff --git a/internal/processing/workers/workers_test.go b/internal/processing/workers/workers_test.go
index ffd40d8fb..ddd5bd0e4 100644
--- a/internal/processing/workers/workers_test.go
+++ b/internal/processing/workers/workers_test.go
@@ -20,12 +20,12 @@ package workers_test
import (
"context"
+ apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/processing"
+ "code.superseriousbusiness.org/gotosocial/internal/stream"
+ "code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/processing"
- "github.com/superseriousbusiness/gotosocial/internal/stream"
- "github.com/superseriousbusiness/gotosocial/testrig"
)
const (
@@ -39,7 +39,6 @@ type WorkersTestSuite 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
@@ -48,7 +47,7 @@ type WorkersTestSuite struct {
testStatuses map[string]*gtsmodel.Status
testTags map[string]*gtsmodel.Tag
testMentions map[string]*gtsmodel.Mention
- testAutheds map[string]*oauth.Auth
+ testAutheds map[string]*apiutil.Auth
testBlocks map[string]*gtsmodel.Block
testActivities map[string]testrig.ActivityWithSignature
testLists map[string]*gtsmodel.List
@@ -57,7 +56,6 @@ type WorkersTestSuite struct {
func (suite *WorkersTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
@@ -66,7 +64,7 @@ func (suite *WorkersTestSuite) SetupSuite() {
suite.testStatuses = testrig.NewTestStatuses()
suite.testTags = testrig.NewTestTags()
suite.testMentions = testrig.NewTestMentions()
- suite.testAutheds = map[string]*oauth.Auth{
+ suite.testAutheds = map[string]*apiutil.Auth{
"local_account_1": {
Application: suite.testApplications["local_account_1"],
User: suite.testUsers["local_account_1"],