feat: sync durable moderation and appeals
This commit is contained in:
parent
e1a95c7318
commit
9f467f4be7
140 changed files with 13730 additions and 316 deletions
441
internal/store/postgres/moderation.go
Normal file
441
internal/store/postgres/moderation.go
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
type ModerationReportStore struct {
|
||||
db sqlcgen.DBTX
|
||||
}
|
||||
|
||||
func NewModerationReportStore(db sqlcgen.DBTX) *ModerationReportStore {
|
||||
return &ModerationReportStore{db: db}
|
||||
}
|
||||
|
||||
func (s *ModerationReportStore) CreateModerationReport(ctx context.Context, report domain.ModerationReport) (domain.ModerationReport, bool, error) {
|
||||
if s == nil || s.db == nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("moderation report store is not configured")
|
||||
}
|
||||
if err := report.Validate(); err != nil {
|
||||
return domain.ModerationReport{}, false, err
|
||||
}
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
if !ok {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("moderation report store requires transaction-capable postgres handle")
|
||||
}
|
||||
tx, err := beginner.Begin(ctx)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("begin moderation report: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
stored, created, err := createModerationReportTx(ctx, tx, report)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("commit moderation report: %w", err)
|
||||
}
|
||||
return stored, created, nil
|
||||
}
|
||||
|
||||
func createModerationReportTx(ctx context.Context, tx pgx.Tx, report domain.ModerationReport) (domain.ModerationReport, bool, error) {
|
||||
var (
|
||||
reportID int64
|
||||
err error
|
||||
)
|
||||
if _, err := tx.Exec(ctx, `
|
||||
SELECT pg_advisory_xact_lock(
|
||||
hashtextextended('moderation-report:' || $1::bigint::text, 0)
|
||||
)`, report.ReporterUserID); err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("lock moderation reporter: %w", err)
|
||||
}
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT id
|
||||
FROM moderation_reports
|
||||
WHERE reporter_user_id = $1 AND fingerprint = $2`,
|
||||
report.ReporterUserID, report.Fingerprint[:]).Scan(&reportID)
|
||||
if err == nil {
|
||||
existing, found, err := getModerationReport(ctx, tx, reportID)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, err
|
||||
}
|
||||
if !found {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("duplicate moderation report disappeared")
|
||||
}
|
||||
return existing, false, nil
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("lookup moderation report fingerprint: %w", err)
|
||||
}
|
||||
var hourly, daily int
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT
|
||||
count(*) FILTER (WHERE created_at >= $2::timestamptz - interval '1 hour'),
|
||||
count(*) FILTER (WHERE created_at >= $2::timestamptz - interval '24 hours')
|
||||
FROM moderation_reports
|
||||
WHERE reporter_user_id = $1
|
||||
AND created_at <= $2::timestamptz`,
|
||||
report.ReporterUserID, report.CreatedAt).Scan(&hourly, &daily); err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("count moderation reporter submissions: %w", err)
|
||||
}
|
||||
if hourly >= domain.MaxModerationReportsPerHour || daily >= domain.MaxModerationReportsPerDay {
|
||||
return domain.ModerationReport{}, false, domain.ErrModerationRateLimited
|
||||
}
|
||||
reportID, created, err := insertModerationReport(ctx, tx, report)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, err
|
||||
}
|
||||
if !created {
|
||||
existing, found, err := getModerationReport(ctx, tx, reportID)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, err
|
||||
}
|
||||
if !found {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("duplicate moderation report disappeared")
|
||||
}
|
||||
return existing, false, nil
|
||||
}
|
||||
report.ID = reportID
|
||||
return domain.CloneModerationReport(report), true, nil
|
||||
}
|
||||
|
||||
func (s *ModerationReportStore) ImportLegacyEphemeralReport(ctx context.Context, legacyReportID int64, report domain.ModerationReport) (domain.ModerationReport, bool, error) {
|
||||
if s == nil || s.db == nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("moderation report store is not configured")
|
||||
}
|
||||
if legacyReportID <= 0 {
|
||||
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
|
||||
}
|
||||
if err := report.Validate(); err != nil {
|
||||
return domain.ModerationReport{}, false, err
|
||||
}
|
||||
if report.Source != domain.ModerationSourceEphemeral {
|
||||
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
|
||||
}
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
if !ok {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("moderation report store requires transaction-capable postgres handle")
|
||||
}
|
||||
tx, err := beginner.Begin(ctx)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("begin legacy ephemeral report import: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
SELECT pg_advisory_xact_lock(
|
||||
hashtextextended('moderation-legacy-ephemeral:' || $1::bigint::text, 0)
|
||||
)`, legacyReportID); err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("lock legacy ephemeral report: %w", err)
|
||||
}
|
||||
var reportID int64
|
||||
err = tx.QueryRow(ctx, `
|
||||
SELECT moderation_report_id
|
||||
FROM moderation_legacy_ephemeral_migrations
|
||||
WHERE legacy_report_id = $1`, legacyReportID).Scan(&reportID)
|
||||
if err == nil {
|
||||
existing, found, err := getModerationReport(ctx, tx, reportID)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, err
|
||||
}
|
||||
if !found {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("legacy ephemeral report mapping points to missing moderation report")
|
||||
}
|
||||
return existing, false, nil
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("lookup legacy ephemeral report mapping: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
SELECT pg_advisory_xact_lock(
|
||||
hashtextextended('moderation-report:' || $1::bigint::text, 0)
|
||||
)`, report.ReporterUserID); err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("lock moderation reporter: %w", err)
|
||||
}
|
||||
reportID, created, err := insertModerationReport(ctx, tx, report)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO moderation_legacy_ephemeral_migrations (
|
||||
legacy_report_id, moderation_report_id, migrated_at
|
||||
) VALUES ($1,$2,clock_timestamp())`,
|
||||
legacyReportID, reportID,
|
||||
); err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("insert legacy ephemeral report mapping: %w", err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("commit legacy ephemeral report import: %w", err)
|
||||
}
|
||||
if created {
|
||||
report.ID = reportID
|
||||
return domain.CloneModerationReport(report), true, nil
|
||||
}
|
||||
existing, found, err := getModerationReport(ctx, s.db, reportID)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, err
|
||||
}
|
||||
if !found {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("imported duplicate moderation report disappeared")
|
||||
}
|
||||
return existing, false, nil
|
||||
}
|
||||
|
||||
func insertModerationReport(ctx context.Context, tx pgx.Tx, report domain.ModerationReport) (int64, bool, error) {
|
||||
var reportID int64
|
||||
err := tx.QueryRow(ctx, `
|
||||
INSERT INTO moderation_reports (
|
||||
reporter_user_id, source, target_peer_type, target_peer_id, reason,
|
||||
report_option, report_comment, comment_hash, fingerprint,
|
||||
taxonomy_version, created_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
|
||||
ON CONFLICT (reporter_user_id, fingerprint) DO NOTHING
|
||||
RETURNING id`,
|
||||
report.ReporterUserID, string(report.Source), string(report.Target.Type),
|
||||
report.Target.ID, string(report.Reason), report.Option, report.Comment,
|
||||
report.CommentHash[:], report.Fingerprint[:], report.TaxonomyVersion,
|
||||
report.CreatedAt,
|
||||
).Scan(&reportID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT id
|
||||
FROM moderation_reports
|
||||
WHERE reporter_user_id = $1 AND fingerprint = $2`,
|
||||
report.ReporterUserID, report.Fingerprint[:]).Scan(&reportID); err != nil {
|
||||
return 0, false, fmt.Errorf("lookup duplicate moderation report: %w", err)
|
||||
}
|
||||
return reportID, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("insert moderation report: %w", err)
|
||||
}
|
||||
for ordinal, item := range report.Items {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO moderation_report_items (
|
||||
report_id, ordinal, item_kind, peer_type, peer_id, item_id,
|
||||
secondary_id, author_user_id, evidence_schema_version, evidence,
|
||||
evidence_hash
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10::jsonb,$11)`,
|
||||
reportID, ordinal, string(item.Kind), string(item.Peer.Type),
|
||||
item.Peer.ID, item.ItemID, item.SecondaryID, item.AuthorUserID,
|
||||
item.EvidenceSchemaVersion, []byte(item.Evidence), item.EvidenceHash[:],
|
||||
); err != nil {
|
||||
return 0, false, fmt.Errorf("insert moderation report item %d: %w", ordinal, err)
|
||||
}
|
||||
}
|
||||
for _, hold := range report.MediaHolds {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO moderation_media_holds (
|
||||
report_id, item_ordinal, media_kind, storage_key, created_at
|
||||
) VALUES ($1,$2,$3,$4,$5)`,
|
||||
reportID, hold.ItemIndex, string(hold.Kind), hold.StorageKey,
|
||||
report.CreatedAt,
|
||||
); err != nil {
|
||||
return 0, false, fmt.Errorf("insert moderation media hold: %w", err)
|
||||
}
|
||||
}
|
||||
if err := attachModerationReportToCase(ctx, tx, reportID, report); err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return reportID, true, nil
|
||||
}
|
||||
|
||||
func attachModerationReportToCase(ctx context.Context, tx pgx.Tx, reportID int64, report domain.ModerationReport) error {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
SELECT pg_advisory_xact_lock(
|
||||
hashtextextended(
|
||||
'moderation-case:' || $1::text || ':' || $2::bigint::text,
|
||||
0
|
||||
)
|
||||
)`, string(report.Target.Type), report.Target.ID); err != nil {
|
||||
return fmt.Errorf("lock moderation case target: %w", err)
|
||||
}
|
||||
var caseID int64
|
||||
err := tx.QueryRow(ctx, `
|
||||
SELECT id
|
||||
FROM moderation_cases
|
||||
WHERE target_peer_type = $1
|
||||
AND target_peer_id = $2
|
||||
AND status IN ('open', 'in_review')
|
||||
FOR UPDATE`,
|
||||
string(report.Target.Type), report.Target.ID,
|
||||
).Scan(&caseID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO moderation_cases (
|
||||
target_peer_type, target_peer_id, status, severity, assigned_to,
|
||||
version, report_count, distinct_reporter_count, first_report_at,
|
||||
last_report_at, created_at, updated_at
|
||||
) VALUES ($1,$2,'open',$3,'',1,1,1,$4,$4,$4,$4)
|
||||
RETURNING id`,
|
||||
string(report.Target.Type), report.Target.ID,
|
||||
int16(domain.ModerationSeverityForReason(report.Reason)),
|
||||
report.CreatedAt,
|
||||
).Scan(&caseID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create moderation case: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO moderation_case_reports (case_id, report_id, attached_at)
|
||||
VALUES ($1,$2,$3)`, caseID, reportID, report.CreatedAt); err != nil {
|
||||
return fmt.Errorf("attach report to new moderation case: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("find active moderation case: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO moderation_case_reports (case_id, report_id, attached_at)
|
||||
VALUES ($1,$2,$3)`, caseID, reportID, report.CreatedAt); err != nil {
|
||||
return fmt.Errorf("attach report to moderation case: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE moderation_cases c
|
||||
SET severity = greatest(c.severity, $2),
|
||||
version = c.version + 1,
|
||||
report_count = (
|
||||
SELECT count(*)::integer
|
||||
FROM moderation_case_reports cr
|
||||
WHERE cr.case_id = c.id
|
||||
),
|
||||
distinct_reporter_count = (
|
||||
SELECT count(DISTINCT r.reporter_user_id)::integer
|
||||
FROM moderation_case_reports cr
|
||||
JOIN moderation_reports r ON r.id = cr.report_id
|
||||
WHERE cr.case_id = c.id
|
||||
),
|
||||
first_report_at = least(c.first_report_at, $3),
|
||||
last_report_at = greatest(c.last_report_at, $3),
|
||||
updated_at = greatest(c.updated_at, $3)
|
||||
WHERE c.id = $1`,
|
||||
caseID, int16(domain.ModerationSeverityForReason(report.Reason)),
|
||||
report.CreatedAt,
|
||||
); err != nil {
|
||||
return fmt.Errorf("update moderation case aggregates: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ModerationReportStore) GetModerationReport(ctx context.Context, reportID int64) (domain.ModerationReport, bool, error) {
|
||||
if s == nil || s.db == nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("moderation report store is not configured")
|
||||
}
|
||||
if reportID <= 0 {
|
||||
return domain.ModerationReport{}, false, domain.ErrModerationReportInvalid
|
||||
}
|
||||
return getModerationReport(ctx, s.db, reportID)
|
||||
}
|
||||
|
||||
func getModerationReport(ctx context.Context, db sqlcgen.DBTX, reportID int64) (domain.ModerationReport, bool, error) {
|
||||
var (
|
||||
report domain.ModerationReport
|
||||
source, target, reason string
|
||||
commentHash []byte
|
||||
fingerprint []byte
|
||||
)
|
||||
err := db.QueryRow(ctx, `
|
||||
SELECT id, reporter_user_id, source, target_peer_type, target_peer_id,
|
||||
reason, report_option, report_comment, comment_hash, fingerprint,
|
||||
taxonomy_version, created_at
|
||||
FROM moderation_reports
|
||||
WHERE id = $1`, reportID).Scan(
|
||||
&report.ID, &report.ReporterUserID, &source, &target,
|
||||
&report.Target.ID, &reason, &report.Option, &report.Comment,
|
||||
&commentHash, &fingerprint, &report.TaxonomyVersion,
|
||||
&report.CreatedAt,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.ModerationReport{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("get moderation report: %w", err)
|
||||
}
|
||||
if len(commentHash) != len(report.CommentHash) || len(fingerprint) != len(report.Fingerprint) {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("get moderation report: invalid persisted hash length")
|
||||
}
|
||||
copy(report.CommentHash[:], commentHash)
|
||||
copy(report.Fingerprint[:], fingerprint)
|
||||
report.Source = domain.ModerationReportSource(source)
|
||||
report.Target.Type = domain.PeerType(target)
|
||||
report.Reason = domain.ModerationReason(reason)
|
||||
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT ordinal, item_kind, peer_type, peer_id, item_id, secondary_id,
|
||||
author_user_id, evidence_schema_version, evidence, evidence_hash
|
||||
FROM moderation_report_items
|
||||
WHERE report_id = $1
|
||||
ORDER BY ordinal`, reportID)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("list moderation report items: %w", err)
|
||||
}
|
||||
for rows.Next() {
|
||||
var (
|
||||
ordinal int
|
||||
item domain.ModerationReportItem
|
||||
kind, peerType string
|
||||
evidence, evidenceHash []byte
|
||||
)
|
||||
if err := rows.Scan(
|
||||
&ordinal, &kind, &peerType, &item.Peer.ID, &item.ItemID,
|
||||
&item.SecondaryID, &item.AuthorUserID,
|
||||
&item.EvidenceSchemaVersion, &evidence, &evidenceHash,
|
||||
); err != nil {
|
||||
rows.Close()
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("scan moderation report item: %w", err)
|
||||
}
|
||||
if ordinal != len(report.Items) || len(evidenceHash) != len(item.EvidenceHash) {
|
||||
rows.Close()
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("scan moderation report item: invalid persisted ordering or hash")
|
||||
}
|
||||
canonical, err := domain.CanonicalModerationEvidence(evidence)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("scan moderation report item evidence: %w", err)
|
||||
}
|
||||
item.Kind = domain.ModerationReportItemKind(kind)
|
||||
item.Peer.Type = domain.PeerType(peerType)
|
||||
item.Evidence = canonical
|
||||
copy(item.EvidenceHash[:], evidenceHash)
|
||||
report.Items = append(report.Items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("iterate moderation report items: %w", err)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
holdRows, err := db.Query(ctx, `
|
||||
SELECT item_ordinal, media_kind, storage_key
|
||||
FROM moderation_media_holds
|
||||
WHERE report_id = $1
|
||||
ORDER BY item_ordinal, media_kind, storage_key`, reportID)
|
||||
if err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("list moderation media holds: %w", err)
|
||||
}
|
||||
defer holdRows.Close()
|
||||
for holdRows.Next() {
|
||||
var hold domain.ModerationMediaHold
|
||||
var kind string
|
||||
if err := holdRows.Scan(&hold.ItemIndex, &kind, &hold.StorageKey); err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("scan moderation media hold: %w", err)
|
||||
}
|
||||
hold.Kind = domain.ModerationMediaKind(kind)
|
||||
report.MediaHolds = append(report.MediaHolds, hold)
|
||||
}
|
||||
if err := holdRows.Err(); err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("iterate moderation media holds: %w", err)
|
||||
}
|
||||
if err := report.Validate(); err != nil {
|
||||
return domain.ModerationReport{}, false, fmt.Errorf("validate persisted moderation report: %w", err)
|
||||
}
|
||||
return report, true, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue