From 9770d54237bea828cab7e50aec7dff452c203138 Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Wed, 9 Aug 2023 19:14:33 +0200
Subject: [PATCH] [feature] List replies policy, refactor async workers (#2087)
* Add/update some DB functions.
* move async workers into subprocessor
* rename FromFederator -> FromFediAPI
* update home timeline check to include check for current status first before moving to parent status
* change streamMap to pointer to mollify linter
* update followtoas func signature
* fix merge
* remove errant debug log
* don't use separate errs.Combine() check to wrap errs
* wrap parts of workers functionality in sub-structs
* populate report using new db funcs
* embed federator (tiny bit tidier)
* flesh out error msg, add continue(!)
* fix other error messages to be more specific
* better, nicer
* give parseURI util function a bit more util
* missing headers
* use pointers for subprocessors
---
cmd/gotosocial/action/server/server.go | 4 +-
internal/db/bundb/account.go | 6 +-
internal/db/bundb/instance.go | 6 +-
internal/db/bundb/list.go | 22 +-
internal/db/bundb/list_test.go | 21 +
internal/db/bundb/relationship_block.go | 51 +-
internal/db/bundb/relationship_follow.go | 6 +-
internal/db/bundb/relationship_follow_req.go | 51 +-
internal/db/bundb/report.go | 85 +-
internal/db/bundb/statusfave.go | 6 +-
internal/db/list.go | 3 +
internal/db/relationship.go | 6 +
internal/db/report.go | 7 +
internal/federation/federatingdb/accept.go | 4 +-
internal/federation/federatingdb/announce.go | 2 +-
internal/federation/federatingdb/create.go | 12 +-
internal/federation/federatingdb/delete.go | 4 +-
.../federatingdb/federatingdb_test.go | 6 +-
.../federation/federatingdb/reject_test.go | 2 +-
internal/federation/federatingdb/update.go | 2 +-
internal/gtserror/new.go | 22 +-
internal/messages/messages.go | 4 +-
internal/processing/fromclientapi.go | 1021 -----------------
internal/processing/fromclientapi_test.go | 273 -----
internal/processing/fromcommon.go | 587 ----------
internal/processing/fromfederator.go | 486 --------
internal/processing/processor.go | 97 +-
internal/processing/processor_test.go | 4 +-
internal/processing/stream/stream.go | 3 +-
internal/processing/user/email.go | 54 -
internal/processing/user/email_test.go | 31 -
internal/processing/workers/federate.go | 892 ++++++++++++++
internal/processing/workers/fromclientapi.go | 548 +++++++++
.../processing/workers/fromclientapi_test.go | 589 ++++++++++
internal/processing/workers/fromfediapi.go | 540 +++++++++
.../fromfediapi_test.go} | 40 +-
internal/processing/workers/surface.go | 40 +
internal/processing/workers/surfaceemail.go | 160 +++
internal/processing/workers/surfacenotify.go | 221 ++++
.../processing/workers/surfacetimeline.go | 401 +++++++
internal/processing/workers/wipestatus.go | 119 ++
internal/processing/workers/workers.go | 92 ++
internal/processing/workers/workers_test.go | 169 +++
internal/typeutils/converter.go | 2 +-
internal/typeutils/internaltoas.go | 14 +-
internal/visibility/home_timeline.go | 47 +-
internal/workers/workers.go | 2 +-
testrig/processor.go | 4 +-
testrig/util.go | 2 +-
49 files changed, 4110 insertions(+), 2660 deletions(-)
delete mode 100644 internal/processing/fromclientapi.go
delete mode 100644 internal/processing/fromclientapi_test.go
delete mode 100644 internal/processing/fromcommon.go
delete mode 100644 internal/processing/fromfederator.go
create mode 100644 internal/processing/workers/federate.go
create mode 100644 internal/processing/workers/fromclientapi.go
create mode 100644 internal/processing/workers/fromclientapi_test.go
create mode 100644 internal/processing/workers/fromfediapi.go
rename internal/processing/{fromfederator_test.go => workers/fromfediapi_test.go} (93%)
create mode 100644 internal/processing/workers/surface.go
create mode 100644 internal/processing/workers/surfaceemail.go
create mode 100644 internal/processing/workers/surfacenotify.go
create mode 100644 internal/processing/workers/surfacetimeline.go
create mode 100644 internal/processing/workers/wipestatus.go
create mode 100644 internal/processing/workers/workers.go
create mode 100644 internal/processing/workers/workers_test.go
diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go
index 8dd6a026d..eb76b8f43 100644
--- a/cmd/gotosocial/action/server/server.go
+++ b/cmd/gotosocial/action/server/server.go
@@ -177,8 +177,8 @@ var Start action.GTSAction = func(ctx context.Context) error {
processor := processing.NewProcessor(typeConverter, federator, oauthServer, mediaManager, &state, emailSender)
// Set state client / federator worker enqueue functions
- state.Workers.EnqueueClientAPI = processor.EnqueueClientAPI
- state.Workers.EnqueueFederator = processor.EnqueueFederator
+ state.Workers.EnqueueClientAPI = processor.Workers().EnqueueClientAPI
+ state.Workers.EnqueueFediAPI = processor.Workers().EnqueueFediAPI
/*
HTTP router initialization
diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go
index 83b3c13f5..2d9a64454 100644
--- a/internal/db/bundb/account.go
+++ b/internal/db/bundb/account.go
@@ -290,11 +290,7 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
}
}
- if err := errs.Combine(); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- return nil
+ return errs.Combine()
}
func (a *accountDB) PutAccount(ctx context.Context, account *gtsmodel.Account) error {
diff --git a/internal/db/bundb/instance.go b/internal/db/bundb/instance.go
index 6657072fd..09084642f 100644
--- a/internal/db/bundb/instance.go
+++ b/internal/db/bundb/instance.go
@@ -198,11 +198,7 @@ func (i *instanceDB) populateInstance(ctx context.Context, instance *gtsmodel.In
}
}
- if err := errs.Combine(); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- return nil
+ return errs.Combine()
}
func (i *instanceDB) PutInstance(ctx context.Context, instance *gtsmodel.Instance) error {
diff --git a/internal/db/bundb/list.go b/internal/db/bundb/list.go
index 5cf10ce3c..ec96f1dfc 100644
--- a/internal/db/bundb/list.go
+++ b/internal/db/bundb/list.go
@@ -143,11 +143,7 @@ func (l *listDB) PopulateList(ctx context.Context, list *gtsmodel.List) error {
}
}
- if err := errs.Combine(); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- return nil
+ return errs.Combine()
}
func (l *listDB) PutList(ctx context.Context, list *gtsmodel.List) error {
@@ -503,6 +499,22 @@ func (l *listDB) DeleteListEntriesForFollowID(ctx context.Context, followID stri
return nil
}
+func (l *listDB) ListIncludesAccount(ctx context.Context, listID string, accountID string) (bool, error) {
+ exists, err := l.db.
+ NewSelect().
+ TableExpr("? AS ?", bun.Ident("list_entries"), bun.Ident("list_entry")).
+ Join(
+ "JOIN ? AS ? ON ? = ?",
+ bun.Ident("follows"), bun.Ident("follow"),
+ bun.Ident("list_entry.follow_id"), bun.Ident("follow.id"),
+ ).
+ Where("? = ?", bun.Ident("list_entry.list_id"), listID).
+ Where("? = ?", bun.Ident("follow.target_account_id"), accountID).
+ Exists(ctx)
+
+ return exists, l.db.ProcessError(err)
+}
+
// collate will collect the values of type T from an expected slice of length 'len',
// passing the expected index to each call of 'get' and deduplicating the end result.
func collate[T comparable](get func(int) T, len int) []T {
diff --git a/internal/db/bundb/list_test.go b/internal/db/bundb/list_test.go
index 296ab7c1a..ca078d086 100644
--- a/internal/db/bundb/list_test.go
+++ b/internal/db/bundb/list_test.go
@@ -310,6 +310,27 @@ func (suite *ListTestSuite) TestDeleteListEntriesForFollowID() {
suite.checkList(testList, dbList)
}
+func (suite *ListTestSuite) TestListIncludesAccount() {
+ ctx := context.Background()
+ testList, _ := suite.testStructs()
+
+ for accountID, expected := range map[string]bool{
+ suite.testAccounts["admin_account"].ID: true,
+ suite.testAccounts["local_account_1"].ID: false,
+ suite.testAccounts["local_account_2"].ID: true,
+ "01H7074GEZJ56J5C86PFB0V2CT": false,
+ } {
+ includes, err := suite.db.ListIncludesAccount(ctx, testList.ID, accountID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if includes != expected {
+ suite.FailNow("", "expected %t for accountID %s got %t", expected, accountID, includes)
+ }
+ }
+}
+
func TestListTestSuite(t *testing.T) {
suite.Run(t, new(ListTestSuite))
}
diff --git a/internal/db/bundb/relationship_block.go b/internal/db/bundb/relationship_block.go
index 2a042bed4..33a3b85fa 100644
--- a/internal/db/bundb/relationship_block.go
+++ b/internal/db/bundb/relationship_block.go
@@ -20,10 +20,10 @@ package bundb
import (
"context"
"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/log"
"github.com/uptrace/bun"
@@ -139,27 +139,44 @@ func (r *relationshipDB) getBlock(ctx context.Context, lookup string, dbQuery fu
return block, nil
}
- // Set the block source account
- block.Account, err = r.state.DB.GetAccountByID(
- gtscontext.SetBarebones(ctx),
- block.AccountID,
- )
- if err != nil {
- return nil, fmt.Errorf("error getting block source account: %w", err)
- }
-
- // Set the block target account
- block.TargetAccount, err = r.state.DB.GetAccountByID(
- gtscontext.SetBarebones(ctx),
- block.TargetAccountID,
- )
- if err != nil {
- return nil, fmt.Errorf("error getting block target account: %w", err)
+ if err := r.state.DB.PopulateBlock(ctx, block); err != nil {
+ return nil, err
}
return block, nil
}
+func (r *relationshipDB) PopulateBlock(ctx context.Context, block *gtsmodel.Block) error {
+ var (
+ err error
+ errs = gtserror.NewMultiError(2)
+ )
+
+ if block.Account == nil {
+ // Block origin account is not set, fetch from database.
+ block.Account, err = r.state.DB.GetAccountByID(
+ gtscontext.SetBarebones(ctx),
+ block.AccountID,
+ )
+ if err != nil {
+ errs.Appendf("error populating block account: %w", err)
+ }
+ }
+
+ if block.TargetAccount == nil {
+ // Block target account is not set, fetch from database.
+ block.TargetAccount, err = r.state.DB.GetAccountByID(
+ gtscontext.SetBarebones(ctx),
+ block.TargetAccountID,
+ )
+ if err != nil {
+ errs.Appendf("error populating block target account: %w", err)
+ }
+ }
+
+ return errs.Combine()
+}
+
func (r *relationshipDB) PutBlock(ctx context.Context, block *gtsmodel.Block) error {
return r.state.Caches.GTS.Block().Store(block, func() error {
_, err := r.db.NewInsert().Model(block).Exec(ctx)
diff --git a/internal/db/bundb/relationship_follow.go b/internal/db/bundb/relationship_follow.go
index e22ed30de..b693269df 100644
--- a/internal/db/bundb/relationship_follow.go
+++ b/internal/db/bundb/relationship_follow.go
@@ -185,11 +185,7 @@ func (r *relationshipDB) PopulateFollow(ctx context.Context, follow *gtsmodel.Fo
}
}
- if err := errs.Combine(); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- return nil
+ return errs.Combine()
}
func (r *relationshipDB) PutFollow(ctx context.Context, follow *gtsmodel.Follow) error {
diff --git a/internal/db/bundb/relationship_follow_req.go b/internal/db/bundb/relationship_follow_req.go
index dc5e760e6..cde9dc187 100644
--- a/internal/db/bundb/relationship_follow_req.go
+++ b/internal/db/bundb/relationship_follow_req.go
@@ -20,11 +20,11 @@ package bundb
import (
"context"
"errors"
- "fmt"
"time"
"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/uptrace/bun"
@@ -127,27 +127,44 @@ func (r *relationshipDB) getFollowRequest(ctx context.Context, lookup string, db
return followReq, nil
}
- // Set the follow request source account
- followReq.Account, err = r.state.DB.GetAccountByID(
- gtscontext.SetBarebones(ctx),
- followReq.AccountID,
- )
- if err != nil {
- return nil, fmt.Errorf("error getting follow request source account: %w", err)
- }
-
- // Set the follow request target account
- followReq.TargetAccount, err = r.state.DB.GetAccountByID(
- gtscontext.SetBarebones(ctx),
- followReq.TargetAccountID,
- )
- if err != nil {
- return nil, fmt.Errorf("error getting follow request target account: %w", err)
+ if err := r.state.DB.PopulateFollowRequest(ctx, followReq); err != nil {
+ return nil, err
}
return followReq, nil
}
+func (r *relationshipDB) PopulateFollowRequest(ctx context.Context, follow *gtsmodel.FollowRequest) error {
+ var (
+ err error
+ errs = gtserror.NewMultiError(2)
+ )
+
+ if follow.Account == nil {
+ // Follow account is not set, fetch from the database.
+ follow.Account, err = r.state.DB.GetAccountByID(
+ gtscontext.SetBarebones(ctx),
+ follow.AccountID,
+ )
+ if err != nil {
+ errs.Appendf("error populating follow request account: %w", err)
+ }
+ }
+
+ if follow.TargetAccount == nil {
+ // Follow target account is not set, fetch from the database.
+ follow.TargetAccount, err = r.state.DB.GetAccountByID(
+ gtscontext.SetBarebones(ctx),
+ follow.TargetAccountID,
+ )
+ if err != nil {
+ errs.Appendf("error populating follow target request account: %w", err)
+ }
+ }
+
+ return errs.Combine()
+}
+
func (r *relationshipDB) PutFollowRequest(ctx context.Context, follow *gtsmodel.FollowRequest) error {
return r.state.Caches.GTS.FollowRequest().Store(follow, func() error {
_, err := r.db.NewInsert().Model(follow).Exec(ctx)
diff --git a/internal/db/bundb/report.go b/internal/db/bundb/report.go
index 3a1e18789..eaeac4860 100644
--- a/internal/db/bundb/report.go
+++ b/internal/db/bundb/report.go
@@ -20,11 +20,11 @@ package bundb
import (
"context"
"errors"
- "fmt"
"time"
"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/state"
@@ -135,37 +135,72 @@ func (r *reportDB) getReport(ctx context.Context, lookup string, dbQuery func(*g
return nil, err
}
- // Set the report author account
- report.Account, err = r.state.DB.GetAccountByID(ctx, report.AccountID)
- if err != nil {
- return nil, fmt.Errorf("error getting report account: %w", err)
+ if gtscontext.Barebones(ctx) {
+ // Only a barebones model was requested.
+ return report, nil
}
- // Set the report target account
- report.TargetAccount, err = r.state.DB.GetAccountByID(ctx, report.TargetAccountID)
- if err != nil {
- return nil, fmt.Errorf("error getting report target account: %w", err)
- }
-
- if len(report.StatusIDs) > 0 {
- // Fetch reported statuses
- report.Statuses, err = r.state.DB.GetStatusesByIDs(ctx, report.StatusIDs)
- if err != nil {
- return nil, fmt.Errorf("error getting status mentions: %w", err)
- }
- }
-
- if report.ActionTakenByAccountID != "" {
- // Set the report action taken by account
- report.ActionTakenByAccount, err = r.state.DB.GetAccountByID(ctx, report.ActionTakenByAccountID)
- if err != nil {
- return nil, fmt.Errorf("error getting report action taken by account: %w", err)
- }
+ if err := r.state.DB.PopulateReport(ctx, report); err != nil {
+ return nil, err
}
return report, nil
}
+func (r *reportDB) PopulateReport(ctx context.Context, report *gtsmodel.Report) error {
+ var (
+ err error
+ errs = gtserror.NewMultiError(4)
+ )
+
+ if report.Account == nil {
+ // Report account is not set, fetch from the database.
+ report.Account, err = r.state.DB.GetAccountByID(
+ gtscontext.SetBarebones(ctx),
+ report.AccountID,
+ )
+ if err != nil {
+ errs.Appendf("error populating report account: %w", err)
+ }
+ }
+
+ if report.TargetAccount == nil {
+ // Report target account is not set, fetch from the database.
+ report.TargetAccount, err = r.state.DB.GetAccountByID(
+ gtscontext.SetBarebones(ctx),
+ report.TargetAccountID,
+ )
+ if err != nil {
+ errs.Appendf("error populating report target account: %w", err)
+ }
+ }
+
+ if l := len(report.StatusIDs); l > 0 && l != len(report.Statuses) {
+ // Report target statuses not set, fetch from the database.
+ report.Statuses, err = r.state.DB.GetStatusesByIDs(
+ gtscontext.SetBarebones(ctx),
+ report.StatusIDs,
+ )
+ if err != nil {
+ errs.Appendf("error populating report statuses: %w", err)
+ }
+ }
+
+ if report.ActionTakenByAccountID != "" &&
+ report.ActionTakenByAccount == nil {
+ // Report action account is not set, fetch from the database.
+ report.ActionTakenByAccount, err = r.state.DB.GetAccountByID(
+ gtscontext.SetBarebones(ctx),
+ report.ActionTakenByAccountID,
+ )
+ if err != nil {
+ errs.Appendf("error populating report action taken by account: %w", err)
+ }
+ }
+
+ return errs.Combine()
+}
+
func (r *reportDB) PutReport(ctx context.Context, report *gtsmodel.Report) error {
return r.state.Caches.GTS.Report().Store(report, func() error {
_, err := r.db.NewInsert().Model(report).Exec(ctx)
diff --git a/internal/db/bundb/statusfave.go b/internal/db/bundb/statusfave.go
index ab09fb1ba..37b88326b 100644
--- a/internal/db/bundb/statusfave.go
+++ b/internal/db/bundb/statusfave.go
@@ -197,11 +197,7 @@ func (s *statusFaveDB) PopulateStatusFave(ctx context.Context, statusFave *gtsmo
}
}
- if err := errs.Combine(); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- return nil
+ return errs.Combine()
}
func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusFave) error {
diff --git a/internal/db/list.go b/internal/db/list.go
index 4472589dc..91a540486 100644
--- a/internal/db/list.go
+++ b/internal/db/list.go
@@ -64,4 +64,7 @@ type List interface {
// DeleteListEntryForFollowID deletes all list entries with the given followID.
DeleteListEntriesForFollowID(ctx context.Context, followID string) error
+
+ // ListIncludesAccount returns true if the given listID includes the given accountID.
+ ListIncludesAccount(ctx context.Context, listID string, accountID string) (bool, error)
}
diff --git a/internal/db/relationship.go b/internal/db/relationship.go
index 6ba9fdf8c..50f615ef3 100644
--- a/internal/db/relationship.go
+++ b/internal/db/relationship.go
@@ -41,6 +41,9 @@ type Relationship interface {
// GetBlock returns the block from account1 targeting account2, if it exists, or an error if it doesn't.
GetBlock(ctx context.Context, account1 string, account2 string) (*gtsmodel.Block, error)
+ // PopulateBlock populates the struct pointers on the given block.
+ PopulateBlock(ctx context.Context, block *gtsmodel.Block) error
+
// PutBlock attempts to place the given account block in the database.
PutBlock(ctx context.Context, block *gtsmodel.Block) error
@@ -77,6 +80,9 @@ type Relationship interface {
// GetFollowRequest retrieves a follow request if it exists between source and target accounts.
GetFollowRequest(ctx context.Context, sourceAccountID string, targetAccountID string) (*gtsmodel.FollowRequest, error)
+ // PopulateFollowRequest populates the struct pointers on the given follow request.
+ PopulateFollowRequest(ctx context.Context, follow *gtsmodel.FollowRequest) error
+
// IsFollowing returns true if sourceAccount follows target account, or an error if something goes wrong while finding out.
IsFollowing(ctx context.Context, sourceAccountID string, targetAccountID string) (bool, error)
diff --git a/internal/db/report.go b/internal/db/report.go
index f39e53140..a04b4d3fa 100644
--- a/internal/db/report.go
+++ b/internal/db/report.go
@@ -27,17 +27,24 @@ import (
type Report interface {
// GetReportByID gets one report by its db id
GetReportByID(ctx context.Context, id string) (*gtsmodel.Report, error)
+
// GetReports gets limit n reports using the given parameters.
// Parameters that are empty / zero are ignored.
GetReports(ctx context.Context, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) ([]*gtsmodel.Report, error)
+
+ // PopulateReport populates the struct pointers on the given report.
+ PopulateReport(ctx context.Context, report *gtsmodel.Report) error
+
// PutReport puts the given report in the database.
PutReport(ctx context.Context, report *gtsmodel.Report) error
+
// UpdateReport updates one report by its db id.
// The given columns will be updated; if no columns are
// provided, then all columns will be updated.
// updated_at will also be updated, no need to pass this
// as a specific column.
UpdateReport(ctx context.Context, report *gtsmodel.Report, columns ...string) (*gtsmodel.Report, error)
+
// DeleteReportByID deletes report with the given id.
DeleteReportByID(ctx context.Context, id string) error
}
diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go
index 7d3e16d4e..27dcec612 100644
--- a/internal/federation/federatingdb/accept.go
+++ b/internal/federation/federatingdb/accept.go
@@ -72,7 +72,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
return err
}
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityAccept,
GTSModel: follow,
@@ -107,7 +107,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
return err
}
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityAccept,
GTSModel: follow,
diff --git a/internal/federation/federatingdb/announce.go b/internal/federation/federatingdb/announce.go
index 0246cff7c..334c46ba5 100644
--- a/internal/federation/federatingdb/announce.go
+++ b/internal/federation/federatingdb/announce.go
@@ -56,7 +56,7 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre
}
// This is a new boost. Process side effects asynchronously.
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: boost,
diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go
index 0075aa97a..3c9eaf9a5 100644
--- a/internal/federation/federatingdb/create.go
+++ b/internal/federation/federatingdb/create.go
@@ -105,7 +105,7 @@ func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, rec
return fmt.Errorf("activityBlock: database error inserting block: %s", err)
}
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityBlock,
APActivityType: ap.ActivityCreate,
GTSModel: block,
@@ -233,7 +233,7 @@ func (f *federatingDB) createStatusable(
if forward {
// Pass the statusable URI (APIri) into the processor worker
// and do the rest of the processing asynchronously.
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APIri: statusableURI,
@@ -291,7 +291,7 @@ func (f *federatingDB) createStatusable(
// Do the rest of the processing asynchronously. The processor
// will handle inserting/updating + further dereferencing the status.
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APIri: nil,
@@ -344,7 +344,7 @@ func (f *federatingDB) activityFollow(ctx context.Context, asType vocab.Type, re
return fmt.Errorf("activityFollow: database error inserting follow request: %s", err)
}
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: followRequest,
@@ -381,7 +381,7 @@ func (f *federatingDB) activityLike(ctx context.Context, asType vocab.Type, rece
return fmt.Errorf("activityLike: database error inserting fave: %w", err)
}
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
@@ -412,7 +412,7 @@ func (f *federatingDB) activityFlag(ctx context.Context, asType vocab.Type, rece
return fmt.Errorf("activityFlag: database error inserting report: %w", err)
}
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFlag,
APActivityType: ap.ActivityCreate,
GTSModel: report,
diff --git a/internal/federation/federatingdb/delete.go b/internal/federation/federatingdb/delete.go
index 95f9be354..cca5fdcad 100644
--- a/internal/federation/federatingdb/delete.go
+++ b/internal/federation/federatingdb/delete.go
@@ -49,7 +49,7 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {
// so we have to try a few different things...
if s, err := f.state.DB.GetStatusByURI(ctx, id.String()); err == nil && requestingAccount.ID == s.AccountID {
l.Debugf("uri is for STATUS with id: %s", s.ID)
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityDelete,
GTSModel: s,
@@ -59,7 +59,7 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {
if a, err := f.state.DB.GetAccountByURI(ctx, id.String()); err == nil && requestingAccount.ID == a.ID {
l.Debugf("uri is for ACCOUNT with id %s", a.ID)
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityDelete,
GTSModel: a,
diff --git a/internal/federation/federatingdb/federatingdb_test.go b/internal/federation/federatingdb/federatingdb_test.go
index 6a8754519..ea5ebf0c3 100644
--- a/internal/federation/federatingdb/federatingdb_test.go
+++ b/internal/federation/federatingdb/federatingdb_test.go
@@ -36,7 +36,7 @@ type FederatingDBTestSuite struct {
suite.Suite
db db.DB
tc typeutils.TypeConverter
- fromFederator chan messages.FromFederator
+ fromFederator chan messages.FromFediAPI
federatingDB federatingdb.DB
state state.State
@@ -69,8 +69,8 @@ func (suite *FederatingDBTestSuite) SetupTest() {
suite.state.Caches.Init()
testrig.StartWorkers(&suite.state)
- suite.fromFederator = make(chan messages.FromFederator, 10)
- suite.state.Workers.EnqueueFederator = func(ctx context.Context, msgs ...messages.FromFederator) {
+ suite.fromFederator = make(chan messages.FromFediAPI, 10)
+ suite.state.Workers.EnqueueFediAPI = func(ctx context.Context, msgs ...messages.FromFediAPI) {
for _, msg := range msgs {
suite.fromFederator <- msg
}
diff --git a/internal/federation/federatingdb/reject_test.go b/internal/federation/federatingdb/reject_test.go
index f7d30b228..d4c537a92 100644
--- a/internal/federation/federatingdb/reject_test.go
+++ b/internal/federation/federatingdb/reject_test.go
@@ -52,7 +52,7 @@ func (suite *RejectTestSuite) TestRejectFollowRequest() {
err := suite.db.Put(ctx, fr)
suite.NoError(err)
- asFollow, err := suite.tc.FollowToAS(ctx, suite.tc.FollowRequestToFollow(ctx, fr), followingAccount, followedAccount)
+ asFollow, err := suite.tc.FollowToAS(ctx, suite.tc.FollowRequestToFollow(ctx, fr))
suite.NoError(err)
rejectingAccountURI := testrig.URLMustParse(followedAccount.URI)
diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go
index 5ac4cc289..8e452eb3c 100644
--- a/internal/federation/federatingdb/update.go
+++ b/internal/federation/federatingdb/update.go
@@ -93,7 +93,7 @@ func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gts
// was delivered along with the Update, for further asynchronous
// updating of eg., avatar/header, emojis, etc. The actual db
// inserts/updates will take place there.
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityUpdate,
GTSModel: requestingAcct,
diff --git a/internal/gtserror/new.go b/internal/gtserror/new.go
index bb88d5f6a..c360d3345 100644
--- a/internal/gtserror/new.go
+++ b/internal/gtserror/new.go
@@ -21,16 +21,34 @@ import (
"net/http"
)
-// New returns a new error, prepended with caller function name if gtserror.Caller is enabled.
+// New returns a new error, prepended with caller
+// function name if gtserror.Caller is enabled.
func New(msg string) error {
return newAt(3, msg)
}
-// Newf returns a new formatted error, prepended with caller function name if gtserror.Caller is enabled.
+// Newf returns a new formatted error, prepended with
+// caller function name if gtserror.Caller is enabled.
func Newf(msgf string, args ...any) error {
return newfAt(3, msgf, args...)
}
+// NewfAt returns a new formatted error with the given
+// calldepth+1, useful when you want to wrap an error
+// from within an anonymous function or utility function,
+// but preserve the name in the error of the wrapping
+// function that did the calling.
+//
+// Provide calldepth 2 to prepend only the name of the
+// current containing function, 3 to prepend the name
+// of the function containing *that* function, and so on.
+//
+// This function is just exposed for dry-dick optimization
+// purposes. Most callers should just call Newf instead.
+func NewfAt(calldepth int, msgf string, args ...any) error {
+ return newfAt(calldepth+1, msgf, args...)
+}
+
// NewResponseError crafts an error from provided HTTP response
// including the method, status and body (if any provided). This
// will also wrap the returned error using WithStatusCode() and
diff --git a/internal/messages/messages.go b/internal/messages/messages.go
index 7f9b3f37c..236aea722 100644
--- a/internal/messages/messages.go
+++ b/internal/messages/messages.go
@@ -32,8 +32,8 @@ type FromClientAPI struct {
TargetAccount *gtsmodel.Account
}
-// FromFederator wraps a message that travels from the federator into the processor.
-type FromFederator struct {
+// FromFediAPI wraps a message that travels from the federating API into the processor.
+type FromFediAPI struct {
APObjectType string
APActivityType string
APIri *url.URL
diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go
deleted file mode 100644
index 412403c44..000000000
--- a/internal/processing/fromclientapi.go
+++ /dev/null
@@ -1,1021 +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 .
-
-package processing
-
-import (
- "context"
- "errors"
- "fmt"
- "net/url"
-
- "codeberg.org/gruf/go-kv"
- "codeberg.org/gruf/go-logger/v2/level"
- "github.com/superseriousbusiness/activity/pub"
- "github.com/superseriousbusiness/activity/streams"
- "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/messages"
-)
-
-func (p *Processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- // Allocate new log fields slice
- fields := make([]kv.Field, 3, 4)
- fields[0] = kv.Field{"activityType", clientMsg.APActivityType}
- fields[1] = kv.Field{"objectType", clientMsg.APObjectType}
- fields[2] = kv.Field{"fromAccount", clientMsg.OriginAccount.Username}
-
- if clientMsg.GTSModel != nil &&
- log.Level() >= level.DEBUG {
- // Append converted model to log
- fields = append(fields, kv.Field{
- "model", clientMsg.GTSModel,
- })
- }
-
- // Log this federated message
- l := log.WithContext(ctx).WithFields(fields...)
- l.Info("processing from client")
-
- switch clientMsg.APActivityType {
- case ap.ActivityCreate:
- // CREATE
- switch clientMsg.APObjectType {
- case ap.ObjectProfile, ap.ActorPerson:
- // CREATE ACCOUNT/PROFILE
- return p.processCreateAccountFromClientAPI(ctx, clientMsg)
- case ap.ObjectNote:
- // CREATE NOTE
- return p.processCreateStatusFromClientAPI(ctx, clientMsg)
- case ap.ActivityFollow:
- // CREATE FOLLOW REQUEST
- return p.processCreateFollowRequestFromClientAPI(ctx, clientMsg)
- case ap.ActivityLike:
- // CREATE LIKE/FAVE
- return p.processCreateFaveFromClientAPI(ctx, clientMsg)
- case ap.ActivityAnnounce:
- // CREATE BOOST/ANNOUNCE
- return p.processCreateAnnounceFromClientAPI(ctx, clientMsg)
- case ap.ActivityBlock:
- // CREATE BLOCK
- return p.processCreateBlockFromClientAPI(ctx, clientMsg)
- }
- case ap.ActivityUpdate:
- // UPDATE
- switch clientMsg.APObjectType {
- case ap.ObjectProfile, ap.ActorPerson:
- // UPDATE ACCOUNT/PROFILE
- return p.processUpdateAccountFromClientAPI(ctx, clientMsg)
- case ap.ActivityFlag:
- // UPDATE A FLAG/REPORT (mark as resolved/closed)
- return p.processUpdateReportFromClientAPI(ctx, clientMsg)
- }
- case ap.ActivityAccept:
- // ACCEPT
- if clientMsg.APObjectType == ap.ActivityFollow {
- // ACCEPT FOLLOW
- return p.processAcceptFollowFromClientAPI(ctx, clientMsg)
- }
- case ap.ActivityReject:
- // REJECT
- if clientMsg.APObjectType == ap.ActivityFollow {
- // REJECT FOLLOW (request)
- return p.processRejectFollowFromClientAPI(ctx, clientMsg)
- }
- case ap.ActivityUndo:
- // UNDO
- switch clientMsg.APObjectType {
- case ap.ActivityFollow:
- // UNDO FOLLOW
- return p.processUndoFollowFromClientAPI(ctx, clientMsg)
- case ap.ActivityBlock:
- // UNDO BLOCK
- return p.processUndoBlockFromClientAPI(ctx, clientMsg)
- case ap.ActivityLike:
- // UNDO LIKE/FAVE
- return p.processUndoFaveFromClientAPI(ctx, clientMsg)
- case ap.ActivityAnnounce:
- // UNDO ANNOUNCE/BOOST
- return p.processUndoAnnounceFromClientAPI(ctx, clientMsg)
- }
- case ap.ActivityDelete:
- // DELETE
- switch clientMsg.APObjectType {
- case ap.ObjectNote:
- // DELETE STATUS/NOTE
- return p.processDeleteStatusFromClientAPI(ctx, clientMsg)
- case ap.ObjectProfile, ap.ActorPerson:
- // DELETE ACCOUNT/PROFILE
- return p.processDeleteAccountFromClientAPI(ctx, clientMsg)
- }
- case ap.ActivityFlag:
- // FLAG
- if clientMsg.APObjectType == ap.ObjectProfile {
- // FLAG/REPORT A PROFILE
- return p.processReportAccountFromClientAPI(ctx, clientMsg)
- }
- }
- return nil
-}
-
-func (p *Processor) processCreateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- account, ok := clientMsg.GTSModel.(*gtsmodel.Account)
- if !ok {
- return errors.New("account was not parseable as *gtsmodel.Account")
- }
-
- // Do nothing if this isn't our activity.
- if !account.IsLocal() {
- return nil
- }
-
- // get the user this account belongs to
- user, err := p.state.DB.GetUserByAccountID(ctx, account.ID)
- if err != nil {
- return err
- }
-
- // email a confirmation to this user
- return p.User().EmailSendConfirmation(ctx, user, account.Username)
-}
-
-func (p *Processor) processCreateStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return gtserror.New("status was not parseable as *gtsmodel.Status")
- }
-
- if err := p.timelineAndNotifyStatus(ctx, status); err != nil {
- return gtserror.Newf("error timelining status: %w", err)
- }
-
- if status.InReplyToID != "" {
- // Interaction counts changed on the replied status;
- // uncache the prepared version from all timelines.
- p.invalidateStatusFromTimelines(ctx, status.InReplyToID)
- }
-
- if err := p.federateStatus(ctx, status); err != nil {
- return gtserror.Newf("error federating status: %w", err)
- }
-
- return nil
-}
-
-func (p *Processor) processCreateFollowRequestFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)
- if !ok {
- return errors.New("followrequest was not parseable as *gtsmodel.FollowRequest")
- }
-
- if err := p.notifyFollowRequest(ctx, followRequest); err != nil {
- return err
- }
-
- return p.federateFollow(ctx, followRequest, clientMsg.OriginAccount, clientMsg.TargetAccount)
-}
-
-func (p *Processor) processCreateFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- statusFave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
- if !ok {
- return gtserror.New("statusFave was not parseable as *gtsmodel.StatusFave")
- }
-
- if err := p.notifyFave(ctx, statusFave); err != nil {
- return gtserror.Newf("error notifying status fave: %w", err)
- }
-
- // Interaction counts changed on the faved status;
- // uncache the prepared version from all timelines.
- p.invalidateStatusFromTimelines(ctx, statusFave.StatusID)
-
- if err := p.federateFave(ctx, statusFave, clientMsg.OriginAccount, clientMsg.TargetAccount); err != nil {
- return gtserror.Newf("error federating status fave: %w", err)
- }
-
- return nil
-}
-
-func (p *Processor) processCreateAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return errors.New("boost was not parseable as *gtsmodel.Status")
- }
-
- // Timeline and notify.
- if err := p.timelineAndNotifyStatus(ctx, status); err != nil {
- return gtserror.Newf("error timelining boost: %w", err)
- }
-
- if err := p.notifyAnnounce(ctx, status); err != nil {
- return gtserror.Newf("error notifying boost: %w", err)
- }
-
- // Interaction counts changed on the boosted status;
- // uncache the prepared version from all timelines.
- p.invalidateStatusFromTimelines(ctx, status.BoostOfID)
-
- if err := p.federateAnnounce(ctx, status, clientMsg.OriginAccount, clientMsg.TargetAccount); err != nil {
- return gtserror.Newf("error federating boost: %w", err)
- }
-
- return nil
-}
-
-func (p *Processor) processCreateBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- block, ok := clientMsg.GTSModel.(*gtsmodel.Block)
- if !ok {
- return errors.New("block was not parseable as *gtsmodel.Block")
- }
-
- // remove any of the blocking account's statuses from the blocked account's timeline, and vice versa
- if err := p.state.Timelines.Home.WipeItemsFromAccountID(ctx, block.AccountID, block.TargetAccountID); err != nil {
- return err
- }
- if err := p.state.Timelines.Home.WipeItemsFromAccountID(ctx, block.TargetAccountID, block.AccountID); err != nil {
- return err
- }
-
- // TODO: same with notifications
- // TODO: same with bookmarks
-
- return p.federateBlock(ctx, block)
-}
-
-func (p *Processor) processUpdateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- account, ok := clientMsg.GTSModel.(*gtsmodel.Account)
- if !ok {
- return errors.New("account was not parseable as *gtsmodel.Account")
- }
-
- return p.federateAccountUpdate(ctx, account, clientMsg.OriginAccount)
-}
-
-func (p *Processor) processUpdateReportFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- report, ok := clientMsg.GTSModel.(*gtsmodel.Report)
- if !ok {
- return errors.New("report was not parseable as *gtsmodel.Report")
- }
-
- if report.Account.IsRemote() {
- // Report creator is a remote account,
- // we shouldn't email or notify them.
- return nil
- }
-
- return p.emailReportClosed(ctx, report)
-}
-
-func (p *Processor) processAcceptFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
- if !ok {
- return errors.New("accept was not parseable as *gtsmodel.Follow")
- }
-
- if err := p.notifyFollow(ctx, follow, clientMsg.TargetAccount); err != nil {
- return err
- }
-
- return p.federateAcceptFollowRequest(ctx, follow)
-}
-
-func (p *Processor) processRejectFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)
- if !ok {
- return errors.New("reject was not parseable as *gtsmodel.FollowRequest")
- }
-
- return p.federateRejectFollowRequest(ctx, followRequest)
-}
-
-func (p *Processor) processUndoFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow)
- if !ok {
- return errors.New("undo was not parseable as *gtsmodel.Follow")
- }
- return p.federateUnfollow(ctx, follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
-}
-
-func (p *Processor) processUndoBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- block, ok := clientMsg.GTSModel.(*gtsmodel.Block)
- if !ok {
- return errors.New("undo was not parseable as *gtsmodel.Block")
- }
- return p.federateUnblock(ctx, block)
-}
-
-func (p *Processor) processUndoFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- statusFave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
- if !ok {
- return gtserror.New("statusFave was not parseable as *gtsmodel.StatusFave")
- }
-
- // Interaction counts changed on the faved status;
- // uncache the prepared version from all timelines.
- p.invalidateStatusFromTimelines(ctx, statusFave.StatusID)
-
- if err := p.federateUnfave(ctx, statusFave, clientMsg.OriginAccount, clientMsg.TargetAccount); err != nil {
- return gtserror.Newf("error federating status unfave: %w", err)
- }
-
- return nil
-}
-
-func (p *Processor) processUndoAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return errors.New("boost was not parseable as *gtsmodel.Status")
- }
-
- if err := p.state.DB.DeleteStatusByID(ctx, status.ID); err != nil {
- return gtserror.Newf("db error deleting boost: %w", err)
- }
-
- if err := p.deleteStatusFromTimelines(ctx, status.ID); err != nil {
- return gtserror.Newf("error removing boost from timelines: %w", err)
- }
-
- // Interaction counts changed on the boosted status;
- // uncache the prepared version from all timelines.
- p.invalidateStatusFromTimelines(ctx, status.BoostOfID)
-
- if err := p.federateUnannounce(ctx, status, clientMsg.OriginAccount, clientMsg.TargetAccount); err != nil {
- return gtserror.Newf("error federating status unboost: %w", err)
- }
-
- return nil
-}
-
-func (p *Processor) processDeleteStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- status, ok := clientMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return gtserror.New("status was not parseable as *gtsmodel.Status")
- }
-
- if err := p.state.DB.PopulateStatus(ctx, status); err != nil {
- return gtserror.Newf("db error populating status: %w", err)
- }
-
- // Don't delete attachments, just unattach them: this
- // request comes from the client API and the poster
- // may want to use attachments again in a new post.
- deleteAttachments := false
- if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil {
- return gtserror.Newf("error wiping status: %w", err)
- }
-
- if status.InReplyToID != "" {
- // Interaction counts changed on the replied status;
- // uncache the prepared version from all timelines.
- p.invalidateStatusFromTimelines(ctx, status.InReplyToID)
- }
-
- if err := p.federateStatusDelete(ctx, status); err != nil {
- return gtserror.Newf("error federating status delete: %w", err)
- }
-
- return nil
-}
-
-func (p *Processor) processDeleteAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- // the origin of the delete could be either a domain block, or an action by another (or this) account
- var origin string
- if domainBlock, ok := clientMsg.GTSModel.(*gtsmodel.DomainBlock); ok {
- // origin is a domain block
- origin = domainBlock.ID
- } else {
- // origin is whichever account caused this message
- origin = clientMsg.OriginAccount.ID
- }
-
- if err := p.federateAccountDelete(ctx, clientMsg.TargetAccount); err != nil {
- return err
- }
-
- return p.account.Delete(ctx, clientMsg.TargetAccount, origin)
-}
-
-func (p *Processor) processReportAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error {
- report, ok := clientMsg.GTSModel.(*gtsmodel.Report)
- if !ok {
- return errors.New("report was not parseable as *gtsmodel.Report")
- }
-
- if *report.Forwarded {
- if err := p.federateReport(ctx, report); err != nil {
- return fmt.Errorf("processReportAccountFromClientAPI: error federating report: %w", err)
- }
- }
-
- if err := p.emailReport(ctx, report); err != nil {
- return fmt.Errorf("processReportAccountFromClientAPI: error notifying report: %w", err)
- }
-
- return nil
-}
-
-// TODO: move all the below functions into federation.Federator
-
-func (p *Processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error {
- // Do nothing if this isn't our activity.
- if !account.IsLocal() {
- return nil
- }
-
- outboxIRI, err := url.Parse(account.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateAccountDelete: error parsing outboxURI %s: %s", account.OutboxURI, err)
- }
-
- actorIRI, err := url.Parse(account.URI)
- if err != nil {
- return fmt.Errorf("federateAccountDelete: error parsing actorIRI %s: %s", account.URI, err)
- }
-
- followersIRI, err := url.Parse(account.FollowersURI)
- if err != nil {
- return fmt.Errorf("federateAccountDelete: error parsing followersIRI %s: %s", account.FollowersURI, err)
- }
-
- publicIRI, err := url.Parse(pub.PublicActivityPubIRI)
- if err != nil {
- return fmt.Errorf("federateAccountDelete: error parsing url %s: %s", pub.PublicActivityPubIRI, err)
- }
-
- // create a delete and set the appropriate actor on it
- delete := streams.NewActivityStreamsDelete()
-
- // set the actor for the delete; no matter who deleted it we should use the account owner for this
- deleteActor := streams.NewActivityStreamsActorProperty()
- deleteActor.AppendIRI(actorIRI)
- delete.SetActivityStreamsActor(deleteActor)
-
- // Set the account IRI as the 'object' property.
- deleteObject := streams.NewActivityStreamsObjectProperty()
- deleteObject.AppendIRI(actorIRI)
- delete.SetActivityStreamsObject(deleteObject)
-
- // send to followers...
- deleteTo := streams.NewActivityStreamsToProperty()
- deleteTo.AppendIRI(followersIRI)
- delete.SetActivityStreamsTo(deleteTo)
-
- // ... and CC to public
- deleteCC := streams.NewActivityStreamsCcProperty()
- deleteCC.AppendIRI(publicIRI)
- delete.SetActivityStreamsCc(deleteCC)
-
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, delete)
- return err
-}
-
-func (p *Processor) federateStatus(ctx context.Context, status *gtsmodel.Status) error {
- // do nothing if the status shouldn't be federated
- if !*status.Federated {
- return nil
- }
-
- if status.Account == nil {
- statusAccount, err := p.state.DB.GetAccountByID(ctx, status.AccountID)
- if err != nil {
- return fmt.Errorf("federateStatus: error fetching status author account: %s", err)
- }
- status.Account = statusAccount
- }
-
- // Do nothing if this isn't our activity.
- if !status.Account.IsLocal() {
- return nil
- }
-
- asStatus, err := p.tc.StatusToAS(ctx, status)
- if err != nil {
- return fmt.Errorf("federateStatus: error converting status to as format: %s", err)
- }
-
- create, err := p.tc.WrapNoteInCreate(asStatus, false)
- if err != nil {
- return fmt.Errorf("federateStatus: error wrapping status in create: %s", err)
- }
-
- outboxIRI, err := url.Parse(status.Account.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateStatus: error parsing outboxURI %s: %s", status.Account.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, create)
- return err
-}
-
-func (p *Processor) federateStatusDelete(ctx context.Context, status *gtsmodel.Status) error {
- if status.Account == nil {
- statusAccount, err := p.state.DB.GetAccountByID(ctx, status.AccountID)
- if err != nil {
- return fmt.Errorf("federateStatusDelete: error fetching status author account: %s", err)
- }
- status.Account = statusAccount
- }
-
- // Do nothing if this isn't our activity.
- if !status.Account.IsLocal() {
- return nil
- }
-
- delete, err := p.tc.StatusToASDelete(ctx, status)
- if err != nil {
- return fmt.Errorf("federateStatusDelete: error creating Delete: %w", err)
- }
-
- outboxIRI, err := url.Parse(status.Account.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %w", status.Account.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, delete)
- return err
-}
-
-func (p *Processor) federateFollow(ctx context.Context, followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
- // Do nothing if both accounts are local.
- if originAccount.IsLocal() && targetAccount.IsLocal() {
- return nil
- }
-
- follow := p.tc.FollowRequestToFollow(ctx, followRequest)
-
- asFollow, err := p.tc.FollowToAS(ctx, follow, originAccount, targetAccount)
- if err != nil {
- return fmt.Errorf("federateFollow: error converting follow to as format: %s", err)
- }
-
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateFollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, asFollow)
- return err
-}
-
-func (p *Processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
- // Do nothing if both accounts are local.
- if originAccount.IsLocal() && targetAccount.IsLocal() {
- return nil
- }
-
- // recreate the follow
- asFollow, err := p.tc.FollowToAS(ctx, follow, originAccount, targetAccount)
- if err != nil {
- return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
- }
-
- targetAccountURI, err := url.Parse(targetAccount.URI)
- if err != nil {
- return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
- }
-
- // create an Undo and set the appropriate actor on it
- undo := streams.NewActivityStreamsUndo()
- undo.SetActivityStreamsActor(asFollow.GetActivityStreamsActor())
-
- // Set the recreated follow as the 'object' property.
- undoObject := streams.NewActivityStreamsObjectProperty()
- undoObject.AppendActivityStreamsFollow(asFollow)
- undo.SetActivityStreamsObject(undoObject)
-
- // Set the To of the undo as the target of the recreated follow
- undoTo := streams.NewActivityStreamsToProperty()
- undoTo.AppendIRI(targetAccountURI)
- undo.SetActivityStreamsTo(undoTo)
-
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateUnfollow: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
-
- // send off the Undo
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, undo)
- return err
-}
-
-func (p *Processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
- // Do nothing if both accounts are local.
- if originAccount.IsLocal() && targetAccount.IsLocal() {
- return nil
- }
-
- // create the AS fave
- asFave, err := p.tc.FaveToAS(ctx, fave)
- if err != nil {
- return fmt.Errorf("federateFave: error converting fave to as format: %s", err)
- }
-
- targetAccountURI, err := url.Parse(targetAccount.URI)
- if err != nil {
- return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
- }
-
- // create an Undo and set the appropriate actor on it
- undo := streams.NewActivityStreamsUndo()
- undo.SetActivityStreamsActor(asFave.GetActivityStreamsActor())
-
- // Set the fave as the 'object' property.
- undoObject := streams.NewActivityStreamsObjectProperty()
- undoObject.AppendActivityStreamsLike(asFave)
- undo.SetActivityStreamsObject(undoObject)
-
- // Set the To of the undo as the target of the fave
- undoTo := streams.NewActivityStreamsToProperty()
- undoTo.AppendIRI(targetAccountURI)
- undo.SetActivityStreamsTo(undoTo)
-
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateFave: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, undo)
- return err
-}
-
-func (p *Processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
- // Do nothing if this isn't our activity.
- if !originAccount.IsLocal() {
- return nil
- }
-
- asAnnounce, err := p.tc.BoostToAS(ctx, boost, originAccount, targetAccount)
- if err != nil {
- return fmt.Errorf("federateUnannounce: error converting status to announce: %s", err)
- }
-
- // create an Undo and set the appropriate actor on it
- undo := streams.NewActivityStreamsUndo()
- undo.SetActivityStreamsActor(asAnnounce.GetActivityStreamsActor())
-
- // Set the boost as the 'object' property.
- undoObject := streams.NewActivityStreamsObjectProperty()
- undoObject.AppendActivityStreamsAnnounce(asAnnounce)
- undo.SetActivityStreamsObject(undoObject)
-
- // set the to
- undo.SetActivityStreamsTo(asAnnounce.GetActivityStreamsTo())
-
- // set the cc
- undo.SetActivityStreamsCc(asAnnounce.GetActivityStreamsCc())
-
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateUnannounce: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, undo)
- return err
-}
-
-func (p *Processor) federateAcceptFollowRequest(ctx context.Context, follow *gtsmodel.Follow) error {
- if follow.Account == nil {
- a, err := p.state.DB.GetAccountByID(ctx, follow.AccountID)
- if err != nil {
- return err
- }
- follow.Account = a
- }
- originAccount := follow.Account
-
- if follow.TargetAccount == nil {
- a, err := p.state.DB.GetAccountByID(ctx, follow.TargetAccountID)
- if err != nil {
- return err
- }
- follow.TargetAccount = a
- }
- targetAccount := follow.TargetAccount
-
- // Do nothing if target account *isn't* local,
- // or both origin + target *are* local.
- if targetAccount.IsRemote() || originAccount.IsLocal() {
- return nil
- }
-
- // recreate the AS follow
- asFollow, err := p.tc.FollowToAS(ctx, follow, originAccount, targetAccount)
- if err != nil {
- return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
- }
-
- acceptingAccountURI, err := url.Parse(targetAccount.URI)
- if err != nil {
- return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
- }
-
- requestingAccountURI, err := url.Parse(originAccount.URI)
- if err != nil {
- return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
- }
-
- // create an Accept
- accept := streams.NewActivityStreamsAccept()
-
- // set the accepting actor on it
- acceptActorProp := streams.NewActivityStreamsActorProperty()
- acceptActorProp.AppendIRI(acceptingAccountURI)
- accept.SetActivityStreamsActor(acceptActorProp)
-
- // Set the recreated follow as the 'object' property.
- acceptObject := streams.NewActivityStreamsObjectProperty()
- acceptObject.AppendActivityStreamsFollow(asFollow)
- accept.SetActivityStreamsObject(acceptObject)
-
- // Set the To of the accept as the originator of the follow
- acceptTo := streams.NewActivityStreamsToProperty()
- acceptTo.AppendIRI(requestingAccountURI)
- accept.SetActivityStreamsTo(acceptTo)
-
- outboxIRI, err := url.Parse(targetAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateAcceptFollowRequest: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
-
- // send off the accept using the accepter's outbox
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, accept)
- return err
-}
-
-func (p *Processor) federateRejectFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error {
- if followRequest.Account == nil {
- a, err := p.state.DB.GetAccountByID(ctx, followRequest.AccountID)
- if err != nil {
- return err
- }
- followRequest.Account = a
- }
- originAccount := followRequest.Account
-
- if followRequest.TargetAccount == nil {
- a, err := p.state.DB.GetAccountByID(ctx, followRequest.TargetAccountID)
- if err != nil {
- return err
- }
- followRequest.TargetAccount = a
- }
- targetAccount := followRequest.TargetAccount
-
- // Do nothing if target account *isn't* local,
- // or both origin + target *are* local.
- if targetAccount.IsRemote() || originAccount.IsLocal() {
- return nil
- }
-
- // recreate the AS follow
- follow := p.tc.FollowRequestToFollow(ctx, followRequest)
- asFollow, err := p.tc.FollowToAS(ctx, follow, originAccount, targetAccount)
- if err != nil {
- return fmt.Errorf("federateUnfollow: error converting follow to as format: %s", err)
- }
-
- rejectingAccountURI, err := url.Parse(targetAccount.URI)
- if err != nil {
- return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
- }
-
- requestingAccountURI, err := url.Parse(originAccount.URI)
- if err != nil {
- return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
- }
-
- // create a Reject
- reject := streams.NewActivityStreamsReject()
-
- // set the rejecting actor on it
- acceptActorProp := streams.NewActivityStreamsActorProperty()
- acceptActorProp.AppendIRI(rejectingAccountURI)
- reject.SetActivityStreamsActor(acceptActorProp)
-
- // Set the recreated follow as the 'object' property.
- acceptObject := streams.NewActivityStreamsObjectProperty()
- acceptObject.AppendActivityStreamsFollow(asFollow)
- reject.SetActivityStreamsObject(acceptObject)
-
- // Set the To of the reject as the originator of the follow
- acceptTo := streams.NewActivityStreamsToProperty()
- acceptTo.AppendIRI(requestingAccountURI)
- reject.SetActivityStreamsTo(acceptTo)
-
- outboxIRI, err := url.Parse(targetAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateRejectFollowRequest: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
-
- // send off the reject using the rejecting account's outbox
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, reject)
- return err
-}
-
-func (p *Processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
- // Do nothing if both accounts are local.
- if originAccount.IsLocal() && targetAccount.IsLocal() {
- return nil
- }
-
- // create the AS fave
- asFave, err := p.tc.FaveToAS(ctx, fave)
- if err != nil {
- return fmt.Errorf("federateFave: error converting fave to as format: %s", err)
- }
-
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateFave: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, asFave)
- return err
-}
-
-func (p *Processor) federateAnnounce(ctx context.Context, boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) error {
- announce, err := p.tc.BoostToAS(ctx, boostWrapperStatus, boostingAccount, boostedAccount)
- if err != nil {
- return fmt.Errorf("federateAnnounce: error converting status to announce: %s", err)
- }
-
- outboxIRI, err := url.Parse(boostingAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateAnnounce: error parsing outboxURI %s: %s", boostingAccount.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, announce)
- return err
-}
-
-func (p *Processor) federateAccountUpdate(ctx context.Context, updatedAccount *gtsmodel.Account, originAccount *gtsmodel.Account) error {
- person, err := p.tc.AccountToAS(ctx, updatedAccount)
- if err != nil {
- return fmt.Errorf("federateAccountUpdate: error converting account to person: %s", err)
- }
-
- update, err := p.tc.WrapPersonInUpdate(person, originAccount)
- if err != nil {
- return fmt.Errorf("federateAccountUpdate: error wrapping person in update: %s", err)
- }
-
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateAnnounce: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, update)
- return err
-}
-
-func (p *Processor) federateBlock(ctx context.Context, block *gtsmodel.Block) error {
- if block.Account == nil {
- blockAccount, err := p.state.DB.GetAccountByID(ctx, block.AccountID)
- if err != nil {
- return fmt.Errorf("federateBlock: error getting block account from database: %s", err)
- }
- block.Account = blockAccount
- }
-
- if block.TargetAccount == nil {
- blockTargetAccount, err := p.state.DB.GetAccountByID(ctx, block.TargetAccountID)
- if err != nil {
- return fmt.Errorf("federateBlock: error getting block target account from database: %s", err)
- }
- block.TargetAccount = blockTargetAccount
- }
-
- // Do nothing if both accounts are local.
- if block.Account.IsLocal() && block.TargetAccount.IsLocal() {
- return nil
- }
-
- asBlock, err := p.tc.BlockToAS(ctx, block)
- if err != nil {
- return fmt.Errorf("federateBlock: error converting block to AS format: %s", err)
- }
-
- outboxIRI, err := url.Parse(block.Account.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateBlock: error parsing outboxURI %s: %s", block.Account.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, asBlock)
- return err
-}
-
-func (p *Processor) federateUnblock(ctx context.Context, block *gtsmodel.Block) error {
- if block.Account == nil {
- blockAccount, err := p.state.DB.GetAccountByID(ctx, block.AccountID)
- if err != nil {
- return fmt.Errorf("federateUnblock: error getting block account from database: %s", err)
- }
- block.Account = blockAccount
- }
-
- if block.TargetAccount == nil {
- blockTargetAccount, err := p.state.DB.GetAccountByID(ctx, block.TargetAccountID)
- if err != nil {
- return fmt.Errorf("federateUnblock: error getting block target account from database: %s", err)
- }
- block.TargetAccount = blockTargetAccount
- }
-
- // Do nothing if both accounts are local.
- if block.Account.IsLocal() && block.TargetAccount.IsLocal() {
- return nil
- }
-
- asBlock, err := p.tc.BlockToAS(ctx, block)
- if err != nil {
- return fmt.Errorf("federateUnblock: error converting block to AS format: %s", err)
- }
-
- targetAccountURI, err := url.Parse(block.TargetAccount.URI)
- if err != nil {
- return fmt.Errorf("federateUnblock: error parsing uri %s: %s", block.TargetAccount.URI, err)
- }
-
- // create an Undo and set the appropriate actor on it
- undo := streams.NewActivityStreamsUndo()
- undo.SetActivityStreamsActor(asBlock.GetActivityStreamsActor())
-
- // Set the block as the 'object' property.
- undoObject := streams.NewActivityStreamsObjectProperty()
- undoObject.AppendActivityStreamsBlock(asBlock)
- undo.SetActivityStreamsObject(undoObject)
-
- // Set the To of the undo as the target of the block
- undoTo := streams.NewActivityStreamsToProperty()
- undoTo.AppendIRI(targetAccountURI)
- undo.SetActivityStreamsTo(undoTo)
-
- outboxIRI, err := url.Parse(block.Account.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateUnblock: error parsing outboxURI %s: %s", block.Account.OutboxURI, err)
- }
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, undo)
- return err
-}
-
-func (p *Processor) federateReport(ctx context.Context, report *gtsmodel.Report) error {
- if report.TargetAccount == nil {
- reportTargetAccount, err := p.state.DB.GetAccountByID(ctx, report.TargetAccountID)
- if err != nil {
- return fmt.Errorf("federateReport: error getting report target account from database: %w", err)
- }
- report.TargetAccount = reportTargetAccount
- }
-
- if len(report.StatusIDs) > 0 && len(report.Statuses) == 0 {
- statuses, err := p.state.DB.GetStatusesByIDs(ctx, report.StatusIDs)
- if err != nil {
- return fmt.Errorf("federateReport: error getting report statuses from database: %w", err)
- }
- report.Statuses = statuses
- }
-
- flag, err := p.tc.ReportToASFlag(ctx, report)
- if err != nil {
- return fmt.Errorf("federateReport: error converting report to AS flag: %w", err)
- }
-
- // add bto so that our federating actor knows where to
- // send the Flag; it'll still use a shared inbox if possible
- reportTargetURI, err := url.Parse(report.TargetAccount.URI)
- if err != nil {
- return fmt.Errorf("federateReport: error parsing outboxURI %s: %w", report.TargetAccount.URI, err)
- }
- bTo := streams.NewActivityStreamsBtoProperty()
- bTo.AppendIRI(reportTargetURI)
- flag.SetActivityStreamsBto(bTo)
-
- // deliver the flag using the outbox of the
- // instance account to anonymize the report
- instanceAccount, err := p.state.DB.GetInstanceAccount(ctx, "")
- if err != nil {
- return fmt.Errorf("federateReport: error getting instance account: %w", err)
- }
-
- outboxIRI, err := url.Parse(instanceAccount.OutboxURI)
- if err != nil {
- return fmt.Errorf("federateReport: error parsing outboxURI %s: %w", instanceAccount.OutboxURI, err)
- }
-
- _, err = p.federator.FederatingActor().Send(ctx, outboxIRI, flag)
- return err
-}
diff --git a/internal/processing/fromclientapi_test.go b/internal/processing/fromclientapi_test.go
deleted file mode 100644
index 94068e192..000000000
--- a/internal/processing/fromclientapi_test.go
+++ /dev/null
@@ -1,273 +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 .
-
-package processing_test
-
-import (
- "context"
- "encoding/json"
- "errors"
- "testing"
-
- "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/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 FromClientAPITestSuite struct {
- ProcessingStandardTestSuite
-}
-
-// This test ensures that when admin_account posts a new
-// status, it ends up in the correct streaming timelines
-// of local_account_1, which follows it.
-func (suite *FromClientAPITestSuite) TestProcessStreamNewStatus() {
- var (
- ctx = context.Background()
- postingAccount = suite.testAccounts["admin_account"]
- receivingAccount = suite.testAccounts["local_account_1"]
- testList = suite.testLists["local_account_1_list_1"]
- streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
- homeStream = streams[stream.TimelineHome]
- listStream = streams[stream.TimelineList+":"+testList.ID]
- )
-
- // Make a new status from admin account.
- newStatus := >smodel.Status{
- ID: "01FN4B2F88TF9676DYNXWE1WSS",
- URI: "http://localhost:8080/users/admin/statuses/01FN4B2F88TF9676DYNXWE1WSS",
- URL: "http://localhost:8080/@admin/statuses/01FN4B2F88TF9676DYNXWE1WSS",
- Content: "this status should stream :)",
- AttachmentIDs: []string{},
- TagIDs: []string{},
- MentionIDs: []string{},
- EmojiIDs: []string{},
- CreatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"),
- UpdatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"),
- Local: util.Ptr(true),
- AccountURI: "http://localhost:8080/users/admin",
- AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
- InReplyToID: "",
- BoostOfID: "",
- ContentWarning: "",
- Visibility: gtsmodel.VisibilityFollowersOnly,
- Sensitive: util.Ptr(false),
- Language: "en",
- CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
- Federated: util.Ptr(false),
- Boostable: util.Ptr(true),
- Replyable: util.Ptr(true),
- Likeable: util.Ptr(true),
- ActivityStreamsType: ap.ObjectNote,
- }
-
- // Put the status in the db first, to mimic what
- // would have already happened earlier up the flow.
- if err := suite.db.PutStatus(ctx, newStatus); err != nil {
- suite.FailNow(err.Error())
- }
-
- // Process the new status.
- if err := suite.processor.ProcessFromClientAPI(ctx, messages.FromClientAPI{
- APObjectType: ap.ObjectNote,
- APActivityType: ap.ActivityCreate,
- GTSModel: newStatus,
- OriginAccount: postingAccount,
- }); err != nil {
- suite.FailNow(err.Error())
- }
-
- // Check message in home stream.
- homeMsg := <-homeStream.Messages
- suite.Equal(stream.EventTypeUpdate, homeMsg.Event)
- suite.EqualValues([]string{stream.TimelineHome}, homeMsg.Stream)
- suite.Empty(homeStream.Messages) // Stream should now be empty.
-
- // Check status from home stream.
- homeStreamStatus := &apimodel.Status{}
- if err := json.Unmarshal([]byte(homeMsg.Payload), homeStreamStatus); err != nil {
- suite.FailNow(err.Error())
- }
- suite.Equal(newStatus.ID, homeStreamStatus.ID)
- suite.Equal(newStatus.Content, homeStreamStatus.Content)
-
- // Check message in list stream.
- listMsg := <-listStream.Messages
- suite.Equal(stream.EventTypeUpdate, listMsg.Event)
- suite.EqualValues([]string{stream.TimelineList + ":" + testList.ID}, listMsg.Stream)
- suite.Empty(listStream.Messages) // Stream should now be empty.
-
- // Check status from list stream.
- listStreamStatus := &apimodel.Status{}
- if err := json.Unmarshal([]byte(listMsg.Payload), listStreamStatus); err != nil {
- suite.FailNow(err.Error())
- }
- suite.Equal(newStatus.ID, listStreamStatus.ID)
- suite.Equal(newStatus.Content, listStreamStatus.Content)
-}
-
-func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
- var (
- ctx = context.Background()
- deletingAccount = suite.testAccounts["local_account_1"]
- receivingAccount = suite.testAccounts["local_account_2"]
- deletedStatus = suite.testStatuses["local_account_1_status_1"]
- boostOfDeletedStatus = suite.testStatuses["admin_account_status_4"]
- streams = suite.openStreams(ctx, receivingAccount, nil)
- homeStream = streams[stream.TimelineHome]
- )
-
- // Delete the status from the db first, to mimic what
- // would have already happened earlier up the flow
- if err := suite.db.DeleteStatusByID(ctx, deletedStatus.ID); err != nil {
- suite.FailNow(err.Error())
- }
-
- // Process the status delete.
- if err := suite.processor.ProcessFromClientAPI(ctx, messages.FromClientAPI{
- APObjectType: ap.ObjectNote,
- APActivityType: ap.ActivityDelete,
- GTSModel: deletedStatus,
- OriginAccount: deletingAccount,
- }); err != nil {
- suite.FailNow(err.Error())
- }
-
- // Stream should have the delete of admin's boost in it now.
- msg := <-homeStream.Messages
- suite.Equal(stream.EventTypeDelete, msg.Event)
- suite.Equal(boostOfDeletedStatus.ID, msg.Payload)
- suite.EqualValues([]string{stream.TimelineHome}, msg.Stream)
-
- // Stream should also have the delete of the message itself in it.
- msg = <-homeStream.Messages
- suite.Equal(stream.EventTypeDelete, msg.Event)
- suite.Equal(deletedStatus.ID, msg.Payload)
- suite.EqualValues([]string{stream.TimelineHome}, msg.Stream)
-
- // Stream should now be empty.
- suite.Empty(homeStream.Messages)
-
- // Boost should no longer be in the database.
- if !testrig.WaitFor(func() bool {
- _, err := suite.db.GetStatusByID(ctx, boostOfDeletedStatus.ID)
- return errors.Is(err, db.ErrNoEntries)
- }) {
- suite.FailNow("timed out waiting for status delete")
- }
-}
-
-func (suite *FromClientAPITestSuite) TestProcessNewStatusWithNotification() {
- var (
- ctx = context.Background()
- postingAccount = suite.testAccounts["admin_account"]
- receivingAccount = suite.testAccounts["local_account_1"]
- streams = suite.openStreams(ctx, receivingAccount, nil)
- notifStream = streams[stream.TimelineNotifications]
- )
-
- // Update the follow from receiving account -> posting account so
- // that receiving account wants notifs when posting account posts.
- follow := >smodel.Follow{}
- *follow = *suite.testFollows["local_account_1_admin_account"]
- follow.Notify = util.Ptr(true)
- if err := suite.db.UpdateFollow(ctx, follow); err != nil {
- suite.FailNow(err.Error())
- }
-
- // Make a new status from admin account.
- newStatus := >smodel.Status{
- ID: "01FN4B2F88TF9676DYNXWE1WSS",
- URI: "http://localhost:8080/users/admin/statuses/01FN4B2F88TF9676DYNXWE1WSS",
- URL: "http://localhost:8080/@admin/statuses/01FN4B2F88TF9676DYNXWE1WSS",
- Content: "this status should create a notification",
- AttachmentIDs: []string{},
- TagIDs: []string{},
- MentionIDs: []string{},
- EmojiIDs: []string{},
- CreatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"),
- UpdatedAt: testrig.TimeMustParse("2021-10-20T11:36:45Z"),
- Local: util.Ptr(true),
- AccountURI: "http://localhost:8080/users/admin",
- AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
- InReplyToID: "",
- BoostOfID: "",
- ContentWarning: "",
- Visibility: gtsmodel.VisibilityFollowersOnly,
- Sensitive: util.Ptr(false),
- Language: "en",
- CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
- Federated: util.Ptr(false),
- Boostable: util.Ptr(true),
- Replyable: util.Ptr(true),
- Likeable: util.Ptr(true),
- ActivityStreamsType: ap.ObjectNote,
- }
-
- // Put the status in the db first, to mimic what
- // would have already happened earlier up the flow.
- if err := suite.db.PutStatus(ctx, newStatus); err != nil {
- suite.FailNow(err.Error())
- }
-
- // Process the new status.
- if err := suite.processor.ProcessFromClientAPI(ctx, messages.FromClientAPI{
- APObjectType: ap.ObjectNote,
- APActivityType: ap.ActivityCreate,
- GTSModel: newStatus,
- OriginAccount: postingAccount,
- }); err != nil {
- suite.FailNow(err.Error())
- }
-
- // Wait for a notification to appear for the status.
- if !testrig.WaitFor(func() bool {
- _, err := suite.db.GetNotification(
- ctx,
- gtsmodel.NotificationStatus,
- receivingAccount.ID,
- postingAccount.ID,
- newStatus.ID,
- )
- return err == nil
- }) {
- suite.FailNow("timed out waiting for new status notification")
- }
-
- // Check message in notification stream.
- notifMsg := <-notifStream.Messages
- suite.Equal(stream.EventTypeNotification, notifMsg.Event)
- suite.EqualValues([]string{stream.TimelineNotifications}, notifMsg.Stream)
- suite.Empty(notifStream.Messages) // Stream should now be empty.
-
- // Check notif.
- notif := &apimodel.Notification{}
- if err := json.Unmarshal([]byte(notifMsg.Payload), notif); err != nil {
- suite.FailNow(err.Error())
- }
- suite.Equal(newStatus.ID, notif.Status.ID)
-}
-
-func TestFromClientAPITestSuite(t *testing.T) {
- suite.Run(t, &FromClientAPITestSuite{})
-}
diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go
deleted file mode 100644
index 07895b6ba..000000000
--- a/internal/processing/fromcommon.go
+++ /dev/null
@@ -1,587 +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 .
-
-package processing
-
-import (
- "context"
- "errors"
-
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "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/stream"
- "github.com/superseriousbusiness/gotosocial/internal/timeline"
-)
-
-// timelineAndNotifyStatus processes the given new status and inserts it into
-// the HOME and LIST timelines of accounts that follow the status author.
-//
-// It will also handle notifications for any mentions attached to the account, and
-// also notifications for any local accounts that want to know when this account posts.
-func (p *Processor) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error {
- // Ensure status fully populated; including account, mentions, etc.
- if err := p.state.DB.PopulateStatus(ctx, status); err != nil {
- return gtserror.Newf("error populating status with id %s: %w", status.ID, err)
- }
-
- // Get local followers of the account that posted the status.
- follows, err := p.state.DB.GetAccountLocalFollowers(ctx, status.AccountID)
- if err != nil {
- return gtserror.Newf("error getting local followers for account id %s: %w", status.AccountID, err)
- }
-
- // If the poster is also local, add a fake entry for them
- // so they can see their own status in their timeline.
- if status.Account.IsLocal() {
- follows = append(follows, >smodel.Follow{
- AccountID: status.AccountID,
- Account: status.Account,
- Notify: func() *bool { b := false; return &b }(), // Account shouldn't notify itself.
- ShowReblogs: func() *bool { b := true; return &b }(), // Account should show own reblogs.
- })
- }
-
- // 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.
- if err := p.timelineAndNotifyStatusForFollowers(ctx, status, follows); err != nil {
- return gtserror.Newf("error timelining status %s for followers: %w", status.ID, err)
- }
-
- // Notify each local account that's mentioned by this status.
- if err := p.notifyStatusMentions(ctx, status); err != nil {
- return gtserror.Newf("error notifying status mentions for status %s: %w", status.ID, err)
- }
-
- return nil
-}
-
-func (p *Processor) timelineAndNotifyStatusForFollowers(ctx context.Context, status *gtsmodel.Status, follows []*gtsmodel.Follow) error {
- var (
- errs = gtserror.NewMultiError(len(follows))
- boost = status.BoostOfID != ""
- reply = status.InReplyToURI != ""
- )
-
- for _, follow := range follows {
- if sr := follow.ShowReblogs; boost && (sr == nil || !*sr) {
- // This is a boost, but this follower
- // doesn't want to see those from this
- // account, so just skip everything.
- continue
- }
-
- // Add status to each list that this follow
- // is included in, and stream it if applicable.
- listEntries, err := p.state.DB.GetListEntriesForFollowID(
- // We only need the list IDs.
- gtscontext.SetBarebones(ctx),
- follow.ID,
- )
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- errs.Appendf("error list timelining status: %w", err)
- continue
- }
-
- for _, listEntry := range listEntries {
- if _, err := p.timelineStatus(
- ctx,
- p.state.Timelines.List.IngestOne,
- listEntry.ListID, // list timelines are keyed by list ID
- follow.Account,
- status,
- stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list
- ); err != nil {
- errs.Appendf("error list timelining status: %w", err)
- continue
- }
- }
-
- // Add status to home timeline for this
- // follower, and stream it if applicable.
- if timelined, err := p.timelineStatus(
- ctx,
- p.state.Timelines.Home.IngestOne,
- follow.AccountID, // home timelines are keyed by account ID
- follow.Account,
- status,
- stream.TimelineHome,
- ); err != nil {
- errs.Appendf("error home timelining status: %w", err)
- continue
- } else if !timelined {
- // Status wasn't added to home tomeline,
- // so we shouldn't notify it either.
- continue
- }
-
- if n := follow.Notify; n == nil || !*n {
- // This follower doesn't have notifications
- // set for this account's new posts, so bail.
- continue
- }
-
- if boost || reply {
- // Don't notify for boosts or replies.
- continue
- }
-
- // If we reach here, we know:
- //
- // - This follower wants to be notified when this account posts.
- // - This is a top-level post (not a reply).
- // - This is not a boost of another post.
- // - The post is visible in this follower's home timeline.
- //
- // That means we can officially notify this one.
- if err := p.notify(
- ctx,
- gtsmodel.NotificationStatus,
- follow.AccountID,
- status.AccountID,
- status.ID,
- ); err != nil {
- errs.Appendf("error notifying account %s about new status: %w", follow.AccountID, err)
- }
- }
-
- if err := errs.Combine(); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- return nil
-}
-
-// 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.
-func (p *Processor) timelineStatus(
- ctx context.Context,
- ingest func(context.Context, string, timeline.Timelineable) (bool, error),
- timelineID string,
- account *gtsmodel.Account,
- status *gtsmodel.Status,
- streamType string,
-) (bool, error) {
- // Make sure the status is timelineable.
- // This works for both home and list timelines.
- if timelineable, err := p.filter.StatusHomeTimelineable(ctx, account, status); err != nil {
- err = gtserror.Newf("error getting timelineability for status for timeline with id %s: %w", account.ID, err)
- return false, err
- } else if !timelineable {
- // Nothing to do.
- return false, nil
- }
-
- // Ingest status into given timeline using provided function.
- if inserted, err := ingest(ctx, timelineID, status); err != nil {
- err = gtserror.Newf("error ingesting status %s: %w", status.ID, err)
- return false, err
- } else if !inserted {
- // Nothing more to do.
- return false, nil
- }
-
- // The status was inserted so stream it to the user.
- apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, account)
- if err != nil {
- err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
- return true, err
- }
-
- if err := p.stream.Update(apiStatus, account, []string{streamType}); err != nil {
- err = gtserror.Newf("error streaming update for status %s: %w", status.ID, err)
- return true, err
- }
-
- return true, nil
-}
-
-func (p *Processor) notifyStatusMentions(ctx context.Context, status *gtsmodel.Status) error {
- errs := gtserror.NewMultiError(len(status.Mentions))
-
- for _, m := range status.Mentions {
- if err := p.notify(
- ctx,
- gtsmodel.NotificationMention,
- m.TargetAccountID,
- m.OriginAccountID,
- m.StatusID,
- ); err != nil {
- errs.Append(err)
- }
- }
-
- if err := errs.Combine(); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- return nil
-}
-
-func (p *Processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error {
- return p.notify(
- ctx,
- gtsmodel.NotificationFollowRequest,
- followRequest.TargetAccountID,
- followRequest.AccountID,
- "",
- )
-}
-
-func (p *Processor) notifyFollow(ctx context.Context, follow *gtsmodel.Follow, targetAccount *gtsmodel.Account) error {
- // Remove previous follow request notification, if it exists.
- prevNotif, err := p.state.DB.GetNotification(
- gtscontext.SetBarebones(ctx),
- gtsmodel.NotificationFollowRequest,
- targetAccount.ID,
- follow.AccountID,
- "",
- )
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- // Proper error while checking.
- return gtserror.Newf("db error checking for previous follow request notification: %w", err)
- }
-
- if prevNotif != nil {
- // Previous notification existed, delete.
- if err := p.state.DB.DeleteNotificationByID(ctx, prevNotif.ID); err != nil {
- return gtserror.Newf("db error removing previous follow request notification %s: %w", prevNotif.ID, err)
- }
- }
-
- // Now notify the follow itself.
- return p.notify(
- ctx,
- gtsmodel.NotificationFollow,
- targetAccount.ID,
- follow.AccountID,
- "",
- )
-}
-
-func (p *Processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) error {
- if fave.TargetAccountID == fave.AccountID {
- // Self-fave, nothing to do.
- return nil
- }
-
- return p.notify(
- ctx,
- gtsmodel.NotificationFave,
- fave.TargetAccountID,
- fave.AccountID,
- fave.StatusID,
- )
-}
-
-func (p *Processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) error {
- if status.BoostOfID == "" {
- // Not a boost, nothing to do.
- return nil
- }
-
- if status.BoostOfAccountID == status.AccountID {
- // Self-boost, nothing to do.
- return nil
- }
-
- return p.notify(
- ctx,
- gtsmodel.NotificationReblog,
- status.BoostOfAccountID,
- status.AccountID,
- status.ID,
- )
-}
-
-func (p *Processor) notify(
- ctx context.Context,
- notificationType gtsmodel.NotificationType,
- targetAccountID string,
- originAccountID string,
- statusID string,
-) error {
- targetAccount, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
- if err != nil {
- return gtserror.Newf("error getting target account %s: %w", targetAccountID, err)
- }
-
- if !targetAccount.IsLocal() {
- // Nothing to do.
- return nil
- }
-
- // Make sure a notification doesn't
- // already exist with these params.
- if _, err := p.state.DB.GetNotification(
- ctx,
- notificationType,
- targetAccountID,
- originAccountID,
- statusID,
- ); err == nil {
- // Notification exists, nothing to do.
- return nil
- } else if !errors.Is(err, db.ErrNoEntries) {
- // Real error.
- return gtserror.Newf("error checking existence of notification: %w", err)
- }
-
- // Notification doesn't yet exist, so
- // we need to create + store one.
- notif := >smodel.Notification{
- ID: id.NewULID(),
- NotificationType: notificationType,
- TargetAccountID: targetAccountID,
- OriginAccountID: originAccountID,
- StatusID: statusID,
- }
-
- if err := p.state.DB.PutNotification(ctx, notif); err != nil {
- return gtserror.Newf("error putting notification in database: %w", err)
- }
-
- // Stream notification to the user.
- apiNotif, err := p.tc.NotificationToAPINotification(ctx, notif)
- if err != nil {
- return gtserror.Newf("error converting notification to api representation: %w", err)
- }
-
- if err := p.stream.Notify(apiNotif, targetAccount); err != nil {
- return gtserror.Newf("error streaming notification to account: %w", err)
- }
-
- return nil
-}
-
-// wipeStatus contains common logic used to totally delete a status
-// + all its attachments, notifications, boosts, and timeline entries.
-func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Status, deleteAttachments bool) error {
- var errs gtserror.MultiError
-
- // either delete all attachments for this status, or simply
- // unattach all attachments for this status, so they'll be
- // cleaned later by a separate process; reason to unattach rather
- // than delete is that the poster might want to reattach them
- // to another status immediately (in case of delete + redraft)
- if deleteAttachments {
- // todo: p.state.DB.DeleteAttachmentsForStatus
- for _, a := range statusToDelete.AttachmentIDs {
- if err := p.media.Delete(ctx, a); err != nil {
- errs.Appendf("error deleting media: %w", err)
- }
- }
- } else {
- // todo: p.state.DB.UnattachAttachmentsForStatus
- for _, a := range statusToDelete.AttachmentIDs {
- if _, err := p.media.Unattach(ctx, statusToDelete.Account, a); err != nil {
- errs.Appendf("error unattaching media: %w", err)
- }
- }
- }
-
- // delete all mention entries generated by this status
- // todo: p.state.DB.DeleteMentionsForStatus
- for _, id := range statusToDelete.MentionIDs {
- if err := p.state.DB.DeleteMentionByID(ctx, id); err != nil {
- errs.Appendf("error deleting status mention: %w", err)
- }
- }
-
- // delete all notification entries generated by this status
- if err := p.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status notifications: %w", err)
- }
-
- // delete all bookmarks that point to this status
- if err := p.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status bookmarks: %w", err)
- }
-
- // delete all faves of this status
- if err := p.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status faves: %w", err)
- }
-
- // delete all boosts for this status + remove them from timelines
- boosts, err := p.state.DB.GetStatusBoosts(
- // we MUST set a barebones context here,
- // as depending on where it came from the
- // original BoostOf may already be gone.
- gtscontext.SetBarebones(ctx),
- statusToDelete.ID)
- if err != nil {
- errs.Appendf("error fetching status boosts: %w", err)
- }
- for _, b := range boosts {
- if err := p.deleteStatusFromTimelines(ctx, b.ID); err != nil {
- errs.Appendf("error deleting boost from timelines: %w", err)
- }
- if err := p.state.DB.DeleteStatusByID(ctx, b.ID); err != nil {
- errs.Appendf("error deleting boost: %w", err)
- }
- }
-
- // delete this status from any and all timelines
- if err := p.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status from timelines: %w", err)
- }
-
- // finally, delete the status itself
- if err := p.state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status: %w", err)
- }
-
- 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 (p *Processor) deleteStatusFromTimelines(ctx context.Context, statusID string) error {
- if err := p.state.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil {
- return err
- }
-
- if err := p.state.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil {
- return err
- }
-
- return p.stream.Delete(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 (p *Processor) invalidateStatusFromTimelines(ctx context.Context, statusID string) {
- if err := p.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 := p.state.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
- log.
- WithContext(ctx).
- WithField("statusID", statusID).
- Errorf("error unpreparing status from list timelines: %v", err)
- }
-}
-
-/*
- EMAIL FUNCTIONS
-*/
-
-func (p *Processor) emailReport(ctx context.Context, report *gtsmodel.Report) error {
- instance, err := p.state.DB.GetInstance(ctx, config.GetHost())
- if err != nil {
- return gtserror.Newf("error getting instance: %w", err)
- }
-
- toAddresses, err := p.state.DB.GetInstanceModeratorAddresses(ctx)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- // No registered moderator addresses.
- return nil
- }
- return gtserror.Newf("error getting instance moderator addresses: %w", err)
- }
-
- if report.Account == nil {
- report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID)
- if err != nil {
- return gtserror.Newf("error getting report account: %w", err)
- }
- }
-
- if report.TargetAccount == nil {
- report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID)
- if err != nil {
- return gtserror.Newf("error getting report target account: %w", err)
- }
- }
-
- reportData := email.NewReportData{
- InstanceURL: instance.URI,
- InstanceName: instance.Title,
- ReportURL: instance.URI + "/settings/admin/reports/" + report.ID,
- ReportDomain: report.Account.Domain,
- ReportTargetDomain: report.TargetAccount.Domain,
- }
-
- if err := p.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil {
- return gtserror.Newf("error emailing instance moderators: %w", err)
- }
-
- return nil
-}
-
-func (p *Processor) emailReportClosed(ctx context.Context, report *gtsmodel.Report) error {
- user, err := p.state.DB.GetUserByAccountID(ctx, report.Account.ID)
- if err != nil {
- return gtserror.Newf("db error getting user: %w", err)
- }
-
- if user.ConfirmedAt.IsZero() || !*user.Approved || *user.Disabled || user.Email == "" {
- // Only email users who:
- // - are confirmed
- // - are approved
- // - are not disabled
- // - have an email address
- return nil
- }
-
- instance, err := p.state.DB.GetInstance(ctx, config.GetHost())
- if err != nil {
- return gtserror.Newf("db error getting instance: %w", err)
- }
-
- if report.Account == nil {
- report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID)
- if err != nil {
- return gtserror.Newf("error getting report account: %w", err)
- }
- }
-
- if report.TargetAccount == nil {
- report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID)
- if err != nil {
- return gtserror.Newf("error getting report target account: %w", err)
- }
- }
-
- reportClosedData := email.ReportClosedData{
- Username: report.Account.Username,
- InstanceURL: instance.URI,
- InstanceName: instance.Title,
- ReportTargetUsername: report.TargetAccount.Username,
- ReportTargetDomain: report.TargetAccount.Domain,
- ActionTakenComment: report.ActionTaken,
- }
-
- return p.emailSender.SendReportClosedEmail(user.Email, reportClosedData)
-}
diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go
deleted file mode 100644
index 2790d31ee..000000000
--- a/internal/processing/fromfederator.go
+++ /dev/null
@@ -1,486 +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 .
-
-package processing
-
-import (
- "context"
- "errors"
- "net/url"
-
- "codeberg.org/gruf/go-kv"
- "codeberg.org/gruf/go-logger/v2/level"
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- "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"
-)
-
-// ProcessFromFederator reads the APActivityType and APObjectType of an incoming message from the federator,
-// and directs the message into the appropriate side effect handler function, or simply does nothing if there's
-// no handler function defined for the combination of Activity and Object.
-func (p *Processor) ProcessFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
- // Allocate new log fields slice
- fields := make([]kv.Field, 3, 5)
- fields[0] = kv.Field{"activityType", federatorMsg.APActivityType}
- fields[1] = kv.Field{"objectType", federatorMsg.APObjectType}
- fields[2] = kv.Field{"toAccount", federatorMsg.ReceivingAccount.Username}
-
- if federatorMsg.APIri != nil {
- // An IRI was supplied, append to log
- fields = append(fields, kv.Field{
- "iri", federatorMsg.APIri,
- })
- }
-
- if federatorMsg.GTSModel != nil &&
- log.Level() >= level.DEBUG {
- // Append converted model to log
- fields = append(fields, kv.Field{
- "model", federatorMsg.GTSModel,
- })
- }
-
- // Log this federated message
- l := log.WithContext(ctx).WithFields(fields...)
- l.Info("processing from federator")
-
- switch federatorMsg.APActivityType {
- case ap.ActivityCreate:
- // CREATE SOMETHING
- switch federatorMsg.APObjectType {
- case ap.ObjectNote:
- // CREATE A STATUS
- return p.processCreateStatusFromFederator(ctx, federatorMsg)
- case ap.ActivityLike:
- // CREATE A FAVE
- return p.processCreateFaveFromFederator(ctx, federatorMsg)
- case ap.ActivityFollow:
- // CREATE A FOLLOW REQUEST
- return p.processCreateFollowRequestFromFederator(ctx, federatorMsg)
- case ap.ActivityAnnounce:
- // CREATE AN ANNOUNCE
- return p.processCreateAnnounceFromFederator(ctx, federatorMsg)
- case ap.ActivityBlock:
- // CREATE A BLOCK
- return p.processCreateBlockFromFederator(ctx, federatorMsg)
- case ap.ActivityFlag:
- // CREATE A FLAG / REPORT
- return p.processCreateFlagFromFederator(ctx, federatorMsg)
- }
- case ap.ActivityUpdate:
- // UPDATE SOMETHING
- if federatorMsg.APObjectType == ap.ObjectProfile {
- // UPDATE AN ACCOUNT
- return p.processUpdateAccountFromFederator(ctx, federatorMsg)
- }
- case ap.ActivityDelete:
- // DELETE SOMETHING
- switch federatorMsg.APObjectType {
- case ap.ObjectNote:
- // DELETE A STATUS
- return p.processDeleteStatusFromFederator(ctx, federatorMsg)
- case ap.ObjectProfile:
- // DELETE A PROFILE/ACCOUNT
- return p.processDeleteAccountFromFederator(ctx, federatorMsg)
- }
- }
-
- // not a combination we can/need to process
- return nil
-}
-
-// processCreateStatusFromFederator handles Activity Create and Object Note.
-func (p *Processor) processCreateStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
- var (
- status *gtsmodel.Status
- err error
-
- // Check the federatorMsg for either an already dereferenced
- // and converted status pinned to the message, or a forwarded
- // AP IRI that we still need to deref.
- forwarded = (federatorMsg.GTSModel == nil)
- )
-
- if forwarded {
- // Model was not set, deref with IRI.
- // This will also cause the status to be inserted into the db.
- status, err = p.statusFromAPIRI(ctx, federatorMsg)
- } else {
- // Model is set, ensure we have the most up-to-date model.
- status, err = p.statusFromGTSModel(ctx, federatorMsg)
- }
-
- if err != nil {
- return gtserror.Newf("error extracting status from federatorMsg: %w", err)
- }
-
- if status.Account == nil || status.Account.IsRemote() {
- // Either no account attached yet, or a remote account.
- // Both situations we need to parse account URI to fetch it.
- accountURI, err := url.Parse(status.AccountURI)
- if err != nil {
- return err
- }
-
- // Ensure that account for this status has been deref'd.
- status.Account, _, err = p.federator.GetAccountByURI(ctx,
- federatorMsg.ReceivingAccount.Username,
- accountURI,
- )
- if err != nil {
- return err
- }
- }
-
- // Ensure status ancestors dereferenced. We need at least the
- // immediate parent (if present) to ascertain timelineability.
- if err := p.federator.DereferenceStatusAncestors(ctx,
- federatorMsg.ReceivingAccount.Username,
- status,
- ); err != nil {
- return err
- }
-
- if status.InReplyToID != "" {
- // Interaction counts changed on the replied status;
- // uncache the prepared version from all timelines.
- p.invalidateStatusFromTimelines(ctx, status.InReplyToID)
- }
-
- if err := p.timelineAndNotifyStatus(ctx, status); err != nil {
- return gtserror.Newf("error timelining status: %w", err)
- }
-
- return nil
-}
-
-func (p *Processor) statusFromGTSModel(ctx context.Context, federatorMsg messages.FromFederator) (*gtsmodel.Status, error) {
- // There should be a status pinned to the federatorMsg
- // (we've already checked to ensure this is not nil).
- status, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- err := gtserror.New("Note was not parseable as *gtsmodel.Status")
- return nil, err
- }
-
- // AP statusable representation may have also
- // been set on message (no problem if not).
- statusable, _ := federatorMsg.APObjectModel.(ap.Statusable)
-
- // Call refresh on status to update
- // it (deref remote) if necessary.
- var err error
- status, _, err = p.federator.RefreshStatus(
- ctx,
- federatorMsg.ReceivingAccount.Username,
- status,
- statusable,
- false,
- )
- if err != nil {
- return nil, gtserror.Newf("%w", err)
- }
-
- return status, nil
-}
-
-func (p *Processor) statusFromAPIRI(ctx context.Context, federatorMsg messages.FromFederator) (*gtsmodel.Status, error) {
- // There should be a status IRI pinned to
- // the federatorMsg for us to dereference.
- if federatorMsg.APIri == nil {
- err := gtserror.New("status was not pinned to federatorMsg, and neither was an IRI for us to dereference")
- return nil, err
- }
-
- // Get the status + ensure we have
- // the most up-to-date version.
- status, _, err := p.federator.GetStatusByURI(
- ctx,
- federatorMsg.ReceivingAccount.Username,
- federatorMsg.APIri,
- )
- if err != nil {
- return nil, gtserror.Newf("%w", err)
- }
-
- return status, nil
-}
-
-// processCreateFaveFromFederator handles Activity Create with Object Like.
-func (p *Processor) processCreateFaveFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
- statusFave, ok := federatorMsg.GTSModel.(*gtsmodel.StatusFave)
- if !ok {
- return gtserror.New("Like was not parseable as *gtsmodel.StatusFave")
- }
-
- if err := p.notifyFave(ctx, statusFave); err != nil {
- return gtserror.Newf("error notifying status fave: %w", err)
- }
-
- // Interaction counts changed on the faved status;
- // uncache the prepared version from all timelines.
- p.invalidateStatusFromTimelines(ctx, statusFave.StatusID)
-
- return nil
-}
-
-// processCreateFollowRequestFromFederator handles Activity Create and Object Follow
-func (p *Processor) processCreateFollowRequestFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
- followRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest)
- if !ok {
- return errors.New("incomingFollowRequest was not parseable as *gtsmodel.FollowRequest")
- }
-
- // make sure the account is pinned
- if followRequest.Account == nil {
- a, err := p.state.DB.GetAccountByID(ctx, followRequest.AccountID)
- if err != nil {
- return err
- }
- followRequest.Account = a
- }
-
- // Get the remote account to make sure the avi and header are cached.
- if followRequest.Account.Domain != "" {
- remoteAccountID, err := url.Parse(followRequest.Account.URI)
- if err != nil {
- return err
- }
-
- a, _, err := p.federator.GetAccountByURI(ctx,
- federatorMsg.ReceivingAccount.Username,
- remoteAccountID,
- )
- if err != nil {
- return err
- }
-
- followRequest.Account = a
- }
-
- if followRequest.TargetAccount == nil {
- a, err := p.state.DB.GetAccountByID(ctx, followRequest.TargetAccountID)
- if err != nil {
- return err
- }
- followRequest.TargetAccount = a
- }
-
- if *followRequest.TargetAccount.Locked {
- // if the account is locked just notify the follow request and nothing else
- return p.notifyFollowRequest(ctx, followRequest)
- }
-
- // if the target account isn't locked, we should already accept the follow and notify about the new follower instead
- follow, err := p.state.DB.AcceptFollowRequest(ctx, followRequest.AccountID, followRequest.TargetAccountID)
- if err != nil {
- return err
- }
-
- if err := p.federateAcceptFollowRequest(ctx, follow); err != nil {
- return err
- }
-
- return p.notifyFollow(ctx, follow, followRequest.TargetAccount)
-}
-
-// processCreateAnnounceFromFederator handles Activity Create with Object Announce.
-func (p *Processor) processCreateAnnounceFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
- status, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return gtserror.New("Announce was not parseable as *gtsmodel.Status")
- }
-
- // Dereference status that this status boosts.
- if err := p.federator.DereferenceAnnounce(ctx, status, federatorMsg.ReceivingAccount.Username); err != nil {
- return gtserror.Newf("error dereferencing announce: %w", err)
- }
-
- // Generate an ID for the boost wrapper status.
- statusID, err := id.NewULIDFromTime(status.CreatedAt)
- if err != nil {
- return gtserror.Newf("error generating id: %w", err)
- }
- status.ID = statusID
-
- // Store the boost wrapper status.
- if err := p.state.DB.PutStatus(ctx, status); err != nil {
- return gtserror.Newf("db error inserting status: %w", err)
- }
-
- // Ensure boosted status ancestors dereferenced. We need at least
- // the immediate parent (if present) to ascertain timelineability.
- if err := p.federator.DereferenceStatusAncestors(ctx,
- federatorMsg.ReceivingAccount.Username,
- status.BoostOf,
- ); err != nil {
- return err
- }
-
- // Timeline and notify the announce.
- if err := p.timelineAndNotifyStatus(ctx, status); err != nil {
- return gtserror.Newf("error timelining status: %w", err)
- }
-
- if err := p.notifyAnnounce(ctx, status); err != nil {
- return gtserror.Newf("error notifying status: %w", err)
- }
-
- // Interaction counts changed on the boosted status;
- // uncache the prepared version from all timelines.
- p.invalidateStatusFromTimelines(ctx, status.ID)
-
- return nil
-}
-
-// processCreateBlockFromFederator handles Activity Create and Object Block
-func (p *Processor) processCreateBlockFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
- block, ok := federatorMsg.GTSModel.(*gtsmodel.Block)
- if !ok {
- return gtserror.New("block was not parseable as *gtsmodel.Block")
- }
-
- // Remove each account's posts from the other's timelines.
- //
- // First home timelines.
- if err := p.state.Timelines.Home.WipeItemsFromAccountID(ctx, block.AccountID, block.TargetAccountID); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- if err := p.state.Timelines.Home.WipeItemsFromAccountID(ctx, block.TargetAccountID, block.AccountID); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- // Now list timelines.
- if err := p.state.Timelines.List.WipeItemsFromAccountID(ctx, block.AccountID, block.TargetAccountID); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- if err := p.state.Timelines.List.WipeItemsFromAccountID(ctx, block.TargetAccountID, block.AccountID); err != nil {
- return gtserror.Newf("%w", err)
- }
-
- // Remove any follows that existed between blocker + blockee.
- if err := p.state.DB.DeleteFollow(ctx, block.AccountID, block.TargetAccountID); err != nil {
- return gtserror.Newf(
- "db error deleting follow from %s targeting %s: %w",
- block.AccountID, block.TargetAccountID, err,
- )
- }
-
- if err := p.state.DB.DeleteFollow(ctx, block.TargetAccountID, block.AccountID); err != nil {
- return gtserror.Newf(
- "db error deleting follow from %s targeting %s: %w",
- block.TargetAccountID, block.AccountID, err,
- )
- }
-
- // Remove any follow requests that existed between blocker + blockee.
- if err := p.state.DB.DeleteFollowRequest(ctx, block.AccountID, block.TargetAccountID); err != nil {
- return gtserror.Newf(
- "db error deleting follow request from %s targeting %s: %w",
- block.AccountID, block.TargetAccountID, err,
- )
- }
-
- if err := p.state.DB.DeleteFollowRequest(ctx, block.TargetAccountID, block.AccountID); err != nil {
- return gtserror.Newf(
- "db error deleting follow request from %s targeting %s: %w",
- block.TargetAccountID, block.AccountID, err,
- )
- }
-
- return nil
-}
-
-func (p *Processor) processCreateFlagFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
- incomingReport, ok := federatorMsg.GTSModel.(*gtsmodel.Report)
- if !ok {
- return errors.New("flag was not parseable as *gtsmodel.Report")
- }
-
- // TODO: handle additional side effects of flag creation:
- // - notify admins by dm / notification
-
- return p.emailReport(ctx, incomingReport)
-}
-
-// processUpdateAccountFromFederator handles Activity Update and Object Profile
-func (p *Processor) processUpdateAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
- // Parse the old/existing account model.
- account, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
- if !ok {
- return gtserror.New("account was not parseable as *gtsmodel.Account")
- }
-
- // Because this was an Update, the new Accountable should be set on the message.
- apubAcc, ok := federatorMsg.APObjectModel.(ap.Accountable)
- if !ok {
- return gtserror.New("Accountable was not parseable on update account message")
- }
-
- // Fetch up-to-date bio, avatar, header, etc.
- _, _, err := p.federator.RefreshAccount(
- ctx,
- federatorMsg.ReceivingAccount.Username,
- account,
- apubAcc,
- true, // Force refresh.
- )
- if err != nil {
- return gtserror.Newf("error refreshing updated account: %w", err)
- }
-
- return nil
-}
-
-// processDeleteStatusFromFederator handles Activity Delete and Object Note
-func (p *Processor) processDeleteStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
- status, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
- if !ok {
- return errors.New("Note was not parseable as *gtsmodel.Status")
- }
-
- // Delete attachments from this status, since this request
- // comes from the federating API, and there's no way the
- // poster can do a delete + redraft for it on our instance.
- deleteAttachments := true
- if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil {
- return gtserror.Newf("error wiping status: %w", err)
- }
-
- if status.InReplyToID != "" {
- // Interaction counts changed on the replied status;
- // uncache the prepared version from all timelines.
- p.invalidateStatusFromTimelines(ctx, status.InReplyToID)
- }
-
- return nil
-}
-
-// processDeleteAccountFromFederator handles Activity Delete and Object Profile
-func (p *Processor) processDeleteAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
- account, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
- if !ok {
- return errors.New("account delete was not parseable as *gtsmodel.Account")
- }
-
- return p.account.Delete(ctx, account, account.ID)
-}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index 2f1f43826..c0fd15a24 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -18,13 +18,9 @@
package processing
import (
- "context"
-
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
- "github.com/superseriousbusiness/gotosocial/internal/log"
mm "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
"github.com/superseriousbusiness/gotosocial/internal/processing/admin"
@@ -38,19 +34,23 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing/stream"
"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/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/visibility"
)
+// Processor groups together processing functions and
+// sub processors for handling actions + events coming
+// from either the client or federating APIs.
+//
+// Many of the functions available through this struct
+// or sub processors will trigger asynchronous processing
+// via the workers contained in state.
type Processor struct {
- federator federation.Federator
- tc typeutils.TypeConverter
- oauthServer oauth.Server
- mediaManager *mm.Manager
- state *state.State
- emailSender email.Sender
- filter *visibility.Filter
+ tc typeutils.TypeConverter
+ oauthServer oauth.Server
+ state *state.State
/*
SUB-PROCESSORS
@@ -68,6 +68,7 @@ type Processor struct {
stream stream.Processor
timeline timeline.Processor
user user.Processor
+ workers workers.Processor
}
func (p *Processor) Account() *account.Processor {
@@ -118,6 +119,10 @@ func (p *Processor) User() *user.Processor {
return &p.user
}
+func (p *Processor) Workers() *workers.Processor {
+ return &p.workers
+}
+
// NewProcessor returns a new Processor.
func NewProcessor(
tc typeutils.TypeConverter,
@@ -127,57 +132,53 @@ func NewProcessor(
state *state.State,
emailSender email.Sender,
) *Processor {
- parseMentionFunc := GetParseMentionFunc(state.DB, federator)
-
- filter := visibility.NewFilter(state)
+ var (
+ parseMentionFunc = GetParseMentionFunc(state.DB, federator)
+ filter = visibility.NewFilter(state)
+ )
processor := &Processor{
- federator: federator,
- tc: tc,
- oauthServer: oauthServer,
- mediaManager: mediaManager,
- state: state,
- filter: filter,
- emailSender: emailSender,
+ tc: tc,
+ oauthServer: oauthServer,
+ state: state,
}
// Instantiate sub processors.
- processor.account = account.New(state, tc, mediaManager, oauthServer, federator, filter, parseMentionFunc)
+ //
+ // Start with sub processors that will
+ // be required by the workers processor.
+ accountProcessor := account.New(state, tc, mediaManager, oauthServer, federator, filter, parseMentionFunc)
+ mediaProcessor := media.New(state, tc, mediaManager, federator.TransportController())
+ streamProcessor := stream.New(state, oauthServer)
+
+ // Instantiate the rest of the sub
+ // processors + pin them to this struct.
+ processor.account = accountProcessor
processor.admin = admin.New(state, tc, mediaManager, federator.TransportController(), emailSender)
processor.fedi = fedi.New(state, tc, federator, filter)
processor.list = list.New(state, tc)
processor.markers = markers.New(state, tc)
- processor.media = media.New(state, tc, mediaManager, federator.TransportController())
+ processor.media = mediaProcessor
processor.report = report.New(state, tc)
processor.timeline = timeline.New(state, tc, filter)
processor.search = search.New(state, federator, tc, filter)
processor.status = status.New(state, federator, tc, filter, parseMentionFunc)
- processor.stream = stream.New(state, oauthServer)
+ processor.stream = streamProcessor
processor.user = user.New(state, emailSender)
+ // Workers processor handles asynchronous
+ // worker jobs; instantiate it separately
+ // and pass subset of sub processors it needs.
+ processor.workers = workers.New(
+ state,
+ federator,
+ tc,
+ filter,
+ emailSender,
+ &accountProcessor,
+ &mediaProcessor,
+ &streamProcessor,
+ )
+
return processor
}
-
-func (p *Processor) EnqueueClientAPI(ctx context.Context, msgs ...messages.FromClientAPI) {
- log.Trace(ctx, "enqueuing")
- _ = p.state.Workers.ClientAPI.MustEnqueueCtx(ctx, func(ctx context.Context) {
- for _, msg := range msgs {
- log.Trace(ctx, "processing: %+v", msg)
- if err := p.ProcessFromClientAPI(ctx, msg); err != nil {
- log.Errorf(ctx, "error processing client API message: %v", err)
- }
- }
- })
-}
-
-func (p *Processor) EnqueueFederator(ctx context.Context, msgs ...messages.FromFederator) {
- log.Trace(ctx, "enqueuing")
- _ = p.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
- for _, msg := range msgs {
- log.Trace(ctx, "processing: %+v", msg)
- if err := p.ProcessFromFederator(ctx, msg); err != nil {
- log.Errorf(ctx, "error processing federator message: %v", err)
- }
- }
- })
-}
diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go
index fc85b38ba..383e1dc9f 100644
--- a/internal/processing/processor_test.go
+++ b/internal/processing/processor_test.go
@@ -123,8 +123,8 @@ func (suite *ProcessingStandardTestSuite) SetupTest() {
suite.emailSender = testrig.NewEmailSender("../../web/template/", nil)
suite.processor = processing.NewProcessor(suite.typeconverter, suite.federator, suite.oauthServer, suite.mediaManager, &suite.state, suite.emailSender)
- suite.state.Workers.EnqueueClientAPI = suite.processor.EnqueueClientAPI
- suite.state.Workers.EnqueueFederator = suite.processor.EnqueueFederator
+ suite.state.Workers.EnqueueClientAPI = suite.processor.Workers().EnqueueClientAPI
+ suite.state.Workers.EnqueueFediAPI = suite.processor.Workers().EnqueueFediAPI
testrig.StandardDBSetup(suite.db, suite.testAccounts)
testrig.StandardStorageSetup(suite.storage, "../../testrig/media")
diff --git a/internal/processing/stream/stream.go b/internal/processing/stream/stream.go
index bd49a330c..972173c7a 100644
--- a/internal/processing/stream/stream.go
+++ b/internal/processing/stream/stream.go
@@ -28,13 +28,14 @@ import (
type Processor struct {
state *state.State
oauthServer oauth.Server
- streamMap sync.Map
+ streamMap *sync.Map
}
func New(state *state.State, oauthServer oauth.Server) Processor {
return Processor{
state: state,
oauthServer: oauthServer,
+ streamMap: &sync.Map{},
}
}
diff --git a/internal/processing/user/email.go b/internal/processing/user/email.go
index 32c8e760a..dd2a96ae3 100644
--- a/internal/processing/user/email.go
+++ b/internal/processing/user/email.go
@@ -23,67 +23,13 @@ import (
"fmt"
"time"
- "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"
)
var oneWeek = 168 * time.Hour
-// EmailSendConfirmation sends an email address confirmation request email to the given user.
-func (p *Processor) EmailSendConfirmation(ctx context.Context, user *gtsmodel.User, username string) error {
- if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email {
- // user has already confirmed this email address, so there's nothing to do
- return nil
- }
-
- // We need a token and a link for the user to click on.
- // We'll use a uuid as our token since it's basically impossible to guess.
- // From the uuid package we use (which uses crypto/rand under the hood):
- // Randomly generated UUIDs have 122 random bits. One's annual risk of being
- // hit by a meteorite is estimated to be one chance in 17 billion, that
- // means the probability is about 0.00000000006 (6 × 10−11),
- // equivalent to the odds of creating a few tens of trillions of UUIDs in a
- // year and having one duplicate.
- confirmationToken := uuid.NewString()
- confirmationLink := uris.GenerateURIForEmailConfirm(confirmationToken)
-
- // pull our instance entry from the database so we can greet the user nicely in the email
- instance := >smodel.Instance{}
- host := config.GetHost()
- if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "domain", Value: host}}, instance); err != nil {
- return fmt.Errorf("SendConfirmEmail: error getting instance: %s", err)
- }
-
- // assemble the email contents and send the email
- confirmData := email.ConfirmData{
- Username: username,
- InstanceURL: instance.URI,
- InstanceName: instance.Title,
- ConfirmLink: confirmationLink,
- }
- if err := p.emailSender.SendConfirmEmail(user.UnconfirmedEmail, confirmData); err != nil {
- return fmt.Errorf("SendConfirmEmail: error sending to email address %s belonging to user %s: %s", user.UnconfirmedEmail, username, err)
- }
-
- // email sent, now we need to update the user entry with the token we just sent them
- updatingColumns := []string{"confirmation_sent_at", "confirmation_token", "last_emailed_at", "updated_at"}
- user.ConfirmationSentAt = time.Now()
- user.ConfirmationToken = confirmationToken
- user.LastEmailedAt = time.Now()
- user.UpdatedAt = time.Now()
-
- if err := p.state.DB.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil {
- return fmt.Errorf("SendConfirmEmail: error updating user entry after email sent: %s", err)
- }
-
- return nil
-}
-
// EmailConfirm processes an email confirmation request, usually initiated as a result of clicking on a link
// in a 'confirm your email address' type email.
func (p *Processor) EmailConfirm(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
diff --git a/internal/processing/user/email_test.go b/internal/processing/user/email_test.go
index 038307e55..b42446991 100644
--- a/internal/processing/user/email_test.go
+++ b/internal/processing/user/email_test.go
@@ -19,7 +19,6 @@ package user_test
import (
"context"
- "fmt"
"testing"
"time"
@@ -30,36 +29,6 @@ type EmailConfirmTestSuite struct {
UserStandardTestSuite
}
-func (suite *EmailConfirmTestSuite) TestSendConfirmEmail() {
- user := suite.testUsers["local_account_1"]
-
- // set a bunch of stuff on the user as though zork hasn't been confirmed (perish the thought)
- user.UnconfirmedEmail = "some.email@example.org"
- user.Email = ""
- user.ConfirmedAt = time.Time{}
- user.ConfirmationSentAt = time.Time{}
- user.ConfirmationToken = ""
-
- err := suite.user.EmailSendConfirmation(context.Background(), user, "the_mighty_zork")
- suite.NoError(err)
-
- // zork should have an email now
- suite.Len(suite.sentEmails, 1)
- email, ok := suite.sentEmails["some.email@example.org"]
- suite.True(ok)
-
- // a token should be set on zork
- token := user.ConfirmationToken
- suite.NotEmpty(token)
-
- // email should contain the token
- emailShould := fmt.Sprintf("To: some.email@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Email Confirmation\r\n\r\nHello the_mighty_zork!\r\n\r\nYou are receiving this mail because you've requested an account on http://localhost:8080.\r\n\r\nWe just need to confirm that this is your email address. To confirm your email, paste the following in your browser's address bar:\r\n\r\nhttp://localhost:8080/confirm_email?token=%s\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of http://localhost:8080\r\n\r\n", token)
- suite.Equal(emailShould, email)
-
- // confirmationSentAt should be recent
- suite.WithinDuration(time.Now(), user.ConfirmationSentAt, 1*time.Minute)
-}
-
func (suite *EmailConfirmTestSuite) TestConfirmEmail() {
ctx := context.Background()
diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go
new file mode 100644
index 000000000..76bfc892e
--- /dev/null
+++ b/internal/processing/workers/federate.go
@@ -0,0 +1,892 @@
+// 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 .
+
+package workers
+
+import (
+ "context"
+ "net/url"
+
+ "github.com/superseriousbusiness/activity/pub"
+ "github.com/superseriousbusiness/activity/streams"
+ "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"
+)
+
+// federate wraps functions for federating
+// something out via ActivityPub in response
+// to message processing.
+type federate struct {
+ // Embed federator to give access
+ // to send and retrieve functions.
+ federation.Federator
+ state *state.State
+ tc typeutils.TypeConverter
+}
+
+// parseURI is a cheeky little
+// shortcut to wrap parsing errors.
+//
+// The returned err will be prepended
+// with the name of the function that
+// called this function, so it can be
+// returned without further wrapping.
+func parseURI(s string) (*url.URL, error) {
+ const (
+ // Provides enough calldepth to
+ // prepend the name of whatever
+ // function called *this* one,
+ // so that they don't have to
+ // wrap the error themselves.
+ calldepth = 3
+ errFmt = "error parsing uri %s: %w"
+ )
+
+ uri, err := url.Parse(s)
+ if err != nil {
+ return nil, gtserror.NewfAt(calldepth, errFmt, s, err)
+ }
+
+ return uri, err
+}
+
+func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account) error {
+ // Do nothing if it's not our
+ // account that's been deleted.
+ if !account.IsLocal() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ actorIRI, err := parseURI(account.URI)
+ if err != nil {
+ return err
+ }
+
+ followersIRI, err := parseURI(account.FollowersURI)
+ if err != nil {
+ return err
+ }
+
+ publicIRI, err := parseURI(pub.PublicActivityPubIRI)
+ if err != nil {
+ return err
+ }
+
+ // Create a new delete.
+ // todo: tc.AccountToASDelete
+ delete := streams.NewActivityStreamsDelete()
+
+ // Set the Actor for the delete; no matter
+ // who actually did the delete, we should
+ // use the account owner for this.
+ deleteActor := streams.NewActivityStreamsActorProperty()
+ deleteActor.AppendIRI(actorIRI)
+ delete.SetActivityStreamsActor(deleteActor)
+
+ // Set the account's IRI as the 'object' property.
+ deleteObject := streams.NewActivityStreamsObjectProperty()
+ deleteObject.AppendIRI(actorIRI)
+ delete.SetActivityStreamsObject(deleteObject)
+
+ // Address the delete To followers.
+ deleteTo := streams.NewActivityStreamsToProperty()
+ deleteTo.AppendIRI(followersIRI)
+ delete.SetActivityStreamsTo(deleteTo)
+
+ // Address the delete CC public.
+ deleteCC := streams.NewActivityStreamsCcProperty()
+ deleteCC.AppendIRI(publicIRI)
+ delete.SetActivityStreamsCc(deleteCC)
+
+ // Send the Delete via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, delete,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ delete, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) error {
+ // Do nothing if the status
+ // shouldn't be federated.
+ if !*status.Federated {
+ return nil
+ }
+
+ // Do nothing if this
+ // isn't our status.
+ if !*status.Local {
+ return nil
+ }
+
+ // Populate model.
+ if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
+ return gtserror.Newf("error populating status: %w", err)
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(status.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ // Convert status to an ActivityStreams
+ // Note, wrapped in a Create activity.
+ asStatus, err := f.tc.StatusToAS(ctx, status)
+ if err != nil {
+ return gtserror.Newf("error converting status to AS: %w", err)
+ }
+
+ create, err := f.tc.WrapNoteInCreate(asStatus, false)
+ if err != nil {
+ return gtserror.Newf("error wrapping status in create: %w", err)
+ }
+
+ // Send the Create via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, create,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ create, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) DeleteStatus(ctx context.Context, status *gtsmodel.Status) error {
+ // Do nothing if the status
+ // shouldn't be federated.
+ if !*status.Federated {
+ return nil
+ }
+
+ // Do nothing if this
+ // isn't our status.
+ if !*status.Local {
+ return nil
+ }
+
+ // Populate model.
+ if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
+ return gtserror.Newf("error populating status: %w", err)
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(status.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ // Wrap the status URI in a Delete activity.
+ delete, err := f.tc.StatusToASDelete(ctx, status)
+ if err != nil {
+ return gtserror.Newf("error creating Delete: %w", err)
+ }
+
+ // Send the Delete via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, delete,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ delete, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) Follow(ctx context.Context, follow *gtsmodel.Follow) error {
+ // Populate model.
+ if err := f.state.DB.PopulateFollow(ctx, follow); err != nil {
+ return gtserror.Newf("error populating follow: %w", err)
+ }
+
+ // Do nothing if both accounts are local.
+ if follow.Account.IsLocal() &&
+ follow.TargetAccount.IsLocal() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(follow.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ // Convert follow to ActivityStreams Follow.
+ asFollow, err := f.tc.FollowToAS(ctx, follow)
+ if err != nil {
+ return gtserror.Newf("error converting follow to AS: %s", err)
+ }
+
+ // Send the Follow via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, asFollow,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ asFollow, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) UndoFollow(ctx context.Context, follow *gtsmodel.Follow) error {
+ // Populate model.
+ if err := f.state.DB.PopulateFollow(ctx, follow); err != nil {
+ return gtserror.Newf("error populating follow: %w", err)
+ }
+
+ // Do nothing if both accounts are local.
+ if follow.Account.IsLocal() &&
+ follow.TargetAccount.IsLocal() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(follow.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ targetAccountIRI, err := parseURI(follow.TargetAccount.URI)
+ if err != nil {
+ return err
+ }
+
+ // Recreate the ActivityStreams Follow.
+ asFollow, err := f.tc.FollowToAS(ctx, follow)
+ if err != nil {
+ return gtserror.Newf("error converting follow to AS: %w", err)
+ }
+
+ // Create a new Undo.
+ // todo: tc.FollowToASUndo
+ undo := streams.NewActivityStreamsUndo()
+
+ // Set the Actor for the Undo:
+ // same as the actor for the Follow.
+ undo.SetActivityStreamsActor(asFollow.GetActivityStreamsActor())
+
+ // Set recreated Follow as the 'object' property.
+ //
+ // For most AP implementations, it's not enough
+ // to just send the URI of the original Follow,
+ // we have to send the whole object again.
+ undoObject := streams.NewActivityStreamsObjectProperty()
+ undoObject.AppendActivityStreamsFollow(asFollow)
+ undo.SetActivityStreamsObject(undoObject)
+
+ // Address the Undo To the target account.
+ undoTo := streams.NewActivityStreamsToProperty()
+ undoTo.AppendIRI(targetAccountIRI)
+ undo.SetActivityStreamsTo(undoTo)
+
+ // Send the Undo via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, undo,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ undo, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) UndoLike(ctx context.Context, fave *gtsmodel.StatusFave) error {
+ // Populate model.
+ if err := f.state.DB.PopulateStatusFave(ctx, fave); err != nil {
+ return gtserror.Newf("error populating fave: %w", err)
+ }
+
+ // Do nothing if both accounts are local.
+ if fave.Account.IsLocal() &&
+ fave.TargetAccount.IsLocal() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(fave.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ targetAccountIRI, err := parseURI(fave.TargetAccount.URI)
+ if err != nil {
+ return err
+ }
+
+ // Recreate the ActivityStreams Like.
+ like, err := f.tc.FaveToAS(ctx, fave)
+ if err != nil {
+ return gtserror.Newf("error converting fave to AS: %w", err)
+ }
+
+ // Create a new Undo.
+ // todo: tc.FaveToASUndo
+ undo := streams.NewActivityStreamsUndo()
+
+ // Set the Actor for the Undo:
+ // same as the actor for the Like.
+ undo.SetActivityStreamsActor(like.GetActivityStreamsActor())
+
+ // Set recreated Like as the 'object' property.
+ //
+ // For most AP implementations, it's not enough
+ // to just send the URI of the original Like,
+ // we have to send the whole object again.
+ undoObject := streams.NewActivityStreamsObjectProperty()
+ undoObject.AppendActivityStreamsLike(like)
+ undo.SetActivityStreamsObject(undoObject)
+
+ // Address the Undo To the target account.
+ undoTo := streams.NewActivityStreamsToProperty()
+ undoTo.AppendIRI(targetAccountIRI)
+ undo.SetActivityStreamsTo(undoTo)
+
+ // Send the Undo via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, undo,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ undo, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) UndoAnnounce(ctx context.Context, boost *gtsmodel.Status) error {
+ // Populate model.
+ if err := f.state.DB.PopulateStatus(ctx, boost); err != nil {
+ return gtserror.Newf("error populating status: %w", err)
+ }
+
+ // Do nothing if boosting
+ // account isn't ours.
+ if !boost.Account.IsLocal() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(boost.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ // Recreate the ActivityStreams Announce.
+ asAnnounce, err := f.tc.BoostToAS(
+ ctx,
+ boost,
+ boost.Account,
+ boost.BoostOfAccount,
+ )
+ if err != nil {
+ return gtserror.Newf("error converting boost to AS: %w", err)
+ }
+
+ // Create a new Undo.
+ // todo: tc.AnnounceToASUndo
+ undo := streams.NewActivityStreamsUndo()
+
+ // Set the Actor for the Undo:
+ // same as the actor for the Announce.
+ undo.SetActivityStreamsActor(asAnnounce.GetActivityStreamsActor())
+
+ // Set recreated Announce as the 'object' property.
+ //
+ // For most AP implementations, it's not enough
+ // to just send the URI of the original Announce,
+ // we have to send the whole object again.
+ undoObject := streams.NewActivityStreamsObjectProperty()
+ undoObject.AppendActivityStreamsAnnounce(asAnnounce)
+ undo.SetActivityStreamsObject(undoObject)
+
+ // Address the Undo To the Announce To.
+ undo.SetActivityStreamsTo(asAnnounce.GetActivityStreamsTo())
+
+ // Address the Undo CC the Announce CC.
+ undo.SetActivityStreamsCc(asAnnounce.GetActivityStreamsCc())
+
+ // Send the Undo via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, undo,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ undo, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) AcceptFollow(ctx context.Context, follow *gtsmodel.Follow) error {
+ // Populate model.
+ if err := f.state.DB.PopulateFollow(ctx, follow); err != nil {
+ return gtserror.Newf("error populating follow: %w", err)
+ }
+
+ // Bail if requesting account is ours:
+ // we've already accepted internally and
+ // shouldn't send an Accept to ourselves.
+ if follow.Account.IsLocal() {
+ return nil
+ }
+
+ // Bail if target account isn't ours:
+ // we can't Accept a follow on
+ // another instance's behalf.
+ if follow.TargetAccount.IsRemote() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(follow.TargetAccount.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ acceptingAccountIRI, err := parseURI(follow.TargetAccount.URI)
+ if err != nil {
+ return err
+ }
+
+ requestingAccountIRI, err := parseURI(follow.Account.URI)
+ if err != nil {
+ return err
+ }
+
+ // Recreate the ActivityStreams Follow.
+ asFollow, err := f.tc.FollowToAS(ctx, follow)
+ if err != nil {
+ return gtserror.Newf("error converting follow to AS: %w", err)
+ }
+
+ // Create a new Accept.
+ // todo: tc.FollowToASAccept
+ accept := streams.NewActivityStreamsAccept()
+
+ // Set the requestee as Actor of the Accept.
+ acceptActorProp := streams.NewActivityStreamsActorProperty()
+ acceptActorProp.AppendIRI(acceptingAccountIRI)
+ accept.SetActivityStreamsActor(acceptActorProp)
+
+ // Set recreated Follow as the 'object' property.
+ //
+ // For most AP implementations, it's not enough
+ // to just send the URI of the original Follow,
+ // we have to send the whole object again.
+ acceptObject := streams.NewActivityStreamsObjectProperty()
+ acceptObject.AppendActivityStreamsFollow(asFollow)
+ accept.SetActivityStreamsObject(acceptObject)
+
+ // Address the Accept To the Follow requester.
+ acceptTo := streams.NewActivityStreamsToProperty()
+ acceptTo.AppendIRI(requestingAccountIRI)
+ accept.SetActivityStreamsTo(acceptTo)
+
+ // Send the Accept via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, accept,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ accept, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) RejectFollow(ctx context.Context, follow *gtsmodel.Follow) error {
+ // Ensure follow populated before proceeding.
+ if err := f.state.DB.PopulateFollow(ctx, follow); err != nil {
+ return gtserror.Newf("error populating follow: %w", err)
+ }
+
+ // Bail if requesting account is ours:
+ // we've already rejected internally and
+ // shouldn't send an Reject to ourselves.
+ if follow.Account.IsLocal() {
+ return nil
+ }
+
+ // Bail if target account isn't ours:
+ // we can't Reject a follow on
+ // another instance's behalf.
+ if follow.TargetAccount.IsRemote() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(follow.TargetAccount.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ rejectingAccountIRI, err := parseURI(follow.TargetAccount.URI)
+ if err != nil {
+ return err
+ }
+
+ requestingAccountIRI, err := parseURI(follow.Account.URI)
+ if err != nil {
+ return err
+ }
+
+ // Recreate the ActivityStreams Follow.
+ asFollow, err := f.tc.FollowToAS(ctx, follow)
+ if err != nil {
+ return gtserror.Newf("error converting follow to AS: %w", err)
+ }
+
+ // Create a new Reject.
+ // todo: tc.FollowRequestToASReject
+ reject := streams.NewActivityStreamsReject()
+
+ // Set the requestee as Actor of the Reject.
+ rejectActorProp := streams.NewActivityStreamsActorProperty()
+ rejectActorProp.AppendIRI(rejectingAccountIRI)
+ reject.SetActivityStreamsActor(rejectActorProp)
+
+ // Set recreated Follow as the 'object' property.
+ //
+ // For most AP implementations, it's not enough
+ // to just send the URI of the original Follow,
+ // we have to send the whole object again.
+ rejectObject := streams.NewActivityStreamsObjectProperty()
+ rejectObject.AppendActivityStreamsFollow(asFollow)
+ reject.SetActivityStreamsObject(rejectObject)
+
+ // Address the Reject To the Follow requester.
+ rejectTo := streams.NewActivityStreamsToProperty()
+ rejectTo.AppendIRI(requestingAccountIRI)
+ reject.SetActivityStreamsTo(rejectTo)
+
+ // Send the Reject via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, reject,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ reject, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error {
+ // Populate model.
+ if err := f.state.DB.PopulateStatusFave(ctx, fave); err != nil {
+ return gtserror.Newf("error populating fave: %w", err)
+ }
+
+ // Do nothing if both accounts are local.
+ if fave.Account.IsLocal() &&
+ fave.TargetAccount.IsLocal() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(fave.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ // Create the ActivityStreams Like.
+ like, err := f.tc.FaveToAS(ctx, fave)
+ if err != nil {
+ return gtserror.Newf("error converting fave to AS Like: %w", err)
+ }
+
+ // Send the Like via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, like,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ like, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
+ // Populate model.
+ if err := f.state.DB.PopulateStatus(ctx, boost); err != nil {
+ return gtserror.Newf("error populating status: %w", err)
+ }
+
+ // Do nothing if boosting
+ // account isn't ours.
+ if !boost.Account.IsLocal() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(boost.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ // Create the ActivityStreams Announce.
+ announce, err := f.tc.BoostToAS(
+ ctx,
+ boost,
+ boost.Account,
+ boost.BoostOfAccount,
+ )
+ if err != nil {
+ return gtserror.Newf("error converting boost to AS: %w", err)
+ }
+
+ // Send the Announce via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, announce,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ announce, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) UpdateAccount(ctx context.Context, account *gtsmodel.Account) error {
+ // Populate model.
+ if err := f.state.DB.PopulateAccount(ctx, account); err != nil {
+ return gtserror.Newf("error populating account: %w", err)
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ // Convert account to ActivityStreams Person.
+ person, err := f.tc.AccountToAS(ctx, account)
+ if err != nil {
+ return gtserror.Newf("error converting account to Person: %w", err)
+ }
+
+ // Use ActivityStreams Person as Object of Update.
+ update, err := f.tc.WrapPersonInUpdate(person, account)
+ if err != nil {
+ return gtserror.Newf("error wrapping Person in Update: %w", err)
+ }
+
+ // Send the Update via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, update,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ update, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) Block(ctx context.Context, block *gtsmodel.Block) error {
+ // Populate model.
+ if err := f.state.DB.PopulateBlock(ctx, block); err != nil {
+ return gtserror.Newf("error populating block: %w", err)
+ }
+
+ // Do nothing if both accounts are local.
+ if block.Account.IsLocal() &&
+ block.TargetAccount.IsLocal() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(block.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ // Convert block to ActivityStreams Block.
+ asBlock, err := f.tc.BlockToAS(ctx, block)
+ if err != nil {
+ return gtserror.Newf("error converting block to AS: %w", err)
+ }
+
+ // Send the Block via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, asBlock,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ asBlock, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) UndoBlock(ctx context.Context, block *gtsmodel.Block) error {
+ // Populate model.
+ if err := f.state.DB.PopulateBlock(ctx, block); err != nil {
+ return gtserror.Newf("error populating block: %w", err)
+ }
+
+ // Do nothing if both accounts are local.
+ if block.Account.IsLocal() &&
+ block.TargetAccount.IsLocal() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(block.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ targetAccountIRI, err := parseURI(block.TargetAccount.URI)
+ if err != nil {
+ return err
+ }
+
+ // Convert block to ActivityStreams Block.
+ asBlock, err := f.tc.BlockToAS(ctx, block)
+ if err != nil {
+ return gtserror.Newf("error converting block to AS: %w", err)
+ }
+
+ // Create a new Undo.
+ // todo: tc.BlockToASUndo
+ undo := streams.NewActivityStreamsUndo()
+
+ // Set the Actor for the Undo:
+ // same as the actor for the Block.
+ undo.SetActivityStreamsActor(asBlock.GetActivityStreamsActor())
+
+ // Set Block as the 'object' property.
+ //
+ // For most AP implementations, it's not enough
+ // to just send the URI of the original Block,
+ // we have to send the whole object again.
+ undoObject := streams.NewActivityStreamsObjectProperty()
+ undoObject.AppendActivityStreamsBlock(asBlock)
+ undo.SetActivityStreamsObject(undoObject)
+
+ // Address the Undo To the target account.
+ undoTo := streams.NewActivityStreamsToProperty()
+ undoTo.AppendIRI(targetAccountIRI)
+ undo.SetActivityStreamsTo(undoTo)
+
+ // Send the Undo via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, undo,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ undo, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) Flag(ctx context.Context, report *gtsmodel.Report) error {
+ // Populate model.
+ if err := f.state.DB.PopulateReport(ctx, report); err != nil {
+ return gtserror.Newf("error populating report: %w", err)
+ }
+
+ // Do nothing if report target
+ // is not remote account.
+ if report.TargetAccount.IsLocal() {
+ return nil
+ }
+
+ // Get our instance account from the db:
+ // to anonymize the report, we'll deliver
+ // using the outbox of the instance account.
+ instanceAcct, err := f.state.DB.GetInstanceAccount(ctx, "")
+ if err != nil {
+ return gtserror.Newf("error getting instance account: %w", err)
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(instanceAcct.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ targetAccountIRI, err := parseURI(report.TargetAccount.URI)
+ if err != nil {
+ return err
+ }
+
+ // Convert report to ActivityStreams Flag.
+ flag, err := f.tc.ReportToASFlag(ctx, report)
+ if err != nil {
+ return gtserror.Newf("error converting report to AS: %w", err)
+ }
+
+ // To is not set explicitly on Flags. Instead,
+ // address Flag BTo report target account URI.
+ // This ensures that our federating actor still
+ // knows where to send the report, but the BTo
+ // property will be stripped before sending.
+ //
+ // Happily, BTo does not prevent federating
+ // actor from using shared inbox to deliver.
+ bTo := streams.NewActivityStreamsBtoProperty()
+ bTo.AppendIRI(targetAccountIRI)
+ flag.SetActivityStreamsBto(bTo)
+
+ // Send the Flag via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, flag,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ flag, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go
new file mode 100644
index 000000000..40efc20bb
--- /dev/null
+++ b/internal/processing/workers/fromclientapi.go
@@ -0,0 +1,548 @@
+// 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 .
+
+package workers
+
+import (
+ "context"
+ "errors"
+
+ "codeberg.org/gruf/go-kv"
+ "codeberg.org/gruf/go-logger/v2/level"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "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/messages"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/account"
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+)
+
+// clientAPI wraps processing functions
+// specifically for messages originating
+// from the client/REST API.
+type clientAPI struct {
+ state *state.State
+ tc typeutils.TypeConverter
+ surface *surface
+ federate *federate
+ wipeStatus wipeStatus
+ account *account.Processor
+}
+
+func (p *Processor) EnqueueClientAPI(ctx context.Context, msgs ...messages.FromClientAPI) {
+ log.Trace(ctx, "enqueuing")
+ _ = p.workers.ClientAPI.MustEnqueueCtx(ctx, func(ctx context.Context) {
+ for _, msg := range msgs {
+ log.Trace(ctx, "processing: %+v", msg)
+ if err := p.ProcessFromClientAPI(ctx, msg); err != nil {
+ log.Errorf(ctx, "error processing client API message: %v", err)
+ }
+ }
+ })
+}
+
+func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg messages.FromClientAPI) error {
+ // Allocate new log fields slice
+ fields := make([]kv.Field, 3, 4)
+ fields[0] = kv.Field{"activityType", cMsg.APActivityType}
+ fields[1] = kv.Field{"objectType", cMsg.APObjectType}
+ fields[2] = kv.Field{"fromAccount", cMsg.OriginAccount.Username}
+
+ // Include GTSModel in logs if appropriate.
+ if cMsg.GTSModel != nil &&
+ log.Level() >= level.DEBUG {
+ fields = append(fields, kv.Field{
+ "model", cMsg.GTSModel,
+ })
+ }
+
+ l := log.WithContext(ctx).WithFields(fields...)
+ l.Info("processing from client API")
+
+ switch cMsg.APActivityType {
+
+ // CREATE SOMETHING
+ case ap.ActivityCreate:
+ switch cMsg.APObjectType {
+
+ // CREATE PROFILE/ACCOUNT
+ case ap.ObjectProfile, ap.ActorPerson:
+ return p.clientAPI.CreateAccount(ctx, cMsg)
+
+ // CREATE NOTE/STATUS
+ case ap.ObjectNote:
+ return p.clientAPI.CreateStatus(ctx, cMsg)
+
+ // CREATE FOLLOW (request)
+ case ap.ActivityFollow:
+ return p.clientAPI.CreateFollowReq(ctx, cMsg)
+
+ // CREATE LIKE/FAVE
+ case ap.ActivityLike:
+ return p.clientAPI.CreateLike(ctx, cMsg)
+
+ // CREATE ANNOUNCE/BOOST
+ case ap.ActivityAnnounce:
+ return p.clientAPI.CreateAnnounce(ctx, cMsg)
+
+ // CREATE BLOCK
+ case ap.ActivityBlock:
+ return p.clientAPI.CreateBlock(ctx, cMsg)
+ }
+
+ // UPDATE SOMETHING
+ case ap.ActivityUpdate:
+ switch cMsg.APObjectType {
+
+ // UPDATE PROFILE/ACCOUNT
+ case ap.ObjectProfile, ap.ActorPerson:
+ return p.clientAPI.UpdateAccount(ctx, cMsg)
+
+ // UPDATE A FLAG/REPORT (mark as resolved/closed)
+ case ap.ActivityFlag:
+ return p.clientAPI.UpdateReport(ctx, cMsg)
+ }
+
+ // ACCEPT SOMETHING
+ case ap.ActivityAccept:
+ switch cMsg.APObjectType { //nolint:gocritic
+
+ // ACCEPT FOLLOW (request)
+ case ap.ActivityFollow:
+ return p.clientAPI.AcceptFollow(ctx, cMsg)
+ }
+
+ // REJECT SOMETHING
+ case ap.ActivityReject:
+ switch cMsg.APObjectType { //nolint:gocritic
+
+ // REJECT FOLLOW (request)
+ case ap.ActivityFollow:
+ return p.clientAPI.RejectFollowRequest(ctx, cMsg)
+ }
+
+ // UNDO SOMETHING
+ case ap.ActivityUndo:
+ switch cMsg.APObjectType {
+
+ // UNDO FOLLOW (request)
+ case ap.ActivityFollow:
+ return p.clientAPI.UndoFollow(ctx, cMsg)
+
+ // UNDO BLOCK
+ case ap.ActivityBlock:
+ return p.clientAPI.UndoBlock(ctx, cMsg)
+
+ // UNDO LIKE/FAVE
+ case ap.ActivityLike:
+ return p.clientAPI.UndoFave(ctx, cMsg)
+
+ // UNDO ANNOUNCE/BOOST
+ case ap.ActivityAnnounce:
+ return p.clientAPI.UndoAnnounce(ctx, cMsg)
+ }
+
+ // DELETE SOMETHING
+ case ap.ActivityDelete:
+ switch cMsg.APObjectType {
+
+ // DELETE NOTE/STATUS
+ case ap.ObjectNote:
+ return p.clientAPI.DeleteStatus(ctx, cMsg)
+
+ // DELETE PROFILE/ACCOUNT
+ case ap.ObjectProfile, ap.ActorPerson:
+ return p.clientAPI.DeleteAccount(ctx, cMsg)
+ }
+
+ // FLAG/REPORT SOMETHING
+ case ap.ActivityFlag:
+ switch cMsg.APObjectType { //nolint:gocritic
+
+ // FLAG/REPORT A PROFILE
+ case ap.ObjectProfile:
+ return p.clientAPI.ReportAccount(ctx, cMsg)
+ }
+ }
+
+ return nil
+}
+
+func (p *clientAPI) CreateAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
+ account, ok := cMsg.GTSModel.(*gtsmodel.Account)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Account", cMsg.GTSModel)
+ }
+
+ // Send a confirmation email to the newly created account.
+ user, err := p.state.DB.GetUserByAccountID(ctx, account.ID)
+ if err != nil {
+ return gtserror.Newf("db error getting user for account id %s: %w", account.ID, err)
+ }
+
+ if err := p.surface.emailPleaseConfirm(ctx, user, account.Username); err != nil {
+ return gtserror.Newf("error emailing %s: %w", account.Username, err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) CreateStatus(ctx context.Context, cMsg messages.FromClientAPI) error {
+ status, ok := cMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
+ }
+
+ if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
+ return gtserror.Newf("error timelining status: %w", err)
+ }
+
+ if status.InReplyToID != "" {
+ // Interaction counts changed on the replied status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+ }
+
+ if err := p.federate.CreateStatus(ctx, status); err != nil {
+ return gtserror.Newf("error federating status: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) CreateFollowReq(ctx context.Context, cMsg messages.FromClientAPI) error {
+ followRequest, ok := cMsg.GTSModel.(*gtsmodel.FollowRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.FollowRequest", cMsg.GTSModel)
+ }
+
+ if err := p.surface.notifyFollowRequest(ctx, followRequest); err != nil {
+ return gtserror.Newf("error notifying follow request: %w", err)
+ }
+
+ if err := p.federate.Follow(
+ ctx,
+ p.tc.FollowRequestToFollow(ctx, followRequest),
+ ); err != nil {
+ return gtserror.Newf("error federating follow: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) CreateLike(ctx context.Context, cMsg messages.FromClientAPI) error {
+ fave, ok := cMsg.GTSModel.(*gtsmodel.StatusFave)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", cMsg.GTSModel)
+ }
+
+ if err := p.surface.notifyFave(ctx, fave); err != nil {
+ return gtserror.Newf("error notifying fave: %w", err)
+ }
+
+ // Interaction counts changed on the faved status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, fave.StatusID)
+
+ if err := p.federate.Like(ctx, fave); err != nil {
+ return gtserror.Newf("error federating like: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg messages.FromClientAPI) error {
+ boost, ok := cMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
+ }
+
+ // Timeline and notify the boost wrapper status.
+ if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
+ return gtserror.Newf("error timelining boost: %w", err)
+ }
+
+ // Notify the boost target account.
+ if err := p.surface.notifyAnnounce(ctx, boost); err != nil {
+ return gtserror.Newf("error notifying boost: %w", err)
+ }
+
+ // Interaction counts changed on the boosted status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
+
+ if err := p.federate.Announce(ctx, boost); err != nil {
+ return gtserror.Newf("error federating announce: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) CreateBlock(ctx context.Context, cMsg messages.FromClientAPI) error {
+ block, ok := cMsg.GTSModel.(*gtsmodel.Block)
+ if !ok {
+ 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)
+ }
+
+ // 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)
+ }
+
+ // TODO: same with notifications?
+ // TODO: same with bookmarks?
+
+ if err := p.federate.Block(ctx, block); err != nil {
+ return gtserror.Newf("error federating block: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) UpdateAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
+ account, ok := cMsg.GTSModel.(*gtsmodel.Account)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Account", cMsg.GTSModel)
+ }
+
+ if err := p.federate.UpdateAccount(ctx, account); err != nil {
+ return gtserror.Newf("error federating account update: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) UpdateReport(ctx context.Context, cMsg messages.FromClientAPI) error {
+ report, ok := cMsg.GTSModel.(*gtsmodel.Report)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Report", cMsg.GTSModel)
+ }
+
+ if report.Account.IsRemote() {
+ // Report creator is a remote account,
+ // we shouldn't try to email them!
+ return nil
+ }
+
+ if err := p.surface.emailReportClosed(ctx, report); err != nil {
+ return gtserror.Newf("error sending report closed email: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) AcceptFollow(ctx context.Context, cMsg messages.FromClientAPI) error {
+ follow, ok := cMsg.GTSModel.(*gtsmodel.Follow)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Follow", cMsg.GTSModel)
+ }
+
+ if err := p.surface.notifyFollow(ctx, follow); err != nil {
+ return gtserror.Newf("error notifying follow: %w", err)
+ }
+
+ if err := p.federate.AcceptFollow(ctx, follow); err != nil {
+ return gtserror.Newf("error federating follow request accept: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) RejectFollowRequest(ctx context.Context, cMsg messages.FromClientAPI) error {
+ followReq, ok := cMsg.GTSModel.(*gtsmodel.FollowRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.FollowRequest", cMsg.GTSModel)
+ }
+
+ if err := p.federate.RejectFollow(
+ ctx,
+ p.tc.FollowRequestToFollow(ctx, followReq),
+ ); err != nil {
+ return gtserror.Newf("error federating reject follow: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) UndoFollow(ctx context.Context, cMsg messages.FromClientAPI) error {
+ follow, ok := cMsg.GTSModel.(*gtsmodel.Follow)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Follow", cMsg.GTSModel)
+ }
+
+ if err := p.federate.UndoFollow(ctx, follow); err != nil {
+ return gtserror.Newf("error federating undo follow: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) UndoBlock(ctx context.Context, cMsg messages.FromClientAPI) error {
+ block, ok := cMsg.GTSModel.(*gtsmodel.Block)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Block", cMsg.GTSModel)
+ }
+
+ if err := p.federate.UndoBlock(ctx, block); err != nil {
+ return gtserror.Newf("error federating undo block: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) UndoFave(ctx context.Context, cMsg messages.FromClientAPI) error {
+ statusFave, ok := cMsg.GTSModel.(*gtsmodel.StatusFave)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", cMsg.GTSModel)
+ }
+
+ // Interaction counts changed on the faved status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, statusFave.StatusID)
+
+ if err := p.federate.UndoLike(ctx, statusFave); err != nil {
+ return gtserror.Newf("error federating undo like: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg messages.FromClientAPI) error {
+ status, ok := cMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
+ }
+
+ if err := p.state.DB.DeleteStatusByID(ctx, status.ID); err != nil {
+ return gtserror.Newf("db error deleting status: %w", err)
+ }
+
+ if err := p.surface.deleteStatusFromTimelines(ctx, status.ID); err != nil {
+ return gtserror.Newf("error removing status from timelines: %w", err)
+ }
+
+ // Interaction counts changed on the boosted status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, status.BoostOfID)
+
+ if err := p.federate.UndoAnnounce(ctx, status); err != nil {
+ return gtserror.Newf("error federating undo announce: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg messages.FromClientAPI) error {
+ // Don't delete attachments, just unattach them:
+ // this request comes from the client API and the
+ // poster may want to use attachments again later.
+ const deleteAttachments = false
+
+ status, ok := cMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
+ }
+
+ // Try to populate status structs if possible,
+ // in order to more thoroughly remove them.
+ if err := p.state.DB.PopulateStatus(
+ ctx, status,
+ ); err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return gtserror.Newf("db error populating status: %w", err)
+ }
+
+ if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil {
+ return gtserror.Newf("error wiping status: %w", err)
+ }
+
+ if status.InReplyToID != "" {
+ // Interaction counts changed on the replied status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+ }
+
+ if err := p.federate.DeleteStatus(ctx, status); err != nil {
+ return gtserror.Newf("error federating status delete: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) DeleteAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
+ // The originID of the delete, one of:
+ // - ID of a domain block, for which
+ // this account delete is a side effect.
+ // - ID of the deleted account itself (self delete).
+ // - ID of an admin account (account suspension).
+ var originID string
+
+ if domainBlock, ok := cMsg.GTSModel.(*gtsmodel.DomainBlock); ok {
+ // Origin is a domain block.
+ originID = domainBlock.ID
+ } else {
+ // Origin is whichever account
+ // originated this message.
+ originID = cMsg.OriginAccount.ID
+ }
+
+ if err := p.federate.DeleteAccount(ctx, cMsg.TargetAccount); err != nil {
+ return gtserror.Newf("error federating account delete: %w", err)
+ }
+
+ if err := p.account.Delete(ctx, cMsg.TargetAccount, originID); err != nil {
+ return gtserror.Newf("error deleting account: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) ReportAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
+ report, ok := cMsg.GTSModel.(*gtsmodel.Report)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Report", cMsg.GTSModel)
+ }
+
+ // Federate this report to the
+ // remote instance if desired.
+ if *report.Forwarded {
+ if err := p.federate.Flag(ctx, report); err != nil {
+ return gtserror.Newf("error federating report: %w", err)
+ }
+ }
+
+ if err := p.surface.emailReportOpened(ctx, report); err != nil {
+ return gtserror.Newf("error sending report opened email: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go
new file mode 100644
index 000000000..6690a43db
--- /dev/null
+++ b/internal/processing/workers/fromclientapi_test.go
@@ -0,0 +1,589 @@
+// 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 .
+
+package workers_test
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+ "github.com/superseriousbusiness/gotosocial/internal/stream"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type FromClientAPITestSuite struct {
+ WorkersTestSuite
+}
+
+func (suite *FromClientAPITestSuite) newStatus(
+ ctx context.Context,
+ account *gtsmodel.Account,
+ visibility gtsmodel.Visibility,
+ replyToStatus *gtsmodel.Status,
+ boostOfStatus *gtsmodel.Status,
+) *gtsmodel.Status {
+ var (
+ protocol = config.GetProtocol()
+ host = config.GetHost()
+ statusID = id.NewULID()
+ )
+
+ // Make a new status from given account.
+ newStatus := >smodel.Status{
+ ID: statusID,
+ URI: protocol + "://" + host + "/users/" + account.Username + "/statuses/" + statusID,
+ URL: protocol + "://" + host + "/@" + account.Username + "/statuses/" + statusID,
+ Content: "pee pee poo poo",
+ Local: util.Ptr(true),
+ AccountURI: account.URI,
+ AccountID: account.ID,
+ Visibility: visibility,
+ ActivityStreamsType: ap.ObjectNote,
+ Federated: util.Ptr(true),
+ Boostable: util.Ptr(true),
+ Replyable: util.Ptr(true),
+ Likeable: util.Ptr(true),
+ }
+
+ if replyToStatus != nil {
+ // Status is a reply.
+ newStatus.InReplyToAccountID = replyToStatus.AccountID
+ newStatus.InReplyToID = replyToStatus.ID
+ newStatus.InReplyToURI = replyToStatus.URI
+
+ // Mention the replied-to account.
+ mention := >smodel.Mention{
+ ID: id.NewULID(),
+ StatusID: statusID,
+ OriginAccountID: account.ID,
+ OriginAccountURI: account.URI,
+ TargetAccountID: replyToStatus.AccountID,
+ }
+
+ if err := suite.db.PutMention(ctx, mention); err != nil {
+ suite.FailNow(err.Error())
+ }
+ newStatus.Mentions = []*gtsmodel.Mention{mention}
+ newStatus.MentionIDs = []string{mention.ID}
+ }
+
+ if boostOfStatus != nil {
+ // Status is a boost.
+
+ }
+
+ // Put the status in the db, to mimic what would
+ // have already happened earlier up the flow.
+ if err := suite.db.PutStatus(ctx, newStatus); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ return newStatus
+}
+
+func (suite *FromClientAPITestSuite) checkStreamed(
+ str *stream.Stream,
+ expectMessage bool,
+ expectPayload string,
+ expectEventType string,
+) {
+ var msg *stream.Message
+streamLoop:
+ for {
+ select {
+ case msg = <-str.Messages:
+ break streamLoop // Got it.
+ case <-time.After(5 * time.Second):
+ break streamLoop // Didn't get it.
+ }
+ }
+
+ if expectMessage && msg == nil {
+ suite.FailNow("expected a message but message was nil")
+ }
+
+ if !expectMessage && msg != nil {
+ suite.FailNow("expected no message but message was not nil")
+ }
+
+ if expectPayload != "" && msg.Payload != expectPayload {
+ suite.FailNow("", "expected payload %s but payload was: %s", expectPayload, msg.Payload)
+ }
+
+ if expectEventType != "" && msg.Event != expectEventType {
+ suite.FailNow("", "expected event type %s but event type was: %s", expectEventType, msg.Event)
+ }
+}
+
+func (suite *FromClientAPITestSuite) statusJSON(
+ ctx context.Context,
+ status *gtsmodel.Status,
+ requestingAccount *gtsmodel.Account,
+) string {
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(
+ ctx,
+ status,
+ requestingAccount,
+ )
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ statusJSON, err := json.Marshal(apiStatus)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ return string(statusJSON)
+}
+
+func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
+ var (
+ ctx = context.Background()
+ postingAccount = suite.testAccounts["admin_account"]
+ receivingAccount = suite.testAccounts["local_account_1"]
+ testList = suite.testLists["local_account_1_list_1"]
+ streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
+ 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(
+ ctx,
+ postingAccount,
+ gtsmodel.VisibilityPublic,
+ nil,
+ nil,
+ )
+ statusJSON = suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+ )
+
+ // Update the follow from receiving account -> posting account so
+ // that receiving account wants notifs when posting account posts.
+ follow := new(gtsmodel.Follow)
+ *follow = *suite.testFollows["local_account_1_admin_account"]
+
+ follow.Notify = util.Ptr(true)
+ if err := suite.db.UpdateFollow(ctx, follow); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Process the new status.
+ if err := suite.processor.Workers().ProcessFromClientAPI(
+ ctx,
+ messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: status,
+ OriginAccount: postingAccount,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Check message in home stream.
+ suite.checkStreamed(
+ homeStream,
+ true,
+ statusJSON,
+ stream.EventTypeUpdate,
+ )
+
+ // Check message in list stream.
+ suite.checkStreamed(
+ listStream,
+ true,
+ statusJSON,
+ stream.EventTypeUpdate,
+ )
+
+ // Wait for a notification to appear for the status.
+ var notif *gtsmodel.Notification
+ if !testrig.WaitFor(func() bool {
+ var err error
+ notif, err = suite.db.GetNotification(
+ ctx,
+ gtsmodel.NotificationStatus,
+ receivingAccount.ID,
+ postingAccount.ID,
+ status.ID,
+ )
+ return err == nil
+ }) {
+ suite.FailNow("timed out waiting for new status notification")
+ }
+
+ apiNotif, err := suite.typeconverter.NotificationToAPINotification(ctx, notif)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ notifJSON, err := json.Marshal(apiNotif)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Check message in notification stream.
+ suite.checkStreamed(
+ notifStream,
+ true,
+ string(notifJSON),
+ stream.EventTypeNotification,
+ )
+}
+
+func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
+ var (
+ ctx = context.Background()
+ postingAccount = suite.testAccounts["admin_account"]
+ receivingAccount = suite.testAccounts["local_account_1"]
+ testList = suite.testLists["local_account_1_list_1"]
+ streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
+ homeStream = streams[stream.TimelineHome]
+ listStream = streams[stream.TimelineList+":"+testList.ID]
+
+ // Admin account posts a reply to turtle.
+ // Since turtle is followed by zork, and
+ // the default replies policy for this list
+ // is to show replies to followed accounts,
+ // post should also show in the list stream.
+ status = suite.newStatus(
+ ctx,
+ postingAccount,
+ gtsmodel.VisibilityPublic,
+ suite.testStatuses["local_account_2_status_1"],
+ nil,
+ )
+ statusJSON = suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+ )
+
+ // Process the new status.
+ if err := suite.processor.Workers().ProcessFromClientAPI(
+ ctx,
+ messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: status,
+ OriginAccount: postingAccount,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Check message in home stream.
+ suite.checkStreamed(
+ homeStream,
+ true,
+ statusJSON,
+ stream.EventTypeUpdate,
+ )
+
+ // Check message in list stream.
+ suite.checkStreamed(
+ listStream,
+ true,
+ statusJSON,
+ stream.EventTypeUpdate,
+ )
+}
+
+func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyOK() {
+ // We're modifying the test list so take a copy.
+ testList := new(gtsmodel.List)
+ *testList = *suite.testLists["local_account_1_list_1"]
+
+ var (
+ ctx = context.Background()
+ postingAccount = suite.testAccounts["admin_account"]
+ receivingAccount = suite.testAccounts["local_account_1"]
+ streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
+ homeStream = streams[stream.TimelineHome]
+ listStream = streams[stream.TimelineList+":"+testList.ID]
+
+ // Admin account posts a reply to turtle.
+ status = suite.newStatus(
+ ctx,
+ postingAccount,
+ gtsmodel.VisibilityPublic,
+ suite.testStatuses["local_account_2_status_1"],
+ nil,
+ )
+ statusJSON = suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+ )
+
+ // Modify replies policy of test list to show replies
+ // only to other accounts in the same list. Since turtle
+ // and admin are in the same list, this means the reply
+ // should be shown in the list.
+ testList.RepliesPolicy = gtsmodel.RepliesPolicyList
+ if err := suite.db.UpdateList(ctx, testList, "replies_policy"); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Process the new status.
+ if err := suite.processor.Workers().ProcessFromClientAPI(
+ ctx,
+ messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: status,
+ OriginAccount: postingAccount,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Check message in home stream.
+ suite.checkStreamed(
+ homeStream,
+ true,
+ statusJSON,
+ stream.EventTypeUpdate,
+ )
+
+ // Check message in list stream.
+ suite.checkStreamed(
+ listStream,
+ true,
+ statusJSON,
+ stream.EventTypeUpdate,
+ )
+}
+
+func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyNo() {
+ // We're modifying the test list so take a copy.
+ testList := new(gtsmodel.List)
+ *testList = *suite.testLists["local_account_1_list_1"]
+
+ var (
+ ctx = context.Background()
+ postingAccount = suite.testAccounts["admin_account"]
+ receivingAccount = suite.testAccounts["local_account_1"]
+ streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
+ homeStream = streams[stream.TimelineHome]
+ listStream = streams[stream.TimelineList+":"+testList.ID]
+
+ // Admin account posts a reply to turtle.
+ status = suite.newStatus(
+ ctx,
+ postingAccount,
+ gtsmodel.VisibilityPublic,
+ suite.testStatuses["local_account_2_status_1"],
+ nil,
+ )
+ statusJSON = suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+ )
+
+ // Modify replies policy of test list to show replies
+ // only to other accounts in the same list. We're
+ // about to remove turtle from the same list as admin,
+ // so the new post should not be streamed to the list.
+ testList.RepliesPolicy = gtsmodel.RepliesPolicyList
+ if err := suite.db.UpdateList(ctx, testList, "replies_policy"); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Remove turtle from the list.
+ if err := suite.db.DeleteListEntry(ctx, suite.testListEntries["local_account_1_list_1_entry_1"].ID); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Process the new status.
+ if err := suite.processor.Workers().ProcessFromClientAPI(
+ ctx,
+ messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: status,
+ OriginAccount: postingAccount,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Check message in home stream.
+ suite.checkStreamed(
+ homeStream,
+ true,
+ statusJSON,
+ stream.EventTypeUpdate,
+ )
+
+ // Check message NOT in list stream.
+ suite.checkStreamed(
+ listStream,
+ false,
+ "",
+ "",
+ )
+}
+
+func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPolicyNone() {
+ // We're modifying the test list so take a copy.
+ testList := new(gtsmodel.List)
+ *testList = *suite.testLists["local_account_1_list_1"]
+
+ var (
+ ctx = context.Background()
+ postingAccount = suite.testAccounts["admin_account"]
+ receivingAccount = suite.testAccounts["local_account_1"]
+ streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
+ homeStream = streams[stream.TimelineHome]
+ listStream = streams[stream.TimelineList+":"+testList.ID]
+
+ // Admin account posts a reply to turtle.
+ status = suite.newStatus(
+ ctx,
+ postingAccount,
+ gtsmodel.VisibilityPublic,
+ suite.testStatuses["local_account_2_status_1"],
+ nil,
+ )
+ statusJSON = suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+ )
+
+ // Modify replies policy of test list.
+ // Since we're modifying the list to not
+ // show any replies, the post should not
+ // be streamed to the list.
+ testList.RepliesPolicy = gtsmodel.RepliesPolicyNone
+ if err := suite.db.UpdateList(ctx, testList, "replies_policy"); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Process the new status.
+ if err := suite.processor.Workers().ProcessFromClientAPI(
+ ctx,
+ messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: status,
+ OriginAccount: postingAccount,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Check message in home stream.
+ suite.checkStreamed(
+ homeStream,
+ true,
+ statusJSON,
+ stream.EventTypeUpdate,
+ )
+
+ // Check message NOT in list stream.
+ suite.checkStreamed(
+ listStream,
+ false,
+ "",
+ "",
+ )
+}
+
+func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
+ var (
+ ctx = context.Background()
+ deletingAccount = suite.testAccounts["local_account_1"]
+ receivingAccount = suite.testAccounts["local_account_2"]
+ deletedStatus = suite.testStatuses["local_account_1_status_1"]
+ boostOfDeletedStatus = suite.testStatuses["admin_account_status_4"]
+ streams = suite.openStreams(ctx, receivingAccount, nil)
+ homeStream = streams[stream.TimelineHome]
+ )
+
+ // Delete the status from the db first, to mimic what
+ // would have already happened earlier up the flow
+ if err := suite.db.DeleteStatusByID(ctx, deletedStatus.ID); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Process the status delete.
+ if err := suite.processor.Workers().ProcessFromClientAPI(
+ ctx,
+ messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityDelete,
+ GTSModel: deletedStatus,
+ OriginAccount: deletingAccount,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Stream should have the delete
+ // of admin's boost in it now.
+ suite.checkStreamed(
+ homeStream,
+ true,
+ boostOfDeletedStatus.ID,
+ stream.EventTypeDelete,
+ )
+
+ // Stream should also have the delete
+ // of the message itself in it.
+ suite.checkStreamed(
+ homeStream,
+ true,
+ deletedStatus.ID,
+ stream.EventTypeDelete,
+ )
+
+ // Boost should no longer be in the database.
+ if !testrig.WaitFor(func() bool {
+ _, err := suite.db.GetStatusByID(ctx, boostOfDeletedStatus.ID)
+ return errors.Is(err, db.ErrNoEntries)
+ }) {
+ suite.FailNow("timed out waiting for status delete")
+ }
+}
+
+func TestFromClientAPITestSuite(t *testing.T) {
+ suite.Run(t, &FromClientAPITestSuite{})
+}
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
new file mode 100644
index 000000000..5fbb0066b
--- /dev/null
+++ b/internal/processing/workers/fromfediapi.go
@@ -0,0 +1,540 @@
+// 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 .
+
+package workers
+
+import (
+ "context"
+ "net/url"
+
+ "codeberg.org/gruf/go-kv"
+ "codeberg.org/gruf/go-logger/v2/level"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "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/state"
+)
+
+// fediAPI wraps processing functions
+// specifically for messages originating
+// from the federation/ActivityPub API.
+type fediAPI struct {
+ state *state.State
+ surface *surface
+ federate *federate
+ wipeStatus wipeStatus
+ account *account.Processor
+}
+
+func (p *Processor) EnqueueFediAPI(ctx context.Context, msgs ...messages.FromFediAPI) {
+ log.Trace(ctx, "enqueuing")
+ _ = p.workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
+ for _, msg := range msgs {
+ log.Trace(ctx, "processing: %+v", msg)
+ if err := p.ProcessFromFediAPI(ctx, msg); err != nil {
+ log.Errorf(ctx, "error processing fedi API message: %v", err)
+ }
+ }
+ })
+}
+
+func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg messages.FromFediAPI) error {
+ // Allocate new log fields slice
+ fields := make([]kv.Field, 3, 5)
+ fields[0] = kv.Field{"activityType", fMsg.APActivityType}
+ fields[1] = kv.Field{"objectType", fMsg.APObjectType}
+ fields[2] = kv.Field{"toAccount", fMsg.ReceivingAccount.Username}
+
+ if fMsg.APIri != nil {
+ // An IRI was supplied, append to log
+ fields = append(fields, kv.Field{
+ "iri", fMsg.APIri,
+ })
+ }
+
+ // Include GTSModel in logs if appropriate.
+ if fMsg.GTSModel != nil &&
+ log.Level() >= level.DEBUG {
+ fields = append(fields, kv.Field{
+ "model", fMsg.GTSModel,
+ })
+ }
+
+ l := log.WithContext(ctx).WithFields(fields...)
+ l.Info("processing from fedi API")
+
+ switch fMsg.APActivityType {
+
+ // CREATE SOMETHING
+ case ap.ActivityCreate:
+ switch fMsg.APObjectType {
+
+ // CREATE NOTE/STATUS
+ case ap.ObjectNote:
+ return p.fediAPI.CreateStatus(ctx, fMsg)
+
+ // CREATE FOLLOW (request)
+ case ap.ActivityFollow:
+ return p.fediAPI.CreateFollowReq(ctx, fMsg)
+
+ // CREATE LIKE/FAVE
+ case ap.ActivityLike:
+ return p.fediAPI.CreateLike(ctx, fMsg)
+
+ // CREATE ANNOUNCE/BOOST
+ case ap.ActivityAnnounce:
+ return p.fediAPI.CreateAnnounce(ctx, fMsg)
+
+ // CREATE BLOCK
+ case ap.ActivityBlock:
+ return p.fediAPI.CreateBlock(ctx, fMsg)
+
+ // CREATE FLAG/REPORT
+ case ap.ActivityFlag:
+ return p.fediAPI.CreateFlag(ctx, fMsg)
+ }
+
+ // UPDATE SOMETHING
+ case ap.ActivityUpdate:
+ switch fMsg.APObjectType { //nolint:gocritic
+
+ // UPDATE PROFILE/ACCOUNT
+ case ap.ObjectProfile:
+ return p.fediAPI.UpdateAccount(ctx, fMsg)
+ }
+
+ // DELETE SOMETHING
+ case ap.ActivityDelete:
+ switch fMsg.APObjectType {
+
+ // DELETE NOTE/STATUS
+ case ap.ObjectNote:
+ return p.fediAPI.DeleteStatus(ctx, fMsg)
+
+ // DELETE PROFILE/ACCOUNT
+ case ap.ObjectProfile:
+ return p.fediAPI.DeleteAccount(ctx, fMsg)
+ }
+ }
+
+ return nil
+}
+
+func (p *fediAPI) CreateStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
+ var (
+ status *gtsmodel.Status
+ err error
+
+ // Check the federatorMsg for either an already dereferenced
+ // and converted status pinned to the message, or a forwarded
+ // AP IRI that we still need to deref.
+ forwarded = (fMsg.GTSModel == nil)
+ )
+
+ if forwarded {
+ // Model was not set, deref with IRI.
+ // This will also cause the status to be inserted into the db.
+ status, err = p.statusFromAPIRI(ctx, fMsg)
+ } else {
+ // Model is set, ensure we have the most up-to-date model.
+ status, err = p.statusFromGTSModel(ctx, fMsg)
+ }
+
+ if err != nil {
+ return gtserror.Newf("error extracting status from federatorMsg: %w", err)
+ }
+
+ if status.Account == nil || status.Account.IsRemote() {
+ // Either no account attached yet, or a remote account.
+ // Both situations we need to parse account URI to fetch it.
+ accountURI, err := url.Parse(status.AccountURI)
+ if err != nil {
+ return err
+ }
+
+ // Ensure that account for this status has been deref'd.
+ status.Account, _, err = p.federate.GetAccountByURI(
+ ctx,
+ fMsg.ReceivingAccount.Username,
+ accountURI,
+ )
+ if err != nil {
+ return err
+ }
+ }
+
+ // Ensure status ancestors dereferenced. We need at least the
+ // immediate parent (if present) to ascertain timelineability.
+ if err := p.federate.DereferenceStatusAncestors(
+ ctx,
+ fMsg.ReceivingAccount.Username,
+ status,
+ ); err != nil {
+ return err
+ }
+
+ if status.InReplyToID != "" {
+ // Interaction counts changed on the replied status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+ }
+
+ if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
+ return gtserror.Newf("error timelining status: %w", err)
+ }
+
+ return nil
+}
+
+func (p *fediAPI) statusFromGTSModel(ctx context.Context, fMsg messages.FromFediAPI) (*gtsmodel.Status, error) {
+ // There should be a status pinned to the message:
+ // we've already checked to ensure this is not nil.
+ status, ok := fMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ err := gtserror.New("Note was not parseable as *gtsmodel.Status")
+ return nil, err
+ }
+
+ // AP statusable representation may have also
+ // been set on message (no problem if not).
+ statusable, _ := fMsg.APObjectModel.(ap.Statusable)
+
+ // Call refresh on status to update
+ // it (deref remote) if necessary.
+ var err error
+ status, _, err = p.federate.RefreshStatus(
+ ctx,
+ fMsg.ReceivingAccount.Username,
+ status,
+ statusable,
+ false, // Don't force refresh.
+ )
+ if err != nil {
+ return nil, gtserror.Newf("%w", err)
+ }
+
+ return status, nil
+}
+
+func (p *fediAPI) statusFromAPIRI(ctx context.Context, fMsg messages.FromFediAPI) (*gtsmodel.Status, error) {
+ // There should be a status IRI pinned to
+ // the federatorMsg for us to dereference.
+ if fMsg.APIri == nil {
+ err := gtserror.New(
+ "status was not pinned to federatorMsg, " +
+ "and neither was an IRI for us to dereference",
+ )
+ return nil, err
+ }
+
+ // Get the status + ensure we have
+ // the most up-to-date version.
+ status, _, err := p.federate.GetStatusByURI(
+ ctx,
+ fMsg.ReceivingAccount.Username,
+ fMsg.APIri,
+ )
+ if err != nil {
+ return nil, gtserror.Newf("%w", err)
+ }
+
+ return status, nil
+}
+
+func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg messages.FromFediAPI) error {
+ followRequest, ok := fMsg.GTSModel.(*gtsmodel.FollowRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.FollowRequest", fMsg.GTSModel)
+ }
+
+ if *followRequest.TargetAccount.Locked {
+ // Account on our instance is locked:
+ // just notify the follow request.
+ if err := p.surface.notifyFollowRequest(ctx, followRequest); err != nil {
+ return gtserror.Newf("error notifying follow request: %w", err)
+ }
+
+ return nil
+ }
+
+ // Account on our instance is not locked:
+ // Automatically accept the follow request
+ // and notify about the new follower.
+ follow, err := p.state.DB.AcceptFollowRequest(
+ ctx,
+ followRequest.AccountID,
+ followRequest.TargetAccountID,
+ )
+ if err != nil {
+ return gtserror.Newf("error accepting follow request: %w", err)
+ }
+
+ if err := p.federate.AcceptFollow(ctx, follow); err != nil {
+ return gtserror.Newf("error federating accept follow request: %w", err)
+ }
+
+ if err := p.surface.notifyFollow(ctx, follow); err != nil {
+ return gtserror.Newf("error notifying follow: %w", err)
+ }
+
+ return nil
+}
+
+func (p *fediAPI) CreateLike(ctx context.Context, fMsg messages.FromFediAPI) error {
+ fave, ok := fMsg.GTSModel.(*gtsmodel.StatusFave)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", fMsg.GTSModel)
+ }
+
+ if err := p.surface.notifyFave(ctx, fave); err != nil {
+ return gtserror.Newf("error notifying fave: %w", err)
+ }
+
+ // Interaction counts changed on the faved status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, fave.StatusID)
+
+ return nil
+}
+
+func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg messages.FromFediAPI) error {
+ status, ok := fMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
+ }
+
+ // Dereference status that this status boosts.
+ if err := p.federate.DereferenceAnnounce(
+ ctx,
+ status,
+ fMsg.ReceivingAccount.Username,
+ ); err != nil {
+ return gtserror.Newf("error dereferencing announce: %w", err)
+ }
+
+ // Generate an ID for the boost wrapper status.
+ statusID, err := id.NewULIDFromTime(status.CreatedAt)
+ if err != nil {
+ return gtserror.Newf("error generating id: %w", err)
+ }
+ status.ID = statusID
+
+ // Store the boost wrapper status.
+ if err := p.state.DB.PutStatus(ctx, status); err != nil {
+ return gtserror.Newf("db error inserting status: %w", err)
+ }
+
+ // Ensure boosted status ancestors dereferenced. We need at least
+ // the immediate parent (if present) to ascertain timelineability.
+ if err := p.federate.DereferenceStatusAncestors(ctx,
+ fMsg.ReceivingAccount.Username,
+ status.BoostOf,
+ ); err != nil {
+ return err
+ }
+
+ // Timeline and notify the announce.
+ if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
+ return gtserror.Newf("error timelining status: %w", err)
+ }
+
+ if err := p.surface.notifyAnnounce(ctx, status); err != nil {
+ return gtserror.Newf("error notifying status: %w", err)
+ }
+
+ // Interaction counts changed on the boosted status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, status.ID)
+
+ 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)
+ }
+
+ // Remove each account's posts from the other's timelines.
+ //
+ // First home timelines.
+ if err := p.state.Timelines.Home.WipeItemsFromAccountID(
+ ctx,
+ block.AccountID,
+ block.TargetAccountID,
+ ); err != nil {
+ return gtserror.Newf("%w", err)
+ }
+
+ if err := p.state.Timelines.Home.WipeItemsFromAccountID(
+ ctx,
+ block.TargetAccountID,
+ block.AccountID,
+ ); err != nil {
+ return gtserror.Newf("%w", err)
+ }
+
+ // Now list timelines.
+ if err := p.state.Timelines.List.WipeItemsFromAccountID(
+ ctx,
+ block.AccountID,
+ block.TargetAccountID,
+ ); err != nil {
+ return gtserror.Newf("%w", err)
+ }
+
+ if err := p.state.Timelines.List.WipeItemsFromAccountID(
+ ctx,
+ block.TargetAccountID,
+ block.AccountID,
+ ); err != nil {
+ return gtserror.Newf("%w", err)
+ }
+
+ // Remove any follows that existed between blocker + blockee.
+ if err := p.state.DB.DeleteFollow(
+ ctx,
+ block.AccountID,
+ block.TargetAccountID,
+ ); err != nil {
+ return gtserror.Newf(
+ "db error deleting follow from %s targeting %s: %w",
+ block.AccountID, block.TargetAccountID, err,
+ )
+ }
+
+ if err := p.state.DB.DeleteFollow(
+ ctx,
+ block.TargetAccountID,
+ block.AccountID,
+ ); err != nil {
+ return gtserror.Newf(
+ "db error deleting follow from %s targeting %s: %w",
+ block.TargetAccountID, block.AccountID, err,
+ )
+ }
+
+ // Remove any follow requests that existed between blocker + blockee.
+ if err := p.state.DB.DeleteFollowRequest(
+ ctx,
+ block.AccountID,
+ block.TargetAccountID,
+ ); err != nil {
+ return gtserror.Newf(
+ "db error deleting follow request from %s targeting %s: %w",
+ block.AccountID, block.TargetAccountID, err,
+ )
+ }
+
+ if err := p.state.DB.DeleteFollowRequest(
+ ctx,
+ block.TargetAccountID,
+ block.AccountID,
+ ); err != nil {
+ return gtserror.Newf(
+ "db error deleting follow request from %s targeting %s: %w",
+ block.TargetAccountID, block.AccountID, err,
+ )
+ }
+
+ return nil
+}
+
+func (p *fediAPI) CreateFlag(ctx context.Context, fMsg messages.FromFediAPI) error {
+ incomingReport, ok := fMsg.GTSModel.(*gtsmodel.Report)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Report", fMsg.GTSModel)
+ }
+
+ // TODO: handle additional side effects of flag creation:
+ // - notify admins by dm / notification
+
+ if err := p.surface.emailReportOpened(ctx, incomingReport); err != nil {
+ return gtserror.Newf("error sending report opened email: %w", err)
+ }
+
+ return nil
+}
+
+func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg messages.FromFediAPI) error {
+ // Parse the old/existing account model.
+ account, ok := fMsg.GTSModel.(*gtsmodel.Account)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Account", fMsg.GTSModel)
+ }
+
+ // Because this was an Update, the new Accountable should be set on the message.
+ apubAcc, ok := fMsg.APObjectModel.(ap.Accountable)
+ if !ok {
+ return gtserror.Newf("%T not parseable as ap.Accountable", fMsg.APObjectModel)
+ }
+
+ // Fetch up-to-date bio, avatar, header, etc.
+ _, _, err := p.federate.RefreshAccount(
+ ctx,
+ fMsg.ReceivingAccount.Username,
+ account,
+ apubAcc,
+ true, // Force refresh.
+ )
+ if err != nil {
+ return gtserror.Newf("error refreshing updated account: %w", err)
+ }
+
+ return nil
+}
+
+func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
+ // Delete attachments from this status, since this request
+ // comes from the federating API, and there's no way the
+ // poster can do a delete + redraft for it on our instance.
+ const deleteAttachments = true
+
+ status, ok := fMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
+ }
+
+ if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil {
+ return gtserror.Newf("error wiping status: %w", err)
+ }
+
+ if status.InReplyToID != "" {
+ // Interaction counts changed on the replied status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+ }
+
+ return nil
+}
+
+func (p *fediAPI) DeleteAccount(ctx context.Context, fMsg messages.FromFediAPI) error {
+ account, ok := fMsg.GTSModel.(*gtsmodel.Account)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.Account", fMsg.GTSModel)
+ }
+
+ if err := p.account.Delete(ctx, account, account.ID); err != nil {
+ return gtserror.Newf("error deleting account: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/fromfederator_test.go b/internal/processing/workers/fromfediapi_test.go
similarity index 93%
rename from internal/processing/fromfederator_test.go
rename to internal/processing/workers/fromfediapi_test.go
index 0b0e52811..f8e3941fc 100644
--- a/internal/processing/fromfederator_test.go
+++ b/internal/processing/workers/fromfediapi_test.go
@@ -15,7 +15,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-package processing_test
+package workers_test
import (
"context"
@@ -36,12 +36,12 @@ import (
"github.com/superseriousbusiness/gotosocial/testrig"
)
-type FromFederatorTestSuite struct {
- ProcessingStandardTestSuite
+type FromFediAPITestSuite struct {
+ WorkersTestSuite
}
// remote_account_1 boosts the first status of local_account_1
-func (suite *FromFederatorTestSuite) TestProcessFederationAnnounce() {
+func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
boostedStatus := suite.testStatuses["local_account_1_status_1"]
boostingAccount := suite.testAccounts["remote_account_1"]
announceStatus := >smodel.Status{}
@@ -56,7 +56,7 @@ func (suite *FromFederatorTestSuite) TestProcessFederationAnnounce() {
announceStatus.Account = boostingAccount
announceStatus.Visibility = boostedStatus.Visibility
- err := suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
+ err := suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: announceStatus,
@@ -87,7 +87,7 @@ func (suite *FromFederatorTestSuite) TestProcessFederationAnnounce() {
suite.False(*notif.Read)
}
-func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
+func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
repliedAccount := suite.testAccounts["local_account_1"]
repliedStatus := suite.testStatuses["local_account_1_status_1"]
replyingAccount := suite.testAccounts["remote_account_1"]
@@ -128,7 +128,7 @@ func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
err = suite.db.PutStatus(context.Background(), replyingStatus)
suite.NoError(err)
- err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
+ err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: replyingStatus,
@@ -173,7 +173,7 @@ func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
suite.Equal(replyingAccount.ID, notifStreamed.Account.ID)
}
-func (suite *FromFederatorTestSuite) TestProcessFave() {
+func (suite *FromFediAPITestSuite) TestProcessFave() {
favedAccount := suite.testAccounts["local_account_1"]
favedStatus := suite.testStatuses["local_account_1_status_1"]
favingAccount := suite.testAccounts["remote_account_1"]
@@ -197,7 +197,7 @@ func (suite *FromFederatorTestSuite) TestProcessFave() {
err := suite.db.Put(context.Background(), fave)
suite.NoError(err)
- err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
+ err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
@@ -245,7 +245,7 @@ func (suite *FromFederatorTestSuite) TestProcessFave() {
//
// This tests for an issue we were seeing where Misskey sends out faves to inboxes of people that don't own
// the fave, but just follow the actor who received the fave.
-func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccount() {
+func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount() {
receivingAccount := suite.testAccounts["local_account_2"]
favedAccount := suite.testAccounts["local_account_1"]
favedStatus := suite.testStatuses["local_account_1_status_1"]
@@ -270,7 +270,7 @@ func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccoun
err := suite.db.Put(context.Background(), fave)
suite.NoError(err)
- err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
+ err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
@@ -304,7 +304,7 @@ func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccoun
suite.Empty(wssStream.Messages)
}
-func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
+func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
ctx := context.Background()
deletedAccount := suite.testAccounts["remote_account_1"]
@@ -339,7 +339,7 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
suite.NoError(err)
// now they are mufos!
- err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
+ err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityDelete,
GTSModel: deletedAccount,
@@ -386,7 +386,7 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin)
}
-func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
+func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
ctx := context.Background()
originAccount := suite.testAccounts["remote_account_1"]
@@ -414,7 +414,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
err := suite.db.Put(ctx, satanFollowRequestTurtle)
suite.NoError(err)
- err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
+ err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: satanFollowRequestTurtle,
@@ -443,7 +443,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
suite.Empty(suite.httpClient.SentMessages)
}
-func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
+func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
ctx := context.Background()
originAccount := suite.testAccounts["remote_account_1"]
@@ -471,7 +471,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
err := suite.db.Put(ctx, satanFollowRequestTurtle)
suite.NoError(err)
- err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
+ err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: satanFollowRequestTurtle,
@@ -539,13 +539,13 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
}
// TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor.
-func (suite *FromFederatorTestSuite) TestCreateStatusFromIRI() {
+func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
ctx := context.Background()
receivingAccount := suite.testAccounts["local_account_1"]
statusCreator := suite.testAccounts["remote_account_2"]
- err := suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
+ err := suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: nil, // gtsmodel is nil because this is a forwarded status -- we want to dereference it using the iri
@@ -561,5 +561,5 @@ func (suite *FromFederatorTestSuite) TestCreateStatusFromIRI() {
}
func TestFromFederatorTestSuite(t *testing.T) {
- suite.Run(t, &FromFederatorTestSuite{})
+ suite.Run(t, &FromFediAPITestSuite{})
}
diff --git a/internal/processing/workers/surface.go b/internal/processing/workers/surface.go
new file mode 100644
index 000000000..a3cf9a3e1
--- /dev/null
+++ b/internal/processing/workers/surface.go
@@ -0,0 +1,40 @@
+// 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 .
+
+package workers
+
+import (
+ "github.com/superseriousbusiness/gotosocial/internal/email"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/stream"
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
+)
+
+// surface wraps functions for 'surfacing' the result
+// of processing a message, eg:
+// - timelining a status
+// - removing a status from timelines
+// - sending a notification to a user
+// - sending an email
+type surface struct {
+ state *state.State
+ tc typeutils.TypeConverter
+ stream *stream.Processor
+ filter *visibility.Filter
+ emailSender email.Sender
+}
diff --git a/internal/processing/workers/surfaceemail.go b/internal/processing/workers/surfaceemail.go
new file mode 100644
index 000000000..a6c97f48f
--- /dev/null
+++ b/internal/processing/workers/surfaceemail.go
@@ -0,0 +1,160 @@
+// 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 .
+
+package workers
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "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"
+)
+
+func (s *surface) emailReportOpened(ctx context.Context, report *gtsmodel.Report) error {
+ instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
+ if err != nil {
+ return gtserror.Newf("error getting instance: %w", err)
+ }
+
+ toAddresses, err := s.state.DB.GetInstanceModeratorAddresses(ctx)
+ if err != nil {
+ if errors.Is(err, db.ErrNoEntries) {
+ // No registered moderator addresses.
+ return nil
+ }
+ return gtserror.Newf("error getting instance moderator addresses: %w", err)
+ }
+
+ if err := s.state.DB.PopulateReport(ctx, report); err != nil {
+ return gtserror.Newf("error populating report: %w", err)
+ }
+
+ reportData := email.NewReportData{
+ InstanceURL: instance.URI,
+ InstanceName: instance.Title,
+ ReportURL: instance.URI + "/settings/admin/reports/" + report.ID,
+ ReportDomain: report.Account.Domain,
+ ReportTargetDomain: report.TargetAccount.Domain,
+ }
+
+ if err := s.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil {
+ return gtserror.Newf("error emailing instance moderators: %w", err)
+ }
+
+ return nil
+}
+
+func (s *surface) emailReportClosed(ctx context.Context, report *gtsmodel.Report) error {
+ user, err := s.state.DB.GetUserByAccountID(ctx, report.Account.ID)
+ if err != nil {
+ return gtserror.Newf("db error getting user: %w", err)
+ }
+
+ if user.ConfirmedAt.IsZero() ||
+ !*user.Approved ||
+ *user.Disabled ||
+ user.Email == "" {
+ // Only email users who:
+ // - are confirmed
+ // - are approved
+ // - are not disabled
+ // - have an email address
+ return nil
+ }
+
+ instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
+ if err != nil {
+ return gtserror.Newf("db error getting instance: %w", err)
+ }
+
+ if err := s.state.DB.PopulateReport(ctx, report); err != nil {
+ return gtserror.Newf("error populating report: %w", err)
+ }
+
+ reportClosedData := email.ReportClosedData{
+ Username: report.Account.Username,
+ InstanceURL: instance.URI,
+ InstanceName: instance.Title,
+ ReportTargetUsername: report.TargetAccount.Username,
+ ReportTargetDomain: report.TargetAccount.Domain,
+ ActionTakenComment: report.ActionTaken,
+ }
+
+ return s.emailSender.SendReportClosedEmail(user.Email, reportClosedData)
+}
+
+func (s *surface) emailPleaseConfirm(ctx context.Context, user *gtsmodel.User, username string) error {
+ if user.UnconfirmedEmail == "" ||
+ user.UnconfirmedEmail == user.Email {
+ // User has already confirmed this
+ // email address; nothing to do.
+ return nil
+ }
+
+ instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
+ if err != nil {
+ return gtserror.Newf("db error getting instance: %w", err)
+ }
+
+ // We need a token and a link for the
+ // user to click on. We'll use a uuid
+ // as our token since it's secure enough
+ // for this purpose.
+ var (
+ confirmToken = uuid.NewString()
+ confirmLink = uris.GenerateURIForEmailConfirm(confirmToken)
+ )
+
+ // Assemble email contents and send the email.
+ if err := s.emailSender.SendConfirmEmail(
+ user.UnconfirmedEmail,
+ email.ConfirmData{
+ Username: username,
+ InstanceURL: instance.URI,
+ InstanceName: instance.Title,
+ ConfirmLink: confirmLink,
+ },
+ ); err != nil {
+ return err
+ }
+
+ // Email sent, update the user entry
+ // with the new confirmation token.
+ now := time.Now()
+ user.ConfirmationToken = confirmToken
+ user.ConfirmationSentAt = now
+ user.LastEmailedAt = now
+
+ if err := s.state.DB.UpdateUser(
+ ctx,
+ user,
+ "confirmation_token",
+ "confirmation_sent_at",
+ "last_emailed_at",
+ ); err != nil {
+ return gtserror.Newf("error updating user entry after email sent: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go
new file mode 100644
index 000000000..00e1205e6
--- /dev/null
+++ b/internal/processing/workers/surfacenotify.go
@@ -0,0 +1,221 @@
+// 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 .
+
+package workers
+
+import (
+ "context"
+ "errors"
+
+ "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"
+)
+
+// notifyMentions notifies each targeted account in
+// the given mentions that they have a new mention.
+func (s *surface) notifyMentions(
+ ctx context.Context,
+ mentions []*gtsmodel.Mention,
+) error {
+ var errs = gtserror.NewMultiError(len(mentions))
+
+ for _, mention := range mentions {
+ if err := s.notify(
+ ctx,
+ gtsmodel.NotificationMention,
+ mention.TargetAccountID,
+ mention.OriginAccountID,
+ mention.StatusID,
+ ); err != nil {
+ errs.Append(err)
+ }
+ }
+
+ return errs.Combine()
+}
+
+// notifyFollowRequest notifies the target of the given
+// follow request that they have a new follow request.
+func (s *surface) notifyFollowRequest(
+ ctx context.Context,
+ followRequest *gtsmodel.FollowRequest,
+) error {
+ return s.notify(
+ ctx,
+ gtsmodel.NotificationFollowRequest,
+ followRequest.TargetAccountID,
+ followRequest.AccountID,
+ "",
+ )
+}
+
+// notifyFollow notifies the target of the given follow that
+// they have a new follow. It will also remove any previous
+// notification of a follow request, essentially replacing
+// that notification.
+func (s *surface) notifyFollow(
+ ctx context.Context,
+ follow *gtsmodel.Follow,
+) error {
+ // Check if previous follow req notif exists.
+ prevNotif, err := s.state.DB.GetNotification(
+ gtscontext.SetBarebones(ctx),
+ gtsmodel.NotificationFollowRequest,
+ follow.TargetAccountID,
+ follow.AccountID,
+ "",
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return gtserror.Newf("db error checking for previous follow request notification: %w", err)
+ }
+
+ if prevNotif != nil {
+ // Previous notif existed, delete it.
+ if err := s.state.DB.DeleteNotificationByID(ctx, prevNotif.ID); err != nil {
+ return gtserror.Newf("db error removing previous follow request notification %s: %w", prevNotif.ID, err)
+ }
+ }
+
+ // Now notify the follow itself.
+ return s.notify(
+ ctx,
+ gtsmodel.NotificationFollow,
+ follow.TargetAccountID,
+ follow.AccountID,
+ "",
+ )
+}
+
+// notifyFave notifies the target of the given
+// fave that their status has been liked/faved.
+func (s *surface) notifyFave(
+ ctx context.Context,
+ fave *gtsmodel.StatusFave,
+) error {
+ if fave.TargetAccountID == fave.AccountID {
+ // Self-fave, nothing to do.
+ return nil
+ }
+
+ return s.notify(
+ ctx,
+ gtsmodel.NotificationFave,
+ fave.TargetAccountID,
+ fave.AccountID,
+ fave.StatusID,
+ )
+}
+
+// notifyAnnounce notifies the status boost target
+// account that their status has been boosted.
+func (s *surface) notifyAnnounce(
+ ctx context.Context,
+ status *gtsmodel.Status,
+) error {
+ if status.BoostOfID == "" {
+ // Not a boost, nothing to do.
+ return nil
+ }
+
+ if status.BoostOfAccountID == status.AccountID {
+ // Self-boost, nothing to do.
+ return nil
+ }
+
+ return s.notify(
+ ctx,
+ gtsmodel.NotificationReblog,
+ status.BoostOfAccountID,
+ status.AccountID,
+ status.ID,
+ )
+}
+
+// notify creates, inserts, and streams a new
+// notification to the target account if it
+// doesn't yet exist with the given parameters.
+//
+// It filters out non-local target accounts, so
+// it is safe to pass all sorts of notification
+// targets into this function without filtering
+// for non-local first.
+//
+// targetAccountID and originAccountID must be
+// set, but statusID can be an empty string.
+func (s *surface) notify(
+ ctx context.Context,
+ notificationType gtsmodel.NotificationType,
+ targetAccountID string,
+ originAccountID string,
+ statusID string,
+) error {
+ targetAccount, err := s.state.DB.GetAccountByID(ctx, targetAccountID)
+ if err != nil {
+ return gtserror.Newf("error getting target account %s: %w", targetAccountID, err)
+ }
+
+ if !targetAccount.IsLocal() {
+ // Nothing to do.
+ return nil
+ }
+
+ // Make sure a notification doesn't
+ // already exist with these params.
+ if _, err := s.state.DB.GetNotification(
+ gtscontext.SetBarebones(ctx),
+ notificationType,
+ targetAccountID,
+ originAccountID,
+ statusID,
+ ); err == nil {
+ // Notification exists;
+ // nothing to do.
+ return nil
+ } else if !errors.Is(err, db.ErrNoEntries) {
+ // Real error.
+ return gtserror.Newf("error checking existence of notification: %w", err)
+ }
+
+ // Notification doesn't yet exist, so
+ // we need to create + store one.
+ notif := >smodel.Notification{
+ ID: id.NewULID(),
+ NotificationType: notificationType,
+ TargetAccountID: targetAccountID,
+ OriginAccountID: originAccountID,
+ StatusID: statusID,
+ }
+
+ if err := s.state.DB.PutNotification(ctx, notif); err != nil {
+ return gtserror.Newf("error putting notification in database: %w", err)
+ }
+
+ // Stream notification to the user.
+ apiNotif, err := s.tc.NotificationToAPINotification(ctx, notif)
+ if err != nil {
+ return gtserror.Newf("error converting notification to api representation: %w", err)
+ }
+
+ if err := s.stream.Notify(apiNotif, targetAccount); err != nil {
+ return gtserror.Newf("error streaming notification to account: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/workers/surfacetimeline.go b/internal/processing/workers/surfacetimeline.go
new file mode 100644
index 000000000..827cbe2f8
--- /dev/null
+++ b/internal/processing/workers/surfacetimeline.go
@@ -0,0 +1,401 @@
+// 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 .
+
+package workers
+
+import (
+ "context"
+ "errors"
+
+ "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/stream"
+ "github.com/superseriousbusiness/gotosocial/internal/timeline"
+)
+
+// timelineAndNotifyStatus inserts the given status into the HOME
+// and LIST timelines of accounts that follow the status author.
+//
+// It will also handle notifications for any mentions attached to
+// the account, and notifications for any local accounts that want
+// to know when this account posts.
+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)
+ }
+
+ // Get all local followers of the account that posted the status.
+ follows, err := s.state.DB.GetAccountLocalFollowers(ctx, status.AccountID)
+ if err != nil {
+ return gtserror.Newf("error getting local followers of account %s: %w", status.AccountID, err)
+ }
+
+ // If the poster is also local, add a fake entry for them
+ // so they can see their own status in their timeline.
+ if status.Account.IsLocal() {
+ follows = append(follows, >smodel.Follow{
+ AccountID: status.AccountID,
+ Account: status.Account,
+ Notify: func() *bool { b := false; return &b }(), // Account shouldn't notify itself.
+ ShowReblogs: func() *bool { b := true; return &b }(), // Account should show own reblogs.
+ })
+ }
+
+ // 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.
+ if err := s.timelineAndNotifyStatusForFollowers(ctx, status, follows); err != nil {
+ return gtserror.Newf("error timelining status %s for followers: %w", status.ID, err)
+ }
+
+ // Notify each local account that's mentioned by this status.
+ if err := s.notifyMentions(ctx, status.Mentions); err != nil {
+ return gtserror.Newf("error notifying status mentions for status %s: %w", status.ID, err)
+ }
+
+ 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
+// follower, as appropriate, and notifying each follower of the
+// new status, if the status is eligible for notification.
+func (s *surface) timelineAndNotifyStatusForFollowers(
+ ctx context.Context,
+ status *gtsmodel.Status,
+ follows []*gtsmodel.Follow,
+) error {
+ var (
+ errs = new(gtserror.MultiError)
+ boost = status.BoostOfID != ""
+ reply = status.InReplyToURI != ""
+ )
+
+ for _, follow := range follows {
+ // Do an initial rough-grained check to see if the
+ // status is timelineable for this follower at all
+ // based on its visibility and who it replies to etc.
+ timelineable, err := s.filter.StatusHomeTimelineable(
+ ctx, follow.Account, status,
+ )
+ if err != nil {
+ errs.Appendf("error checking status %s hometimelineability: %w", status.ID, err)
+ continue
+ }
+
+ if !timelineable {
+ // Nothing to do.
+ continue
+ }
+
+ if boost && !*follow.ShowReblogs {
+ // Status is a boost, but the owner of
+ // this follow doesn't want to see boosts
+ // from this account. We can safely skip
+ // everything, then, because we also know
+ // that the follow owner won't want to be
+ // have the status put in any list timelines,
+ // or be notified about the status either.
+ continue
+ }
+
+ // Add status to any relevant lists
+ // for this follow, if applicable.
+ s.listTimelineStatusForFollow(
+ ctx,
+ status,
+ follow,
+ errs,
+ )
+
+ // Add status to home timeline for owner
+ // of this follow, if applicable.
+ homeTimelined, err := s.timelineStatus(
+ ctx,
+ s.state.Timelines.Home.IngestOne,
+ follow.AccountID, // home timelines are keyed by account ID
+ follow.Account,
+ status,
+ stream.TimelineHome,
+ )
+ if err != nil {
+ errs.Appendf("error home timelining status: %w", err)
+ continue
+ }
+
+ if !homeTimelined {
+ // If status wasn't added to home
+ // timeline, we shouldn't notify it.
+ continue
+ }
+
+ if !*follow.Notify {
+ // This follower doesn't have notifs
+ // set for this account's new posts.
+ continue
+ }
+
+ if boost || reply {
+ // Don't notify for boosts or replies.
+ continue
+ }
+
+ // If we reach here, we know:
+ //
+ // - This status is hometimelineable.
+ // - This status was added to the home timeline for this follower.
+ // - This follower wants to be notified when this account posts.
+ // - This is a top-level post (not a reply or boost).
+ //
+ // That means we can officially notify this one.
+ if err := s.notify(
+ ctx,
+ gtsmodel.NotificationStatus,
+ follow.AccountID,
+ status.AccountID,
+ status.ID,
+ ); err != nil {
+ errs.Appendf("error notifying account %s about new status: %w", follow.AccountID, err)
+ }
+ }
+
+ return errs.Combine()
+}
+
+// listTimelineStatusForFollow puts the given status
+// in any eligible lists owned by the given follower.
+func (s *surface) listTimelineStatusForFollow(
+ ctx context.Context,
+ status *gtsmodel.Status,
+ follow *gtsmodel.Follow,
+ errs *gtserror.MultiError,
+) {
+ // To put this status in appropriate list timelines,
+ // we need to get each listEntry that pertains to
+ // this follow. Then, we want to iterate through all
+ // those list entries, and add the status to the list
+ // that the entry belongs to if it meets criteria for
+ // inclusion in the list.
+
+ // Get every list entry that targets this follow's ID.
+ listEntries, err := s.state.DB.GetListEntriesForFollowID(
+ // We only need the list IDs.
+ gtscontext.SetBarebones(ctx),
+ follow.ID,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ errs.Appendf("error getting list entries: %w", err)
+ return
+ }
+
+ // Check eligibility for each list entry (if any).
+ for _, listEntry := range listEntries {
+ eligible, err := s.listEligible(ctx, listEntry, status)
+ if err != nil {
+ errs.Appendf("error checking list eligibility: %w", err)
+ continue
+ }
+
+ if !eligible {
+ // Don't add this.
+ continue
+ }
+
+ // At this point we are certain this status
+ // should be included in the timeline of the
+ // list that this list entry belongs to.
+ if _, err := s.timelineStatus(
+ ctx,
+ s.state.Timelines.List.IngestOne,
+ listEntry.ListID, // list timelines are keyed by list ID
+ follow.Account,
+ status,
+ stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list
+ ); err != nil {
+ errs.Appendf("error adding status to timeline for list %s: %w", listEntry.ListID, err)
+ // implicit continue
+ }
+ }
+}
+
+// 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.
+func (s *surface) listEligible(
+ ctx context.Context,
+ listEntry *gtsmodel.ListEntry,
+ status *gtsmodel.Status,
+) (bool, error) {
+ if status.InReplyToURI == "" {
+ // If status is not a reply,
+ // then it's all gravy baby.
+ return true, nil
+ }
+
+ if status.InReplyToID == "" {
+ // Status is a reply but we don't
+ // have the replied-to account!
+ return false, nil
+ }
+
+ // Status is a reply to a known account.
+ // We need to fetch the list that this
+ // entry belongs to, in order to check
+ // the list's replies policy.
+ list, err := s.state.DB.GetListByID(
+ ctx, listEntry.ListID,
+ )
+ if err != nil {
+ err := gtserror.Newf("db error getting list %s: %w", listEntry.ListID, err)
+ return false, err
+ }
+
+ switch list.RepliesPolicy {
+ case gtsmodel.RepliesPolicyNone:
+ // This list should not show
+ // replies at all, so skip it.
+ return false, nil
+
+ case gtsmodel.RepliesPolicyList:
+ // This list should show replies
+ // only to other people in the list.
+ //
+ // Check if replied-to account is
+ // also included in this list.
+ includes, err := s.state.DB.ListIncludesAccount(
+ ctx,
+ list.ID,
+ status.InReplyToAccountID,
+ )
+
+ if err != nil {
+ err := gtserror.Newf(
+ "db error checking if account %s in list %s: %w",
+ status.InReplyToAccountID, listEntry.ListID, err,
+ )
+ return false, err
+ }
+
+ return includes, nil
+
+ case gtsmodel.RepliesPolicyFollowed:
+ // This list should show replies
+ // only to people that the list
+ // owner also follows.
+ //
+ // Check if replied-to account is
+ // followed by list owner account.
+ follows, err := s.state.DB.IsFollowing(
+ ctx,
+ list.AccountID,
+ status.InReplyToAccountID,
+ )
+ if err != nil {
+ err := gtserror.Newf(
+ "db error checking if account %s is followed by %s: %w",
+ status.InReplyToAccountID, list.AccountID, err,
+ )
+ return false, err
+ }
+
+ return follows, nil
+
+ default:
+ // HUH??
+ err := gtserror.Newf(
+ "reply policy '%s' not recognized on list %s",
+ list.RepliesPolicy, list.ID,
+ )
+ return false, err
+ }
+}
+
+// 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.
+func (s *surface) timelineStatus(
+ ctx context.Context,
+ ingest func(context.Context, string, timeline.Timelineable) (bool, error),
+ timelineID string,
+ account *gtsmodel.Account,
+ status *gtsmodel.Status,
+ streamType string,
+) (bool, error) {
+ // Ingest status into given timeline using provided function.
+ if inserted, err := ingest(ctx, timelineID, status); err != nil {
+ err = gtserror.Newf("error ingesting status %s: %w", status.ID, err)
+ return false, err
+ } else if !inserted {
+ // Nothing more to do.
+ return false, nil
+ }
+
+ // The status was inserted so stream it to the user.
+ apiStatus, err := s.tc.StatusToAPIStatus(ctx, status, account)
+ if err != nil {
+ err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
+ return true, err
+ }
+
+ if err := s.stream.Update(apiStatus, account, []string{streamType}); err != nil {
+ err = gtserror.Newf("error streaming update for status %s: %w", status.ID, err)
+ return true, err
+ }
+
+ return true, nil
+}
+
+// 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
+ }
+
+ return s.stream.Delete(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(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)
+ }
+}
diff --git a/internal/processing/workers/wipestatus.go b/internal/processing/workers/wipestatus.go
new file mode 100644
index 000000000..0891d9e24
--- /dev/null
+++ b/internal/processing/workers/wipestatus.go
@@ -0,0 +1,119 @@
+// 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 .
+
+package workers
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/media"
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+)
+
+// wipeStatus encapsulates common logic used to totally delete a status
+// + all its attachments, notifications, boosts, and timeline entries.
+type wipeStatus func(context.Context, *gtsmodel.Status, bool) error
+
+// wipeStatusF returns a wipeStatus util function.
+func wipeStatusF(state *state.State, media *media.Processor, surface *surface) wipeStatus {
+ return func(
+ ctx context.Context,
+ statusToDelete *gtsmodel.Status,
+ deleteAttachments bool,
+ ) error {
+ errs := new(gtserror.MultiError)
+
+ // Either delete all attachments for this status,
+ // or simply unattach + clean them separately later.
+ //
+ // Reason to unattach rather than delete is that
+ // the poster might want to reattach them to another
+ // status immediately (in case of delete + redraft)
+ if deleteAttachments {
+ // todo:state.DB.DeleteAttachmentsForStatus
+ for _, a := range statusToDelete.AttachmentIDs {
+ if err := media.Delete(ctx, a); err != nil {
+ errs.Appendf("error deleting media: %w", err)
+ }
+ }
+ } else {
+ // todo:state.DB.UnattachAttachmentsForStatus
+ for _, a := range statusToDelete.AttachmentIDs {
+ if _, err := media.Unattach(ctx, statusToDelete.Account, a); err != nil {
+ errs.Appendf("error unattaching media: %w", err)
+ }
+ }
+ }
+
+ // delete all mention entries generated by this status
+ // todo:state.DB.DeleteMentionsForStatus
+ for _, id := range statusToDelete.MentionIDs {
+ if err := state.DB.DeleteMentionByID(ctx, id); err != nil {
+ errs.Appendf("error deleting status mention: %w", err)
+ }
+ }
+
+ // delete all notification entries generated by this status
+ if err := state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
+ errs.Appendf("error deleting status notifications: %w", err)
+ }
+
+ // delete all bookmarks that point to this status
+ if err := state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
+ errs.Appendf("error deleting status bookmarks: %w", err)
+ }
+
+ // delete all faves of this status
+ if err := state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
+ errs.Appendf("error deleting status faves: %w", err)
+ }
+
+ // delete all boosts for this status + remove them from timelines
+ boosts, err := state.DB.GetStatusBoosts(
+ // we MUST set a barebones context here,
+ // as depending on where it came from the
+ // original BoostOf may already be gone.
+ gtscontext.SetBarebones(ctx),
+ statusToDelete.ID)
+ if err != nil {
+ errs.Appendf("error fetching status boosts: %w", err)
+ }
+ for _, b := range boosts {
+ if err := surface.deleteStatusFromTimelines(ctx, b.ID); err != nil {
+ errs.Appendf("error deleting boost from timelines: %w", err)
+ }
+ if err := state.DB.DeleteStatusByID(ctx, b.ID); err != nil {
+ errs.Appendf("error deleting boost: %w", err)
+ }
+ }
+
+ // delete this status from any and all timelines
+ if err := surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil {
+ errs.Appendf("error deleting status from timelines: %w", err)
+ }
+
+ // finally, delete the status itself
+ if err := state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil {
+ errs.Appendf("error deleting status: %w", err)
+ }
+
+ return errs.Combine()
+ }
+}
diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go
new file mode 100644
index 000000000..24b18a405
--- /dev/null
+++ b/internal/processing/workers/workers.go
@@ -0,0 +1,92 @@
+// 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 .
+
+package workers
+
+import (
+ "github.com/superseriousbusiness/gotosocial/internal/email"
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/account"
+ "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/visibility"
+ "github.com/superseriousbusiness/gotosocial/internal/workers"
+)
+
+type Processor struct {
+ workers *workers.Workers
+ clientAPI *clientAPI
+ fediAPI *fediAPI
+}
+
+func New(
+ state *state.State,
+ federator federation.Federator,
+ tc typeutils.TypeConverter,
+ filter *visibility.Filter,
+ emailSender email.Sender,
+ account *account.Processor,
+ media *media.Processor,
+ stream *stream.Processor,
+) Processor {
+ // Init surface logic
+ // wrapper struct.
+ surface := &surface{
+ state: state,
+ tc: tc,
+ stream: stream,
+ filter: filter,
+ emailSender: emailSender,
+ }
+
+ // Init federate logic
+ // wrapper struct.
+ federate := &federate{
+ Federator: federator,
+ state: state,
+ tc: tc,
+ }
+
+ // Init shared logic wipe
+ // status util func.
+ wipeStatus := wipeStatusF(
+ state,
+ media,
+ surface,
+ )
+
+ return Processor{
+ workers: &state.Workers,
+ clientAPI: &clientAPI{
+ state: state,
+ tc: tc,
+ surface: surface,
+ federate: federate,
+ wipeStatus: wipeStatus,
+ account: account,
+ },
+ fediAPI: &fediAPI{
+ state: state,
+ surface: surface,
+ federate: federate,
+ wipeStatus: wipeStatus,
+ account: account,
+ },
+ }
+}
diff --git a/internal/processing/workers/workers_test.go b/internal/processing/workers/workers_test.go
new file mode 100644
index 000000000..2d5a7f5d3
--- /dev/null
+++ b/internal/processing/workers/workers_test.go
@@ -0,0 +1,169 @@
+// 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 .
+
+package workers_test
+
+import (
+ "context"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/email"
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
+ "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/transport"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type WorkersTestSuite struct {
+ // standard suite interfaces
+ suite.Suite
+ db db.DB
+ storage *storage.Driver
+ state state.State
+ mediaManager *media.Manager
+ typeconverter typeutils.TypeConverter
+ httpClient *testrig.MockHTTPClient
+ transportController transport.Controller
+ federator federation.Federator
+ oauthServer oauth.Server
+ emailSender email.Sender
+
+ // 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
+ testFollows map[string]*gtsmodel.Follow
+ testAttachments map[string]*gtsmodel.MediaAttachment
+ testStatuses map[string]*gtsmodel.Status
+ testTags map[string]*gtsmodel.Tag
+ testMentions map[string]*gtsmodel.Mention
+ testAutheds map[string]*oauth.Auth
+ testBlocks map[string]*gtsmodel.Block
+ testActivities map[string]testrig.ActivityWithSignature
+ testLists map[string]*gtsmodel.List
+ testListEntries map[string]*gtsmodel.ListEntry
+
+ processor *processing.Processor
+}
+
+func (suite *WorkersTestSuite) SetupSuite() {
+ suite.testTokens = testrig.NewTestTokens()
+ suite.testClients = testrig.NewTestClients()
+ suite.testApplications = testrig.NewTestApplications()
+ suite.testUsers = testrig.NewTestUsers()
+ suite.testAccounts = testrig.NewTestAccounts()
+ suite.testFollows = testrig.NewTestFollows()
+ suite.testAttachments = testrig.NewTestAttachments()
+ suite.testStatuses = testrig.NewTestStatuses()
+ suite.testTags = testrig.NewTestTags()
+ suite.testMentions = testrig.NewTestMentions()
+ suite.testAutheds = map[string]*oauth.Auth{
+ "local_account_1": {
+ Application: suite.testApplications["local_account_1"],
+ User: suite.testUsers["local_account_1"],
+ Account: suite.testAccounts["local_account_1"],
+ },
+ }
+ suite.testBlocks = testrig.NewTestBlocks()
+ suite.testLists = testrig.NewTestLists()
+ suite.testListEntries = testrig.NewTestListEntries()
+}
+
+func (suite *WorkersTestSuite) SetupTest() {
+ suite.state.Caches.Init()
+ testrig.StartWorkers(&suite.state)
+
+ testrig.InitTestConfig()
+ testrig.InitTestLog()
+
+ suite.db = testrig.NewTestDB(&suite.state)
+ suite.state.DB = suite.db
+ suite.testActivities = testrig.NewTestActivities(suite.testAccounts)
+ suite.storage = testrig.NewInMemoryStorage()
+ suite.state.Storage = suite.storage
+ suite.typeconverter = testrig.NewTestTypeConverter(suite.db)
+
+ 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()
+
+ 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.emailSender = testrig.NewEmailSender("../../../web/template/", nil)
+
+ suite.processor = processing.NewProcessor(suite.typeconverter, suite.federator, suite.oauthServer, suite.mediaManager, &suite.state, suite.emailSender)
+ suite.state.Workers.EnqueueClientAPI = suite.processor.Workers().EnqueueClientAPI
+ suite.state.Workers.EnqueueFediAPI = suite.processor.Workers().EnqueueFediAPI
+
+ testrig.StandardDBSetup(suite.db, suite.testAccounts)
+ testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
+}
+
+func (suite *WorkersTestSuite) TearDownTest() {
+ testrig.StandardDBTeardown(suite.db)
+ testrig.StandardStorageTeardown(suite.storage)
+ testrig.StopWorkers(&suite.state)
+}
+
+func (suite *WorkersTestSuite) openStreams(ctx context.Context, account *gtsmodel.Account, listIDs []string) map[string]*stream.Stream {
+ streams := make(map[string]*stream.Stream)
+
+ for _, streamType := range []string{
+ stream.TimelineHome,
+ stream.TimelinePublic,
+ stream.TimelineNotifications,
+ } {
+ stream, err := suite.processor.Stream().Open(ctx, account, streamType)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ streams[streamType] = stream
+ }
+
+ for _, listID := range listIDs {
+ streamType := stream.TimelineList + ":" + listID
+
+ stream, err := suite.processor.Stream().Open(ctx, account, streamType)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ streams[streamType] = stream
+ }
+
+ return streams
+}
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index cb69cba5d..73992fc0e 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -156,7 +156,7 @@ type TypeConverter interface {
// URI of the status as object, and addressing the Delete appropriately.
StatusToASDelete(ctx context.Context, status *gtsmodel.Status) (vocab.ActivityStreamsDelete, error)
// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation
- FollowToAS(ctx context.Context, f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error)
+ FollowToAS(ctx context.Context, f *gtsmodel.Follow) (vocab.ActivityStreamsFollow, error)
// MentionToAS converts a gts model mention into an activity streams Mention, suitable for federation
MentionToAS(ctx context.Context, m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error)
// EmojiToAS converts a gts emoji into a mastodon ns Emoji, suitable for federation
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index 60ab24383..f10205b13 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -774,10 +774,14 @@ func (c *converter) StatusToASDelete(ctx context.Context, s *gtsmodel.Status) (v
return delete, nil
}
-func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) {
- // parse out the various URIs we need for this
- // origin account (who's doing the follow)
- originAccountURI, err := url.Parse(originAccount.URI)
+func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow) (vocab.ActivityStreamsFollow, error) {
+ if err := c.db.PopulateFollow(ctx, f); err != nil {
+ return nil, gtserror.Newf("error populating follow: %w", err)
+ }
+
+ // Parse out the various URIs we need for this
+ // origin account (who's doing the follow).
+ originAccountURI, err := url.Parse(f.Account.URI)
if err != nil {
return nil, fmt.Errorf("followtoasfollow: error parsing origin account uri: %s", err)
}
@@ -785,7 +789,7 @@ func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow, originAc
originActor.AppendIRI(originAccountURI)
// target account (who's being followed)
- targetAccountURI, err := url.Parse(targetAccount.URI)
+ targetAccountURI, err := url.Parse(f.TargetAccount.URI)
if err != nil {
return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err)
}
diff --git a/internal/visibility/home_timeline.go b/internal/visibility/home_timeline.go
index e3af03d83..d8bbc3979 100644
--- a/internal/visibility/home_timeline.go
+++ b/internal/visibility/home_timeline.go
@@ -24,6 +24,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/cache"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
)
@@ -97,33 +98,17 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A
}
var (
- next *gtsmodel.Status
+ next = status
oneAuthor = true // Assume one author until proven otherwise.
included bool
converstn bool
)
- for next = status; next.InReplyToURI != ""; {
- // Fetch next parent to lookup.
- parentID := next.InReplyToID
- if parentID == "" {
- log.Warnf(ctx, "status not yet deref'd: %s", next.InReplyToURI)
- return false, cache.SentinelError
- }
-
- // Get the next parent in the chain from DB.
- next, err = f.state.DB.GetStatusByID(
- gtscontext.SetBarebones(ctx),
- parentID,
- )
- if err != nil {
- return false, fmt.Errorf("isStatusHomeTimelineable: error getting status parent %s: %w", parentID, err)
- }
-
+ for {
// Populate account mention objects before account mention checks.
next.Mentions, err = f.state.DB.GetMentions(ctx, next.MentionIDs)
if err != nil {
- return false, fmt.Errorf("isStatusHomeTimelineable: error populating status parent %s mentions: %w", parentID, err)
+ return false, gtserror.Newf("error populating status %s mentions: %w", next.ID, err)
}
if (next.AccountID == owner.ID) ||
@@ -139,7 +124,7 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A
// is it between accounts on owner timeline that they follow?
converstn, err = f.isVisibleConversation(ctx, owner, next)
if err != nil {
- return false, fmt.Errorf("isStatusHomeTimelineable: error checking conversation visibility: %w", err)
+ return false, gtserror.Newf("error checking conversation visibility: %w", err)
}
if converstn {
@@ -152,6 +137,26 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A
// Check if this continues to be a single-author thread.
oneAuthor = (next.AccountID == status.AccountID)
}
+
+ if next.InReplyToURI == "" {
+ // Reached the top of the thread.
+ break
+ }
+
+ // Fetch next parent in thread.
+ parentID := next.InReplyToID
+ if parentID == "" {
+ log.Warnf(ctx, "status not yet deref'd: %s", next.InReplyToURI)
+ return false, cache.SentinelError
+ }
+
+ next, err = f.state.DB.GetStatusByID(
+ gtscontext.SetBarebones(ctx),
+ parentID,
+ )
+ if err != nil {
+ return false, gtserror.Newf("error getting status parent %s: %w", parentID, err)
+ }
}
if next != status && !oneAuthor && !included && !converstn {
@@ -177,7 +182,7 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A
status.AccountID,
)
if err != nil {
- return false, fmt.Errorf("isStatusHomeTimelineable: error checking follow %s->%s: %w", owner.ID, status.AccountID, err)
+ return false, gtserror.Newf("error checking follow %s->%s: %w", owner.ID, status.AccountID, err)
}
if !follow {
diff --git a/internal/workers/workers.go b/internal/workers/workers.go
index aa8e40e1c..965cf1d2a 100644
--- a/internal/workers/workers.go
+++ b/internal/workers/workers.go
@@ -43,7 +43,7 @@ type Workers struct {
// these are pointers to Processor{}.Enqueue___() msg functions.
// This prevents dependency cycling as Processor depends on Workers.
EnqueueClientAPI func(context.Context, ...messages.FromClientAPI)
- EnqueueFederator func(context.Context, ...messages.FromFederator)
+ EnqueueFediAPI func(context.Context, ...messages.FromFediAPI)
// Media manager worker pools.
Media runners.WorkerPool
diff --git a/testrig/processor.go b/testrig/processor.go
index 1839b482a..0c6d97253 100644
--- a/testrig/processor.go
+++ b/testrig/processor.go
@@ -28,7 +28,7 @@ import (
// NewTestProcessor returns a Processor suitable for testing purposes
func NewTestProcessor(state *state.State, federator federation.Federator, emailSender email.Sender, mediaManager *media.Manager) *processing.Processor {
p := processing.NewProcessor(NewTestTypeConverter(state.DB), federator, NewTestOauthServer(state.DB), mediaManager, state, emailSender)
- state.Workers.EnqueueClientAPI = p.EnqueueClientAPI
- state.Workers.EnqueueFederator = p.EnqueueFederator
+ state.Workers.EnqueueClientAPI = p.Workers().EnqueueClientAPI
+ state.Workers.EnqueueFediAPI = p.Workers().EnqueueFediAPI
return p
}
diff --git a/testrig/util.go b/testrig/util.go
index 4e52d12b5..483064e0a 100644
--- a/testrig/util.go
+++ b/testrig/util.go
@@ -37,7 +37,7 @@ import (
func StartWorkers(state *state.State) {
state.Workers.EnqueueClientAPI = func(context.Context, ...messages.FromClientAPI) {}
- state.Workers.EnqueueFederator = func(context.Context, ...messages.FromFederator) {}
+ state.Workers.EnqueueFediAPI = func(context.Context, ...messages.FromFediAPI) {}
_ = state.Workers.Scheduler.Start(nil)
_ = state.Workers.ClientAPI.Start(1, 10)