diff options
author | 2023-03-19 13:11:46 +0100 | |
---|---|---|
committer | 2023-03-19 13:11:46 +0100 | |
commit | 7db81cde444f6bc95e79527af0997de1788d48c7 (patch) | |
tree | f6c077ec298a4f018d0870798bc46bd64ba70069 /internal/processing | |
parent | [docs] Update docs on how to login (#1626) (diff) | |
download | gotosocial-7db81cde444f6bc95e79527af0997de1788d48c7.tar.xz |
[feature] Email notifications for new / closed moderation reports (#1628)
* start fiddling about with email sending to allow multiple recipients
* do some fiddling
* notifs working
* notify on closed report
* finishing up
* envparsing
* use strings.ContainsAny
Diffstat (limited to 'internal/processing')
-rw-r--r-- | internal/processing/admin/report.go | 16 | ||||
-rw-r--r-- | internal/processing/fromclientapi.go | 31 | ||||
-rw-r--r-- | internal/processing/fromcommon.go | 93 | ||||
-rw-r--r-- | internal/processing/fromfederator.go | 13 | ||||
-rw-r--r-- | internal/processing/processor.go | 6 |
5 files changed, 147 insertions, 12 deletions
diff --git a/internal/processing/admin/report.go b/internal/processing/admin/report.go index 3a5435d9e..174bd3c24 100644 --- a/internal/processing/admin/report.go +++ b/internal/processing/admin/report.go @@ -23,10 +23,12 @@ import ( "strconv" "time" + "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/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -110,7 +112,10 @@ func (p *Processor) ReportGet(ctx context.Context, account *gtsmodel.Account, id return apimodelReport, nil } -// ReportResolve marks a report with the given id as resolved, and stores the provided actionTakenComment (if not null). +// ReportResolve marks a report with the given id as resolved, +// and stores the provided actionTakenComment (if not null). +// If the report creator is from this instance, an email will +// be sent to them to let them know that the report is resolved. func (p *Processor) ReportResolve(ctx context.Context, account *gtsmodel.Account, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) { report, err := p.state.DB.GetReportByID(ctx, id) if err != nil { @@ -138,6 +143,15 @@ func (p *Processor) ReportResolve(ctx context.Context, account *gtsmodel.Account return nil, gtserror.NewErrorInternalError(err) } + // Process side effects of closing the report. + p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ + APObjectType: ap.ActivityFlag, + APActivityType: ap.ActivityUpdate, + GTSModel: report, + OriginAccount: account, + TargetAccount: report.Account, + }) + apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, updatedReport, account) if err != nil { return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 391239abc..a4d4521ce 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -81,6 +81,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages case ap.ObjectProfile, ap.ActorPerson: // UPDATE ACCOUNT/PROFILE return p.processUpdateAccountFromClientAPI(ctx, clientMsg) + case ap.ActivityFlag: + // UPDATE A FLAG/REPORT (mark as resolved/closed) + return p.processUpdateReportFromClientAPI(ctx, clientMsg) } case ap.ActivityAccept: // ACCEPT @@ -240,6 +243,21 @@ func (p *Processor) processUpdateAccountFromClientAPI(ctx context.Context, clien return p.federateAccountUpdate(ctx, account, clientMsg.OriginAccount) } +func (p *Processor) processUpdateReportFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + report, ok := clientMsg.GTSModel.(*gtsmodel.Report) + if !ok { + return errors.New("report was not parseable as *gtsmodel.Report") + } + + if report.Account.IsRemote() { + // Report creator is a remote account, + // we shouldn't email or notify them. + return nil + } + + return p.notifyReportClosed(ctx, report) +} + func (p *Processor) processAcceptFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) if !ok { @@ -349,14 +367,17 @@ func (p *Processor) processReportAccountFromClientAPI(ctx context.Context, clien return errors.New("report was not parseable as *gtsmodel.Report") } - // TODO: in a separate PR, also email admin(s) + if *report.Forwarded { + if err := p.federateReport(ctx, report); err != nil { + return fmt.Errorf("processReportAccountFromClientAPI: error federating report: %w", err) + } + } - if !*report.Forwarded { - // nothing to do, don't federate the report - return nil + if err := p.notifyReport(ctx, report); err != nil { + return fmt.Errorf("processReportAccountFromClientAPI: error notifying report: %w", err) } - return p.federateReport(ctx, report) + return nil } // TODO: move all the below functions into federation.Federator diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go index ccdeca3c5..c29ada5ba 100644 --- a/internal/processing/fromcommon.go +++ b/internal/processing/fromcommon.go @@ -19,11 +19,14 @@ package processing import ( "context" + "errors" "fmt" "strings" "sync" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/stream" @@ -308,6 +311,96 @@ func (p *Processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) return nil } +func (p *Processor) notifyReport(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) + } + + toAddresses, err := p.state.DB.GetInstanceModeratorAddresses(ctx) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // No registered moderator addresses. + return nil + } + return fmt.Errorf("notifyReport: 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) + } + } + + 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) + } + } + + reportData := email.NewReportData{ + InstanceURL: instance.URI, + InstanceName: instance.Title, + ReportURL: instance.URI + "/settings/admin/reports/" + report.ID, + ReportDomain: report.Account.Domain, + ReportTargetDomain: report.TargetAccount.Domain, + } + + if err := p.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil { + return fmt.Errorf("notifyReport: error emailing instance moderators: %w", err) + } + + return nil +} + +func (p *Processor) notifyReportClosed(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) + } + + if user.ConfirmedAt.IsZero() || !*user.Approved || *user.Disabled || user.Email == "" { + // Only email users who: + // - are confirmed + // - are approved + // - are not disabled + // - have an email address + return nil + } + + instance, err := p.state.DB.GetInstance(ctx, config.GetHost()) + if err != nil { + return fmt.Errorf("notifyReportClosed: 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) + } + } + + 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) + } + } + + reportClosedData := email.ReportClosedData{ + Username: report.Account.Username, + InstanceURL: instance.URI, + InstanceName: instance.Title, + ReportTargetUsername: report.TargetAccount.Username, + ReportTargetDomain: report.TargetAccount.Domain, + ActionTakenComment: report.ActionTaken, + } + + return p.emailSender.SendReportClosedEmail(user.Email, reportClosedData) +} + // 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 { diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index 014c4a324..32a970114 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -359,10 +359,15 @@ func (p *Processor) processCreateBlockFromFederator(ctx context.Context, federat } func (p *Processor) processCreateFlagFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { - // TODO: handle side effects of flag creation: - // - send email to admins - // - notify admins - return nil + incomingReport, ok := federatorMsg.GTSModel.(*gtsmodel.Report) + if !ok { + return errors.New("flag was not parseable as *gtsmodel.Report") + } + + // TODO: handle additional side effects of flag creation: + // - notify admins by dm / notification + + return p.notifyReport(ctx, incomingReport) } // processUpdateAccountFromFederator handles Activity Update and Object Profile diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 98b417ba3..ad485b9ae 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -48,6 +48,7 @@ type Processor struct { statusTimelines timeline.Manager state *state.State filter visibility.Filter + emailSender email.Sender /* SUB-PROCESSORS @@ -119,8 +120,9 @@ func NewProcessor( StatusPrepareFunction(state.DB, tc), StatusSkipInsertFunction(), ), - state: state, - filter: filter, + state: state, + filter: filter, + emailSender: emailSender, } // sub processors |