mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[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
This commit is contained in:
@@ -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)
|
||||
|
@@ -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 {
|
||||
// nothing to do, don't federate the report
|
||||
return nil
|
||||
if *report.Forwarded {
|
||||
if err := p.federateReport(ctx, report); err != nil {
|
||||
return fmt.Errorf("processReportAccountFromClientAPI: error federating report: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return p.federateReport(ctx, report)
|
||||
if err := p.notifyReport(ctx, report); err != nil {
|
||||
return fmt.Errorf("processReportAccountFromClientAPI: error notifying report: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: move all the below functions into federation.Federator
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user