mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 17:42:24 -05:00 
			
		
		
		
	[bugfix] Lock when checking/creating notifs to avoid race (#2890)
* [bugfix] Lock when checking/creating notifs to avoid race * test notif spam
This commit is contained in:
		
					parent
					
						
							
								725a21b027
							
						
					
				
			
			
				commit
				
					
						ebec95a522
					
				
			
		
					 15 changed files with 290 additions and 133 deletions
				
			
		|  | @ -113,7 +113,7 @@ func (p *Processor) MoveSelf( | ||||||
| 	// in quick succession, so get a lock on | 	// in quick succession, so get a lock on | ||||||
| 	// this account. | 	// this account. | ||||||
| 	lockKey := originAcct.URI | 	lockKey := originAcct.URI | ||||||
| 	unlock := p.state.AccountLocks.Lock(lockKey) | 	unlock := p.state.ProcessingLocks.Lock(lockKey) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	// Ensure we have a valid, up-to-date representation of the target account. | 	// Ensure we have a valid, up-to-date representation of the target account. | ||||||
|  |  | ||||||
|  | @ -49,7 +49,7 @@ func (p *Processor) AccountApprove( | ||||||
| 	// Get a lock on the account URI, | 	// Get a lock on the account URI, | ||||||
| 	// to ensure it's not also being | 	// to ensure it's not also being | ||||||
| 	// rejected at the same time! | 	// rejected at the same time! | ||||||
| 	unlock := p.state.AccountLocks.Lock(user.Account.URI) | 	unlock := p.state.ProcessingLocks.Lock(user.Account.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	if !*user.Approved { | 	if !*user.Approved { | ||||||
|  |  | ||||||
|  | @ -52,7 +52,7 @@ func (p *Processor) AccountReject( | ||||||
| 	// Get a lock on the account URI, | 	// Get a lock on the account URI, | ||||||
| 	// since we're going to be deleting | 	// since we're going to be deleting | ||||||
| 	// it and its associated user. | 	// it and its associated user. | ||||||
| 	unlock := p.state.AccountLocks.Lock(user.Account.URI) | 	unlock := p.state.ProcessingLocks.Lock(user.Account.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	// Can't reject an account with a | 	// Can't reject an account with a | ||||||
|  |  | ||||||
|  | @ -83,7 +83,7 @@ func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.A | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Get a lock on this account. | 	// Get a lock on this account. | ||||||
| 	unlock := p.state.AccountLocks.Lock(requestingAccount.URI) | 	unlock := p.state.ProcessingLocks.Lock(requestingAccount.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	if !targetStatus.PinnedAt.IsZero() { | 	if !targetStatus.PinnedAt.IsZero() { | ||||||
|  | @ -148,7 +148,7 @@ func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.A | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Get a lock on this account. | 	// Get a lock on this account. | ||||||
| 	unlock := p.state.AccountLocks.Lock(requestingAccount.URI) | 	unlock := p.state.ProcessingLocks.Lock(requestingAccount.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	if targetStatus.PinnedAt.IsZero() { | 	if targetStatus.PinnedAt.IsZero() { | ||||||
|  |  | ||||||
|  | @ -41,7 +41,7 @@ import ( | ||||||
| type clientAPI struct { | type clientAPI struct { | ||||||
| 	state     *state.State | 	state     *state.State | ||||||
| 	converter *typeutils.Converter | 	converter *typeutils.Converter | ||||||
| 	surface   *surface | 	surface   *Surface | ||||||
| 	federate  *federate | 	federate  *federate | ||||||
| 	account   *account.Processor | 	account   *account.Processor | ||||||
| 	utils     *utils | 	utils     *utils | ||||||
|  |  | ||||||
|  | @ -41,7 +41,7 @@ import ( | ||||||
| // from the federation/ActivityPub API. | // from the federation/ActivityPub API. | ||||||
| type fediAPI struct { | type fediAPI struct { | ||||||
| 	state    *state.State | 	state    *state.State | ||||||
| 	surface  *surface | 	surface  *Surface | ||||||
| 	federate *federate | 	federate *federate | ||||||
| 	account  *account.Processor | 	account  *account.Processor | ||||||
| 	utils    *utils | 	utils    *utils | ||||||
|  |  | ||||||
|  | @ -25,16 +25,16 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // surface wraps functions for 'surfacing' the result | // Surface wraps functions for 'surfacing' the result | ||||||
| // of processing a message, eg: | // of processing a message, eg: | ||||||
| //   - timelining a status | //   - timelining a status | ||||||
| //   - removing a status from timelines | //   - removing a status from timelines | ||||||
| //   - sending a notification to a user | //   - sending a notification to a user | ||||||
| //   - sending an email | //   - sending an email | ||||||
| type surface struct { | type Surface struct { | ||||||
| 	state       *state.State | 	State       *state.State | ||||||
| 	converter   *typeutils.Converter | 	Converter   *typeutils.Converter | ||||||
| 	stream      *stream.Processor | 	Stream      *stream.Processor | ||||||
| 	filter      *visibility.Filter | 	Filter      *visibility.Filter | ||||||
| 	emailSender email.Sender | 	EmailSender email.Sender | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -33,8 +33,8 @@ import ( | ||||||
| 
 | 
 | ||||||
| // emailUserReportClosed emails the user who created the | // emailUserReportClosed emails the user who created the | ||||||
| // given report, to inform them the report has been closed. | // given report, to inform them the report has been closed. | ||||||
| func (s *surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Report) error { | func (s *Surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Report) error { | ||||||
| 	user, err := s.state.DB.GetUserByAccountID(ctx, report.Account.ID) | 	user, err := s.State.DB.GetUserByAccountID(ctx, report.Account.ID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("db error getting user: %w", err) | 		return gtserror.Newf("db error getting user: %w", err) | ||||||
| 	} | 	} | ||||||
|  | @ -51,12 +51,12 @@ func (s *surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Re | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	instance, err := s.state.DB.GetInstance(ctx, config.GetHost()) | 	instance, err := s.State.DB.GetInstance(ctx, config.GetHost()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("db error getting instance: %w", err) | 		return gtserror.Newf("db error getting instance: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := s.state.DB.PopulateReport(ctx, report); err != nil { | 	if err := s.State.DB.PopulateReport(ctx, report); err != nil { | ||||||
| 		return gtserror.Newf("error populating report: %w", err) | 		return gtserror.Newf("error populating report: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -69,12 +69,12 @@ func (s *surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Re | ||||||
| 		ActionTakenComment:   report.ActionTaken, | 		ActionTakenComment:   report.ActionTaken, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return s.emailSender.SendReportClosedEmail(user.Email, reportClosedData) | 	return s.EmailSender.SendReportClosedEmail(user.Email, reportClosedData) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // emailUserPleaseConfirm emails the given user | // emailUserPleaseConfirm emails the given user | ||||||
| // to ask them to confirm their email address. | // to ask them to confirm their email address. | ||||||
| func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.User) error { | func (s *Surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.User) error { | ||||||
| 	if user.UnconfirmedEmail == "" || | 	if user.UnconfirmedEmail == "" || | ||||||
| 		user.UnconfirmedEmail == user.Email { | 		user.UnconfirmedEmail == user.Email { | ||||||
| 		// User has already confirmed this | 		// User has already confirmed this | ||||||
|  | @ -82,7 +82,7 @@ func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	instance, err := s.state.DB.GetInstance(ctx, config.GetHost()) | 	instance, err := s.State.DB.GetInstance(ctx, config.GetHost()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("db error getting instance: %w", err) | 		return gtserror.Newf("db error getting instance: %w", err) | ||||||
| 	} | 	} | ||||||
|  | @ -97,7 +97,7 @@ func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
| 	// Assemble email contents and send the email. | 	// Assemble email contents and send the email. | ||||||
| 	if err := s.emailSender.SendConfirmEmail( | 	if err := s.EmailSender.SendConfirmEmail( | ||||||
| 		user.UnconfirmedEmail, | 		user.UnconfirmedEmail, | ||||||
| 		email.ConfirmData{ | 		email.ConfirmData{ | ||||||
| 			Username:     user.Account.Username, | 			Username:     user.Account.Username, | ||||||
|  | @ -116,7 +116,7 @@ func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use | ||||||
| 	user.ConfirmationSentAt = now | 	user.ConfirmationSentAt = now | ||||||
| 	user.LastEmailedAt = now | 	user.LastEmailedAt = now | ||||||
| 
 | 
 | ||||||
| 	if err := s.state.DB.UpdateUser( | 	if err := s.State.DB.UpdateUser( | ||||||
| 		ctx, | 		ctx, | ||||||
| 		user, | 		user, | ||||||
| 		"confirmation_token", | 		"confirmation_token", | ||||||
|  | @ -131,7 +131,7 @@ func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use | ||||||
| 
 | 
 | ||||||
| // emailUserSignupApproved emails the given user | // emailUserSignupApproved emails the given user | ||||||
| // to inform them their sign-up has been approved. | // to inform them their sign-up has been approved. | ||||||
| func (s *surface) emailUserSignupApproved(ctx context.Context, user *gtsmodel.User) error { | func (s *Surface) emailUserSignupApproved(ctx context.Context, user *gtsmodel.User) error { | ||||||
| 	// User may have been approved without | 	// User may have been approved without | ||||||
| 	// their email address being confirmed | 	// their email address being confirmed | ||||||
| 	// yet. Just send to whatever we have. | 	// yet. Just send to whatever we have. | ||||||
|  | @ -140,13 +140,13 @@ func (s *surface) emailUserSignupApproved(ctx context.Context, user *gtsmodel.Us | ||||||
| 		emailAddr = user.UnconfirmedEmail | 		emailAddr = user.UnconfirmedEmail | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	instance, err := s.state.DB.GetInstance(ctx, config.GetHost()) | 	instance, err := s.State.DB.GetInstance(ctx, config.GetHost()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("db error getting instance: %w", err) | 		return gtserror.Newf("db error getting instance: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Assemble email contents and send the email. | 	// Assemble email contents and send the email. | ||||||
| 	if err := s.emailSender.SendSignupApprovedEmail( | 	if err := s.EmailSender.SendSignupApprovedEmail( | ||||||
| 		emailAddr, | 		emailAddr, | ||||||
| 		email.SignupApprovedData{ | 		email.SignupApprovedData{ | ||||||
| 			Username:     user.Account.Username, | 			Username:     user.Account.Username, | ||||||
|  | @ -162,7 +162,7 @@ func (s *surface) emailUserSignupApproved(ctx context.Context, user *gtsmodel.Us | ||||||
| 	now := time.Now() | 	now := time.Now() | ||||||
| 	user.LastEmailedAt = now | 	user.LastEmailedAt = now | ||||||
| 
 | 
 | ||||||
| 	if err := s.state.DB.UpdateUser( | 	if err := s.State.DB.UpdateUser( | ||||||
| 		ctx, | 		ctx, | ||||||
| 		user, | 		user, | ||||||
| 		"last_emailed_at", | 		"last_emailed_at", | ||||||
|  | @ -175,14 +175,14 @@ func (s *surface) emailUserSignupApproved(ctx context.Context, user *gtsmodel.Us | ||||||
| 
 | 
 | ||||||
| // emailUserSignupApproved emails the given user | // emailUserSignupApproved emails the given user | ||||||
| // to inform them their sign-up has been approved. | // to inform them their sign-up has been approved. | ||||||
| func (s *surface) emailUserSignupRejected(ctx context.Context, deniedUser *gtsmodel.DeniedUser) error { | func (s *Surface) emailUserSignupRejected(ctx context.Context, deniedUser *gtsmodel.DeniedUser) error { | ||||||
| 	instance, err := s.state.DB.GetInstance(ctx, config.GetHost()) | 	instance, err := s.State.DB.GetInstance(ctx, config.GetHost()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("db error getting instance: %w", err) | 		return gtserror.Newf("db error getting instance: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Assemble email contents and send the email. | 	// Assemble email contents and send the email. | ||||||
| 	return s.emailSender.SendSignupRejectedEmail( | 	return s.EmailSender.SendSignupRejectedEmail( | ||||||
| 		deniedUser.Email, | 		deniedUser.Email, | ||||||
| 		email.SignupRejectedData{ | 		email.SignupRejectedData{ | ||||||
| 			Message:      deniedUser.Message, | 			Message:      deniedUser.Message, | ||||||
|  | @ -194,13 +194,13 @@ func (s *surface) emailUserSignupRejected(ctx context.Context, deniedUser *gtsmo | ||||||
| 
 | 
 | ||||||
| // emailAdminReportOpened emails all active moderators/admins | // emailAdminReportOpened emails all active moderators/admins | ||||||
| // of this instance that a new report has been created. | // of this instance that a new report has been created. | ||||||
| func (s *surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.Report) error { | func (s *Surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.Report) error { | ||||||
| 	instance, err := s.state.DB.GetInstance(ctx, config.GetHost()) | 	instance, err := s.State.DB.GetInstance(ctx, config.GetHost()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("error getting instance: %w", err) | 		return gtserror.Newf("error getting instance: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	toAddresses, err := s.state.DB.GetInstanceModeratorAddresses(ctx) | 	toAddresses, err := s.State.DB.GetInstanceModeratorAddresses(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if errors.Is(err, db.ErrNoEntries) { | 		if errors.Is(err, db.ErrNoEntries) { | ||||||
| 			// No registered moderator addresses. | 			// No registered moderator addresses. | ||||||
|  | @ -209,7 +209,7 @@ func (s *surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.R | ||||||
| 		return gtserror.Newf("error getting instance moderator addresses: %w", err) | 		return gtserror.Newf("error getting instance moderator addresses: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := s.state.DB.PopulateReport(ctx, report); err != nil { | 	if err := s.State.DB.PopulateReport(ctx, report); err != nil { | ||||||
| 		return gtserror.Newf("error populating report: %w", err) | 		return gtserror.Newf("error populating report: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -221,7 +221,7 @@ func (s *surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.R | ||||||
| 		ReportTargetDomain: report.TargetAccount.Domain, | 		ReportTargetDomain: report.TargetAccount.Domain, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := s.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil { | 	if err := s.EmailSender.SendNewReportEmail(toAddresses, reportData); err != nil { | ||||||
| 		return gtserror.Newf("error emailing instance moderators: %w", err) | 		return gtserror.Newf("error emailing instance moderators: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -230,13 +230,13 @@ func (s *surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.R | ||||||
| 
 | 
 | ||||||
| // emailAdminNewSignup emails all active moderators/admins of this | // emailAdminNewSignup emails all active moderators/admins of this | ||||||
| // instance that a new account sign-up has been submitted to the instance. | // instance that a new account sign-up has been submitted to the instance. | ||||||
| func (s *surface) emailAdminNewSignup(ctx context.Context, newUser *gtsmodel.User) error { | func (s *Surface) emailAdminNewSignup(ctx context.Context, newUser *gtsmodel.User) error { | ||||||
| 	instance, err := s.state.DB.GetInstance(ctx, config.GetHost()) | 	instance, err := s.State.DB.GetInstance(ctx, config.GetHost()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("error getting instance: %w", err) | 		return gtserror.Newf("error getting instance: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	toAddresses, err := s.state.DB.GetInstanceModeratorAddresses(ctx) | 	toAddresses, err := s.State.DB.GetInstanceModeratorAddresses(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if errors.Is(err, db.ErrNoEntries) { | 		if errors.Is(err, db.ErrNoEntries) { | ||||||
| 			// No registered moderator addresses. | 			// No registered moderator addresses. | ||||||
|  | @ -246,7 +246,7 @@ func (s *surface) emailAdminNewSignup(ctx context.Context, newUser *gtsmodel.Use | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Ensure user populated. | 	// Ensure user populated. | ||||||
| 	if err := s.state.DB.PopulateUser(ctx, newUser); err != nil { | 	if err := s.State.DB.PopulateUser(ctx, newUser); err != nil { | ||||||
| 		return gtserror.Newf("error populating user: %w", err) | 		return gtserror.Newf("error populating user: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -259,7 +259,7 @@ func (s *surface) emailAdminNewSignup(ctx context.Context, newUser *gtsmodel.Use | ||||||
| 		SignupURL:      instance.URI + "/settings/admin/accounts/" + newUser.AccountID, | 		SignupURL:      instance.URI + "/settings/admin/accounts/" + newUser.AccountID, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := s.emailSender.SendNewSignupEmail(toAddresses, newSignupData); err != nil { | 	if err := s.EmailSender.SendNewSignupEmail(toAddresses, newSignupData); err != nil { | ||||||
| 		return gtserror.Newf("error emailing instance moderators: %w", err) | 		return gtserror.Newf("error emailing instance moderators: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,18 +20,20 @@ package workers | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" | 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // notifyMentions iterates through mentions on the | // notifyMentions iterates through mentions on the | ||||||
| // given status, and notifies each mentioned account | // given status, and notifies each mentioned account | ||||||
| // that they have a new mention. | // that they have a new mention. | ||||||
| func (s *surface) notifyMentions( | func (s *Surface) notifyMentions( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| ) error { | ) error { | ||||||
|  | @ -43,7 +45,7 @@ func (s *surface) notifyMentions( | ||||||
| 		mention.Status = status | 		mention.Status = status | ||||||
| 
 | 
 | ||||||
| 		// Beforehand, ensure the passed mention is fully populated. | 		// Beforehand, ensure the passed mention is fully populated. | ||||||
| 		if err := s.state.DB.PopulateMention(ctx, mention); err != nil { | 		if err := s.State.DB.PopulateMention(ctx, mention); err != nil { | ||||||
| 			errs.Appendf("error populating mention %s: %w", mention.ID, err) | 			errs.Appendf("error populating mention %s: %w", mention.ID, err) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  | @ -56,7 +58,7 @@ func (s *surface) notifyMentions( | ||||||
| 
 | 
 | ||||||
| 		// Ensure thread not muted | 		// Ensure thread not muted | ||||||
| 		// by mentioned account. | 		// by mentioned account. | ||||||
| 		muted, err := s.state.DB.IsThreadMutedByAccount( | 		muted, err := s.State.DB.IsThreadMutedByAccount( | ||||||
| 			ctx, | 			ctx, | ||||||
| 			status.ThreadID, | 			status.ThreadID, | ||||||
| 			mention.TargetAccountID, | 			mention.TargetAccountID, | ||||||
|  | @ -75,7 +77,7 @@ func (s *surface) notifyMentions( | ||||||
| 
 | 
 | ||||||
| 		// notify mentioned | 		// notify mentioned | ||||||
| 		// by status author. | 		// by status author. | ||||||
| 		if err := s.notify(ctx, | 		if err := s.Notify(ctx, | ||||||
| 			gtsmodel.NotificationMention, | 			gtsmodel.NotificationMention, | ||||||
| 			mention.TargetAccount, | 			mention.TargetAccount, | ||||||
| 			mention.OriginAccount, | 			mention.OriginAccount, | ||||||
|  | @ -91,12 +93,12 @@ func (s *surface) notifyMentions( | ||||||
| 
 | 
 | ||||||
| // notifyFollowRequest notifies the target of the given | // notifyFollowRequest notifies the target of the given | ||||||
| // follow request that they have a new follow request. | // follow request that they have a new follow request. | ||||||
| func (s *surface) notifyFollowRequest( | func (s *Surface) notifyFollowRequest( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	followReq *gtsmodel.FollowRequest, | 	followReq *gtsmodel.FollowRequest, | ||||||
| ) error { | ) error { | ||||||
| 	// Beforehand, ensure the passed follow request is fully populated. | 	// Beforehand, ensure the passed follow request is fully populated. | ||||||
| 	if err := s.state.DB.PopulateFollowRequest(ctx, followReq); err != nil { | 	if err := s.State.DB.PopulateFollowRequest(ctx, followReq); err != nil { | ||||||
| 		return gtserror.Newf("error populating follow request %s: %w", followReq.ID, err) | 		return gtserror.Newf("error populating follow request %s: %w", followReq.ID, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -107,7 +109,7 @@ func (s *surface) notifyFollowRequest( | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Now notify the follow request itself. | 	// Now notify the follow request itself. | ||||||
| 	if err := s.notify(ctx, | 	if err := s.Notify(ctx, | ||||||
| 		gtsmodel.NotificationFollowRequest, | 		gtsmodel.NotificationFollowRequest, | ||||||
| 		followReq.TargetAccount, | 		followReq.TargetAccount, | ||||||
| 		followReq.Account, | 		followReq.Account, | ||||||
|  | @ -123,12 +125,12 @@ func (s *surface) notifyFollowRequest( | ||||||
| // they have a new follow. It will also remove any previous | // they have a new follow. It will also remove any previous | ||||||
| // notification of a follow request, essentially replacing | // notification of a follow request, essentially replacing | ||||||
| // that notification. | // that notification. | ||||||
| func (s *surface) notifyFollow( | func (s *Surface) notifyFollow( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	follow *gtsmodel.Follow, | 	follow *gtsmodel.Follow, | ||||||
| ) error { | ) error { | ||||||
| 	// Beforehand, ensure the passed follow is fully populated. | 	// Beforehand, ensure the passed follow is fully populated. | ||||||
| 	if err := s.state.DB.PopulateFollow(ctx, follow); err != nil { | 	if err := s.State.DB.PopulateFollow(ctx, follow); err != nil { | ||||||
| 		return gtserror.Newf("error populating follow %s: %w", follow.ID, err) | 		return gtserror.Newf("error populating follow %s: %w", follow.ID, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -139,7 +141,7 @@ func (s *surface) notifyFollow( | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Check if previous follow req notif exists. | 	// Check if previous follow req notif exists. | ||||||
| 	prevNotif, err := s.state.DB.GetNotification( | 	prevNotif, err := s.State.DB.GetNotification( | ||||||
| 		gtscontext.SetBarebones(ctx), | 		gtscontext.SetBarebones(ctx), | ||||||
| 		gtsmodel.NotificationFollowRequest, | 		gtsmodel.NotificationFollowRequest, | ||||||
| 		follow.TargetAccountID, | 		follow.TargetAccountID, | ||||||
|  | @ -152,14 +154,14 @@ func (s *surface) notifyFollow( | ||||||
| 
 | 
 | ||||||
| 	if prevNotif != nil { | 	if prevNotif != nil { | ||||||
| 		// Previous follow request notif existed, delete it before creating new. | 		// Previous follow request notif existed, delete it before creating new. | ||||||
| 		if err := s.state.DB.DeleteNotificationByID(ctx, prevNotif.ID); // nocollapse | 		if err := s.State.DB.DeleteNotificationByID(ctx, prevNotif.ID); // nocollapse | ||||||
| 		err != nil && !errors.Is(err, db.ErrNoEntries) { | 		err != nil && !errors.Is(err, db.ErrNoEntries) { | ||||||
| 			return gtserror.Newf("error deleting notification %s: %w", prevNotif.ID, err) | 			return gtserror.Newf("error deleting notification %s: %w", prevNotif.ID, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Now notify the follow itself. | 	// Now notify the follow itself. | ||||||
| 	if err := s.notify(ctx, | 	if err := s.Notify(ctx, | ||||||
| 		gtsmodel.NotificationFollow, | 		gtsmodel.NotificationFollow, | ||||||
| 		follow.TargetAccount, | 		follow.TargetAccount, | ||||||
| 		follow.Account, | 		follow.Account, | ||||||
|  | @ -173,7 +175,7 @@ func (s *surface) notifyFollow( | ||||||
| 
 | 
 | ||||||
| // notifyFave notifies the target of the given | // notifyFave notifies the target of the given | ||||||
| // fave that their status has been liked/faved. | // fave that their status has been liked/faved. | ||||||
| func (s *surface) notifyFave( | func (s *Surface) notifyFave( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	fave *gtsmodel.StatusFave, | 	fave *gtsmodel.StatusFave, | ||||||
| ) error { | ) error { | ||||||
|  | @ -183,7 +185,7 @@ func (s *surface) notifyFave( | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Beforehand, ensure the passed status fave is fully populated. | 	// Beforehand, ensure the passed status fave is fully populated. | ||||||
| 	if err := s.state.DB.PopulateStatusFave(ctx, fave); err != nil { | 	if err := s.State.DB.PopulateStatusFave(ctx, fave); err != nil { | ||||||
| 		return gtserror.Newf("error populating fave %s: %w", fave.ID, err) | 		return gtserror.Newf("error populating fave %s: %w", fave.ID, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -195,7 +197,7 @@ func (s *surface) notifyFave( | ||||||
| 
 | 
 | ||||||
| 	// Ensure favee hasn't | 	// Ensure favee hasn't | ||||||
| 	// muted the thread. | 	// muted the thread. | ||||||
| 	muted, err := s.state.DB.IsThreadMutedByAccount( | 	muted, err := s.State.DB.IsThreadMutedByAccount( | ||||||
| 		ctx, | 		ctx, | ||||||
| 		fave.Status.ThreadID, | 		fave.Status.ThreadID, | ||||||
| 		fave.TargetAccountID, | 		fave.TargetAccountID, | ||||||
|  | @ -212,7 +214,7 @@ func (s *surface) notifyFave( | ||||||
| 
 | 
 | ||||||
| 	// notify status author | 	// notify status author | ||||||
| 	// of fave by account. | 	// of fave by account. | ||||||
| 	if err := s.notify(ctx, | 	if err := s.Notify(ctx, | ||||||
| 		gtsmodel.NotificationFave, | 		gtsmodel.NotificationFave, | ||||||
| 		fave.TargetAccount, | 		fave.TargetAccount, | ||||||
| 		fave.Account, | 		fave.Account, | ||||||
|  | @ -226,7 +228,7 @@ func (s *surface) notifyFave( | ||||||
| 
 | 
 | ||||||
| // notifyAnnounce notifies the status boost target | // notifyAnnounce notifies the status boost target | ||||||
| // account that their status has been boosted. | // account that their status has been boosted. | ||||||
| func (s *surface) notifyAnnounce( | func (s *Surface) notifyAnnounce( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| ) error { | ) error { | ||||||
|  | @ -241,7 +243,7 @@ func (s *surface) notifyAnnounce( | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Beforehand, ensure the passed status is fully populated. | 	// Beforehand, ensure the passed status is fully populated. | ||||||
| 	if err := s.state.DB.PopulateStatus(ctx, status); err != nil { | 	if err := s.State.DB.PopulateStatus(ctx, status); err != nil { | ||||||
| 		return gtserror.Newf("error populating status %s: %w", status.ID, err) | 		return gtserror.Newf("error populating status %s: %w", status.ID, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -253,7 +255,7 @@ func (s *surface) notifyAnnounce( | ||||||
| 
 | 
 | ||||||
| 	// Ensure boostee hasn't | 	// Ensure boostee hasn't | ||||||
| 	// muted the thread. | 	// muted the thread. | ||||||
| 	muted, err := s.state.DB.IsThreadMutedByAccount( | 	muted, err := s.State.DB.IsThreadMutedByAccount( | ||||||
| 		ctx, | 		ctx, | ||||||
| 		status.BoostOf.ThreadID, | 		status.BoostOf.ThreadID, | ||||||
| 		status.BoostOfAccountID, | 		status.BoostOfAccountID, | ||||||
|  | @ -271,7 +273,7 @@ func (s *surface) notifyAnnounce( | ||||||
| 
 | 
 | ||||||
| 	// notify status author | 	// notify status author | ||||||
| 	// of boost by account. | 	// of boost by account. | ||||||
| 	if err := s.notify(ctx, | 	if err := s.Notify(ctx, | ||||||
| 		gtsmodel.NotificationReblog, | 		gtsmodel.NotificationReblog, | ||||||
| 		status.BoostOfAccount, | 		status.BoostOfAccount, | ||||||
| 		status.Account, | 		status.Account, | ||||||
|  | @ -283,14 +285,14 @@ func (s *surface) notifyAnnounce( | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) error { | func (s *Surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) error { | ||||||
| 	// Beforehand, ensure the passed status is fully populated. | 	// Beforehand, ensure the passed status is fully populated. | ||||||
| 	if err := s.state.DB.PopulateStatus(ctx, status); err != nil { | 	if err := s.State.DB.PopulateStatus(ctx, status); err != nil { | ||||||
| 		return gtserror.Newf("error populating status %s: %w", status.ID, err) | 		return gtserror.Newf("error populating status %s: %w", status.ID, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Fetch all votes in the attached status poll. | 	// Fetch all votes in the attached status poll. | ||||||
| 	votes, err := s.state.DB.GetPollVotes(ctx, status.PollID) | 	votes, err := s.State.DB.GetPollVotes(ctx, status.PollID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("error getting poll %s votes: %w", status.PollID, err) | 		return gtserror.Newf("error getting poll %s votes: %w", status.PollID, err) | ||||||
| 	} | 	} | ||||||
|  | @ -300,7 +302,7 @@ func (s *surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) | ||||||
| 	if status.Account.IsLocal() { | 	if status.Account.IsLocal() { | ||||||
| 		// Send a notification to the status | 		// Send a notification to the status | ||||||
| 		// author that their poll has closed! | 		// author that their poll has closed! | ||||||
| 		if err := s.notify(ctx, | 		if err := s.Notify(ctx, | ||||||
| 			gtsmodel.NotificationPoll, | 			gtsmodel.NotificationPoll, | ||||||
| 			status.Account, | 			status.Account, | ||||||
| 			status.Account, | 			status.Account, | ||||||
|  | @ -319,7 +321,7 @@ func (s *surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) | ||||||
| 
 | 
 | ||||||
| 		// notify voter that | 		// notify voter that | ||||||
| 		// poll has been closed. | 		// poll has been closed. | ||||||
| 		if err := s.notify(ctx, | 		if err := s.Notify(ctx, | ||||||
| 			gtsmodel.NotificationPoll, | 			gtsmodel.NotificationPoll, | ||||||
| 			vote.Account, | 			vote.Account, | ||||||
| 			status.Account, | 			status.Account, | ||||||
|  | @ -333,8 +335,8 @@ func (s *surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) | ||||||
| 	return errs.Combine() | 	return errs.Combine() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) error { | func (s *Surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) error { | ||||||
| 	modAccounts, err := s.state.DB.GetInstanceModerators(ctx) | 	modAccounts, err := s.State.DB.GetInstanceModerators(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if errors.Is(err, db.ErrNoEntries) { | 		if errors.Is(err, db.ErrNoEntries) { | ||||||
| 			// No registered | 			// No registered | ||||||
|  | @ -347,18 +349,18 @@ func (s *surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Ensure user + account populated. | 	// Ensure user + account populated. | ||||||
| 	if err := s.state.DB.PopulateUser(ctx, newUser); err != nil { | 	if err := s.State.DB.PopulateUser(ctx, newUser); err != nil { | ||||||
| 		return gtserror.Newf("db error populating new user: %w", err) | 		return gtserror.Newf("db error populating new user: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := s.state.DB.PopulateAccount(ctx, newUser.Account); err != nil { | 	if err := s.State.DB.PopulateAccount(ctx, newUser.Account); err != nil { | ||||||
| 		return gtserror.Newf("db error populating new user's account: %w", err) | 		return gtserror.Newf("db error populating new user's account: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Notify each moderator. | 	// Notify each moderator. | ||||||
| 	var errs gtserror.MultiError | 	var errs gtserror.MultiError | ||||||
| 	for _, mod := range modAccounts { | 	for _, mod := range modAccounts { | ||||||
| 		if err := s.notify(ctx, | 		if err := s.Notify(ctx, | ||||||
| 			gtsmodel.NotificationSignup, | 			gtsmodel.NotificationSignup, | ||||||
| 			mod, | 			mod, | ||||||
| 			newUser.Account, | 			newUser.Account, | ||||||
|  | @ -372,7 +374,24 @@ func (s *surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro | ||||||
| 	return errs.Combine() | 	return errs.Combine() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // notify creates, inserts, and streams a new | func getNotifyLockURI( | ||||||
|  | 	notificationType gtsmodel.NotificationType, | ||||||
|  | 	targetAccount *gtsmodel.Account, | ||||||
|  | 	originAccount *gtsmodel.Account, | ||||||
|  | 	statusID string, | ||||||
|  | ) string { | ||||||
|  | 	builder := strings.Builder{} | ||||||
|  | 	builder.WriteString("notification:?") | ||||||
|  | 	builder.WriteString("type=" + string(notificationType)) | ||||||
|  | 	builder.WriteString("&target=" + targetAccount.URI) | ||||||
|  | 	builder.WriteString("&origin=" + originAccount.URI) | ||||||
|  | 	if statusID != "" { | ||||||
|  | 		builder.WriteString("&statusID=" + statusID) | ||||||
|  | 	} | ||||||
|  | 	return builder.String() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Notify creates, inserts, and streams a new | ||||||
| // notification to the target account if it | // notification to the target account if it | ||||||
| // doesn't yet exist with the given parameters. | // doesn't yet exist with the given parameters. | ||||||
| // | // | ||||||
|  | @ -383,7 +402,7 @@ func (s *surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro | ||||||
| // | // | ||||||
| // targetAccount and originAccount must be | // targetAccount and originAccount must be | ||||||
| // set, but statusID can be an empty string. | // set, but statusID can be an empty string. | ||||||
| func (s *surface) notify( | func (s *Surface) Notify( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	notificationType gtsmodel.NotificationType, | 	notificationType gtsmodel.NotificationType, | ||||||
| 	targetAccount *gtsmodel.Account, | 	targetAccount *gtsmodel.Account, | ||||||
|  | @ -395,9 +414,24 @@ func (s *surface) notify( | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// We're doing state-y stuff so get a | ||||||
|  | 	// lock on this combo of notif params. | ||||||
|  | 	lockURI := getNotifyLockURI( | ||||||
|  | 		notificationType, | ||||||
|  | 		targetAccount, | ||||||
|  | 		originAccount, | ||||||
|  | 		statusID, | ||||||
|  | 	) | ||||||
|  | 	unlock := s.State.ProcessingLocks.Lock(lockURI) | ||||||
|  | 
 | ||||||
|  | 	// Wrap the unlock so we | ||||||
|  | 	// can do granular unlocking. | ||||||
|  | 	unlock = util.DoOnce(unlock) | ||||||
|  | 	defer unlock() | ||||||
|  | 
 | ||||||
| 	// Make sure a notification doesn't | 	// Make sure a notification doesn't | ||||||
| 	// already exist with these params. | 	// already exist with these params. | ||||||
| 	if _, err := s.state.DB.GetNotification( | 	if _, err := s.State.DB.GetNotification( | ||||||
| 		gtscontext.SetBarebones(ctx), | 		gtscontext.SetBarebones(ctx), | ||||||
| 		notificationType, | 		notificationType, | ||||||
| 		targetAccount.ID, | 		targetAccount.ID, | ||||||
|  | @ -424,16 +458,20 @@ func (s *surface) notify( | ||||||
| 		StatusID:         statusID, | 		StatusID:         statusID, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := s.state.DB.PutNotification(ctx, notif); err != nil { | 	if err := s.State.DB.PutNotification(ctx, notif); err != nil { | ||||||
| 		return gtserror.Newf("error putting notification in database: %w", err) | 		return gtserror.Newf("error putting notification in database: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Unlock already, we're done | ||||||
|  | 	// with the state-y stuff. | ||||||
|  | 	unlock() | ||||||
|  | 
 | ||||||
| 	// Stream notification to the user. | 	// Stream notification to the user. | ||||||
| 	apiNotif, err := s.converter.NotificationToAPINotification(ctx, notif) | 	apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("error converting notification to api representation: %w", err) | 		return gtserror.Newf("error converting notification to api representation: %w", err) | ||||||
| 	} | 	} | ||||||
| 	s.stream.Notify(ctx, targetAccount, apiNotif) | 	s.Stream.Notify(ctx, targetAccount, apiNotif) | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										115
									
								
								internal/processing/workers/surfacenotify_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								internal/processing/workers/surfacenotify_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | ||||||
|  | // 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" | ||||||
|  | 	"sync" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/processing/workers" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type SurfaceNotifyTestSuite struct { | ||||||
|  | 	WorkersTestSuite | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() { | ||||||
|  | 	testStructs := suite.SetupTestStructs() | ||||||
|  | 	defer suite.TearDownTestStructs(testStructs) | ||||||
|  | 
 | ||||||
|  | 	surface := &workers.Surface{ | ||||||
|  | 		State:       testStructs.State, | ||||||
|  | 		Converter:   testStructs.TypeConverter, | ||||||
|  | 		Stream:      testStructs.Processor.Stream(), | ||||||
|  | 		Filter:      visibility.NewFilter(testStructs.State), | ||||||
|  | 		EmailSender: testStructs.EmailSender, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var ( | ||||||
|  | 		ctx              = context.Background() | ||||||
|  | 		notificationType = gtsmodel.NotificationFollow | ||||||
|  | 		targetAccount    = suite.testAccounts["local_account_1"] | ||||||
|  | 		originAccount    = suite.testAccounts["local_account_2"] | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	// Set up a bunch of goroutines to surface | ||||||
|  | 	// a notification at exactly the same time. | ||||||
|  | 	wg := sync.WaitGroup{} | ||||||
|  | 	wg.Add(20) | ||||||
|  | 	startAt := time.Now().Add(2 * time.Second) | ||||||
|  | 
 | ||||||
|  | 	for i := 0; i < 20; i++ { | ||||||
|  | 		go func() { | ||||||
|  | 			defer wg.Done() | ||||||
|  | 
 | ||||||
|  | 			// Wait for it.... | ||||||
|  | 			untilTick := time.Until(startAt) | ||||||
|  | 			<-time.Tick(untilTick) | ||||||
|  | 
 | ||||||
|  | 			// ...Go! | ||||||
|  | 			if err := surface.Notify(ctx, | ||||||
|  | 				notificationType, | ||||||
|  | 				targetAccount, | ||||||
|  | 				originAccount, | ||||||
|  | 				"", | ||||||
|  | 			); err != nil { | ||||||
|  | 				suite.FailNow(err.Error()) | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Wait for all notif creation | ||||||
|  | 	// attempts to complete. | ||||||
|  | 	wg.Wait() | ||||||
|  | 
 | ||||||
|  | 	// Get all notifs for this account. | ||||||
|  | 	notifs, err := testStructs.State.DB.GetAccountNotifications( | ||||||
|  | 		gtscontext.SetBarebones(ctx), | ||||||
|  | 		targetAccount.ID, | ||||||
|  | 		"", "", "", 0, nil, | ||||||
|  | 	) | ||||||
|  | 	if err != nil { | ||||||
|  | 		suite.FailNow(err.Error()) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var gotOne bool | ||||||
|  | 	for _, notif := range notifs { | ||||||
|  | 		if notif.NotificationType == notificationType && | ||||||
|  | 			notif.TargetAccountID == targetAccount.ID && | ||||||
|  | 			notif.OriginAccountID == originAccount.ID { | ||||||
|  | 			// This is the notif... | ||||||
|  | 			if gotOne { | ||||||
|  | 				// We already had | ||||||
|  | 				// the notif, d'oh! | ||||||
|  | 				suite.FailNow("already had notif") | ||||||
|  | 			} else { | ||||||
|  | 				gotOne = true | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestSurfaceNotifyTestSuite(t *testing.T) { | ||||||
|  | 	suite.Run(t, new(SurfaceNotifyTestSuite)) | ||||||
|  | } | ||||||
|  | @ -36,14 +36,14 @@ import ( | ||||||
| // It will also handle notifications for any mentions attached to | // It will also handle notifications for any mentions attached to | ||||||
| // the account, and notifications for any local accounts that want | // the account, and notifications for any local accounts that want | ||||||
| // to know when this account posts. | // to know when this account posts. | ||||||
| func (s *surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error { | func (s *Surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error { | ||||||
| 	// Ensure status fully populated; including account, mentions, etc. | 	// Ensure status fully populated; including account, mentions, etc. | ||||||
| 	if err := s.state.DB.PopulateStatus(ctx, status); err != nil { | 	if err := s.State.DB.PopulateStatus(ctx, status); err != nil { | ||||||
| 		return gtserror.Newf("error populating status with id %s: %w", status.ID, err) | 		return gtserror.Newf("error populating status with id %s: %w", status.ID, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Get all local followers of the account that posted the status. | 	// Get all local followers of the account that posted the status. | ||||||
| 	follows, err := s.state.DB.GetAccountLocalFollowers(ctx, status.AccountID) | 	follows, err := s.State.DB.GetAccountLocalFollowers(ctx, status.AccountID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("error getting local followers of account %s: %w", status.AccountID, err) | 		return gtserror.Newf("error getting local followers of account %s: %w", status.AccountID, err) | ||||||
| 	} | 	} | ||||||
|  | @ -79,7 +79,7 @@ func (s *surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel. | ||||||
| // adding the status to list timelines + home timelines of each | // adding the status to list timelines + home timelines of each | ||||||
| // follower, as appropriate, and notifying each follower of the | // follower, as appropriate, and notifying each follower of the | ||||||
| // new status, if the status is eligible for notification. | // new status, if the status is eligible for notification. | ||||||
| func (s *surface) timelineAndNotifyStatusForFollowers( | func (s *Surface) timelineAndNotifyStatusForFollowers( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	follows []*gtsmodel.Follow, | 	follows []*gtsmodel.Follow, | ||||||
|  | @ -98,7 +98,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers( | ||||||
| 		// If it's not timelineable, we can just stop early, since lists | 		// If it's not timelineable, we can just stop early, since lists | ||||||
| 		// are prettymuch subsets of the home timeline, so if it shouldn't | 		// are prettymuch subsets of the home timeline, so if it shouldn't | ||||||
| 		// appear there, it shouldn't appear in lists either. | 		// appear there, it shouldn't appear in lists either. | ||||||
| 		timelineable, err := s.filter.StatusHomeTimelineable( | 		timelineable, err := s.Filter.StatusHomeTimelineable( | ||||||
| 			ctx, follow.Account, status, | 			ctx, follow.Account, status, | ||||||
| 		) | 		) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -124,7 +124,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers( | ||||||
| 		// of this follow, if applicable. | 		// of this follow, if applicable. | ||||||
| 		homeTimelined, err := s.timelineStatus( | 		homeTimelined, err := s.timelineStatus( | ||||||
| 			ctx, | 			ctx, | ||||||
| 			s.state.Timelines.Home.IngestOne, | 			s.State.Timelines.Home.IngestOne, | ||||||
| 			follow.AccountID, // home timelines are keyed by account ID | 			follow.AccountID, // home timelines are keyed by account ID | ||||||
| 			follow.Account, | 			follow.Account, | ||||||
| 			status, | 			status, | ||||||
|  | @ -160,7 +160,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers( | ||||||
| 		//   - This is a top-level post (not a reply or boost). | 		//   - This is a top-level post (not a reply or boost). | ||||||
| 		// | 		// | ||||||
| 		// That means we can officially notify this one. | 		// That means we can officially notify this one. | ||||||
| 		if err := s.notify(ctx, | 		if err := s.Notify(ctx, | ||||||
| 			gtsmodel.NotificationStatus, | 			gtsmodel.NotificationStatus, | ||||||
| 			follow.Account, | 			follow.Account, | ||||||
| 			status.Account, | 			status.Account, | ||||||
|  | @ -175,7 +175,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers( | ||||||
| 
 | 
 | ||||||
| // listTimelineStatusForFollow puts the given status | // listTimelineStatusForFollow puts the given status | ||||||
| // in any eligible lists owned by the given follower. | // in any eligible lists owned by the given follower. | ||||||
| func (s *surface) listTimelineStatusForFollow( | func (s *Surface) listTimelineStatusForFollow( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	follow *gtsmodel.Follow, | 	follow *gtsmodel.Follow, | ||||||
|  | @ -189,7 +189,7 @@ func (s *surface) listTimelineStatusForFollow( | ||||||
| 	// inclusion in the list. | 	// inclusion in the list. | ||||||
| 
 | 
 | ||||||
| 	// Get every list entry that targets this follow's ID. | 	// Get every list entry that targets this follow's ID. | ||||||
| 	listEntries, err := s.state.DB.GetListEntriesForFollowID( | 	listEntries, err := s.State.DB.GetListEntriesForFollowID( | ||||||
| 		// We only need the list IDs. | 		// We only need the list IDs. | ||||||
| 		gtscontext.SetBarebones(ctx), | 		gtscontext.SetBarebones(ctx), | ||||||
| 		follow.ID, | 		follow.ID, | ||||||
|  | @ -217,7 +217,7 @@ func (s *surface) listTimelineStatusForFollow( | ||||||
| 		// list that this list entry belongs to. | 		// list that this list entry belongs to. | ||||||
| 		if _, err := s.timelineStatus( | 		if _, err := s.timelineStatus( | ||||||
| 			ctx, | 			ctx, | ||||||
| 			s.state.Timelines.List.IngestOne, | 			s.State.Timelines.List.IngestOne, | ||||||
| 			listEntry.ListID, // list timelines are keyed by list ID | 			listEntry.ListID, // list timelines are keyed by list ID | ||||||
| 			follow.Account, | 			follow.Account, | ||||||
| 			status, | 			status, | ||||||
|  | @ -232,7 +232,7 @@ func (s *surface) listTimelineStatusForFollow( | ||||||
| // listEligible checks if the given status is eligible | // listEligible checks if the given status is eligible | ||||||
| // for inclusion in the list that that the given listEntry | // for inclusion in the list that that the given listEntry | ||||||
| // belongs to, based on the replies policy of the list. | // belongs to, based on the replies policy of the list. | ||||||
| func (s *surface) listEligible( | func (s *Surface) listEligible( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	listEntry *gtsmodel.ListEntry, | 	listEntry *gtsmodel.ListEntry, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
|  | @ -253,7 +253,7 @@ func (s *surface) listEligible( | ||||||
| 	// We need to fetch the list that this | 	// We need to fetch the list that this | ||||||
| 	// entry belongs to, in order to check | 	// entry belongs to, in order to check | ||||||
| 	// the list's replies policy. | 	// the list's replies policy. | ||||||
| 	list, err := s.state.DB.GetListByID( | 	list, err := s.State.DB.GetListByID( | ||||||
| 		ctx, listEntry.ListID, | 		ctx, listEntry.ListID, | ||||||
| 	) | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -273,7 +273,7 @@ func (s *surface) listEligible( | ||||||
| 		// | 		// | ||||||
| 		// Check if replied-to account is | 		// Check if replied-to account is | ||||||
| 		// also included in this list. | 		// also included in this list. | ||||||
| 		includes, err := s.state.DB.ListIncludesAccount( | 		includes, err := s.State.DB.ListIncludesAccount( | ||||||
| 			ctx, | 			ctx, | ||||||
| 			list.ID, | 			list.ID, | ||||||
| 			status.InReplyToAccountID, | 			status.InReplyToAccountID, | ||||||
|  | @ -295,7 +295,7 @@ func (s *surface) listEligible( | ||||||
| 		// | 		// | ||||||
| 		// Check if replied-to account is | 		// Check if replied-to account is | ||||||
| 		// followed by list owner account. | 		// followed by list owner account. | ||||||
| 		follows, err := s.state.DB.IsFollowing( | 		follows, err := s.State.DB.IsFollowing( | ||||||
| 			ctx, | 			ctx, | ||||||
| 			list.AccountID, | 			list.AccountID, | ||||||
| 			status.InReplyToAccountID, | 			status.InReplyToAccountID, | ||||||
|  | @ -325,7 +325,7 @@ func (s *surface) listEligible( | ||||||
| // | // | ||||||
| // If the status was inserted into the timeline, true will be returned | // If the status was inserted into the timeline, true will be returned | ||||||
| // + it will also be streamed to the user using the given streamType. | // + it will also be streamed to the user using the given streamType. | ||||||
| func (s *surface) timelineStatus( | func (s *Surface) timelineStatus( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	ingest func(context.Context, string, timeline.Timelineable) (bool, error), | 	ingest func(context.Context, string, timeline.Timelineable) (bool, error), | ||||||
| 	timelineID string, | 	timelineID string, | ||||||
|  | @ -343,26 +343,26 @@ func (s *surface) timelineStatus( | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// The status was inserted so stream it to the user. | 	// The status was inserted so stream it to the user. | ||||||
| 	apiStatus, err := s.converter.StatusToAPIStatus(ctx, status, account) | 	apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | 		err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | ||||||
| 		return true, err | 		return true, err | ||||||
| 	} | 	} | ||||||
| 	s.stream.Update(ctx, account, apiStatus, streamType) | 	s.Stream.Update(ctx, account, apiStatus, streamType) | ||||||
| 
 | 
 | ||||||
| 	return true, nil | 	return true, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // deleteStatusFromTimelines completely removes the given status from all timelines. | // deleteStatusFromTimelines completely removes the given status from all timelines. | ||||||
| // It will also stream deletion of the status to all open streams. | // It will also stream deletion of the status to all open streams. | ||||||
| func (s *surface) deleteStatusFromTimelines(ctx context.Context, statusID string) error { | func (s *Surface) deleteStatusFromTimelines(ctx context.Context, statusID string) error { | ||||||
| 	if err := s.state.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil { | 	if err := s.State.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if err := s.state.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil { | 	if err := s.State.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	s.stream.Delete(ctx, statusID) | 	s.Stream.Delete(ctx, statusID) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -370,15 +370,15 @@ func (s *surface) deleteStatusFromTimelines(ctx context.Context, statusID string | ||||||
| // unpreparing it from all timelines, forcing it to be prepared again (with updated | // 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 | // 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. | // both for the status itself, and for any boosts of the status. | ||||||
| func (s *surface) invalidateStatusFromTimelines(ctx context.Context, statusID string) { | func (s *Surface) invalidateStatusFromTimelines(ctx context.Context, statusID string) { | ||||||
| 	if err := s.state.Timelines.Home.UnprepareItemFromAllTimelines(ctx, statusID); err != nil { | 	if err := s.State.Timelines.Home.UnprepareItemFromAllTimelines(ctx, statusID); err != nil { | ||||||
| 		log. | 		log. | ||||||
| 			WithContext(ctx). | 			WithContext(ctx). | ||||||
| 			WithField("statusID", statusID). | 			WithField("statusID", statusID). | ||||||
| 			Errorf("error unpreparing status from home timelines: %v", err) | 			Errorf("error unpreparing status from home timelines: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := s.state.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil { | 	if err := s.State.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil { | ||||||
| 		log. | 		log. | ||||||
| 			WithContext(ctx). | 			WithContext(ctx). | ||||||
| 			WithField("statusID", statusID). | 			WithField("statusID", statusID). | ||||||
|  | @ -392,14 +392,14 @@ func (s *surface) invalidateStatusFromTimelines(ctx context.Context, statusID st | ||||||
| // Note that calling invalidateStatusFromTimelines takes care of the | // Note that calling invalidateStatusFromTimelines takes care of the | ||||||
| // state in general, we just need to do this for any streams that are | // state in general, we just need to do this for any streams that are | ||||||
| // open right now. | // open right now. | ||||||
| func (s *surface) timelineStatusUpdate(ctx context.Context, status *gtsmodel.Status) error { | func (s *Surface) timelineStatusUpdate(ctx context.Context, status *gtsmodel.Status) error { | ||||||
| 	// Ensure status fully populated; including account, mentions, etc. | 	// Ensure status fully populated; including account, mentions, etc. | ||||||
| 	if err := s.state.DB.PopulateStatus(ctx, status); err != nil { | 	if err := s.State.DB.PopulateStatus(ctx, status); err != nil { | ||||||
| 		return gtserror.Newf("error populating status with id %s: %w", status.ID, err) | 		return gtserror.Newf("error populating status with id %s: %w", status.ID, err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Get all local followers of the account that posted the status. | 	// Get all local followers of the account that posted the status. | ||||||
| 	follows, err := s.state.DB.GetAccountLocalFollowers(ctx, status.AccountID) | 	follows, err := s.State.DB.GetAccountLocalFollowers(ctx, status.AccountID) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("error getting local followers of account %s: %w", status.AccountID, err) | 		return gtserror.Newf("error getting local followers of account %s: %w", status.AccountID, err) | ||||||
| 	} | 	} | ||||||
|  | @ -427,7 +427,7 @@ func (s *surface) timelineStatusUpdate(ctx context.Context, status *gtsmodel.Sta | ||||||
| // slice of followers of the account that posted the given status, | // slice of followers of the account that posted the given status, | ||||||
| // pushing update messages into open list/home streams of each | // pushing update messages into open list/home streams of each | ||||||
| // follower. | // follower. | ||||||
| func (s *surface) timelineStatusUpdateForFollowers( | func (s *Surface) timelineStatusUpdateForFollowers( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	follows []*gtsmodel.Follow, | 	follows []*gtsmodel.Follow, | ||||||
|  | @ -444,7 +444,7 @@ func (s *surface) timelineStatusUpdateForFollowers( | ||||||
| 		// If it's not timelineable, we can just stop early, since lists | 		// If it's not timelineable, we can just stop early, since lists | ||||||
| 		// are prettymuch subsets of the home timeline, so if it shouldn't | 		// are prettymuch subsets of the home timeline, so if it shouldn't | ||||||
| 		// appear there, it shouldn't appear in lists either. | 		// appear there, it shouldn't appear in lists either. | ||||||
| 		timelineable, err := s.filter.StatusHomeTimelineable( | 		timelineable, err := s.Filter.StatusHomeTimelineable( | ||||||
| 			ctx, follow.Account, status, | 			ctx, follow.Account, status, | ||||||
| 		) | 		) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -485,7 +485,7 @@ func (s *surface) timelineStatusUpdateForFollowers( | ||||||
| 
 | 
 | ||||||
| // listTimelineStatusUpdateForFollow pushes edits of the given status | // listTimelineStatusUpdateForFollow pushes edits of the given status | ||||||
| // into any eligible lists streams opened by the given follower. | // into any eligible lists streams opened by the given follower. | ||||||
| func (s *surface) listTimelineStatusUpdateForFollow( | func (s *Surface) listTimelineStatusUpdateForFollow( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	follow *gtsmodel.Follow, | 	follow *gtsmodel.Follow, | ||||||
|  | @ -499,7 +499,7 @@ func (s *surface) listTimelineStatusUpdateForFollow( | ||||||
| 	// inclusion in the list. | 	// inclusion in the list. | ||||||
| 
 | 
 | ||||||
| 	// Get every list entry that targets this follow's ID. | 	// Get every list entry that targets this follow's ID. | ||||||
| 	listEntries, err := s.state.DB.GetListEntriesForFollowID( | 	listEntries, err := s.State.DB.GetListEntriesForFollowID( | ||||||
| 		// We only need the list IDs. | 		// We only need the list IDs. | ||||||
| 		gtscontext.SetBarebones(ctx), | 		gtscontext.SetBarebones(ctx), | ||||||
| 		follow.ID, | 		follow.ID, | ||||||
|  | @ -539,17 +539,17 @@ func (s *surface) listTimelineStatusUpdateForFollow( | ||||||
| 
 | 
 | ||||||
| // timelineStatusUpdate streams the edited status to the user using the | // timelineStatusUpdate streams the edited status to the user using the | ||||||
| // given streamType. | // given streamType. | ||||||
| func (s *surface) timelineStreamStatusUpdate( | func (s *Surface) timelineStreamStatusUpdate( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	streamType string, | 	streamType string, | ||||||
| ) error { | ) error { | ||||||
| 	apiStatus, err := s.converter.StatusToAPIStatus(ctx, status, account) | 	apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | 		err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	s.stream.StatusUpdate(ctx, account, apiStatus, streamType) | 	s.Stream.StatusUpdate(ctx, account, apiStatus, streamType) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -38,7 +38,7 @@ type utils struct { | ||||||
| 	state   *state.State | 	state   *state.State | ||||||
| 	media   *media.Processor | 	media   *media.Processor | ||||||
| 	account *account.Processor | 	account *account.Processor | ||||||
| 	surface *surface | 	surface *Surface | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // wipeStatus encapsulates common logic | // wipeStatus encapsulates common logic | ||||||
|  | @ -245,7 +245,7 @@ func (u *utils) incrementStatusesCount( | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| ) error { | ) error { | ||||||
| 	// Lock on this account since we're changing stats. | 	// Lock on this account since we're changing stats. | ||||||
| 	unlock := u.state.AccountLocks.Lock(account.URI) | 	unlock := u.state.ProcessingLocks.Lock(account.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	// Populate stats. | 	// Populate stats. | ||||||
|  | @ -276,7 +276,7 @@ func (u *utils) decrementStatusesCount( | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| ) error { | ) error { | ||||||
| 	// Lock on this account since we're changing stats. | 	// Lock on this account since we're changing stats. | ||||||
| 	unlock := u.state.AccountLocks.Lock(account.URI) | 	unlock := u.state.ProcessingLocks.Lock(account.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	// Populate stats. | 	// Populate stats. | ||||||
|  | @ -310,7 +310,7 @@ func (u *utils) incrementFollowersCount( | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| ) error { | ) error { | ||||||
| 	// Lock on this account since we're changing stats. | 	// Lock on this account since we're changing stats. | ||||||
| 	unlock := u.state.AccountLocks.Lock(account.URI) | 	unlock := u.state.ProcessingLocks.Lock(account.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	// Populate stats. | 	// Populate stats. | ||||||
|  | @ -339,7 +339,7 @@ func (u *utils) decrementFollowersCount( | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| ) error { | ) error { | ||||||
| 	// Lock on this account since we're changing stats. | 	// Lock on this account since we're changing stats. | ||||||
| 	unlock := u.state.AccountLocks.Lock(account.URI) | 	unlock := u.state.ProcessingLocks.Lock(account.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	// Populate stats. | 	// Populate stats. | ||||||
|  | @ -373,7 +373,7 @@ func (u *utils) incrementFollowingCount( | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| ) error { | ) error { | ||||||
| 	// Lock on this account since we're changing stats. | 	// Lock on this account since we're changing stats. | ||||||
| 	unlock := u.state.AccountLocks.Lock(account.URI) | 	unlock := u.state.ProcessingLocks.Lock(account.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	// Populate stats. | 	// Populate stats. | ||||||
|  | @ -402,7 +402,7 @@ func (u *utils) decrementFollowingCount( | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| ) error { | ) error { | ||||||
| 	// Lock on this account since we're changing stats. | 	// Lock on this account since we're changing stats. | ||||||
| 	unlock := u.state.AccountLocks.Lock(account.URI) | 	unlock := u.state.ProcessingLocks.Lock(account.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	// Populate stats. | 	// Populate stats. | ||||||
|  | @ -436,7 +436,7 @@ func (u *utils) incrementFollowRequestsCount( | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| ) error { | ) error { | ||||||
| 	// Lock on this account since we're changing stats. | 	// Lock on this account since we're changing stats. | ||||||
| 	unlock := u.state.AccountLocks.Lock(account.URI) | 	unlock := u.state.ProcessingLocks.Lock(account.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	// Populate stats. | 	// Populate stats. | ||||||
|  | @ -465,7 +465,7 @@ func (u *utils) decrementFollowRequestsCount( | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| ) error { | ) error { | ||||||
| 	// Lock on this account since we're changing stats. | 	// Lock on this account since we're changing stats. | ||||||
| 	unlock := u.state.AccountLocks.Lock(account.URI) | 	unlock := u.state.ProcessingLocks.Lock(account.URI) | ||||||
| 	defer unlock() | 	defer unlock() | ||||||
| 
 | 
 | ||||||
| 	// Populate stats. | 	// Populate stats. | ||||||
|  |  | ||||||
|  | @ -55,12 +55,12 @@ func New( | ||||||
| 
 | 
 | ||||||
| 	// Init surface logic | 	// Init surface logic | ||||||
| 	// wrapper struct. | 	// wrapper struct. | ||||||
| 	surface := &surface{ | 	surface := &Surface{ | ||||||
| 		state:       state, | 		State:       state, | ||||||
| 		converter:   converter, | 		Converter:   converter, | ||||||
| 		stream:      stream, | 		Stream:      stream, | ||||||
| 		filter:      filter, | 		Filter:      filter, | ||||||
| 		emailSender: emailSender, | 		EmailSender: emailSender, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Init shared util funcs. | 	// Init shared util funcs. | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/cleaner" | 	"github.com/superseriousbusiness/gotosocial/internal/cleaner" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/email" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility" | 	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | @ -68,6 +69,7 @@ type TestStructs struct { | ||||||
| 	Processor     *processing.Processor | 	Processor     *processing.Processor | ||||||
| 	HTTPClient    *testrig.MockHTTPClient | 	HTTPClient    *testrig.MockHTTPClient | ||||||
| 	TypeConverter *typeutils.Converter | 	TypeConverter *typeutils.Converter | ||||||
|  | 	EmailSender   email.Sender | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (suite *WorkersTestSuite) SetupSuite() { | func (suite *WorkersTestSuite) SetupSuite() { | ||||||
|  | @ -168,6 +170,7 @@ func (suite *WorkersTestSuite) SetupTestStructs() *TestStructs { | ||||||
| 		Processor:     processor, | 		Processor:     processor, | ||||||
| 		HTTPClient:    httpClient, | 		HTTPClient:    httpClient, | ||||||
| 		TypeConverter: typeconverter, | 		TypeConverter: typeconverter, | ||||||
|  | 		EmailSender:   emailSender, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -42,20 +42,21 @@ type State struct { | ||||||
| 	// DB provides access to the database. | 	// DB provides access to the database. | ||||||
| 	DB db.DB | 	DB db.DB | ||||||
| 
 | 
 | ||||||
| 	// FedLocks provides access to this state's | 	// FedLocks provides access to this state's mutex | ||||||
| 	// mutex map of per URI federation locks. | 	// map of per URI federation locks, intended for | ||||||
|  | 	// use in internal/federation functions. | ||||||
| 	// | 	// | ||||||
| 	// Used during account and status dereferencing, | 	// Used during account and status dereferencing, | ||||||
| 	// message processing in the FromFediAPI worker | 	// and by the go-fed/activity library. | ||||||
| 	// functions, and by the go-fed/activity library. |  | ||||||
| 	FedLocks mutexes.MutexMap | 	FedLocks mutexes.MutexMap | ||||||
| 
 | 
 | ||||||
| 	// AccountLocks provides access to this state's | 	// ProcessingLocks provides access to this state's | ||||||
| 	// mutex map of per URI locks, intended for use | 	// mutex map of per URI locks, intended for use | ||||||
|  | 	// in internal/processing functions, for example | ||||||
| 	// when updating accounts, migrating, approving | 	// when updating accounts, migrating, approving | ||||||
| 	// or rejecting an account, changing stats, | 	// or rejecting an account, changing stats or | ||||||
| 	// pinned statuses, etc. | 	// pinned statuses, creating notifs, etc. | ||||||
| 	AccountLocks mutexes.MutexMap | 	ProcessingLocks mutexes.MutexMap | ||||||
| 
 | 
 | ||||||
| 	// Storage provides access to the storage driver. | 	// Storage provides access to the storage driver. | ||||||
| 	Storage *storage.Driver | 	Storage *storage.Driver | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue