Ctrl K
ComunidadGuías…

internal/domain/fact_check.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package domain

import (
	"crypto/sha256"
	"encoding/binary"
	"encoding/json"
	"strings"
)

// FactCheck is a fact-check tag (factCheck). The constructor together with messages.editFactCheck /
// messages.deleteFactCheck / messages.getFactCheck has existed in the protocol since Layer 225;
// it merely had no server implementation before.
//
// The client offers an edit entry only on media messages in a broadcast channel, and whether that entry
// appears is decided by the app config's global can_edit_factcheck switch; whether a write is allowed is adjudicated
// server-side per channel permission. The tag hangs on the message itself, so the same message is the same fact from every subscriber's view.
type FactCheck struct {
	// NeedCheck means the message has not been checked yet (the client renders a "pending check" state).
	NeedCheck bool
	// Country is the country of the fact-checking organization, ISO 3166-1 alpha-2; empty when unknown.
	Country string
	// Text is the verdict text.
	Text string
	// Entities are the rich-text entities of Text (text_url/mention/...).
	Entities []MessageEntity
	// Hash is a stable identifier: the same full verdict on the same message always gets the same hash,
	// which the client uses to tell "has this fact check been changed".
	Hash int64
}

// MaxFactCheckTextLength is the character limit of the verdict text, matching the client's
// default factcheck_length_limit.
const MaxFactCheckTextLength = 1024

// ValidateFactCheck checks whether a fact-check tag can be persisted.
// An empty text means deletion and is not allowed as a save action.
func ValidateFactCheck(check FactCheck) error {
	if strings.TrimSpace(check.Text) == "" {
		return ErrFactCheckInvalid
	}
	if len([]rune(check.Text)) > MaxFactCheckTextLength {
		return ErrFactCheckTooLong
	}
	if check.Country != "" && !isAlpha2Country(check.Country) {
		return ErrFactCheckInvalid
	}
	return nil
}

func isAlpha2Country(country string) bool {
	if len(country) != 2 {
		return false
	}
	for _, r := range country {
		if r < 'A' || r > 'Z' {
			return false
		}
	}
	return true
}

// FactCheckHash covers every visible field, including link targets and formatting.
// Only fixed primitive fields are encoded; Hash itself is deliberately excluded.
func FactCheckHash(channelID int64, messageID int, check FactCheck) int64 {
	entities := append([]MessageEntity{}, check.Entities...)
	payload, _ := json.Marshal(struct {
		ChannelID int64
		MessageID int
		NeedCheck bool
		Country   string
		Text      string
		Entities  []MessageEntity
	}{channelID, messageID, check.NeedCheck, check.Country, check.Text, entities})
	sum := sha256.Sum256(payload)
	hash := int64(binary.BigEndian.Uint64(sum[:8]) &^ (1 << 63))
	if hash == 0 {
		hash = 1
	}
	return hash
}

func FactChecksEqual(a, b *FactCheck) bool {
	if a == nil || b == nil {
		return a == b
	}
	if a.Text != b.Text || a.Country != b.Country || a.NeedCheck != b.NeedCheck || len(a.Entities) != len(b.Entities) {
		return false
	}
	for i := range a.Entities {
		if a.Entities[i] != b.Entities[i] {
			return false
		}
	}
	return true
}

// EditFactCheckRequest is the business request to write one fact check.
type EditFactCheckRequest struct {
	UserID    int64
	ChannelID int64
	MessageID int
	// An empty Text means delete the fact check.
	Text      string
	Entities  []MessageEntity
	NeedCheck bool
	Country   string
	Date      int
}

// EditFactCheckResult returns the message snapshot and tag after the write.
type EditFactCheckResult struct {
	Channel    Channel
	Message    ChannelMessage
	Event      ChannelUpdateEvent
	FactCheck  *FactCheck
	Recipients []int64
}

// FactCheckList is the result set of messages.getFactCheck, in request order.
type FactCheckList struct {
	FactChecks []FactCheck
}

// FactChecksForMessages projects an already-authorized message batch without
// shifting positions when a message is missing, deleted or has no verdict.
func FactChecksForMessages(messages []ChannelMessage, ids []int) FactCheckList {
	byID := make(map[int]*FactCheck, len(messages))
	for _, msg := range messages {
		if !msg.Deleted && msg.Action == nil && msg.FactCheck != nil {
			byID[msg.ID] = msg.FactCheck
		}
	}
	out := FactCheckList{FactChecks: make([]FactCheck, len(ids))}
	for i, id := range ids {
		if check := byID[id]; check != nil {
			out.FactChecks[i] = *check
			out.FactChecks[i].Entities = append([]MessageEntity(nil), check.Entities...)
		}
	}
	return out
}