owpengram-server/internal/app/moderation/legacy.go
2026-07-24 11:56:59 +08:00

106 lines
3.7 KiB
Go

package moderation
import (
"context"
"encoding/json"
"fmt"
"telesrv/internal/domain"
"telesrv/internal/store"
)
// MigrateLegacyEphemeralReports converts every pre-unified durable report into
// the canonical moderation shape. The store commits the new report and its
// legacy provenance mapping atomically; rerunning after a crash is safe.
func (s *Service) MigrateLegacyEphemeralReports(ctx context.Context, source store.LegacyEphemeralReportReader, batchSize int) (int, error) {
if s == nil || s.reports == nil || source == nil {
return 0, fmt.Errorf("legacy ephemeral report migration is not configured")
}
if batchSize <= 0 || batchSize > 1000 {
return 0, fmt.Errorf("legacy ephemeral report batch limit out of range")
}
importer, ok := s.reports.(store.LegacyEphemeralReportImporter)
if !ok {
return 0, fmt.Errorf("moderation report store does not support legacy imports")
}
migrated := 0
for {
rows, err := source.ListUnmigratedEphemeralReports(ctx, batchSize)
if err != nil {
return migrated, err
}
for _, legacy := range rows {
report, err := legacyEphemeralModerationReport(legacy.Report)
if err != nil {
return migrated, fmt.Errorf("convert legacy ephemeral report %d: %w", legacy.ID, err)
}
if _, _, err := importer.ImportLegacyEphemeralReport(ctx, legacy.ID, report); err != nil {
return migrated, fmt.Errorf("import legacy ephemeral report %d: %w", legacy.ID, err)
}
migrated++
}
if len(rows) < batchSize {
return migrated, nil
}
}
}
func legacyEphemeralModerationReport(legacy domain.EphemeralAbuseReport) (domain.ModerationReport, error) {
if err := legacy.Validate(); err != nil {
return domain.ModerationReport{}, err
}
reason, ok := legacyEphemeralModerationReason(legacy.Option)
if !ok {
return domain.ModerationReport{}, fmt.Errorf("%w: unsupported legacy option %q", domain.ErrModerationReportInvalid, legacy.Option)
}
evidence, err := json.Marshal(struct {
SchemaVersion int `json:"schema_version"`
Evidence domain.EphemeralReportEvidence `json:"evidence"`
}{SchemaVersion: 1, Evidence: legacy.Evidence})
if err != nil {
return domain.ModerationReport{}, fmt.Errorf("marshal legacy ephemeral evidence: %w", err)
}
return domain.NewModerationReport(domain.ModerationReportDraft{
ReporterUserID: legacy.ReporterUserID,
Source: domain.ModerationSourceEphemeral,
Target: legacy.Evidence.Peer,
Reason: reason,
Option: legacy.Option,
Comment: legacy.Comment,
Items: []domain.ModerationReportItem{{
Kind: domain.ModerationItemEphemeral,
Peer: legacy.Evidence.Peer,
ItemID: int64(legacy.Evidence.MessageID),
AuthorUserID: legacy.Evidence.SenderUserID,
EvidenceSchemaVersion: 1,
Evidence: evidence,
}},
MediaHolds: mediaHolds(0, legacy.Evidence.Content.Media),
CreatedAt: legacy.CreatedAt,
})
}
func legacyEphemeralModerationReason(option string) (domain.ModerationReason, bool) {
switch option {
case "spam":
return domain.ModerationReasonSpam, true
case "violence":
return domain.ModerationReasonViolence, true
case "pornography":
return domain.ModerationReasonPornography, true
case "child_abuse":
return domain.ModerationReasonChildAbuse, true
case "illegal_drugs":
return domain.ModerationReasonIllegalDrugs, true
case "personal_details":
return domain.ModerationReasonPersonalDetails, true
case "copyright":
return domain.ModerationReasonCopyright, true
case "fake":
return domain.ModerationReasonFake, true
case "other", "other:comment":
return domain.ModerationReasonOther, true
default:
return "", false
}
}