[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
|
@ -177,8 +177,8 @@ var Start action.GTSAction = func(ctx context.Context) error {
|
|||
processor := processing.NewProcessor(typeConverter, federator, oauthServer, mediaManager, &state, emailSender)
|
||||
|
||||
// Set state client / federator worker enqueue functions
|
||||
state.Workers.EnqueueClientAPI = processor.EnqueueClientAPI
|
||||
state.Workers.EnqueueFederator = processor.EnqueueFederator
|
||||
state.Workers.EnqueueClientAPI = processor.Workers().EnqueueClientAPI
|
||||
state.Workers.EnqueueFediAPI = processor.Workers().EnqueueFediAPI
|
||||
|
||||
/*
|
||||
HTTP router initialization
|
||||
|
|
|
@ -290,11 +290,7 @@ func (a *accountDB) PopulateAccount(ctx context.Context, account *gtsmodel.Accou
|
|||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (a *accountDB) PutAccount(ctx context.Context, account *gtsmodel.Account) error {
|
||||
|
|
|
@ -198,11 +198,7 @@ func (i *instanceDB) populateInstance(ctx context.Context, instance *gtsmodel.In
|
|||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (i *instanceDB) PutInstance(ctx context.Context, instance *gtsmodel.Instance) error {
|
||||
|
|
|
@ -143,11 +143,7 @@ func (l *listDB) PopulateList(ctx context.Context, list *gtsmodel.List) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (l *listDB) PutList(ctx context.Context, list *gtsmodel.List) error {
|
||||
|
@ -503,6 +499,22 @@ func (l *listDB) DeleteListEntriesForFollowID(ctx context.Context, followID stri
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l *listDB) ListIncludesAccount(ctx context.Context, listID string, accountID string) (bool, error) {
|
||||
exists, err := l.db.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("list_entries"), bun.Ident("list_entry")).
|
||||
Join(
|
||||
"JOIN ? AS ? ON ? = ?",
|
||||
bun.Ident("follows"), bun.Ident("follow"),
|
||||
bun.Ident("list_entry.follow_id"), bun.Ident("follow.id"),
|
||||
).
|
||||
Where("? = ?", bun.Ident("list_entry.list_id"), listID).
|
||||
Where("? = ?", bun.Ident("follow.target_account_id"), accountID).
|
||||
Exists(ctx)
|
||||
|
||||
return exists, l.db.ProcessError(err)
|
||||
}
|
||||
|
||||
// collate will collect the values of type T from an expected slice of length 'len',
|
||||
// passing the expected index to each call of 'get' and deduplicating the end result.
|
||||
func collate[T comparable](get func(int) T, len int) []T {
|
||||
|
|
|
@ -310,6 +310,27 @@ func (suite *ListTestSuite) TestDeleteListEntriesForFollowID() {
|
|||
suite.checkList(testList, dbList)
|
||||
}
|
||||
|
||||
func (suite *ListTestSuite) TestListIncludesAccount() {
|
||||
ctx := context.Background()
|
||||
testList, _ := suite.testStructs()
|
||||
|
||||
for accountID, expected := range map[string]bool{
|
||||
suite.testAccounts["admin_account"].ID: true,
|
||||
suite.testAccounts["local_account_1"].ID: false,
|
||||
suite.testAccounts["local_account_2"].ID: true,
|
||||
"01H7074GEZJ56J5C86PFB0V2CT": false,
|
||||
} {
|
||||
includes, err := suite.db.ListIncludesAccount(ctx, testList.ID, accountID)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
if includes != expected {
|
||||
suite.FailNow("", "expected %t for accountID %s got %t", expected, accountID, includes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ListTestSuite))
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ package bundb
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/uptrace/bun"
|
||||
|
@ -139,25 +139,42 @@ func (r *relationshipDB) getBlock(ctx context.Context, lookup string, dbQuery fu
|
|||
return block, nil
|
||||
}
|
||||
|
||||
// Set the block source account
|
||||
if err := r.state.DB.PopulateBlock(ctx, block); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return block, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) PopulateBlock(ctx context.Context, block *gtsmodel.Block) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(2)
|
||||
)
|
||||
|
||||
if block.Account == nil {
|
||||
// Block origin account is not set, fetch from database.
|
||||
block.Account, err = r.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
block.AccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting block source account: %w", err)
|
||||
errs.Appendf("error populating block account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the block target account
|
||||
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 {
|
||||
return nil, fmt.Errorf("error getting block target account: %w", err)
|
||||
errs.Appendf("error populating block target account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return block, nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (r *relationshipDB) PutBlock(ctx context.Context, block *gtsmodel.Block) error {
|
||||
|
|
|
@ -185,11 +185,7 @@ func (r *relationshipDB) PopulateFollow(ctx context.Context, follow *gtsmodel.Fo
|
|||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (r *relationshipDB) PutFollow(ctx context.Context, follow *gtsmodel.Follow) error {
|
||||
|
|
|
@ -20,11 +20,11 @@ package bundb
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/uptrace/bun"
|
||||
|
@ -127,27 +127,44 @@ func (r *relationshipDB) getFollowRequest(ctx context.Context, lookup string, db
|
|||
return followReq, nil
|
||||
}
|
||||
|
||||
// Set the follow request source account
|
||||
followReq.Account, err = r.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
followReq.AccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting follow request source account: %w", err)
|
||||
}
|
||||
|
||||
// Set the follow request target account
|
||||
followReq.TargetAccount, err = r.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
followReq.TargetAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting follow request target account: %w", err)
|
||||
if err := r.state.DB.PopulateFollowRequest(ctx, followReq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return followReq, nil
|
||||
}
|
||||
|
||||
func (r *relationshipDB) PopulateFollowRequest(ctx context.Context, follow *gtsmodel.FollowRequest) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(2)
|
||||
)
|
||||
|
||||
if follow.Account == nil {
|
||||
// Follow account is not set, fetch from the database.
|
||||
follow.Account, err = r.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
follow.AccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating follow request account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if follow.TargetAccount == nil {
|
||||
// Follow target account is not set, fetch from the database.
|
||||
follow.TargetAccount, err = r.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
follow.TargetAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating follow target request account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (r *relationshipDB) PutFollowRequest(ctx context.Context, follow *gtsmodel.FollowRequest) error {
|
||||
return r.state.Caches.GTS.FollowRequest().Store(follow, func() error {
|
||||
_, err := r.db.NewInsert().Model(follow).Exec(ctx)
|
||||
|
|
|
@ -20,11 +20,11 @@ package bundb
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
|
@ -135,37 +135,72 @@ func (r *reportDB) getReport(ctx context.Context, lookup string, dbQuery func(*g
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Set the report author account
|
||||
report.Account, err = r.state.DB.GetAccountByID(ctx, report.AccountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting report account: %w", err)
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// Only a barebones model was requested.
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// Set the report target account
|
||||
report.TargetAccount, err = r.state.DB.GetAccountByID(ctx, report.TargetAccountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting report target account: %w", err)
|
||||
}
|
||||
|
||||
if len(report.StatusIDs) > 0 {
|
||||
// Fetch reported statuses
|
||||
report.Statuses, err = r.state.DB.GetStatusesByIDs(ctx, report.StatusIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting status mentions: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if report.ActionTakenByAccountID != "" {
|
||||
// Set the report action taken by account
|
||||
report.ActionTakenByAccount, err = r.state.DB.GetAccountByID(ctx, report.ActionTakenByAccountID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting report action taken by account: %w", err)
|
||||
}
|
||||
if err := r.state.DB.PopulateReport(ctx, report); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func (r *reportDB) PopulateReport(ctx context.Context, report *gtsmodel.Report) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(4)
|
||||
)
|
||||
|
||||
if report.Account == nil {
|
||||
// Report account is not set, fetch from the database.
|
||||
report.Account, err = r.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
report.AccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating report account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if report.TargetAccount == nil {
|
||||
// Report target account is not set, fetch from the database.
|
||||
report.TargetAccount, err = r.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
report.TargetAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating report target account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if l := len(report.StatusIDs); l > 0 && l != len(report.Statuses) {
|
||||
// Report target statuses not set, fetch from the database.
|
||||
report.Statuses, err = r.state.DB.GetStatusesByIDs(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
report.StatusIDs,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating report statuses: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if report.ActionTakenByAccountID != "" &&
|
||||
report.ActionTakenByAccount == nil {
|
||||
// Report action account is not set, fetch from the database.
|
||||
report.ActionTakenByAccount, err = r.state.DB.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
report.ActionTakenByAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating report action taken by account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (r *reportDB) PutReport(ctx context.Context, report *gtsmodel.Report) error {
|
||||
return r.state.Caches.GTS.Report().Store(report, func() error {
|
||||
_, err := r.db.NewInsert().Model(report).Exec(ctx)
|
||||
|
|
|
@ -197,11 +197,7 @@ func (s *statusFaveDB) PopulateStatusFave(ctx context.Context, statusFave *gtsmo
|
|||
}
|
||||
}
|
||||
|
||||
if err := errs.Combine(); err != nil {
|
||||
return gtserror.Newf("%w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (s *statusFaveDB) PutStatusFave(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
||||
|
|
|
@ -64,4 +64,7 @@ type List interface {
|
|||
|
||||
// DeleteListEntryForFollowID deletes all list entries with the given followID.
|
||||
DeleteListEntriesForFollowID(ctx context.Context, followID string) error
|
||||
|
||||
// ListIncludesAccount returns true if the given listID includes the given accountID.
|
||||
ListIncludesAccount(ctx context.Context, listID string, accountID string) (bool, error)
|
||||
}
|
||||
|
|
|
@ -41,6 +41,9 @@ type Relationship interface {
|
|||
// GetBlock returns the block from account1 targeting account2, if it exists, or an error if it doesn't.
|
||||
GetBlock(ctx context.Context, account1 string, account2 string) (*gtsmodel.Block, error)
|
||||
|
||||
// PopulateBlock populates the struct pointers on the given block.
|
||||
PopulateBlock(ctx context.Context, block *gtsmodel.Block) error
|
||||
|
||||
// PutBlock attempts to place the given account block in the database.
|
||||
PutBlock(ctx context.Context, block *gtsmodel.Block) error
|
||||
|
||||
|
@ -77,6 +80,9 @@ type Relationship interface {
|
|||
// GetFollowRequest retrieves a follow request if it exists between source and target accounts.
|
||||
GetFollowRequest(ctx context.Context, sourceAccountID string, targetAccountID string) (*gtsmodel.FollowRequest, error)
|
||||
|
||||
// PopulateFollowRequest populates the struct pointers on the given follow request.
|
||||
PopulateFollowRequest(ctx context.Context, follow *gtsmodel.FollowRequest) error
|
||||
|
||||
// IsFollowing returns true if sourceAccount follows target account, or an error if something goes wrong while finding out.
|
||||
IsFollowing(ctx context.Context, sourceAccountID string, targetAccountID string) (bool, error)
|
||||
|
||||
|
|
|
@ -27,17 +27,24 @@ import (
|
|||
type Report interface {
|
||||
// GetReportByID gets one report by its db id
|
||||
GetReportByID(ctx context.Context, id string) (*gtsmodel.Report, error)
|
||||
|
||||
// GetReports gets limit n reports using the given parameters.
|
||||
// Parameters that are empty / zero are ignored.
|
||||
GetReports(ctx context.Context, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) ([]*gtsmodel.Report, error)
|
||||
|
||||
// PopulateReport populates the struct pointers on the given report.
|
||||
PopulateReport(ctx context.Context, report *gtsmodel.Report) error
|
||||
|
||||
// PutReport puts the given report in the database.
|
||||
PutReport(ctx context.Context, report *gtsmodel.Report) error
|
||||
|
||||
// UpdateReport updates one report by its db id.
|
||||
// The given columns will be updated; if no columns are
|
||||
// provided, then all columns will be updated.
|
||||
// updated_at will also be updated, no need to pass this
|
||||
// as a specific column.
|
||||
UpdateReport(ctx context.Context, report *gtsmodel.Report, columns ...string) (*gtsmodel.Report, error)
|
||||
|
||||
// DeleteReportByID deletes report with the given id.
|
||||
DeleteReportByID(ctx context.Context, id string) error
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
|||
return err
|
||||
}
|
||||
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: follow,
|
||||
|
@ -107,7 +107,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
|||
return err
|
||||
}
|
||||
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: follow,
|
||||
|
|
|
@ -56,7 +56,7 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre
|
|||
}
|
||||
|
||||
// This is a new boost. Process side effects asynchronously.
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityAnnounce,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: boost,
|
||||
|
|
|
@ -105,7 +105,7 @@ func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, rec
|
|||
return fmt.Errorf("activityBlock: database error inserting block: %s", err)
|
||||
}
|
||||
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityBlock,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: block,
|
||||
|
@ -233,7 +233,7 @@ func (f *federatingDB) createStatusable(
|
|||
if forward {
|
||||
// Pass the statusable URI (APIri) into the processor worker
|
||||
// and do the rest of the processing asynchronously.
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
APIri: statusableURI,
|
||||
|
@ -291,7 +291,7 @@ func (f *federatingDB) createStatusable(
|
|||
|
||||
// Do the rest of the processing asynchronously. The processor
|
||||
// will handle inserting/updating + further dereferencing the status.
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
APIri: nil,
|
||||
|
@ -344,7 +344,7 @@ func (f *federatingDB) activityFollow(ctx context.Context, asType vocab.Type, re
|
|||
return fmt.Errorf("activityFollow: database error inserting follow request: %s", err)
|
||||
}
|
||||
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: followRequest,
|
||||
|
@ -381,7 +381,7 @@ func (f *federatingDB) activityLike(ctx context.Context, asType vocab.Type, rece
|
|||
return fmt.Errorf("activityLike: database error inserting fave: %w", err)
|
||||
}
|
||||
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityLike,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: fave,
|
||||
|
@ -412,7 +412,7 @@ func (f *federatingDB) activityFlag(ctx context.Context, asType vocab.Type, rece
|
|||
return fmt.Errorf("activityFlag: database error inserting report: %w", err)
|
||||
}
|
||||
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFlag,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: report,
|
||||
|
|
|
@ -49,7 +49,7 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {
|
|||
// so we have to try a few different things...
|
||||
if s, err := f.state.DB.GetStatusByURI(ctx, id.String()); err == nil && requestingAccount.ID == s.AccountID {
|
||||
l.Debugf("uri is for STATUS with id: %s", s.ID)
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityDelete,
|
||||
GTSModel: s,
|
||||
|
@ -59,7 +59,7 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {
|
|||
|
||||
if a, err := f.state.DB.GetAccountByURI(ctx, id.String()); err == nil && requestingAccount.ID == a.ID {
|
||||
l.Debugf("uri is for ACCOUNT with id %s", a.ID)
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APActivityType: ap.ActivityDelete,
|
||||
GTSModel: a,
|
||||
|
|
|
@ -36,7 +36,7 @@ type FederatingDBTestSuite struct {
|
|||
suite.Suite
|
||||
db db.DB
|
||||
tc typeutils.TypeConverter
|
||||
fromFederator chan messages.FromFederator
|
||||
fromFederator chan messages.FromFediAPI
|
||||
federatingDB federatingdb.DB
|
||||
state state.State
|
||||
|
||||
|
@ -69,8 +69,8 @@ func (suite *FederatingDBTestSuite) SetupTest() {
|
|||
suite.state.Caches.Init()
|
||||
testrig.StartWorkers(&suite.state)
|
||||
|
||||
suite.fromFederator = make(chan messages.FromFederator, 10)
|
||||
suite.state.Workers.EnqueueFederator = func(ctx context.Context, msgs ...messages.FromFederator) {
|
||||
suite.fromFederator = make(chan messages.FromFediAPI, 10)
|
||||
suite.state.Workers.EnqueueFediAPI = func(ctx context.Context, msgs ...messages.FromFediAPI) {
|
||||
for _, msg := range msgs {
|
||||
suite.fromFederator <- msg
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ func (suite *RejectTestSuite) TestRejectFollowRequest() {
|
|||
err := suite.db.Put(ctx, fr)
|
||||
suite.NoError(err)
|
||||
|
||||
asFollow, err := suite.tc.FollowToAS(ctx, suite.tc.FollowRequestToFollow(ctx, fr), followingAccount, followedAccount)
|
||||
asFollow, err := suite.tc.FollowToAS(ctx, suite.tc.FollowRequestToFollow(ctx, fr))
|
||||
suite.NoError(err)
|
||||
|
||||
rejectingAccountURI := testrig.URLMustParse(followedAccount.URI)
|
||||
|
|
|
@ -93,7 +93,7 @@ func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gts
|
|||
// was delivered along with the Update, for further asynchronous
|
||||
// updating of eg., avatar/header, emojis, etc. The actual db
|
||||
// inserts/updates will take place there.
|
||||
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APActivityType: ap.ActivityUpdate,
|
||||
GTSModel: requestingAcct,
|
||||
|
|
|
@ -21,16 +21,34 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
// New returns a new error, prepended with caller function name if gtserror.Caller is enabled.
|
||||
// New returns a new error, prepended with caller
|
||||
// function name if gtserror.Caller is enabled.
|
||||
func New(msg string) error {
|
||||
return newAt(3, msg)
|
||||
}
|
||||
|
||||
// Newf returns a new formatted error, prepended with caller function name if gtserror.Caller is enabled.
|
||||
// Newf returns a new formatted error, prepended with
|
||||
// caller function name if gtserror.Caller is enabled.
|
||||
func Newf(msgf string, args ...any) error {
|
||||
return newfAt(3, msgf, args...)
|
||||
}
|
||||
|
||||
// NewfAt returns a new formatted error with the given
|
||||
// calldepth+1, useful when you want to wrap an error
|
||||
// from within an anonymous function or utility function,
|
||||
// but preserve the name in the error of the wrapping
|
||||
// function that did the calling.
|
||||
//
|
||||
// Provide calldepth 2 to prepend only the name of the
|
||||
// current containing function, 3 to prepend the name
|
||||
// of the function containing *that* function, and so on.
|
||||
//
|
||||
// This function is just exposed for dry-dick optimization
|
||||
// purposes. Most callers should just call Newf instead.
|
||||
func NewfAt(calldepth int, msgf string, args ...any) error {
|
||||
return newfAt(calldepth+1, msgf, args...)
|
||||
}
|
||||
|
||||
// NewResponseError crafts an error from provided HTTP response
|
||||
// including the method, status and body (if any provided). This
|
||||
// will also wrap the returned error using WithStatusCode() and
|
||||
|
|
|
@ -32,8 +32,8 @@ type FromClientAPI struct {
|
|||
TargetAccount *gtsmodel.Account
|
||||
}
|
||||
|
||||
// FromFederator wraps a message that travels from the federator into the processor.
|
||||
type FromFederator struct {
|
||||
// FromFediAPI wraps a message that travels from the federating API into the processor.
|
||||
type FromFediAPI struct {
|
||||
APObjectType string
|
||||
APActivityType string
|
||||
APIri *url.URL
|
||||
|
|
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
mm "github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/admin"
|
||||
|
@ -38,19 +34,23 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/processing/stream"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/timeline"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/workers"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/visibility"
|
||||
)
|
||||
|
||||
// Processor groups together processing functions and
|
||||
// sub processors for handling actions + events coming
|
||||
// from either the client or federating APIs.
|
||||
//
|
||||
// Many of the functions available through this struct
|
||||
// or sub processors will trigger asynchronous processing
|
||||
// via the workers contained in state.
|
||||
type Processor struct {
|
||||
federator federation.Federator
|
||||
tc typeutils.TypeConverter
|
||||
oauthServer oauth.Server
|
||||
mediaManager *mm.Manager
|
||||
state *state.State
|
||||
emailSender email.Sender
|
||||
filter *visibility.Filter
|
||||
|
||||
/*
|
||||
SUB-PROCESSORS
|
||||
|
@ -68,6 +68,7 @@ type Processor struct {
|
|||
stream stream.Processor
|
||||
timeline timeline.Processor
|
||||
user user.Processor
|
||||
workers workers.Processor
|
||||
}
|
||||
|
||||
func (p *Processor) Account() *account.Processor {
|
||||
|
@ -118,6 +119,10 @@ func (p *Processor) User() *user.Processor {
|
|||
return &p.user
|
||||
}
|
||||
|
||||
func (p *Processor) Workers() *workers.Processor {
|
||||
return &p.workers
|
||||
}
|
||||
|
||||
// NewProcessor returns a new Processor.
|
||||
func NewProcessor(
|
||||
tc typeutils.TypeConverter,
|
||||
|
@ -127,57 +132,53 @@ func NewProcessor(
|
|||
state *state.State,
|
||||
emailSender email.Sender,
|
||||
) *Processor {
|
||||
parseMentionFunc := GetParseMentionFunc(state.DB, federator)
|
||||
|
||||
filter := visibility.NewFilter(state)
|
||||
var (
|
||||
parseMentionFunc = GetParseMentionFunc(state.DB, federator)
|
||||
filter = visibility.NewFilter(state)
|
||||
)
|
||||
|
||||
processor := &Processor{
|
||||
federator: federator,
|
||||
tc: tc,
|
||||
oauthServer: oauthServer,
|
||||
mediaManager: mediaManager,
|
||||
state: state,
|
||||
filter: filter,
|
||||
emailSender: emailSender,
|
||||
}
|
||||
|
||||
// Instantiate sub processors.
|
||||
processor.account = account.New(state, tc, mediaManager, oauthServer, federator, filter, parseMentionFunc)
|
||||
//
|
||||
// Start with sub processors that will
|
||||
// be required by the workers processor.
|
||||
accountProcessor := account.New(state, tc, mediaManager, oauthServer, federator, filter, parseMentionFunc)
|
||||
mediaProcessor := media.New(state, tc, mediaManager, federator.TransportController())
|
||||
streamProcessor := stream.New(state, oauthServer)
|
||||
|
||||
// Instantiate the rest of the sub
|
||||
// processors + pin them to this struct.
|
||||
processor.account = accountProcessor
|
||||
processor.admin = admin.New(state, tc, mediaManager, federator.TransportController(), emailSender)
|
||||
processor.fedi = fedi.New(state, tc, federator, filter)
|
||||
processor.list = list.New(state, tc)
|
||||
processor.markers = markers.New(state, tc)
|
||||
processor.media = media.New(state, tc, mediaManager, federator.TransportController())
|
||||
processor.media = mediaProcessor
|
||||
processor.report = report.New(state, tc)
|
||||
processor.timeline = timeline.New(state, tc, filter)
|
||||
processor.search = search.New(state, federator, tc, filter)
|
||||
processor.status = status.New(state, federator, tc, filter, parseMentionFunc)
|
||||
processor.stream = stream.New(state, oauthServer)
|
||||
processor.stream = streamProcessor
|
||||
processor.user = user.New(state, emailSender)
|
||||
|
||||
// Workers processor handles asynchronous
|
||||
// worker jobs; instantiate it separately
|
||||
// and pass subset of sub processors it needs.
|
||||
processor.workers = workers.New(
|
||||
state,
|
||||
federator,
|
||||
tc,
|
||||
filter,
|
||||
emailSender,
|
||||
&accountProcessor,
|
||||
&mediaProcessor,
|
||||
&streamProcessor,
|
||||
)
|
||||
|
||||
return processor
|
||||
}
|
||||
|
||||
func (p *Processor) EnqueueClientAPI(ctx context.Context, msgs ...messages.FromClientAPI) {
|
||||
log.Trace(ctx, "enqueuing")
|
||||
_ = p.state.Workers.ClientAPI.MustEnqueueCtx(ctx, func(ctx context.Context) {
|
||||
for _, msg := range msgs {
|
||||
log.Trace(ctx, "processing: %+v", msg)
|
||||
if err := p.ProcessFromClientAPI(ctx, msg); err != nil {
|
||||
log.Errorf(ctx, "error processing client API message: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Processor) EnqueueFederator(ctx context.Context, msgs ...messages.FromFederator) {
|
||||
log.Trace(ctx, "enqueuing")
|
||||
_ = p.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
|
||||
for _, msg := range msgs {
|
||||
log.Trace(ctx, "processing: %+v", msg)
|
||||
if err := p.ProcessFromFederator(ctx, msg); err != nil {
|
||||
log.Errorf(ctx, "error processing federator message: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -123,8 +123,8 @@ func (suite *ProcessingStandardTestSuite) SetupTest() {
|
|||
suite.emailSender = testrig.NewEmailSender("../../web/template/", nil)
|
||||
|
||||
suite.processor = processing.NewProcessor(suite.typeconverter, suite.federator, suite.oauthServer, suite.mediaManager, &suite.state, suite.emailSender)
|
||||
suite.state.Workers.EnqueueClientAPI = suite.processor.EnqueueClientAPI
|
||||
suite.state.Workers.EnqueueFederator = suite.processor.EnqueueFederator
|
||||
suite.state.Workers.EnqueueClientAPI = suite.processor.Workers().EnqueueClientAPI
|
||||
suite.state.Workers.EnqueueFediAPI = suite.processor.Workers().EnqueueFediAPI
|
||||
|
||||
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../testrig/media")
|
||||
|
|
|
@ -28,13 +28,14 @@ import (
|
|||
type Processor struct {
|
||||
state *state.State
|
||||
oauthServer oauth.Server
|
||||
streamMap sync.Map
|
||||
streamMap *sync.Map
|
||||
}
|
||||
|
||||
func New(state *state.State, oauthServer oauth.Server) Processor {
|
||||
return Processor{
|
||||
state: state,
|
||||
oauthServer: oauthServer,
|
||||
streamMap: &sync.Map{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,67 +23,13 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
)
|
||||
|
||||
var oneWeek = 168 * time.Hour
|
||||
|
||||
// EmailSendConfirmation sends an email address confirmation request email to the given user.
|
||||
func (p *Processor) EmailSendConfirmation(ctx context.Context, user *gtsmodel.User, username string) error {
|
||||
if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email {
|
||||
// user has already confirmed this email address, so there's nothing to do
|
||||
return nil
|
||||
}
|
||||
|
||||
// We need a token and a link for the user to click on.
|
||||
// We'll use a uuid as our token since it's basically impossible to guess.
|
||||
// From the uuid package we use (which uses crypto/rand under the hood):
|
||||
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
|
||||
// hit by a meteorite is estimated to be one chance in 17 billion, that
|
||||
// means the probability is about 0.00000000006 (6 × 10−11),
|
||||
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
|
||||
// year and having one duplicate.
|
||||
confirmationToken := uuid.NewString()
|
||||
confirmationLink := uris.GenerateURIForEmailConfirm(confirmationToken)
|
||||
|
||||
// pull our instance entry from the database so we can greet the user nicely in the email
|
||||
instance := >smodel.Instance{}
|
||||
host := config.GetHost()
|
||||
if err := p.state.DB.GetWhere(ctx, []db.Where{{Key: "domain", Value: host}}, instance); err != nil {
|
||||
return fmt.Errorf("SendConfirmEmail: error getting instance: %s", err)
|
||||
}
|
||||
|
||||
// assemble the email contents and send the email
|
||||
confirmData := email.ConfirmData{
|
||||
Username: username,
|
||||
InstanceURL: instance.URI,
|
||||
InstanceName: instance.Title,
|
||||
ConfirmLink: confirmationLink,
|
||||
}
|
||||
if err := p.emailSender.SendConfirmEmail(user.UnconfirmedEmail, confirmData); err != nil {
|
||||
return fmt.Errorf("SendConfirmEmail: error sending to email address %s belonging to user %s: %s", user.UnconfirmedEmail, username, err)
|
||||
}
|
||||
|
||||
// email sent, now we need to update the user entry with the token we just sent them
|
||||
updatingColumns := []string{"confirmation_sent_at", "confirmation_token", "last_emailed_at", "updated_at"}
|
||||
user.ConfirmationSentAt = time.Now()
|
||||
user.ConfirmationToken = confirmationToken
|
||||
user.LastEmailedAt = time.Now()
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
if err := p.state.DB.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil {
|
||||
return fmt.Errorf("SendConfirmEmail: error updating user entry after email sent: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmailConfirm processes an email confirmation request, usually initiated as a result of clicking on a link
|
||||
// in a 'confirm your email address' type email.
|
||||
func (p *Processor) EmailConfirm(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
|
||||
|
|
|
@ -19,7 +19,6 @@ package user_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -30,36 +29,6 @@ type EmailConfirmTestSuite struct {
|
|||
UserStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *EmailConfirmTestSuite) TestSendConfirmEmail() {
|
||||
user := suite.testUsers["local_account_1"]
|
||||
|
||||
// set a bunch of stuff on the user as though zork hasn't been confirmed (perish the thought)
|
||||
user.UnconfirmedEmail = "some.email@example.org"
|
||||
user.Email = ""
|
||||
user.ConfirmedAt = time.Time{}
|
||||
user.ConfirmationSentAt = time.Time{}
|
||||
user.ConfirmationToken = ""
|
||||
|
||||
err := suite.user.EmailSendConfirmation(context.Background(), user, "the_mighty_zork")
|
||||
suite.NoError(err)
|
||||
|
||||
// zork should have an email now
|
||||
suite.Len(suite.sentEmails, 1)
|
||||
email, ok := suite.sentEmails["some.email@example.org"]
|
||||
suite.True(ok)
|
||||
|
||||
// a token should be set on zork
|
||||
token := user.ConfirmationToken
|
||||
suite.NotEmpty(token)
|
||||
|
||||
// email should contain the token
|
||||
emailShould := fmt.Sprintf("To: some.email@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Email Confirmation\r\n\r\nHello the_mighty_zork!\r\n\r\nYou are receiving this mail because you've requested an account on http://localhost:8080.\r\n\r\nWe just need to confirm that this is your email address. To confirm your email, paste the following in your browser's address bar:\r\n\r\nhttp://localhost:8080/confirm_email?token=%s\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of http://localhost:8080\r\n\r\n", token)
|
||||
suite.Equal(emailShould, email)
|
||||
|
||||
// confirmationSentAt should be recent
|
||||
suite.WithinDuration(time.Now(), user.ConfirmationSentAt, 1*time.Minute)
|
||||
}
|
||||
|
||||
func (suite *EmailConfirmTestSuite) TestConfirmEmail() {
|
||||
ctx := context.Background()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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{})
|
||||
}
|
|
@ -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
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package processing_test
|
||||
package workers_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -36,12 +36,12 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type FromFederatorTestSuite struct {
|
||||
ProcessingStandardTestSuite
|
||||
type FromFediAPITestSuite struct {
|
||||
WorkersTestSuite
|
||||
}
|
||||
|
||||
// remote_account_1 boosts the first status of local_account_1
|
||||
func (suite *FromFederatorTestSuite) TestProcessFederationAnnounce() {
|
||||
func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
|
||||
boostedStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
boostingAccount := suite.testAccounts["remote_account_1"]
|
||||
announceStatus := >smodel.Status{}
|
||||
|
@ -56,7 +56,7 @@ func (suite *FromFederatorTestSuite) TestProcessFederationAnnounce() {
|
|||
announceStatus.Account = boostingAccount
|
||||
announceStatus.Visibility = boostedStatus.Visibility
|
||||
|
||||
err := suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
|
||||
err := suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityAnnounce,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: announceStatus,
|
||||
|
@ -87,7 +87,7 @@ func (suite *FromFederatorTestSuite) TestProcessFederationAnnounce() {
|
|||
suite.False(*notif.Read)
|
||||
}
|
||||
|
||||
func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
|
||||
func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
|
||||
repliedAccount := suite.testAccounts["local_account_1"]
|
||||
repliedStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
replyingAccount := suite.testAccounts["remote_account_1"]
|
||||
|
@ -128,7 +128,7 @@ func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
|
|||
err = suite.db.PutStatus(context.Background(), replyingStatus)
|
||||
suite.NoError(err)
|
||||
|
||||
err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
|
||||
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: replyingStatus,
|
||||
|
@ -173,7 +173,7 @@ func (suite *FromFederatorTestSuite) TestProcessReplyMention() {
|
|||
suite.Equal(replyingAccount.ID, notifStreamed.Account.ID)
|
||||
}
|
||||
|
||||
func (suite *FromFederatorTestSuite) TestProcessFave() {
|
||||
func (suite *FromFediAPITestSuite) TestProcessFave() {
|
||||
favedAccount := suite.testAccounts["local_account_1"]
|
||||
favedStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
favingAccount := suite.testAccounts["remote_account_1"]
|
||||
|
@ -197,7 +197,7 @@ func (suite *FromFederatorTestSuite) TestProcessFave() {
|
|||
err := suite.db.Put(context.Background(), fave)
|
||||
suite.NoError(err)
|
||||
|
||||
err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
|
||||
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityLike,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: fave,
|
||||
|
@ -245,7 +245,7 @@ func (suite *FromFederatorTestSuite) TestProcessFave() {
|
|||
//
|
||||
// This tests for an issue we were seeing where Misskey sends out faves to inboxes of people that don't own
|
||||
// the fave, but just follow the actor who received the fave.
|
||||
func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccount() {
|
||||
func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount() {
|
||||
receivingAccount := suite.testAccounts["local_account_2"]
|
||||
favedAccount := suite.testAccounts["local_account_1"]
|
||||
favedStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
|
@ -270,7 +270,7 @@ func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccoun
|
|||
err := suite.db.Put(context.Background(), fave)
|
||||
suite.NoError(err)
|
||||
|
||||
err = suite.processor.ProcessFromFederator(context.Background(), messages.FromFederator{
|
||||
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityLike,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: fave,
|
||||
|
@ -304,7 +304,7 @@ func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccoun
|
|||
suite.Empty(wssStream.Messages)
|
||||
}
|
||||
|
||||
func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
|
||||
func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
|
||||
ctx := context.Background()
|
||||
|
||||
deletedAccount := suite.testAccounts["remote_account_1"]
|
||||
|
@ -339,7 +339,7 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
|
|||
suite.NoError(err)
|
||||
|
||||
// now they are mufos!
|
||||
err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
|
||||
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APActivityType: ap.ActivityDelete,
|
||||
GTSModel: deletedAccount,
|
||||
|
@ -386,7 +386,7 @@ func (suite *FromFederatorTestSuite) TestProcessAccountDelete() {
|
|||
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin)
|
||||
}
|
||||
|
||||
func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
|
||||
func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
|
||||
ctx := context.Background()
|
||||
|
||||
originAccount := suite.testAccounts["remote_account_1"]
|
||||
|
@ -414,7 +414,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
|
|||
err := suite.db.Put(ctx, satanFollowRequestTurtle)
|
||||
suite.NoError(err)
|
||||
|
||||
err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
|
||||
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: satanFollowRequestTurtle,
|
||||
|
@ -443,7 +443,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() {
|
|||
suite.Empty(suite.httpClient.SentMessages)
|
||||
}
|
||||
|
||||
func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
|
||||
func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
|
||||
ctx := context.Background()
|
||||
|
||||
originAccount := suite.testAccounts["remote_account_1"]
|
||||
|
@ -471,7 +471,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
|
|||
err := suite.db.Put(ctx, satanFollowRequestTurtle)
|
||||
suite.NoError(err)
|
||||
|
||||
err = suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
|
||||
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: satanFollowRequestTurtle,
|
||||
|
@ -539,13 +539,13 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
|
|||
}
|
||||
|
||||
// TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor.
|
||||
func (suite *FromFederatorTestSuite) TestCreateStatusFromIRI() {
|
||||
func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
|
||||
ctx := context.Background()
|
||||
|
||||
receivingAccount := suite.testAccounts["local_account_1"]
|
||||
statusCreator := suite.testAccounts["remote_account_2"]
|
||||
|
||||
err := suite.processor.ProcessFromFederator(ctx, messages.FromFederator{
|
||||
err := suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectNote,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: nil, // gtsmodel is nil because this is a forwarded status -- we want to dereference it using the iri
|
||||
|
@ -561,5 +561,5 @@ func (suite *FromFederatorTestSuite) TestCreateStatusFromIRI() {
|
|||
}
|
||||
|
||||
func TestFromFederatorTestSuite(t *testing.T) {
|
||||
suite.Run(t, &FromFederatorTestSuite{})
|
||||
suite.Run(t, &FromFediAPITestSuite{})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
StatusToASDelete(ctx context.Context, status *gtsmodel.Status) (vocab.ActivityStreamsDelete, error)
|
||||
// FollowToASFollow converts a gts model Follow into an activity streams Follow, suitable for federation
|
||||
FollowToAS(ctx context.Context, f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error)
|
||||
FollowToAS(ctx context.Context, f *gtsmodel.Follow) (vocab.ActivityStreamsFollow, error)
|
||||
// MentionToAS converts a gts model mention into an activity streams Mention, suitable for federation
|
||||
MentionToAS(ctx context.Context, m *gtsmodel.Mention) (vocab.ActivityStreamsMention, error)
|
||||
// EmojiToAS converts a gts emoji into a mastodon ns Emoji, suitable for federation
|
||||
|
|
|
@ -774,10 +774,14 @@ func (c *converter) StatusToASDelete(ctx context.Context, s *gtsmodel.Status) (v
|
|||
return delete, nil
|
||||
}
|
||||
|
||||
func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (vocab.ActivityStreamsFollow, error) {
|
||||
// parse out the various URIs we need for this
|
||||
// origin account (who's doing the follow)
|
||||
originAccountURI, err := url.Parse(originAccount.URI)
|
||||
func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow) (vocab.ActivityStreamsFollow, error) {
|
||||
if err := c.db.PopulateFollow(ctx, f); err != nil {
|
||||
return nil, gtserror.Newf("error populating follow: %w", err)
|
||||
}
|
||||
|
||||
// Parse out the various URIs we need for this
|
||||
// origin account (who's doing the follow).
|
||||
originAccountURI, err := url.Parse(f.Account.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("followtoasfollow: error parsing origin account uri: %s", err)
|
||||
}
|
||||
|
@ -785,7 +789,7 @@ func (c *converter) FollowToAS(ctx context.Context, f *gtsmodel.Follow, originAc
|
|||
originActor.AppendIRI(originAccountURI)
|
||||
|
||||
// target account (who's being followed)
|
||||
targetAccountURI, err := url.Parse(targetAccount.URI)
|
||||
targetAccountURI, err := url.Parse(f.TargetAccount.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("followtoasfollow: error parsing target account uri: %s", err)
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/cache"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
@ -97,33 +98,17 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A
|
|||
}
|
||||
|
||||
var (
|
||||
next *gtsmodel.Status
|
||||
next = status
|
||||
oneAuthor = true // Assume one author until proven otherwise.
|
||||
included bool
|
||||
converstn bool
|
||||
)
|
||||
|
||||
for next = status; next.InReplyToURI != ""; {
|
||||
// Fetch next parent to lookup.
|
||||
parentID := next.InReplyToID
|
||||
if parentID == "" {
|
||||
log.Warnf(ctx, "status not yet deref'd: %s", next.InReplyToURI)
|
||||
return false, cache.SentinelError
|
||||
}
|
||||
|
||||
// Get the next parent in the chain from DB.
|
||||
next, err = f.state.DB.GetStatusByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
parentID,
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("isStatusHomeTimelineable: error getting status parent %s: %w", parentID, err)
|
||||
}
|
||||
|
||||
for {
|
||||
// Populate account mention objects before account mention checks.
|
||||
next.Mentions, err = f.state.DB.GetMentions(ctx, next.MentionIDs)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("isStatusHomeTimelineable: error populating status parent %s mentions: %w", parentID, err)
|
||||
return false, gtserror.Newf("error populating status %s mentions: %w", next.ID, err)
|
||||
}
|
||||
|
||||
if (next.AccountID == owner.ID) ||
|
||||
|
@ -139,7 +124,7 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A
|
|||
// is it between accounts on owner timeline that they follow?
|
||||
converstn, err = f.isVisibleConversation(ctx, owner, next)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("isStatusHomeTimelineable: error checking conversation visibility: %w", err)
|
||||
return false, gtserror.Newf("error checking conversation visibility: %w", err)
|
||||
}
|
||||
|
||||
if converstn {
|
||||
|
@ -152,6 +137,26 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A
|
|||
// Check if this continues to be a single-author thread.
|
||||
oneAuthor = (next.AccountID == status.AccountID)
|
||||
}
|
||||
|
||||
if next.InReplyToURI == "" {
|
||||
// Reached the top of the thread.
|
||||
break
|
||||
}
|
||||
|
||||
// Fetch next parent in thread.
|
||||
parentID := next.InReplyToID
|
||||
if parentID == "" {
|
||||
log.Warnf(ctx, "status not yet deref'd: %s", next.InReplyToURI)
|
||||
return false, cache.SentinelError
|
||||
}
|
||||
|
||||
next, err = f.state.DB.GetStatusByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
parentID,
|
||||
)
|
||||
if err != nil {
|
||||
return false, gtserror.Newf("error getting status parent %s: %w", parentID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if next != status && !oneAuthor && !included && !converstn {
|
||||
|
@ -177,7 +182,7 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A
|
|||
status.AccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("isStatusHomeTimelineable: error checking follow %s->%s: %w", owner.ID, status.AccountID, err)
|
||||
return false, gtserror.Newf("error checking follow %s->%s: %w", owner.ID, status.AccountID, err)
|
||||
}
|
||||
|
||||
if !follow {
|
||||
|
|
|
@ -43,7 +43,7 @@ type Workers struct {
|
|||
// these are pointers to Processor{}.Enqueue___() msg functions.
|
||||
// This prevents dependency cycling as Processor depends on Workers.
|
||||
EnqueueClientAPI func(context.Context, ...messages.FromClientAPI)
|
||||
EnqueueFederator func(context.Context, ...messages.FromFederator)
|
||||
EnqueueFediAPI func(context.Context, ...messages.FromFediAPI)
|
||||
|
||||
// Media manager worker pools.
|
||||
Media runners.WorkerPool
|
||||
|
|
|
@ -28,7 +28,7 @@ import (
|
|||
// NewTestProcessor returns a Processor suitable for testing purposes
|
||||
func NewTestProcessor(state *state.State, federator federation.Federator, emailSender email.Sender, mediaManager *media.Manager) *processing.Processor {
|
||||
p := processing.NewProcessor(NewTestTypeConverter(state.DB), federator, NewTestOauthServer(state.DB), mediaManager, state, emailSender)
|
||||
state.Workers.EnqueueClientAPI = p.EnqueueClientAPI
|
||||
state.Workers.EnqueueFederator = p.EnqueueFederator
|
||||
state.Workers.EnqueueClientAPI = p.Workers().EnqueueClientAPI
|
||||
state.Workers.EnqueueFediAPI = p.Workers().EnqueueFediAPI
|
||||
return p
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ import (
|
|||
|
||||
func StartWorkers(state *state.State) {
|
||||
state.Workers.EnqueueClientAPI = func(context.Context, ...messages.FromClientAPI) {}
|
||||
state.Workers.EnqueueFederator = func(context.Context, ...messages.FromFederator) {}
|
||||
state.Workers.EnqueueFediAPI = func(context.Context, ...messages.FromFediAPI) {}
|
||||
|
||||
_ = state.Workers.Scheduler.Start(nil)
|
||||
_ = state.Workers.ClientAPI.Start(1, 10)
|
||||
|
|
Loading…
Reference in New Issue