owpengram-server/internal/store/postgres/moderation_registries.go
2026-07-24 11:56:59 +08:00

353 lines
13 KiB
Go

package postgres
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"telesrv/internal/domain"
)
func (s *ModerationReportStore) CreateSponsoredMessageImpression(ctx context.Context, impression domain.SponsoredMessageImpression) (domain.SponsoredMessageImpression, bool, error) {
if s == nil || s.db == nil {
return domain.SponsoredMessageImpression{}, false, fmt.Errorf("moderation report store is not configured")
}
if err := impression.Validate(); err != nil || impression.ID != 0 ||
impression.ReportID != 0 {
return domain.SponsoredMessageImpression{}, false, domain.ErrModerationReportInvalid
}
err := s.db.QueryRow(ctx, `
INSERT INTO sponsored_message_impressions (
user_id, random_id_hash, target_peer_type, target_peer_id,
author_user_id, evidence_schema_version, evidence, evidence_hash,
created_at, expires_at
) VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,$8,$9,$10)
ON CONFLICT (user_id, random_id_hash) DO NOTHING
RETURNING id`,
impression.UserID, impression.RandomIDHash[:],
string(impression.Target.Type), impression.Target.ID,
impression.AuthorUserID, impression.EvidenceSchemaVersion,
[]byte(impression.Evidence), impression.EvidenceHash[:],
impression.CreatedAt, impression.ExpiresAt,
).Scan(&impression.ID)
if err == nil {
return impression, true, nil
}
if !errors.Is(err, pgx.ErrNoRows) {
return domain.SponsoredMessageImpression{}, false, fmt.Errorf("insert sponsored impression: %w", err)
}
existing, found, err := s.GetSponsoredMessageImpression(
ctx, impression.UserID, impression.RandomIDHash, impression.CreatedAt,
)
if err != nil {
return domain.SponsoredMessageImpression{}, false, err
}
if !found || existing.Target != impression.Target ||
existing.AuthorUserID != impression.AuthorUserID ||
existing.EvidenceHash != impression.EvidenceHash ||
!existing.ExpiresAt.Equal(impression.ExpiresAt) {
return domain.SponsoredMessageImpression{}, false, domain.ErrModerationActionConflict
}
return existing, false, nil
}
func (s *ModerationReportStore) GetSponsoredMessageImpression(ctx context.Context, userID int64, randomIDHash [32]byte, now time.Time) (domain.SponsoredMessageImpression, bool, error) {
if s == nil || s.db == nil {
return domain.SponsoredMessageImpression{}, false, fmt.Errorf("moderation report store is not configured")
}
if userID <= 0 || randomIDHash == ([32]byte{}) || now.IsZero() {
return domain.SponsoredMessageImpression{}, false, domain.ErrModerationReportInvalid
}
impression, err := scanSponsoredMessageImpression(s.db.QueryRow(ctx, `
SELECT id, user_id, random_id_hash, target_peer_type, target_peer_id,
author_user_id, evidence_schema_version, evidence, evidence_hash,
report_id, created_at, expires_at
FROM sponsored_message_impressions
WHERE user_id = $1 AND random_id_hash = $2 AND expires_at > $3`,
userID, randomIDHash[:], now,
))
if errors.Is(err, pgx.ErrNoRows) {
return domain.SponsoredMessageImpression{}, false, nil
}
if err != nil {
return domain.SponsoredMessageImpression{}, false, fmt.Errorf("get sponsored impression: %w", err)
}
return impression, true, nil
}
func (s *ModerationReportStore) CreateSponsoredModerationReport(ctx context.Context, impressionID 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 impressionID <= 0 {
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 sponsored moderation report: %w", err)
}
defer func() { _ = tx.Rollback(ctx) }()
impression, err := scanSponsoredMessageImpression(tx.QueryRow(ctx, `
SELECT id, user_id, random_id_hash, target_peer_type, target_peer_id,
author_user_id, evidence_schema_version, evidence, evidence_hash,
report_id, created_at, expires_at
FROM sponsored_message_impressions
WHERE id = $1
FOR UPDATE`, impressionID))
if errors.Is(err, pgx.ErrNoRows) {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("lock sponsored impression: %w", err)
}
if !report.CreatedAt.Before(impression.ExpiresAt) {
return domain.ModerationReport{}, false, domain.ErrModerationImpressionExpired
}
if err := domain.ValidateSponsoredModerationReport(impression, report); err != nil {
return domain.ModerationReport{}, false, err
}
if impression.ReportID > 0 {
existing, found, err := getModerationReport(ctx, tx, impression.ReportID)
if err != nil {
return domain.ModerationReport{}, false, err
}
if !found {
return domain.ModerationReport{}, false, domain.ErrModerationReportNotFound
}
return existing, false, nil
}
stored, created, err := createModerationReportTx(ctx, tx, report)
if err != nil {
return domain.ModerationReport{}, false, err
}
tag, err := tx.Exec(ctx, `
UPDATE sponsored_message_impressions
SET report_id = $2
WHERE id = $1 AND report_id IS NULL`, impressionID, stored.ID)
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("link sponsored report: %w", err)
}
if tag.RowsAffected() != 1 {
return domain.ModerationReport{}, false, domain.ErrModerationActionConflict
}
if err := tx.Commit(ctx); err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("commit sponsored moderation report: %w", err)
}
return stored, created, nil
}
func (s *ModerationReportStore) CreateChannelAntiSpamDecision(ctx context.Context, decision domain.ChannelAntiSpamDecision) (domain.ChannelAntiSpamDecision, bool, error) {
if s == nil || s.db == nil {
return domain.ChannelAntiSpamDecision{}, false, fmt.Errorf("moderation report store is not configured")
}
if err := decision.Validate(); err != nil || decision.ID != 0 ||
decision.ReportID != 0 {
return domain.ChannelAntiSpamDecision{}, false, domain.ErrModerationReportInvalid
}
err := s.db.QueryRow(ctx, `
INSERT INTO channel_antispam_decisions (
channel_id, message_id, author_user_id, evidence_schema_version,
evidence, evidence_hash, created_at
) VALUES ($1,$2,$3,$4,$5::jsonb,$6,$7)
ON CONFLICT (channel_id, message_id) DO NOTHING
RETURNING id`,
decision.ChannelID, decision.MessageID, decision.AuthorUserID,
decision.EvidenceSchemaVersion, []byte(decision.Evidence),
decision.EvidenceHash[:], decision.CreatedAt,
).Scan(&decision.ID)
if err == nil {
return decision, true, nil
}
if !errors.Is(err, pgx.ErrNoRows) {
return domain.ChannelAntiSpamDecision{}, false, fmt.Errorf("insert anti-spam decision: %w", err)
}
existing, found, err := s.GetChannelAntiSpamDecision(
ctx, decision.ChannelID, decision.MessageID,
)
if err != nil {
return domain.ChannelAntiSpamDecision{}, false, err
}
if !found || existing.AuthorUserID != decision.AuthorUserID ||
existing.EvidenceHash != decision.EvidenceHash {
return domain.ChannelAntiSpamDecision{}, false, domain.ErrModerationActionConflict
}
return existing, false, nil
}
func (s *ModerationReportStore) GetChannelAntiSpamDecision(ctx context.Context, channelID int64, messageID int) (domain.ChannelAntiSpamDecision, bool, error) {
if s == nil || s.db == nil {
return domain.ChannelAntiSpamDecision{}, false, fmt.Errorf("moderation report store is not configured")
}
if channelID <= 0 || messageID <= 0 || messageID > domain.MaxMessageBoxID {
return domain.ChannelAntiSpamDecision{}, false, domain.ErrModerationReportInvalid
}
decision, err := scanChannelAntiSpamDecision(s.db.QueryRow(ctx, `
SELECT id, channel_id, message_id, author_user_id,
evidence_schema_version, evidence, evidence_hash, report_id,
created_at
FROM channel_antispam_decisions
WHERE channel_id = $1 AND message_id = $2`, channelID, messageID))
if errors.Is(err, pgx.ErrNoRows) {
return domain.ChannelAntiSpamDecision{}, false, nil
}
if err != nil {
return domain.ChannelAntiSpamDecision{}, false, fmt.Errorf("get anti-spam decision: %w", err)
}
return decision, true, nil
}
func (s *ModerationReportStore) CreateAntiSpamFalsePositiveReport(ctx context.Context, decisionID 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 decisionID <= 0 {
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 anti-spam false-positive report: %w", err)
}
defer func() { _ = tx.Rollback(ctx) }()
decision, err := scanChannelAntiSpamDecision(tx.QueryRow(ctx, `
SELECT id, channel_id, message_id, author_user_id,
evidence_schema_version, evidence, evidence_hash, report_id,
created_at
FROM channel_antispam_decisions
WHERE id = $1
FOR UPDATE`, decisionID))
if errors.Is(err, pgx.ErrNoRows) {
return domain.ModerationReport{}, false, domain.ErrModerationEvidenceNotFound
}
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("lock anti-spam decision: %w", err)
}
if err := domain.ValidateAntiSpamFalsePositiveReport(decision, report); err != nil {
return domain.ModerationReport{}, false, err
}
if decision.ReportID > 0 {
existing, found, err := getModerationReport(ctx, tx, decision.ReportID)
if err != nil {
return domain.ModerationReport{}, false, err
}
if !found {
return domain.ModerationReport{}, false, domain.ErrModerationReportNotFound
}
return existing, false, nil
}
stored, created, err := createModerationReportTx(ctx, tx, report)
if err != nil {
return domain.ModerationReport{}, false, err
}
tag, err := tx.Exec(ctx, `
UPDATE channel_antispam_decisions
SET report_id = $2
WHERE id = $1 AND report_id IS NULL`, decisionID, stored.ID)
if err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("link anti-spam report: %w", err)
}
if tag.RowsAffected() != 1 {
return domain.ModerationReport{}, false, domain.ErrModerationActionConflict
}
if err := tx.Commit(ctx); err != nil {
return domain.ModerationReport{}, false, fmt.Errorf("commit anti-spam false-positive report: %w", err)
}
return stored, created, nil
}
func (s *ModerationReportStore) DeleteExpiredSponsoredMessageImpressions(ctx context.Context, olderThan time.Time, limit int) (int, error) {
if s == nil || s.db == nil {
return 0, fmt.Errorf("moderation report store is not configured")
}
if olderThan.IsZero() || limit <= 0 || limit > 10000 {
return 0, domain.ErrModerationReportInvalid
}
tag, err := s.db.Exec(ctx, `
WITH doomed AS (
SELECT id
FROM sponsored_message_impressions
WHERE expires_at < $1
ORDER BY expires_at, id
LIMIT $2
)
DELETE FROM sponsored_message_impressions i
USING doomed d
WHERE i.id = d.id`, olderThan, limit)
if err != nil {
return 0, fmt.Errorf("delete expired sponsored impressions: %w", err)
}
return int(tag.RowsAffected()), nil
}
func scanSponsoredMessageImpression(row moderationCaseScanner) (domain.SponsoredMessageImpression, error) {
var impression domain.SponsoredMessageImpression
var randomIDHash, evidence, evidenceHash []byte
var peerType string
var reportID *int64
if err := row.Scan(
&impression.ID, &impression.UserID, &randomIDHash, &peerType,
&impression.Target.ID, &impression.AuthorUserID,
&impression.EvidenceSchemaVersion, &evidence, &evidenceHash,
&reportID, &impression.CreatedAt, &impression.ExpiresAt,
); err != nil {
return domain.SponsoredMessageImpression{}, err
}
if len(randomIDHash) != len(impression.RandomIDHash) ||
len(evidenceHash) != len(impression.EvidenceHash) {
return domain.SponsoredMessageImpression{}, domain.ErrModerationReportInvalid
}
copy(impression.RandomIDHash[:], randomIDHash)
copy(impression.EvidenceHash[:], evidenceHash)
impression.Target.Type = domain.PeerType(peerType)
canonical, err := domain.CanonicalModerationEvidence(evidence)
if err != nil {
return domain.SponsoredMessageImpression{}, err
}
impression.Evidence = canonical
if reportID != nil {
impression.ReportID = *reportID
}
if err := impression.Validate(); err != nil {
return domain.SponsoredMessageImpression{}, err
}
return impression, nil
}
func scanChannelAntiSpamDecision(row moderationCaseScanner) (domain.ChannelAntiSpamDecision, error) {
var decision domain.ChannelAntiSpamDecision
var evidence, evidenceHash []byte
var reportID *int64
if err := row.Scan(
&decision.ID, &decision.ChannelID, &decision.MessageID,
&decision.AuthorUserID, &decision.EvidenceSchemaVersion,
&evidence, &evidenceHash, &reportID, &decision.CreatedAt,
); err != nil {
return domain.ChannelAntiSpamDecision{}, err
}
if len(evidenceHash) != len(decision.EvidenceHash) {
return domain.ChannelAntiSpamDecision{}, domain.ErrModerationReportInvalid
}
copy(decision.EvidenceHash[:], evidenceHash)
canonical, err := domain.CanonicalModerationEvidence(evidence)
if err != nil {
return domain.ChannelAntiSpamDecision{}, err
}
decision.Evidence = canonical
if reportID != nil {
decision.ReportID = *reportID
}
if err := decision.Validate(); err != nil {
return domain.ChannelAntiSpamDecision{}, err
}
return decision, nil
}