feat: sync durable moderation and appeals
This commit is contained in:
parent
e1a95c7318
commit
9f467f4be7
140 changed files with 13730 additions and 316 deletions
131
internal/domain/client_telemetry.go
Normal file
131
internal/domain/client_telemetry.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxClientTelemetrySubjects = 100
|
||||
MaxClientTelemetryPayloadBytes = 64 << 10
|
||||
MaxClientTelemetryEventsPerHour = 1000
|
||||
MaxClientTelemetryEventsPerDay = 10000
|
||||
)
|
||||
|
||||
var (
|
||||
ErrClientTelemetryInvalid = errors.New("client telemetry invalid")
|
||||
ErrClientTelemetryRateLimited = errors.New("client telemetry rate limited")
|
||||
)
|
||||
|
||||
type ClientTelemetryKind string
|
||||
|
||||
const (
|
||||
ClientTelemetryMessageDelivery ClientTelemetryKind = "message_delivery"
|
||||
ClientTelemetryReadMetrics ClientTelemetryKind = "read_metrics"
|
||||
ClientTelemetryMusicListen ClientTelemetryKind = "music_listen"
|
||||
)
|
||||
|
||||
func (k ClientTelemetryKind) Valid() bool {
|
||||
switch k {
|
||||
case ClientTelemetryMessageDelivery, ClientTelemetryReadMetrics,
|
||||
ClientTelemetryMusicListen:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ClientTelemetryEvent is operational product telemetry. It is deliberately
|
||||
// isolated from moderation reports/cases and has TTL-based retention.
|
||||
type ClientTelemetryEvent struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
Kind ClientTelemetryKind
|
||||
Peer Peer
|
||||
SubjectIDs []int64
|
||||
Payload json.RawMessage
|
||||
Fingerprint [sha256.Size]byte
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func NewClientTelemetryEvent(userID int64, kind ClientTelemetryKind, peer Peer, subjectIDs []int64, payload any, createdAt time.Time) (ClientTelemetryEvent, error) {
|
||||
canonicalIDs := append([]int64(nil), subjectIDs...)
|
||||
sort.Slice(canonicalIDs, func(i, j int) bool { return canonicalIDs[i] < canonicalIDs[j] })
|
||||
for i, id := range canonicalIDs {
|
||||
if id <= 0 || (i > 0 && canonicalIDs[i-1] == id) {
|
||||
return ClientTelemetryEvent{}, ErrClientTelemetryInvalid
|
||||
}
|
||||
}
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return ClientTelemetryEvent{}, ErrClientTelemetryInvalid
|
||||
}
|
||||
var object map[string]any
|
||||
if err := json.Unmarshal(raw, &object); err != nil || object == nil {
|
||||
return ClientTelemetryEvent{}, ErrClientTelemetryInvalid
|
||||
}
|
||||
raw, err = json.Marshal(object)
|
||||
if err != nil || len(raw) > MaxClientTelemetryPayloadBytes {
|
||||
return ClientTelemetryEvent{}, ErrClientTelemetryInvalid
|
||||
}
|
||||
event := ClientTelemetryEvent{
|
||||
UserID: userID, Kind: kind, Peer: peer,
|
||||
SubjectIDs: canonicalIDs, Payload: raw, CreatedAt: createdAt.UTC(),
|
||||
}
|
||||
fingerprintInput, err := json.Marshal(struct {
|
||||
Version int
|
||||
UserID int64
|
||||
Kind ClientTelemetryKind
|
||||
Peer Peer
|
||||
SubjectIDs []int64
|
||||
Payload json.RawMessage
|
||||
Minute int64
|
||||
}{
|
||||
Version: 1, UserID: event.UserID, Kind: event.Kind, Peer: event.Peer,
|
||||
SubjectIDs: event.SubjectIDs, Payload: event.Payload,
|
||||
Minute: event.CreatedAt.Truncate(time.Minute).Unix(),
|
||||
})
|
||||
if err != nil {
|
||||
return ClientTelemetryEvent{}, ErrClientTelemetryInvalid
|
||||
}
|
||||
event.Fingerprint = sha256.Sum256(fingerprintInput)
|
||||
if err := event.Validate(); err != nil {
|
||||
return ClientTelemetryEvent{}, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (e ClientTelemetryEvent) Validate() error {
|
||||
if e.ID < 0 || e.UserID <= 0 || !e.Kind.Valid() ||
|
||||
len(e.SubjectIDs) == 0 ||
|
||||
len(e.SubjectIDs) > MaxClientTelemetrySubjects ||
|
||||
len(e.Payload) == 0 || len(e.Payload) > MaxClientTelemetryPayloadBytes ||
|
||||
e.Fingerprint == ([sha256.Size]byte{}) || e.CreatedAt.IsZero() {
|
||||
return ErrClientTelemetryInvalid
|
||||
}
|
||||
if e.Peer.ID == 0 {
|
||||
if e.Peer.Type != "" || e.Kind != ClientTelemetryMusicListen {
|
||||
return ErrClientTelemetryInvalid
|
||||
}
|
||||
} else if !moderationPeerValid(e.Peer) {
|
||||
return ErrClientTelemetryInvalid
|
||||
}
|
||||
for i, id := range e.SubjectIDs {
|
||||
if id <= 0 || (i > 0 && e.SubjectIDs[i-1] >= id) {
|
||||
return ErrClientTelemetryInvalid
|
||||
}
|
||||
}
|
||||
var object map[string]any
|
||||
if err := json.Unmarshal(e.Payload, &object); err != nil || object == nil {
|
||||
return ErrClientTelemetryInvalid
|
||||
}
|
||||
canonical, err := json.Marshal(object)
|
||||
if err != nil || !bytes.Equal(canonical, e.Payload) {
|
||||
return ErrClientTelemetryInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue