mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-27 10:51:00 +00:00
[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
This commit is contained in:
parent
dbf487effb
commit
9770d54237
49 changed files with 4110 additions and 2660 deletions
|
@ -177,8 +177,8 @@ var Start action.GTSAction = func(ctx context.Context) error {
|
||||||
processor := processing.NewProcessor(typeConverter, federator, oauthServer, mediaManager, &state, emailSender)
|
processor := processing.NewProcessor(typeConverter, federator, oauthServer, mediaManager, &state, emailSender)
|
||||||
|
|
||||||
// Set state client / federator worker enqueue functions
|
// Set state client / federator worker enqueue functions
|
||||||
state.Workers.EnqueueClientAPI = processor.EnqueueClientAPI
|
state.Workers.EnqueueClientAPI = processor.Workers().EnqueueClientAPI
|
||||||
state.Workers.EnqueueFederator = processor.EnqueueFederator
|
state.Workers.EnqueueFediAPI = processor.Workers().EnqueueFediAPI
|
||||||
|
|
||||||
/*
|
/*
|
||||||
HTTP router initialization
|
HTTP router initialization
|
||||||
|
|
|
@ -290,11 +290,7 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := errs.Combine(); err != nil {
|
return errs.Combine()
|
||||||
return gtserror.Newf("%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *accountDB) PutAccount(ctx context.Context, account *gtsmodel.Account) error {
|
func (a *accountDB) PutAccount(ctx context.Context, account *gtsmodel.Account) error {
|
||||||
|
|
|
@ -198,11 +198,7 @@ func (i *instanceDB) populateInstance(ctx context.Context, instance *gtsmodel.In
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := errs.Combine(); err != nil {
|
return errs.Combine()
|
||||||
return gtserror.Newf("%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *instanceDB) PutInstance(ctx context.Context, instance *gtsmodel.Instance) error {
|
func (i *instanceDB) PutInstance(ctx context.Context, instance *gtsmodel.Instance) error {
|
||||||
|
|
|
@ -143,11 +143,7 @@ func (l *listDB) PopulateList(ctx context.Context, list *gtsmodel.List) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := errs.Combine(); err != nil {
|
return errs.Combine()
|
||||||
return gtserror.Newf("%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *listDB) PutList(ctx context.Context, list *gtsmodel.List) error {
|
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
|
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',
|
// 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.
|
// 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 {
|
func collate[T comparable](get func(int) T, len int) []T {
|
||||||
|
|
|
@ -310,6 +310,27 @@ func (suite *ListTestSuite) TestDeleteListEntriesForFollowID() {
|
||||||
suite.checkList(testList, dbList)
|
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) {
|
func TestListTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(ListTestSuite))
|
suite.Run(t, new(ListTestSuite))
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,10 @@ package bundb
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
|
@ -139,27 +139,44 @@ func (r *relationshipDB) getBlock(ctx context.Context, lookup string, dbQuery fu
|
||||||
return block, nil
|
return block, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the block source account
|
if err := r.state.DB.PopulateBlock(ctx, block); err != nil {
|
||||||
block.Account, err = r.state.DB.GetAccountByID(
|
return nil, err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return block, nil
|
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 {
|
func (r *relationshipDB) PutBlock(ctx context.Context, block *gtsmodel.Block) error {
|
||||||
return r.state.Caches.GTS.Block().Store(block, func() error {
|
return r.state.Caches.GTS.Block().Store(block, func() error {
|
||||||
_, err := r.db.NewInsert().Model(block).Exec(ctx)
|
_, err := r.db.NewInsert().Model(block).Exec(ctx)
|
||||||
|
|
|
@ -185,11 +185,7 @@ func (r *relationshipDB) PopulateFollow(ctx context.Context, follow *gtsmodel.Fo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := errs.Combine(); err != nil {
|
return errs.Combine()
|
||||||
return gtserror.Newf("%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *relationshipDB) PutFollow(ctx context.Context, follow *gtsmodel.Follow) error {
|
func (r *relationshipDB) PutFollow(ctx context.Context, follow *gtsmodel.Follow) error {
|
||||||
|
|
|
@ -20,11 +20,11 @@ package bundb
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
|
@ -127,27 +127,44 @@ func (r *relationshipDB) getFollowRequest(ctx context.Context, lookup string, db
|
||||||
return followReq, nil
|
return followReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the follow request source account
|
if err := r.state.DB.PopulateFollowRequest(ctx, followReq); err != nil {
|
||||||
followReq.Account, err = r.state.DB.GetAccountByID(
|
return nil, err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return followReq, nil
|
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 {
|
func (r *relationshipDB) PutFollowRequest(ctx context.Context, follow *gtsmodel.FollowRequest) error {
|
||||||
return r.state.Caches.GTS.FollowRequest().Store(follow, func() error {
|
return r.state.Caches.GTS.FollowRequest().Store(follow, func() error {
|
||||||
_, err := r.db.NewInsert().Model(follow).Exec(ctx)
|
_, err := r.db.NewInsert().Model(follow).Exec(ctx)
|
||||||
|
|
|
@ -20,11 +20,11 @@ package bundb
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the report author account
|
if gtscontext.Barebones(ctx) {
|
||||||
report.Account, err = r.state.DB.GetAccountByID(ctx, report.AccountID)
|
// Only a barebones model was requested.
|
||||||
if err != nil {
|
return report, nil
|
||||||
return nil, fmt.Errorf("error getting report account: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the report target account
|
if err := r.state.DB.PopulateReport(ctx, report); err != nil {
|
||||||
report.TargetAccount, err = r.state.DB.GetAccountByID(ctx, report.TargetAccountID)
|
return nil, err
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return report, nil
|
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 {
|
func (r *reportDB) PutReport(ctx context.Context, report *gtsmodel.Report) error {
|
||||||
return r.state.Caches.GTS.Report().Store(report, func() error {
|
return r.state.Caches.GTS.Report().Store(report, func() error {
|
||||||
_, err := r.db.NewInsert().Model(report).Exec(ctx)
|
_, err := r.db.NewInsert().Model(report).Exec(ctx)
|
||||||
|
|
|
@ -197,11 +197,7 @@ func (s *statusFaveDB) PopulateStatusFave(ctx context.Context, statusFave *gtsmo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := errs.Combine(); err != nil {
|
return errs.Combine()
|
||||||
return gtserror.Newf("%w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
||||||
|
|
|
@ -64,4 +64,7 @@ type List interface {
|
||||||
|
|
||||||
// DeleteListEntryForFollowID deletes all list entries with the given followID.
|
// DeleteListEntryForFollowID deletes all list entries with the given followID.
|
||||||
DeleteListEntriesForFollowID(ctx context.Context, followID string) error
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 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)
|
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 attempts to place the given account block in the database.
|
||||||
PutBlock(ctx context.Context, block *gtsmodel.Block) error
|
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 retrieves a follow request if it exists between source and target accounts.
|
||||||
GetFollowRequest(ctx context.Context, sourceAccountID string, targetAccountID string) (*gtsmodel.FollowRequest, error)
|
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 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)
|
IsFollowing(ctx context.Context, sourceAccountID string, targetAccountID string) (bool, error)
|
||||||
|
|
||||||
|
|
|
@ -27,17 +27,24 @@ import (
|
||||||
type Report interface {
|
type Report interface {
|
||||||
// GetReportByID gets one report by its db id
|
// GetReportByID gets one report by its db id
|
||||||
GetReportByID(ctx context.Context, id string) (*gtsmodel.Report, error)
|
GetReportByID(ctx context.Context, id string) (*gtsmodel.Report, error)
|
||||||
|
|
||||||
// GetReports gets limit n reports using the given parameters.
|
// GetReports gets limit n reports using the given parameters.
|
||||||
// Parameters that are empty / zero are ignored.
|
// 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)
|
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 puts the given report in the database.
|
||||||
PutReport(ctx context.Context, report *gtsmodel.Report) error
|
PutReport(ctx context.Context, report *gtsmodel.Report) error
|
||||||
|
|
||||||
// UpdateReport updates one report by its db id.
|
// UpdateReport updates one report by its db id.
|
||||||
// The given columns will be updated; if no columns are
|
// The given columns will be updated; if no columns are
|
||||||
// provided, then all columns will be updated.
|
// provided, then all columns will be updated.
|
||||||
// updated_at will also be updated, no need to pass this
|
// updated_at will also be updated, no need to pass this
|
||||||
// as a specific column.
|
// as a specific column.
|
||||||
UpdateReport(ctx context.Context, report *gtsmodel.Report, columns ...string) (*gtsmodel.Report, error)
|
UpdateReport(ctx context.Context, report *gtsmodel.Report, columns ...string) (*gtsmodel.Report, error)
|
||||||
|
|
||||||
// DeleteReportByID deletes report with the given id.
|
// DeleteReportByID deletes report with the given id.
|
||||||
DeleteReportByID(ctx context.Context, id string) error
|
DeleteReportByID(ctx context.Context, id string) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||||
APObjectType: ap.ActivityFollow,
|
APObjectType: ap.ActivityFollow,
|
||||||
APActivityType: ap.ActivityAccept,
|
APActivityType: ap.ActivityAccept,
|
||||||
GTSModel: follow,
|
GTSModel: follow,
|
||||||
|
@ -107,7 +107,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||||
APObjectType: ap.ActivityFollow,
|
APObjectType: ap.ActivityFollow,
|
||||||
APActivityType: ap.ActivityAccept,
|
APActivityType: ap.ActivityAccept,
|
||||||
GTSModel: follow,
|
GTSModel: follow,
|
||||||
|
|
|
@ -56,7 +56,7 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a new boost. Process side effects asynchronously.
|
// 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,
|
APObjectType: ap.ActivityAnnounce,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: boost,
|
GTSModel: boost,
|
||||||
|
|
|
@ -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)
|
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,
|
APObjectType: ap.ActivityBlock,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: block,
|
GTSModel: block,
|
||||||
|
@ -233,7 +233,7 @@ func (f *federatingDB) createStatusable(
|
||||||
if forward {
|
if forward {
|
||||||
// Pass the statusable URI (APIri) into the processor worker
|
// Pass the statusable URI (APIri) into the processor worker
|
||||||
// and do the rest of the processing asynchronously.
|
// 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,
|
APObjectType: ap.ObjectNote,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
APIri: statusableURI,
|
APIri: statusableURI,
|
||||||
|
@ -291,7 +291,7 @@ func (f *federatingDB) createStatusable(
|
||||||
|
|
||||||
// Do the rest of the processing asynchronously. The processor
|
// Do the rest of the processing asynchronously. The processor
|
||||||
// will handle inserting/updating + further dereferencing the status.
|
// 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,
|
APObjectType: ap.ObjectNote,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
APIri: nil,
|
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)
|
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,
|
APObjectType: ap.ActivityFollow,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: followRequest,
|
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)
|
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,
|
APObjectType: ap.ActivityLike,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: fave,
|
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)
|
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,
|
APObjectType: ap.ActivityFlag,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: report,
|
GTSModel: report,
|
||||||
|
|
|
@ -49,7 +49,7 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {
|
||||||
// so we have to try a few different things...
|
// 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 {
|
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)
|
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,
|
APObjectType: ap.ObjectNote,
|
||||||
APActivityType: ap.ActivityDelete,
|
APActivityType: ap.ActivityDelete,
|
||||||
GTSModel: s,
|
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 {
|
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)
|
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,
|
APObjectType: ap.ObjectProfile,
|
||||||
APActivityType: ap.ActivityDelete,
|
APActivityType: ap.ActivityDelete,
|
||||||
GTSModel: a,
|
GTSModel: a,
|
||||||
|
|
|
@ -36,7 +36,7 @@ type FederatingDBTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db db.DB
|
db db.DB
|
||||||
tc typeutils.TypeConverter
|
tc typeutils.TypeConverter
|
||||||
fromFederator chan messages.FromFederator
|
fromFederator chan messages.FromFediAPI
|
||||||
federatingDB federatingdb.DB
|
federatingDB federatingdb.DB
|
||||||
state state.State
|
state state.State
|
||||||
|
|
||||||
|
@ -69,8 +69,8 @@ func (suite *FederatingDBTestSuite) SetupTest() {
|
||||||
suite.state.Caches.Init()
|
suite.state.Caches.Init()
|
||||||
testrig.StartWorkers(&suite.state)
|
testrig.StartWorkers(&suite.state)
|
||||||
|
|
||||||
suite.fromFederator = make(chan messages.FromFederator, 10)
|
suite.fromFederator = make(chan messages.FromFediAPI, 10)
|
||||||
suite.state.Workers.EnqueueFederator = func(ctx context.Context, msgs ...messages.FromFederator) {
|
suite.state.Workers.EnqueueFediAPI = func(ctx context.Context, msgs ...messages.FromFediAPI) {
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
suite.fromFederator <- msg
|
suite.fromFederator <- msg
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ func (suite *RejectTestSuite) TestRejectFollowRequest() {
|
||||||
err := suite.db.Put(ctx, fr)
|
err := suite.db.Put(ctx, fr)
|
||||||
suite.NoError(err)
|
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)
|
suite.NoError(err)
|
||||||
|
|
||||||
rejectingAccountURI := testrig.URLMustParse(followedAccount.URI)
|
rejectingAccountURI := testrig.URLMustParse(followedAccount.URI)
|
||||||
|
|
|
@ -93,7 +93,7 @@ func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gts
|
||||||
// was delivered along with the Update, for further asynchronous
|
// was delivered along with the Update, for further asynchronous
|
||||||
// updating of eg., avatar/header, emojis, etc. The actual db
|
// updating of eg., avatar/header, emojis, etc. The actual db
|
||||||
// inserts/updates will take place there.
|
// inserts/updates will take place there.
|
||||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ObjectProfile,
|
||||||
APActivityType: ap.ActivityUpdate,
|
APActivityType: ap.ActivityUpdate,
|
||||||
GTSModel: requestingAcct,
|
GTSModel: requestingAcct,
|
||||||
|
|
|
@ -21,16 +21,34 @@ import (
|
||||||
"net/http"
|
"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 {
|
func New(msg string) error {
|
||||||
return newAt(3, msg)
|
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 {
|
func Newf(msgf string, args ...any) error {
|
||||||
return newfAt(3, msgf, args...)
|
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
|
// NewResponseError crafts an error from provided HTTP response
|
||||||
// including the method, status and body (if any provided). This
|
// including the method, status and body (if any provided). This
|
||||||
// will also wrap the returned error using WithStatusCode() and
|
// will also wrap the returned error using WithStatusCode() and
|
||||||
|
|
|
@ -32,8 +32,8 @@ type FromClientAPI struct {
|
||||||
TargetAccount *gtsmodel.Account
|
TargetAccount *gtsmodel.Account
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromFederator wraps a message that travels from the federator into the processor.
|
// FromFediAPI wraps a message that travels from the federating API into the processor.
|
||||||
type FromFederator struct {
|
type FromFediAPI struct {
|
||||||
APObjectType string
|
APObjectType string
|
||||||
APActivityType string
|
APActivityType string
|
||||||
APIri *url.URL
|
APIri *url.URL
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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{})
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
|
@ -18,13 +18,9 @@
|
||||||
package processing
|
package processing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
|
||||||
mm "github.com/superseriousbusiness/gotosocial/internal/media"
|
mm "github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/admin"
|
"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/stream"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/timeline"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/timeline"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
|
"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/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/visibility"
|
"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 {
|
type Processor struct {
|
||||||
federator federation.Federator
|
tc typeutils.TypeConverter
|
||||||
tc typeutils.TypeConverter
|
oauthServer oauth.Server
|
||||||
oauthServer oauth.Server
|
state *state.State
|
||||||
mediaManager *mm.Manager
|
|
||||||
state *state.State
|
|
||||||
emailSender email.Sender
|
|
||||||
filter *visibility.Filter
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
SUB-PROCESSORS
|
SUB-PROCESSORS
|
||||||
|
@ -68,6 +68,7 @@ type Processor struct {
|
||||||
stream stream.Processor
|
stream stream.Processor
|
||||||
timeline timeline.Processor
|
timeline timeline.Processor
|
||||||
user user.Processor
|
user user.Processor
|
||||||
|
workers workers.Processor
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) Account() *account.Processor {
|
func (p *Processor) Account() *account.Processor {
|
||||||
|
@ -118,6 +119,10 @@ func (p *Processor) User() *user.Processor {
|
||||||
return &p.user
|
return &p.user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Processor) Workers() *workers.Processor {
|
||||||
|
return &p.workers
|
||||||
|
}
|
||||||
|
|
||||||
// NewProcessor returns a new Processor.
|
// NewProcessor returns a new Processor.
|
||||||
func NewProcessor(
|
func NewProcessor(
|
||||||
tc typeutils.TypeConverter,
|
tc typeutils.TypeConverter,
|
||||||
|
@ -127,57 +132,53 @@ func NewProcessor(
|
||||||
state *state.State,
|
state *state.State,
|
||||||
emailSender email.Sender,
|
emailSender email.Sender,
|
||||||
) *Processor {
|
) *Processor {
|
||||||
parseMentionFunc := GetParseMentionFunc(state.DB, federator)
|
var (
|
||||||
|
parseMentionFunc = GetParseMentionFunc(state.DB, federator)
|
||||||
filter := visibility.NewFilter(state)
|
filter = visibility.NewFilter(state)
|
||||||
|
)
|
||||||
|
|
||||||
processor := &Processor{
|
processor := &Processor{
|
||||||
federator: federator,
|
tc: tc,
|
||||||
tc: tc,
|
oauthServer: oauthServer,
|
||||||
oauthServer: oauthServer,
|
state: state,
|
||||||
mediaManager: mediaManager,
|
|
||||||
state: state,
|
|
||||||
filter: filter,
|
|
||||||
emailSender: emailSender,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instantiate sub processors.
|
// 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.admin = admin.New(state, tc, mediaManager, federator.TransportController(), emailSender)
|
||||||
processor.fedi = fedi.New(state, tc, federator, filter)
|
processor.fedi = fedi.New(state, tc, federator, filter)
|
||||||
processor.list = list.New(state, tc)
|
processor.list = list.New(state, tc)
|
||||||
processor.markers = markers.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.report = report.New(state, tc)
|
||||||
processor.timeline = timeline.New(state, tc, filter)
|
processor.timeline = timeline.New(state, tc, filter)
|
||||||
processor.search = search.New(state, federator, tc, filter)
|
processor.search = search.New(state, federator, tc, filter)
|
||||||
processor.status = status.New(state, federator, tc, filter, parseMentionFunc)
|
processor.status = status.New(state, federator, tc, filter, parseMentionFunc)
|
||||||
processor.stream = stream.New(state, oauthServer)
|
processor.stream = streamProcessor
|
||||||
processor.user = user.New(state, emailSender)
|
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
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -123,8 +123,8 @@ func (suite *ProcessingStandardTestSuite) SetupTest() {
|
||||||
suite.emailSender = testrig.NewEmailSender("../../web/template/", nil)
|
suite.emailSender = testrig.NewEmailSender("../../web/template/", nil)
|
||||||
|
|
||||||
suite.processor = processing.NewProcessor(suite.typeconverter, suite.federator, suite.oauthServer, suite.mediaManager, &suite.state, suite.emailSender)
|
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.EnqueueClientAPI = suite.processor.Workers().EnqueueClientAPI
|
||||||
suite.state.Workers.EnqueueFederator = suite.processor.EnqueueFederator
|
suite.state.Workers.EnqueueFediAPI = suite.processor.Workers().EnqueueFediAPI
|
||||||
|
|
||||||
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||||
testrig.StandardStorageSetup(suite.storage, "../../testrig/media")
|
testrig.StandardStorageSetup(suite.storage, "../../testrig/media")
|
||||||
|
|
|
@ -28,13 +28,14 @@ import (
|
||||||
type Processor struct {
|
type Processor struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
oauthServer oauth.Server
|
oauthServer oauth.Server
|
||||||
streamMap sync.Map
|
streamMap *sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(state *state.State, oauthServer oauth.Server) Processor {
|
func New(state *state.State, oauthServer oauth.Server) Processor {
|
||||||
return Processor{
|
return Processor{
|
||||||
state: state,
|
state: state,
|
||||||
oauthServer: oauthServer,
|
oauthServer: oauthServer,
|
||||||
|
streamMap: &sync.Map{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,67 +23,13 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var oneWeek = 168 * time.Hour
|
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
|
// EmailConfirm processes an email confirmation request, usually initiated as a result of clicking on a link
|
||||||
// in a 'confirm your email address' type email.
|
// in a 'confirm your email address' type email.
|
||||||
func (p *Processor) EmailConfirm(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
|
func (p *Processor) EmailConfirm(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
|
||||||
|
|
|
@ -19,7 +19,6 @@ package user_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -30,36 +29,6 @@ type EmailConfirmTestSuite struct {
|
||||||
UserStandardTestSuite
|
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() {
|
func (suite *EmailConfirmTestSuite) TestConfirmEmail() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
892
internal/processing/workers/federate.go
Normal file
892
internal/processing/workers/federate.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
548
internal/processing/workers/fromclientapi.go
Normal file
548
internal/processing/workers/fromclientapi.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
589
internal/processing/workers/fromclientapi_test.go
Normal file
589
internal/processing/workers/fromclientapi_test.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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{})
|
||||||
|
}
|
540
internal/processing/workers/fromfediapi.go
Normal file
540
internal/processing/workers/fromfediapi.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package processing_test
|
package workers_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -36,12 +36,12 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FromFederatorTestSuite struct {
|
type FromFediAPITestSuite struct {
|
||||||
ProcessingStandardTestSuite
|
WorkersTestSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
// remote_account_1 boosts the first status of local_account_1
|
// 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"]
|
boostedStatus := suite.testStatuses["local_account_1_status_1"]
|
||||||
boostingAccount := suite.testAccounts["remote_account_1"]
|
boostingAccount := suite.testAccounts["remote_account_1"]
|
||||||
announceStatus := >smodel.Status{}
|
announceStatus := >smodel.Status{}
|
||||||
|
@ -56,7 +56,7 @@ func (suite *FromFederatorTestSuite) TestProcessFederationAnnounce() {
|
||||||
announceStatus.Account = boostingAccount
|
announceStatus.Account = boostingAccount
|
||||||
announceStatus.Visibility = boostedStatus.Visibility
|
announceStatus.Visibility = boostedStatus.Visibility
|
||||||
|
|
||||||
err := suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
|
err := suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
|
||||||
APObjectType: ap.ActivityAnnounce,
|
APObjectType: ap.ActivityAnnounce,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: announceStatus,
|
GTSModel: announceStatus,
|
||||||
|
@ -87,7 +87,7 @@ func (suite *FromFederatorTestSuite) TestProcessFederationAnnounce() {
|
||||||
suite.False(*notif.Read)
|
suite.False(*notif.Read)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
|
func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
|
||||||
repliedAccount := suite.testAccounts["local_account_1"]
|
repliedAccount := suite.testAccounts["local_account_1"]
|
||||||
repliedStatus := suite.testStatuses["local_account_1_status_1"]
|
repliedStatus := suite.testStatuses["local_account_1_status_1"]
|
||||||
replyingAccount := suite.testAccounts["remote_account_1"]
|
replyingAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
@ -128,7 +128,7 @@ func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
|
||||||
err = suite.db.PutStatus(context.Background(), replyingStatus)
|
err = suite.db.PutStatus(context.Background(), replyingStatus)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
|
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
|
||||||
APObjectType: ap.ObjectNote,
|
APObjectType: ap.ObjectNote,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: replyingStatus,
|
GTSModel: replyingStatus,
|
||||||
|
@ -173,7 +173,7 @@ func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
|
||||||
suite.Equal(replyingAccount.ID, notifStreamed.Account.ID)
|
suite.Equal(replyingAccount.ID, notifStreamed.Account.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFederatorTestSuite) TestProcessFave() {
|
func (suite *FromFediAPITestSuite) TestProcessFave() {
|
||||||
favedAccount := suite.testAccounts["local_account_1"]
|
favedAccount := suite.testAccounts["local_account_1"]
|
||||||
favedStatus := suite.testStatuses["local_account_1_status_1"]
|
favedStatus := suite.testStatuses["local_account_1_status_1"]
|
||||||
favingAccount := suite.testAccounts["remote_account_1"]
|
favingAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
@ -197,7 +197,7 @@ func (suite *FromFederatorTestSuite) TestProcessFave() {
|
||||||
err := suite.db.Put(context.Background(), fave)
|
err := suite.db.Put(context.Background(), fave)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
|
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
|
||||||
APObjectType: ap.ActivityLike,
|
APObjectType: ap.ActivityLike,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: fave,
|
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
|
// 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.
|
// 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"]
|
receivingAccount := suite.testAccounts["local_account_2"]
|
||||||
favedAccount := suite.testAccounts["local_account_1"]
|
favedAccount := suite.testAccounts["local_account_1"]
|
||||||
favedStatus := suite.testStatuses["local_account_1_status_1"]
|
favedStatus := suite.testStatuses["local_account_1_status_1"]
|
||||||
|
@ -270,7 +270,7 @@ func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccoun
|
||||||
err := suite.db.Put(context.Background(), fave)
|
err := suite.db.Put(context.Background(), fave)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
|
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
|
||||||
APObjectType: ap.ActivityLike,
|
APObjectType: ap.ActivityLike,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: fave,
|
GTSModel: fave,
|
||||||
|
@ -304,7 +304,7 @@ func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccoun
|
||||||
suite.Empty(wssStream.Messages)
|
suite.Empty(wssStream.Messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
|
func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
deletedAccount := suite.testAccounts["remote_account_1"]
|
deletedAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
@ -339,7 +339,7 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// now they are mufos!
|
// now they are mufos!
|
||||||
err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
|
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ObjectProfile,
|
||||||
APActivityType: ap.ActivityDelete,
|
APActivityType: ap.ActivityDelete,
|
||||||
GTSModel: deletedAccount,
|
GTSModel: deletedAccount,
|
||||||
|
@ -386,7 +386,7 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
|
||||||
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin)
|
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
|
func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
originAccount := suite.testAccounts["remote_account_1"]
|
originAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
@ -414,7 +414,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
|
||||||
err := suite.db.Put(ctx, satanFollowRequestTurtle)
|
err := suite.db.Put(ctx, satanFollowRequestTurtle)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
|
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
|
||||||
APObjectType: ap.ActivityFollow,
|
APObjectType: ap.ActivityFollow,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: satanFollowRequestTurtle,
|
GTSModel: satanFollowRequestTurtle,
|
||||||
|
@ -443,7 +443,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
|
||||||
suite.Empty(suite.httpClient.SentMessages)
|
suite.Empty(suite.httpClient.SentMessages)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
|
func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
originAccount := suite.testAccounts["remote_account_1"]
|
originAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
@ -471,7 +471,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
|
||||||
err := suite.db.Put(ctx, satanFollowRequestTurtle)
|
err := suite.db.Put(ctx, satanFollowRequestTurtle)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
|
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
|
||||||
APObjectType: ap.ActivityFollow,
|
APObjectType: ap.ActivityFollow,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: satanFollowRequestTurtle,
|
GTSModel: satanFollowRequestTurtle,
|
||||||
|
@ -539,13 +539,13 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor.
|
// TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor.
|
||||||
func (suite *FromFederatorTestSuite) TestCreateStatusFromIRI() {
|
func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
receivingAccount := suite.testAccounts["local_account_1"]
|
receivingAccount := suite.testAccounts["local_account_1"]
|
||||||
statusCreator := suite.testAccounts["remote_account_2"]
|
statusCreator := suite.testAccounts["remote_account_2"]
|
||||||
|
|
||||||
err := suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
|
err := suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
|
||||||
APObjectType: ap.ObjectNote,
|
APObjectType: ap.ObjectNote,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: nil, // gtsmodel is nil because this is a forwarded status -- we want to dereference it using the iri
|
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) {
|
func TestFromFederatorTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &FromFederatorTestSuite{})
|
suite.Run(t, &FromFediAPITestSuite{})
|
||||||
}
|
}
|
40
internal/processing/workers/surface.go
Normal file
40
internal/processing/workers/surface.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
160
internal/processing/workers/surfaceemail.go
Normal file
160
internal/processing/workers/surfaceemail.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
221
internal/processing/workers/surfacenotify.go
Normal file
221
internal/processing/workers/surfacenotify.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
401
internal/processing/workers/surfacetimeline.go
Normal file
401
internal/processing/workers/surfacetimeline.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
119
internal/processing/workers/wipestatus.go
Normal file
119
internal/processing/workers/wipestatus.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
92
internal/processing/workers/workers.go
Normal file
92
internal/processing/workers/workers.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
169
internal/processing/workers/workers_test.go
Normal file
169
internal/processing/workers/workers_test.go
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -156,7 +156,7 @@ type TypeConverter interface {
|
||||||
// URI of the status as object, and addressing the Delete appropriately.
|
// URI of the status as object, and addressing the Delete appropriately.
|
||||||
StatusToASDelete(ctx context.Context, status *gtsmodel.Status) (vocab.ActivityStreamsDelete, error)
|
StatusToASDelete(ctx context.Context, status *gtsmodel.Status) (vocab.ActivityStreamsDelete, error)
|
||||||
// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation
|
// 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 converts a gts model mention into an activity streams Mention, suitable for federation
|
||||||
MentionToAS(ctx context.Context, m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error)
|
MentionToAS(ctx context.Context, m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error)
|
||||||
// EmojiToAS converts a gts emoji into a mastodon ns Emoji, suitable for federation
|
// EmojiToAS converts a gts emoji into a mastodon ns Emoji, suitable for federation
|
||||||
|
|
|
@ -774,10 +774,14 @@ func (c *converter) StatusToASDelete(ctx context.Context, s *gtsmodel.Status) (v
|
||||||
return delete, nil
|
return delete, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) {
|
func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow) (vocab.ActivityStreamsFollow, error) {
|
||||||
// parse out the various URIs we need for this
|
if err := c.db.PopulateFollow(ctx, f); err != nil {
|
||||||
// origin account (who's doing the follow)
|
return nil, gtserror.Newf("error populating follow: %w", err)
|
||||||
originAccountURI, err := url.Parse(originAccount.URI)
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("followtoasfollow: error parsing origin account uri: %s", err)
|
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)
|
originActor.AppendIRI(originAccountURI)
|
||||||
|
|
||||||
// target account (who's being followed)
|
// target account (who's being followed)
|
||||||
targetAccountURI, err := url.Parse(targetAccount.URI)
|
targetAccountURI, err := url.Parse(f.TargetAccount.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err)
|
return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/cache"
|
"github.com/superseriousbusiness/gotosocial/internal/cache"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
)
|
)
|
||||||
|
@ -97,33 +98,17 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
next *gtsmodel.Status
|
next = status
|
||||||
oneAuthor = true // Assume one author until proven otherwise.
|
oneAuthor = true // Assume one author until proven otherwise.
|
||||||
included bool
|
included bool
|
||||||
converstn bool
|
converstn bool
|
||||||
)
|
)
|
||||||
|
|
||||||
for next = status; next.InReplyToURI != ""; {
|
for {
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate account mention objects before account mention checks.
|
// Populate account mention objects before account mention checks.
|
||||||
next.Mentions, err = f.state.DB.GetMentions(ctx, next.MentionIDs)
|
next.Mentions, err = f.state.DB.GetMentions(ctx, next.MentionIDs)
|
||||||
if err != nil {
|
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) ||
|
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?
|
// is it between accounts on owner timeline that they follow?
|
||||||
converstn, err = f.isVisibleConversation(ctx, owner, next)
|
converstn, err = f.isVisibleConversation(ctx, owner, next)
|
||||||
if err != nil {
|
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 {
|
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.
|
// Check if this continues to be a single-author thread.
|
||||||
oneAuthor = (next.AccountID == status.AccountID)
|
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 {
|
if next != status && !oneAuthor && !included && !converstn {
|
||||||
|
@ -177,7 +182,7 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A
|
||||||
status.AccountID,
|
status.AccountID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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 {
|
if !follow {
|
||||||
|
|
|
@ -43,7 +43,7 @@ type Workers struct {
|
||||||
// these are pointers to Processor{}.Enqueue___() msg functions.
|
// these are pointers to Processor{}.Enqueue___() msg functions.
|
||||||
// This prevents dependency cycling as Processor depends on Workers.
|
// This prevents dependency cycling as Processor depends on Workers.
|
||||||
EnqueueClientAPI func(context.Context, ...messages.FromClientAPI)
|
EnqueueClientAPI func(context.Context, ...messages.FromClientAPI)
|
||||||
EnqueueFederator func(context.Context, ...messages.FromFederator)
|
EnqueueFediAPI func(context.Context, ...messages.FromFediAPI)
|
||||||
|
|
||||||
// Media manager worker pools.
|
// Media manager worker pools.
|
||||||
Media runners.WorkerPool
|
Media runners.WorkerPool
|
||||||
|
|
|
@ -28,7 +28,7 @@ import (
|
||||||
// NewTestProcessor returns a Processor suitable for testing purposes
|
// NewTestProcessor returns a Processor suitable for testing purposes
|
||||||
func NewTestProcessor(state *state.State, federator federation.Federator, emailSender email.Sender, mediaManager *media.Manager) *processing.Processor {
|
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)
|
p := processing.NewProcessor(NewTestTypeConverter(state.DB), federator, NewTestOauthServer(state.DB), mediaManager, state, emailSender)
|
||||||
state.Workers.EnqueueClientAPI = p.EnqueueClientAPI
|
state.Workers.EnqueueClientAPI = p.Workers().EnqueueClientAPI
|
||||||
state.Workers.EnqueueFederator = p.EnqueueFederator
|
state.Workers.EnqueueFediAPI = p.Workers().EnqueueFediAPI
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ import (
|
||||||
|
|
||||||
func StartWorkers(state *state.State) {
|
func StartWorkers(state *state.State) {
|
||||||
state.Workers.EnqueueClientAPI = func(context.Context, ...messages.FromClientAPI) {}
|
state.Workers.EnqueueClientAPI = func(context.Context, ...messages.FromClientAPI) {}
|
||||||
state.Workers.EnqueueFederator = func(context.Context, ...messages.FromFederator) {}
|
state.Workers.EnqueueFediAPI = func(context.Context, ...messages.FromFediAPI) {}
|
||||||
|
|
||||||
_ = state.Workers.Scheduler.Start(nil)
|
_ = state.Workers.Scheduler.Start(nil)
|
||||||
_ = state.Workers.ClientAPI.Start(1, 10)
|
_ = state.Workers.ClientAPI.Start(1, 10)
|
||||||
|
|
Loading…
Reference in a new issue