diff options
Diffstat (limited to 'internal/processing')
| -rw-r--r-- | internal/processing/account/account_test.go | 4 | ||||
| -rw-r--r-- | internal/processing/account/follow.go | 92 | ||||
| -rw-r--r-- | internal/processing/account/follow_test.go | 140 | ||||
| -rw-r--r-- | internal/processing/fromclientapi.go | 12 | ||||
| -rw-r--r-- | internal/processing/fromclientapi_test.go | 73 | ||||
| -rw-r--r-- | internal/processing/fromcommon.go | 654 | ||||
| -rw-r--r-- | internal/processing/fromfederator.go | 10 | ||||
| -rw-r--r-- | internal/processing/processor_test.go | 2 | 
8 files changed, 607 insertions, 380 deletions
| diff --git a/internal/processing/account/account_test.go b/internal/processing/account/account_test.go index eed6ad7e3..5d48d1210 100644 --- a/internal/processing/account/account_test.go +++ b/internal/processing/account/account_test.go @@ -59,6 +59,7 @@ type AccountStandardTestSuite struct {  	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 @@ -72,6 +73,7 @@ func (suite *AccountStandardTestSuite) SetupSuite() {  	suite.testApplications = testrig.NewTestApplications()  	suite.testUsers = testrig.NewTestUsers()  	suite.testAccounts = testrig.NewTestAccounts() +	suite.testFollows = testrig.NewTestFollows()  	suite.testAttachments = testrig.NewTestAttachments()  	suite.testStatuses = testrig.NewTestStatuses()  } @@ -80,8 +82,8 @@ func (suite *AccountStandardTestSuite) SetupTest() {  	suite.state.Caches.Init()  	testrig.StartWorkers(&suite.state) -	testrig.InitTestLog()  	testrig.InitTestConfig() +	testrig.InitTestLog()  	suite.db = testrig.NewTestDB(&suite.state)  	suite.state.DB = suite.db diff --git a/internal/processing/account/follow.go b/internal/processing/account/follow.go index ab8fecd94..1aed92e75 100644 --- a/internal/processing/account/follow.go +++ b/internal/processing/account/follow.go @@ -25,6 +25,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/ap"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/db" +	"github.com/superseriousbusiness/gotosocial/internal/gtscontext"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/id" @@ -40,24 +41,47 @@ func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmode  	}  	// Check if a follow exists already. -	if follows, err := p.state.DB.IsFollowing(ctx, requestingAccount.ID, targetAccount.ID); err != nil { -		err = fmt.Errorf("FollowCreate: db error checking follow: %w", err) +	if follow, err := p.state.DB.GetFollow( +		gtscontext.SetBarebones(ctx), +		requestingAccount.ID, +		targetAccount.ID, +	); err != nil && !errors.Is(err, db.ErrNoEntries) { +		err = fmt.Errorf("FollowCreate: db error checking existing follow: %w", err)  		return nil, gtserror.NewErrorInternalError(err) -	} else if follows { -		// Already follows, just return current relationship. -		return p.RelationshipGet(ctx, requestingAccount, form.ID) +	} else if follow != nil { +		// Already follows, update if necessary + return relationship. +		return p.updateFollow( +			ctx, +			requestingAccount, +			form, +			follow.ShowReblogs, +			follow.Notify, +			func(columns ...string) error { return p.state.DB.UpdateFollow(ctx, follow, columns...) }, +		)  	}  	// Check if a follow request exists already. -	if followRequested, err := p.state.DB.IsFollowRequested(ctx, requestingAccount.ID, targetAccount.ID); err != nil { -		err = fmt.Errorf("FollowCreate: db error checking follow request: %w", err) +	if followRequest, err := p.state.DB.GetFollowRequest( +		gtscontext.SetBarebones(ctx), +		requestingAccount.ID, +		targetAccount.ID, +	); err != nil && !errors.Is(err, db.ErrNoEntries) { +		err = fmt.Errorf("FollowCreate: db error checking existing follow request: %w", err)  		return nil, gtserror.NewErrorInternalError(err) -	} else if followRequested { -		// Already follow requested, just return current relationship. -		return p.RelationshipGet(ctx, requestingAccount, form.ID) +	} else if followRequest != nil { +		// Already requested, update if necessary + return relationship. +		return p.updateFollow( +			ctx, +			requestingAccount, +			form, +			followRequest.ShowReblogs, +			followRequest.Notify, +			func(columns ...string) error { return p.state.DB.UpdateFollowRequest(ctx, followRequest, columns...) }, +		)  	} -	// Create and store a new follow request. +	// Neither follows nor follow requests, so +	// create and store a new follow request.  	followID, err := id.NewRandomULID()  	if err != nil {  		return nil, gtserror.NewErrorInternalError(err) @@ -129,6 +153,52 @@ func (p *Processor) FollowRemove(ctx context.Context, requestingAccount *gtsmode  	Utility functions.  */ +// updateFollow is a utility function for updating an existing +// follow or followRequest with the parameters provided in the +// given form. If nothing changes, this function is a no-op and +// will just return the existing relationship between follow +// origin and follow target account. +func (p *Processor) updateFollow( +	ctx context.Context, +	requestingAccount *gtsmodel.Account, +	form *apimodel.AccountFollowRequest, +	currentShowReblogs *bool, +	currentNotify *bool, +	update func(...string) error, +) (*apimodel.Relationship, gtserror.WithCode) { + +	if form.Reblogs == nil && form.Notify == nil { +		// There's nothing to update. +		return p.RelationshipGet(ctx, requestingAccount, form.ID) +	} + +	// Including "updated_at", max 3 columns may change. +	columns := make([]string, 0, 3) + +	// Check what we need to update (if anything). +	if newReblogs := form.Reblogs; newReblogs != nil && *newReblogs != *currentShowReblogs { +		*currentShowReblogs = *newReblogs +		columns = append(columns, "show_reblogs") +	} + +	if newNotify := form.Notify; newNotify != nil && *newNotify != *currentNotify { +		*currentNotify = *newNotify +		columns = append(columns, "notify") +	} + +	if len(columns) == 0 { +		// Nothing actually changed. +		return p.RelationshipGet(ctx, requestingAccount, form.ID) +	} + +	if err := update(columns...); err != nil { +		err = fmt.Errorf("updateFollow: error updating existing follow (request): %w", err) +		return nil, gtserror.NewErrorInternalError(err) +	} + +	return p.RelationshipGet(ctx, requestingAccount, form.ID) +} +  // getFollowTarget is a convenience function which:  //   - Checks if account is trying to follow/unfollow itself.  //   - Returns not found if there's a block in place between accounts. diff --git a/internal/processing/account/follow_test.go b/internal/processing/account/follow_test.go new file mode 100644 index 000000000..70a28eea2 --- /dev/null +++ b/internal/processing/account/follow_test.go @@ -0,0 +1,140 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +package account_test + +import ( +	"context" +	"testing" + +	"github.com/stretchr/testify/suite" +	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +	"github.com/superseriousbusiness/gotosocial/testrig" +) + +type FollowTestSuite struct { +	AccountStandardTestSuite +} + +func (suite *FollowTestSuite) TestUpdateExistingFollowChangeBoth() { +	ctx := context.Background() +	requestingAccount := suite.testAccounts["local_account_1"] +	targetAccount := suite.testAccounts["admin_account"] + +	// Change both Reblogs and Notify. +	// Trace logs should show a query similar to this: +	//	UPDATE "follows" AS "follow" SET "show_reblogs" = FALSE, "notify" = TRUE, "updated_at" = '2023-04-09 11:42:39.424705+00:00' WHERE ("follow"."id" = '01F8PY8RHWRQZV038T4E8T9YK8') +	relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{ +		ID:      targetAccount.ID, +		Reblogs: testrig.FalseBool(), +		Notify:  testrig.TrueBool(), +	}) + +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	suite.False(relationship.ShowingReblogs) +	suite.True(relationship.Notifying) +} + +func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNotifyIgnoreReblogs() { +	ctx := context.Background() +	requestingAccount := suite.testAccounts["local_account_1"] +	targetAccount := suite.testAccounts["admin_account"] + +	// Change Notify, ignore Reblogs. +	// Trace logs should show a query similar to this: +	//	UPDATE "follows" AS "follow" SET "notify" = TRUE, "updated_at" = '2023-04-09 11:40:33.827858+00:00' WHERE ("follow"."id" = '01F8PY8RHWRQZV038T4E8T9YK8') +	relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{ +		ID:     targetAccount.ID, +		Notify: testrig.TrueBool(), +	}) + +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	suite.True(relationship.ShowingReblogs) +	suite.True(relationship.Notifying) +} + +func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNotifySetReblogs() { +	ctx := context.Background() +	requestingAccount := suite.testAccounts["local_account_1"] +	targetAccount := suite.testAccounts["admin_account"] + +	// Change Notify, set Reblogs to same value as before. +	// Trace logs should show a query similar to this: +	//	UPDATE "follows" AS "follow" SET "notify" = TRUE, "updated_at" = '2023-04-09 11:40:33.827858+00:00' WHERE ("follow"."id" = '01F8PY8RHWRQZV038T4E8T9YK8') +	relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{ +		ID:      targetAccount.ID, +		Notify:  testrig.TrueBool(), +		Reblogs: testrig.TrueBool(), +	}) + +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	suite.True(relationship.ShowingReblogs) +	suite.True(relationship.Notifying) +} + +func (suite *FollowTestSuite) TestUpdateExistingFollowChangeNothing() { +	ctx := context.Background() +	requestingAccount := suite.testAccounts["local_account_1"] +	targetAccount := suite.testAccounts["admin_account"] + +	// Set Notify and Reblogs to same values as before. +	// Trace logs should show no update query. +	relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{ +		ID:      targetAccount.ID, +		Notify:  testrig.FalseBool(), +		Reblogs: testrig.TrueBool(), +	}) + +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	suite.True(relationship.ShowingReblogs) +	suite.False(relationship.Notifying) +} + +func (suite *FollowTestSuite) TestUpdateExistingFollowSetNothing() { +	ctx := context.Background() +	requestingAccount := suite.testAccounts["local_account_1"] +	targetAccount := suite.testAccounts["admin_account"] + +	// Don't set Notify or Reblogs. +	// Trace logs should show no update query. +	relationship, err := suite.accountProcessor.FollowCreate(ctx, requestingAccount, &apimodel.AccountFollowRequest{ +		ID: targetAccount.ID, +	}) + +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	suite.True(relationship.ShowingReblogs) +	suite.False(relationship.Notifying) +} + +func TestFollowTestS(t *testing.T) { +	suite.Run(t, new(FollowTestSuite)) +} diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 490fc7d34..082a5ba2e 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -160,11 +160,7 @@ func (p *Processor) processCreateStatusFromClientAPI(ctx context.Context, client  		return errors.New("note was not parseable as *gtsmodel.Status")  	} -	if err := p.timelineStatus(ctx, status); err != nil { -		return err -	} - -	if err := p.notifyStatus(ctx, status); err != nil { +	if err := p.timelineAndNotifyStatus(ctx, status); err != nil {  		return err  	} @@ -203,7 +199,7 @@ func (p *Processor) processCreateAnnounceFromClientAPI(ctx context.Context, clie  		return errors.New("boost was not parseable as *gtsmodel.Status")  	} -	if err := p.timelineStatus(ctx, boostWrapperStatus); err != nil { +	if err := p.timelineAndNotifyStatus(ctx, boostWrapperStatus); err != nil {  		return err  	} @@ -255,7 +251,7 @@ func (p *Processor) processUpdateReportFromClientAPI(ctx context.Context, client  		return nil  	} -	return p.notifyReportClosed(ctx, report) +	return p.emailReportClosed(ctx, report)  }  func (p *Processor) processAcceptFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { @@ -373,7 +369,7 @@ func (p *Processor) processReportAccountFromClientAPI(ctx context.Context, clien  		}  	} -	if err := p.notifyReport(ctx, report); err != nil { +	if err := p.emailReport(ctx, report); err != nil {  		return fmt.Errorf("processReportAccountFromClientAPI: error notifying report: %w", err)  	} diff --git a/internal/processing/fromclientapi_test.go b/internal/processing/fromclientapi_test.go index 0923fdf5b..0b641c091 100644 --- a/internal/processing/fromclientapi_test.go +++ b/internal/processing/fromclientapi_test.go @@ -159,6 +159,79 @@ func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {  	suite.ErrorIs(err, db.ErrNoEntries)  } +func (suite *FromClientAPITestSuite) TestProcessNewStatusWithNotification() { +	ctx := context.Background() +	postingAccount := suite.testAccounts["admin_account"] +	receivingAccount := suite.testAccounts["local_account_1"] + +	// 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 = testrig.TrueBool() +	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:                    testrig.TrueBool(), +		AccountURI:               "http://localhost:8080/users/admin", +		AccountID:                "01F8MH17FWEB39HZJ76B6VXSKF", +		InReplyToID:              "", +		BoostOfID:                "", +		ContentWarning:           "", +		Visibility:               gtsmodel.VisibilityFollowersOnly, +		Sensitive:                testrig.FalseBool(), +		Language:                 "en", +		CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F", +		Federated:                testrig.FalseBool(), +		Boostable:                testrig.TrueBool(), +		Replyable:                testrig.TrueBool(), +		Likeable:                 testrig.TrueBool(), +		ActivityStreamsType:      ap.ObjectNote, +	} + +	// Put the status in the db first, to mimic what +	// would have already happened earlier up the flow. +	err := suite.db.PutStatus(ctx, newStatus) +	suite.NoError(err) + +	// 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") +	} +} +  func TestFromClientAPITestSuite(t *testing.T) {  	suite.Run(t, &FromClientAPITestSuite{})  } diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go index 45c637978..a7ab0b330 100644 --- a/internal/processing/fromcommon.go +++ b/internal/processing/fromcommon.go @@ -25,296 +25,397 @@ import (  	"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/stream"  ) -func (p *Processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) error { -	// if there are no mentions in this status then just bail -	if len(status.MentionIDs) == 0 { -		return nil +// timelineAndNotifyStatus processes the given new status and inserts it into +// the HOME 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 a notif 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 fmt.Errorf("timelineAndNotifyStatus: error populating status with id %s: %w", status.ID, err)  	} -	if status.Mentions == nil { -		// there are mentions but they're not fully populated on the status yet so do this -		mentions, err := p.state.DB.GetMentions(ctx, status.MentionIDs) -		if err != nil { -			return fmt.Errorf("notifyStatus: error getting mentions for status %s from the db: %s", 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 fmt.Errorf("timelineAndNotifyStatus: error getting local followers for account id %s: %w", status.AccountID, err) +	} -		status.Mentions = mentions +	// 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. +		})  	} -	// now we have mentions as full gtsmodel.Mention structs on the status we can continue -	for _, m := range status.Mentions { -		// make sure this is a local account, otherwise we don't need to create a notification for it -		if m.TargetAccount == nil { -			a, err := p.state.DB.GetAccountByID(ctx, m.TargetAccountID) -			if err != nil { -				// we don't have the account or there's been an error -				return fmt.Errorf("notifyStatus: error getting account with id %s from the db: %s", m.TargetAccountID, err) -			} -			m.TargetAccount = a -		} -		if m.TargetAccount.Domain != "" { -			// not a local account so skip it -			continue -		} +	// 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 fmt.Errorf("timelineAndNotifyStatus: 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 fmt.Errorf("timelineAndNotifyStatus: error notifying status mentions for status %s: %w", status.ID, err) +	} -		// make sure a notif doesn't already exist for this mention -		if err := p.state.DB.GetWhere(ctx, []db.Where{ -			{Key: "notification_type", Value: gtsmodel.NotificationMention}, -			{Key: "target_account_id", Value: m.TargetAccountID}, -			{Key: "origin_account_id", Value: m.OriginAccountID}, -			{Key: "status_id", Value: m.StatusID}, -		}, >smodel.Notification{}); err == nil { -			// notification exists already so just continue +	return nil +} + +func (p *Processor) timelineAndNotifyStatusForFollowers(ctx context.Context, status *gtsmodel.Status, follows []*gtsmodel.Follow) error { +	var ( +		errs  = make(gtserror.MultiError, 0, 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 -		} else if err != db.ErrNoEntries { -			// there's a real error in the db -			return fmt.Errorf("notifyStatus: error checking existence of notification for mention with id %s : %s", m.ID, err)  		} -		// if we've reached this point we know the mention is for a local account, and the notification doesn't exist, so create it -		notif := >smodel.Notification{ -			ID:               id.NewULID(), -			NotificationType: gtsmodel.NotificationMention, -			TargetAccountID:  m.TargetAccountID, -			TargetAccount:    m.TargetAccount, -			OriginAccountID:  status.AccountID, -			OriginAccount:    status.Account, -			StatusID:         status.ID, -			Status:           status, +		// Add status to home timeline for this +		// follower, and stream it if applicable. +		if timelined, err := p.timelineStatusForAccount(ctx, follow.Account, status); err != nil { +			errs.Append(fmt.Errorf("timelineAndNotifyStatusForFollowers: error timelining status: %w", err)) +			continue +		} else if !timelined { +			// Status wasn't added to home tomeline, +			// so we shouldn't notify it either. +			continue  		} -		if err := p.state.DB.PutNotification(ctx, notif); err != nil { -			return fmt.Errorf("notifyStatus: error putting notification in database: %s", err) +		if n := follow.Notify; n == nil || !*n { +			// This follower doesn't have notifications +			// set for this account's new posts, so bail. +			continue  		} -		// now stream the notification to the user -		apiNotif, err := p.tc.NotificationToAPINotification(ctx, notif) -		if err != nil { -			return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err) +		if boost || reply { +			// Don't notify for boosts or replies. +			continue  		} -		if err := p.stream.Notify(apiNotif, m.TargetAccount); err != nil { -			return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) +		// 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.Append(fmt.Errorf("timelineAndNotifyStatusForFollowers: error notifying account %s about new status: %w", follow.AccountID, err))  		}  	} -	return nil +	return errs.Combine()  } -func (p *Processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error { -	// make sure we have the target account pinned on the follow request -	if followRequest.TargetAccount == nil { -		a, err := p.state.DB.GetAccountByID(ctx, followRequest.TargetAccountID) -		if err != nil { -			return err -		} -		followRequest.TargetAccount = a +// timelineStatusForAccount puts the given status in the HOME timeline +// of the account with given accountID, if it's HomeTimelineable. +// +// If the status was inserted into the home timeline of the given account, +// true will be returned + it will also be streamed via websockets to the user. +func (p *Processor) timelineStatusForAccount(ctx context.Context, account *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { +	// Make sure the status is timelineable. +	if timelineable, err := p.filter.StatusHomeTimelineable(ctx, account, status); err != nil { +		err = fmt.Errorf("timelineStatusForAccount: 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  	} -	targetAccount := followRequest.TargetAccount -	// return if this isn't a local account -	if targetAccount.Domain != "" { -		// this isn't a local account so we've got nothing to do here -		return nil +	// Insert status in the home timeline of account. +	if inserted, err := p.statusTimelines.IngestOne(ctx, account.ID, status); err != nil { +		err = fmt.Errorf("timelineStatusForAccount: error ingesting status %s: %w", status.ID, err) +		return false, err +	} else if !inserted { +		// Nothing more to do. +		return false, nil  	} -	notif := >smodel.Notification{ -		ID:               id.NewULID(), -		NotificationType: gtsmodel.NotificationFollowRequest, -		TargetAccountID:  followRequest.TargetAccountID, -		OriginAccountID:  followRequest.AccountID, +	// The status was inserted so stream it to the user. +	apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, account) +	if err != nil { +		err = fmt.Errorf("timelineStatusForAccount: error converting status %s to frontend representation: %w", status.ID, err) +		return true, err  	} -	if err := p.state.DB.PutNotification(ctx, notif); err != nil { -		return fmt.Errorf("notifyFollowRequest: error putting notification in database: %s", err) +	if err := p.stream.Update(apiStatus, account, stream.TimelineHome); err != nil { +		err = fmt.Errorf("timelineStatusForAccount: error streaming update for status %s: %w", status.ID, err) +		return true, err  	} -	// now stream the notification to the user -	apiNotif, err := p.tc.NotificationToAPINotification(ctx, notif) -	if err != nil { -		return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err) -	} +	return true, nil +} -	if err := p.stream.Notify(apiNotif, targetAccount); err != nil { -		return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) +func (p *Processor) notifyStatusMentions(ctx context.Context, status *gtsmodel.Status) error { +	errs := make(gtserror.MultiError, 0, 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) +		}  	} -	return nil +	return errs.Combine() +} + +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 { -	// return if this isn't a local account -	if targetAccount.Domain != "" { -		return nil +	// 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 fmt.Errorf("notifyFollow: 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 fmt.Errorf("notifyFollow: db error removing previous follow request notification %s: %w", prevNotif.ID, err) +		}  	} -	// first remove the follow request notification -	if err := p.state.DB.DeleteWhere(ctx, []db.Where{ -		{Key: "notification_type", Value: gtsmodel.NotificationFollowRequest}, -		{Key: "target_account_id", Value: follow.TargetAccountID}, -		{Key: "origin_account_id", Value: follow.AccountID}, -	}, >smodel.Notification{}); err != nil { -		return fmt.Errorf("notifyFollow: error removing old follow request notification from database: %s", err) -	} +	// Now notify the follow itself. +	return p.notify( +		ctx, +		gtsmodel.NotificationFollow, +		targetAccount.ID, +		follow.AccountID, +		"", +	) +} -	// now create the new follow notification -	notif := >smodel.Notification{ -		ID:               id.NewULID(), -		NotificationType: gtsmodel.NotificationFollow, -		TargetAccountID:  follow.TargetAccountID, -		TargetAccount:    follow.TargetAccount, -		OriginAccountID:  follow.AccountID, -		OriginAccount:    follow.Account, -	} -	if err := p.state.DB.PutNotification(ctx, notif); err != nil { -		return fmt.Errorf("notifyFollow: error putting notification in database: %s", err) +func (p *Processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) error { +	if fave.TargetAccountID == fave.AccountID { +		// Self-fave, nothing to do. +		return nil  	} -	// now stream the notification to the user -	apiNotif, err := p.tc.NotificationToAPINotification(ctx, notif) -	if err != nil { -		return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err) +	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 err := p.stream.Notify(apiNotif, targetAccount); err != nil { -		return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) +	if status.BoostOfAccountID == status.AccountID { +		// Self-boost, nothing to do. +		return nil  	} -	return nil +	return p.notify( +		ctx, +		gtsmodel.NotificationReblog, +		status.BoostOfAccountID, +		status.AccountID, +		status.ID, +	)  } -func (p *Processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) error { -	// ignore self-faves -	if fave.TargetAccountID == fave.AccountID { -		return nil +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 fmt.Errorf("notify: error getting target account %s: %w", targetAccountID, err)  	} -	if fave.TargetAccount == nil { -		a, err := p.state.DB.GetAccountByID(ctx, fave.TargetAccountID) -		if err != nil { -			return err -		} -		fave.TargetAccount = a +	if !targetAccount.IsLocal() { +		// Nothing to do. +		return nil  	} -	targetAccount := fave.TargetAccount -	// just return if target isn't a local account -	if targetAccount.Domain != "" { +	// 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 fmt.Errorf("notify: 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: gtsmodel.NotificationFave, -		TargetAccountID:  fave.TargetAccountID, -		TargetAccount:    fave.TargetAccount, -		OriginAccountID:  fave.AccountID, -		OriginAccount:    fave.Account, -		StatusID:         fave.StatusID, -		Status:           fave.Status, +		NotificationType: notificationType, +		TargetAccountID:  targetAccountID, +		OriginAccountID:  originAccountID, +		StatusID:         statusID,  	}  	if err := p.state.DB.PutNotification(ctx, notif); err != nil { -		return fmt.Errorf("notifyFave: error putting notification in database: %s", err) +		return fmt.Errorf("notify: error putting notification in database: %w", err)  	} -	// now stream the notification to the user +	// Stream notification to the user.  	apiNotif, err := p.tc.NotificationToAPINotification(ctx, notif)  	if err != nil { -		return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err) +		return fmt.Errorf("notify: error converting notification to api representation: %w", err)  	}  	if err := p.stream.Notify(apiNotif, targetAccount); err != nil { -		return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) +		return fmt.Errorf("notify: error streaming notification to account: %w", err)  	}  	return nil  } -func (p *Processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) error { -	if status.BoostOfID == "" { -		// not a boost, nothing to do -		return nil -	} - -	if status.BoostOf == nil { -		boostedStatus, err := p.state.DB.GetStatusByID(ctx, status.BoostOfID) -		if err != nil { -			return fmt.Errorf("notifyAnnounce: error getting status with id %s: %s", status.BoostOfID, err) +// 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 { +	// 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 { +				return err +			} +		} +	} else { +		// todo: p.state.DB.UnattachAttachmentsForStatus +		for _, a := range statusToDelete.AttachmentIDs { +			if _, err := p.media.Unattach(ctx, statusToDelete.Account, a); err != nil { +				return err +			}  		} -		status.BoostOf = boostedStatus  	} -	if status.BoostOfAccount == nil { -		boostedAcct, err := p.state.DB.GetAccountByID(ctx, status.BoostOfAccountID) -		if err != nil { -			return fmt.Errorf("notifyAnnounce: error getting account with id %s: %s", status.BoostOfAccountID, 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 { +			return err  		} -		status.BoostOf.Account = boostedAcct -		status.BoostOfAccount = boostedAcct  	} -	if status.BoostOfAccount.Domain != "" { -		// remote account, nothing to do -		return nil +	// delete all notification entries generated by this status +	if err := p.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil { +		return err  	} -	if status.BoostOfAccountID == status.AccountID { -		// it's a self boost, nothing to do -		return nil +	// delete all bookmarks that point to this status +	if err := p.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil { +		return err  	} -	// make sure a notif doesn't already exist for this announce -	err := p.state.DB.GetWhere(ctx, []db.Where{ -		{Key: "notification_type", Value: gtsmodel.NotificationReblog}, -		{Key: "target_account_id", Value: status.BoostOfAccountID}, -		{Key: "origin_account_id", Value: status.AccountID}, -		{Key: "status_id", Value: status.ID}, -	}, >smodel.Notification{}) -	if err == nil { -		// notification exists already so just bail -		return nil +	// delete all faves of this status +	if err := p.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil { +		return err  	} -	// now create the new reblog notification -	notif := >smodel.Notification{ -		ID:               id.NewULID(), -		NotificationType: gtsmodel.NotificationReblog, -		TargetAccountID:  status.BoostOfAccountID, -		TargetAccount:    status.BoostOfAccount, -		OriginAccountID:  status.AccountID, -		OriginAccount:    status.Account, -		StatusID:         status.ID, -		Status:           status, +	// delete all boosts for this status + remove them from timelines +	if boosts, err := p.state.DB.GetStatusReblogs(ctx, statusToDelete); err == nil { +		for _, b := range boosts { +			if err := p.deleteStatusFromTimelines(ctx, b); err != nil { +				return err +			} +			if err := p.state.DB.DeleteStatusByID(ctx, b.ID); err != nil { +				return err +			} +		}  	} -	if err := p.state.DB.PutNotification(ctx, notif); err != nil { -		return fmt.Errorf("notifyAnnounce: error putting notification in database: %s", err) +	// delete this status from any and all timelines +	if err := p.deleteStatusFromTimelines(ctx, statusToDelete); err != nil { +		return err  	} -	// now stream the notification to the user -	apiNotif, err := p.tc.NotificationToAPINotification(ctx, notif) -	if err != nil { -		return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err) +	// delete the status itself +	if err := p.state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil { +		return err  	} -	if err := p.stream.Notify(apiNotif, status.BoostOfAccount); err != nil { -		return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) +	return nil +} + +// 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, status *gtsmodel.Status) error { +	if err := p.statusTimelines.WipeItemFromAllTimelines(ctx, status.ID); err != nil { +		return err  	} -	return nil +	return p.stream.Delete(status.ID)  } -func (p *Processor) notifyReport(ctx context.Context, report *gtsmodel.Report) error { +/* +	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 fmt.Errorf("notifyReport: error getting instance: %w", err) +		return fmt.Errorf("emailReport: error getting instance: %w", err)  	}  	toAddresses, err := p.state.DB.GetInstanceModeratorAddresses(ctx) @@ -323,20 +424,20 @@ func (p *Processor) notifyReport(ctx context.Context, report *gtsmodel.Report) e  			// No registered moderator addresses.  			return nil  		} -		return fmt.Errorf("notifyReport: error getting instance moderator addresses: %w", err) +		return fmt.Errorf("emailReport: 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 fmt.Errorf("notifyReport: error getting report account: %w", err) +			return fmt.Errorf("emailReport: error getting report account: %w", err)  		}  	}  	if report.TargetAccount == nil {  		report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID)  		if err != nil { -			return fmt.Errorf("notifyReport: error getting report target account: %w", err) +			return fmt.Errorf("emailReport: error getting report target account: %w", err)  		}  	} @@ -349,16 +450,16 @@ func (p *Processor) notifyReport(ctx context.Context, report *gtsmodel.Report) e  	}  	if err := p.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil { -		return fmt.Errorf("notifyReport: error emailing instance moderators: %w", err) +		return fmt.Errorf("emailReport: error emailing instance moderators: %w", err)  	}  	return nil  } -func (p *Processor) notifyReportClosed(ctx context.Context, report *gtsmodel.Report) error { +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 fmt.Errorf("notifyReportClosed: db error getting user: %w", err) +		return fmt.Errorf("emailReportClosed: db error getting user: %w", err)  	}  	if user.ConfirmedAt.IsZero() || !*user.Approved || *user.Disabled || user.Email == "" { @@ -372,20 +473,20 @@ func (p *Processor) notifyReportClosed(ctx context.Context, report *gtsmodel.Rep  	instance, err := p.state.DB.GetInstance(ctx, config.GetHost())  	if err != nil { -		return fmt.Errorf("notifyReportClosed: db error getting instance: %w", err) +		return fmt.Errorf("emailReportClosed: db error getting instance: %w", err)  	}  	if report.Account == nil {  		report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID)  		if err != nil { -			return fmt.Errorf("notifyReportClosed: error getting report account: %w", err) +			return fmt.Errorf("emailReportClosed: error getting report account: %w", err)  		}  	}  	if report.TargetAccount == nil {  		report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID)  		if err != nil { -			return fmt.Errorf("notifyReportClosed: error getting report target account: %w", err) +			return fmt.Errorf("emailReportClosed: error getting report target account: %w", err)  		}  	} @@ -400,156 +501,3 @@ func (p *Processor) notifyReportClosed(ctx context.Context, report *gtsmodel.Rep  	return p.emailSender.SendReportClosedEmail(user.Email, reportClosedData)  } - -// timelineStatus processes the given new status and inserts it into -// the HOME timelines of accounts that follow the status author. -func (p *Processor) timelineStatus(ctx context.Context, status *gtsmodel.Status) error { -	if status.Account == nil { -		// ensure status fully populated (including account) -		if err := p.state.DB.PopulateStatus(ctx, status); err != nil { -			return fmt.Errorf("timelineStatus: 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 fmt.Errorf("timelineStatus: error getting 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, -		}) -	} - -	var errs gtserror.MultiError - -	for _, follow := range follows { -		// Timeline the status for each local following account. -		if err := p.timelineStatusForAccount(ctx, follow.Account, status); err != nil { -			errs.Append(err) -		} -	} - -	if len(errs) != 0 { -		return fmt.Errorf("timelineStatus: one or more errors timelining statuses: %w", errs.Combine()) -	} - -	return nil -} - -// timelineStatusForAccount puts the given status in the HOME timeline -// of the account with given accountID, if it's hometimelineable. -// -// If the status was inserted into the home timeline of the given account, -// it will also be streamed via websockets to the user. -func (p *Processor) timelineStatusForAccount(ctx context.Context, account *gtsmodel.Account, status *gtsmodel.Status) error { -	// make sure the status is timelineable -	if timelineable, err := p.filter.StatusHomeTimelineable(ctx, account, status); err != nil { -		return fmt.Errorf("timelineStatusForAccount: error getting timelineability for status for timeline with id %s: %w", account.ID, err) -	} else if !timelineable { -		return nil -	} - -	// stick the status in the timeline for the account -	if inserted, err := p.statusTimelines.IngestOne(ctx, account.ID, status); err != nil { -		return fmt.Errorf("timelineStatusForAccount: error ingesting status %s: %w", status.ID, err) -	} else if !inserted { -		return nil -	} - -	// the status was inserted so stream it to the user -	apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, account) -	if err != nil { -		return fmt.Errorf("timelineStatusForAccount: error converting status %s to frontend representation: %w", status.ID, err) -	} - -	if err := p.stream.Update(apiStatus, account, stream.TimelineHome); err != nil { -		return fmt.Errorf("timelineStatusForAccount: error streaming update for status %s: %w", status.ID, err) -	} - -	return nil -} - -// 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, status *gtsmodel.Status) error { -	if err := p.statusTimelines.WipeItemFromAllTimelines(ctx, status.ID); err != nil { -		return err -	} - -	return p.stream.Delete(status.ID) -} - -// 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 { -	// 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 { -		for _, a := range statusToDelete.AttachmentIDs { -			if err := p.media.Delete(ctx, a); err != nil { -				return err -			} -		} -	} else { -		for _, a := range statusToDelete.AttachmentIDs { -			if _, err := p.media.Unattach(ctx, statusToDelete.Account, a); err != nil { -				return err -			} -		} -	} - -	// delete all mention entries generated by this status -	for _, id := range statusToDelete.MentionIDs { -		if err := p.state.DB.DeleteMentionByID(ctx, id); err != nil { -			return err -		} -	} - -	// delete all notification entries generated by this status -	if err := p.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil { -		return err -	} - -	// delete all bookmarks that point to this status -	if err := p.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil { -		return err -	} - -	// delete all faves of this status -	if err := p.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil { -		return err -	} - -	// delete all boosts for this status + remove them from timelines -	if boosts, err := p.state.DB.GetStatusReblogs(ctx, statusToDelete); err == nil { -		for _, b := range boosts { -			if err := p.deleteStatusFromTimelines(ctx, b); err != nil { -				return err -			} -			if err := p.state.DB.DeleteStatusByID(ctx, b.ID); err != nil { -				return err -			} -		} -	} - -	// delete this status from any and all timelines -	if err := p.deleteStatusFromTimelines(ctx, statusToDelete); err != nil { -		return err -	} - -	// delete the status itself -	if err := p.state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil { -		return err -	} - -	return nil -} diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index 32a970114..55e85a526 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -164,11 +164,7 @@ func (p *Processor) processCreateStatusFromFederator(ctx context.Context, federa  		status.Account = a  	} -	if err := p.timelineStatus(ctx, status); err != nil { -		return err -	} - -	if err := p.notifyStatus(ctx, status); err != nil { +	if err := p.timelineAndNotifyStatus(ctx, status); err != nil {  		return err  	} @@ -327,7 +323,7 @@ func (p *Processor) processCreateAnnounceFromFederator(ctx context.Context, fede  		return fmt.Errorf("error adding dereferenced announce to the db: %s", err)  	} -	if err := p.timelineStatus(ctx, incomingAnnounce); err != nil { +	if err := p.timelineAndNotifyStatus(ctx, incomingAnnounce); err != nil {  		return err  	} @@ -367,7 +363,7 @@ func (p *Processor) processCreateFlagFromFederator(ctx context.Context, federato  	// TODO: handle additional side effects of flag creation:  	// - notify admins by dm / notification -	return p.notifyReport(ctx, incomingReport) +	return p.emailReport(ctx, incomingReport)  }  // processUpdateAccountFromFederator handles Activity Update and Object Profile diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go index 5c77ca730..7c66c6e65 100644 --- a/internal/processing/processor_test.go +++ b/internal/processing/processor_test.go @@ -53,6 +53,7 @@ type ProcessingStandardTestSuite struct {  	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 @@ -70,6 +71,7 @@ func (suite *ProcessingStandardTestSuite) SetupSuite() {  	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() | 
