156 lines
5.5 KiB
Go
156 lines
5.5 KiB
Go
package domain
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
const MaxSponsoredImpressionLifetime = 30 * 24 * time.Hour
|
|
|
|
// SponsoredMessageImpression is the server-issued fact required before a
|
|
// random_id may enter the human moderation pipeline.
|
|
type SponsoredMessageImpression struct {
|
|
ID int64
|
|
UserID int64
|
|
RandomIDHash [sha256.Size]byte
|
|
Target Peer
|
|
AuthorUserID int64
|
|
EvidenceSchemaVersion int
|
|
Evidence json.RawMessage
|
|
EvidenceHash [sha256.Size]byte
|
|
ReportID int64
|
|
CreatedAt time.Time
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
func NewSponsoredMessageImpression(userID int64, randomID []byte, target Peer, authorUserID int64, evidence json.RawMessage, createdAt, expiresAt time.Time) (SponsoredMessageImpression, error) {
|
|
canonical, err := CanonicalModerationEvidence(evidence)
|
|
if err != nil {
|
|
return SponsoredMessageImpression{}, ErrModerationReportInvalid
|
|
}
|
|
impression := SponsoredMessageImpression{
|
|
UserID: userID, RandomIDHash: sha256.Sum256(randomID),
|
|
Target: target, AuthorUserID: authorUserID,
|
|
EvidenceSchemaVersion: 1, Evidence: canonical,
|
|
EvidenceHash: sha256.Sum256(canonical),
|
|
CreatedAt: createdAt.UTC(), ExpiresAt: expiresAt.UTC(),
|
|
}
|
|
if err := impression.Validate(); err != nil {
|
|
return SponsoredMessageImpression{}, err
|
|
}
|
|
return impression, nil
|
|
}
|
|
|
|
func (i SponsoredMessageImpression) Validate() error {
|
|
canonical, err := CanonicalModerationEvidence(i.Evidence)
|
|
if i.ID < 0 || i.UserID <= 0 ||
|
|
i.RandomIDHash == ([sha256.Size]byte{}) ||
|
|
!moderationPeerValid(i.Target) || i.AuthorUserID < 0 ||
|
|
i.EvidenceSchemaVersion <= 0 ||
|
|
i.EvidenceHash != sha256.Sum256(i.Evidence) ||
|
|
err != nil || !bytes.Equal(canonical, i.Evidence) ||
|
|
i.ReportID < 0 || i.CreatedAt.IsZero() ||
|
|
!i.ExpiresAt.After(i.CreatedAt) ||
|
|
i.ExpiresAt.Sub(i.CreatedAt) > MaxSponsoredImpressionLifetime {
|
|
return ErrModerationReportInvalid
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateSponsoredModerationReport(impression SponsoredMessageImpression, report ModerationReport) error {
|
|
if err := impression.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := report.Validate(); err != nil ||
|
|
report.ReporterUserID != impression.UserID ||
|
|
report.Source != ModerationSourceSponsored ||
|
|
report.Target != impression.Target ||
|
|
report.CreatedAt.Before(impression.CreatedAt) ||
|
|
!report.CreatedAt.Before(impression.ExpiresAt) ||
|
|
len(report.Items) != 1 || len(report.MediaHolds) != 0 {
|
|
return ErrModerationReportInvalid
|
|
}
|
|
item := report.Items[0]
|
|
if item.Kind != ModerationItemSponsored ||
|
|
item.Peer != impression.Target ||
|
|
item.ItemID != impression.ID || item.SecondaryID != 0 ||
|
|
item.AuthorUserID != impression.AuthorUserID ||
|
|
item.EvidenceSchemaVersion != impression.EvidenceSchemaVersion ||
|
|
item.EvidenceHash != impression.EvidenceHash ||
|
|
!bytes.Equal(item.Evidence, impression.Evidence) {
|
|
return ErrModerationReportInvalid
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ChannelAntiSpamDecision is immutable evidence that native anti-spam
|
|
// actually removed the referenced message. A false-positive report without
|
|
// this fact must fail closed.
|
|
type ChannelAntiSpamDecision struct {
|
|
ID int64
|
|
ChannelID int64
|
|
MessageID int
|
|
AuthorUserID int64
|
|
EvidenceSchemaVersion int
|
|
Evidence json.RawMessage
|
|
EvidenceHash [sha256.Size]byte
|
|
ReportID int64
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
func NewChannelAntiSpamDecision(channelID int64, messageID int, authorUserID int64, evidence json.RawMessage, createdAt time.Time) (ChannelAntiSpamDecision, error) {
|
|
canonical, err := CanonicalModerationEvidence(evidence)
|
|
if err != nil {
|
|
return ChannelAntiSpamDecision{}, ErrModerationReportInvalid
|
|
}
|
|
decision := ChannelAntiSpamDecision{
|
|
ChannelID: channelID, MessageID: messageID,
|
|
AuthorUserID: authorUserID, EvidenceSchemaVersion: 1,
|
|
Evidence: canonical, EvidenceHash: sha256.Sum256(canonical),
|
|
CreatedAt: createdAt.UTC(),
|
|
}
|
|
if err := decision.Validate(); err != nil {
|
|
return ChannelAntiSpamDecision{}, err
|
|
}
|
|
return decision, nil
|
|
}
|
|
|
|
func (d ChannelAntiSpamDecision) Validate() error {
|
|
canonical, err := CanonicalModerationEvidence(d.Evidence)
|
|
if d.ID < 0 || d.ChannelID <= 0 || d.MessageID <= 0 ||
|
|
d.MessageID > MaxMessageBoxID || d.AuthorUserID <= 0 ||
|
|
d.EvidenceSchemaVersion <= 0 ||
|
|
d.EvidenceHash != sha256.Sum256(d.Evidence) ||
|
|
err != nil || !bytes.Equal(canonical, d.Evidence) ||
|
|
d.ReportID < 0 || d.CreatedAt.IsZero() {
|
|
return ErrModerationReportInvalid
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ValidateAntiSpamFalsePositiveReport(decision ChannelAntiSpamDecision, report ModerationReport) error {
|
|
if err := decision.Validate(); err != nil {
|
|
return err
|
|
}
|
|
target := Peer{Type: PeerTypeChannel, ID: decision.ChannelID}
|
|
if err := report.Validate(); err != nil ||
|
|
report.Source != ModerationSourceAntiSpamFalsePositive ||
|
|
report.Target != target ||
|
|
report.CreatedAt.Before(decision.CreatedAt) ||
|
|
len(report.Items) != 1 || len(report.MediaHolds) != 0 {
|
|
return ErrModerationReportInvalid
|
|
}
|
|
item := report.Items[0]
|
|
if item.Kind != ModerationItemAntiSpamDecision ||
|
|
item.Peer != target || item.ItemID != decision.ID ||
|
|
item.SecondaryID != int64(decision.MessageID) ||
|
|
item.AuthorUserID != decision.AuthorUserID ||
|
|
item.EvidenceSchemaVersion != decision.EvidenceSchemaVersion ||
|
|
item.EvidenceHash != decision.EvidenceHash ||
|
|
!bytes.Equal(item.Evidence, decision.Evidence) {
|
|
return ErrModerationReportInvalid
|
|
}
|
|
return nil
|
|
}
|