summaryrefslogtreecommitdiff
path: root/internal/processing
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-03-19 13:11:46 +0100
committerLibravatar GitHub <noreply@github.com>2023-03-19 13:11:46 +0100
commit7db81cde444f6bc95e79527af0997de1788d48c7 (patch)
treef6c077ec298a4f018d0870798bc46bd64ba70069 /internal/processing
parent[docs] Update docs on how to login (#1626) (diff)
downloadgotosocial-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.go16
-rw-r--r--internal/processing/fromclientapi.go31
-rw-r--r--internal/processing/fromcommon.go93
-rw-r--r--internal/processing/fromfederator.go13
-rw-r--r--internal/processing/processor.go6
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