Ctrl K
ComunidadGuías…

internal/domain/bot_verification.go

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

Archivo original ↗Preguntar a KiBot
package domain

import (
	"errors"
	"fmt"
	"strings"
	"time"
	"unicode/utf8"
)

// Third-party bot verification (core.telegram.org/api/bots/verification).
//
// A verifier bot marks a peer with its OWN icon and description. Clients render
// that icon before the name and the description in the profile, which is
// deliberately different from the official checkmark: only the operator grants the
// platform flag (see verification.go), and the two mechanisms never read each
// other's state.
//
// Layer 228 shapes this feature projects onto:
//
//	botVerification#f93cd45c bot_id:long icon:long description:string
//	botVerifierSettings#b0cd6617 flags:# can_modify_custom_description:flags.1?true
//	    icon:long company:string custom_description:flags.0?string
//	bots.setCustomVerification#8b89dfbd flags:# enabled:flags.1?true
//	    bot:flags.0?InputUser peer:InputPeer custom_description:flags.2?string = Bool
//	user#b1b8cc83        bot_verification_icon:flags2.14?long
//	channel#d49f34c6     bot_verification_icon:flags2.13?long
//	userFull#6cbe645     bot_verification:flags2.12?BotVerification
//	channelFull#a04e8d3a bot_verification:flags2.17?BotVerification
//	chatInvite#5c9d3702  bot_verification:flags.13?BotVerification
//	botInfo#4d8a0299     verifier_settings:flags.9?BotVerifierSettings
//
// The icon is a custom emoji document id. Clients resolve it through
// messages.getCustomEmojiDocuments, so an id that names no fetchable document
// renders as nothing at all -- which is why the catalogue is validated against
// real documents rather than accepting arbitrary numbers.
const (
	// MaxVerifierCompanyLength bounds botVerifierSettings.company.
	MaxVerifierCompanyLength = 128
	// MaxCustomVerificationDescriptionLength is the app-configured limit for text
	// supplied by a verifier (bot_verification_description_length_limit).
	MaxCustomVerificationDescriptionLength = 128
	// MaxBotVerificationDescriptionLength bounds the final wire description. It
	// may exceed the custom-input limit because the server-generated fallback
	// includes the organization name.
	MaxBotVerificationDescriptionLength = 1024
	// MaxVerificationIconNameLength bounds an operator-facing catalogue label.
	MaxVerificationIconNameLength = 128
	// MaxCustomVerificationReasonLength bounds an applicant's stated reason.
	MaxCustomVerificationReasonLength = 4096
	// MaxCustomVerificationNoteLength bounds an operator-only note.
	MaxCustomVerificationNoteLength = 8192
	// MaxVerifierGrantReasonLength bounds the reason a bot was made a verifier.
	MaxVerifierGrantReasonLength = 1024
	// MaxCustomVerificationsPerVerifier bounds how many peers one verifier may
	// mark. Verifier status is granted per deployment, not earned per peer, so an
	// unbounded verifier would be an unbounded badge printer.
	MaxCustomVerificationsPerVerifier = 10000
)

var (
	// ErrVerifierNotFound reports a bot without verifier status.
	ErrVerifierNotFound = errors.New("bot verifier settings not found")
	// ErrVerifierForbidden reports a bot that may not verify: no verifier row, or a
	// row the operator disabled. It maps to BOT_VERIFIER_FORBIDDEN.
	ErrVerifierForbidden = errors.New("bot is not allowed to verify peers")
	// ErrVerifierSettingsInvalid rejects a malformed verifier configuration.
	ErrVerifierSettingsInvalid = errors.New("bot verifier settings invalid")
	// ErrVerifierDescriptionForbidden reports a per-peer description supplied by a
	// verifier whose can_modify_custom_description is false.
	ErrVerifierDescriptionForbidden = errors.New("verifier may not set a custom description")
	// ErrVerificationIconNotFound reports an icon missing from the catalogue.
	ErrVerificationIconNotFound = errors.New("verification icon not found")
	// ErrVerificationIconInactive rejects an icon the operator retired.
	ErrVerificationIconInactive = errors.New("verification icon inactive")
	// ErrVerificationIconInvalid rejects a malformed icon record.
	ErrVerificationIconInvalid = errors.New("verification icon invalid")
	// ErrCustomVerificationNotFound reports a peer this verifier has not marked.
	ErrCustomVerificationNotFound = errors.New("custom verification not found")
	// ErrCustomVerificationLimit reports the per-verifier bound.
	ErrCustomVerificationLimit = errors.New("custom verification limit reached")
	// ErrCustomVerificationTargetInvalid rejects an unverifiable peer.
	ErrCustomVerificationTargetInvalid = errors.New("custom verification target invalid")
	// ErrCustomVerificationRequestNotFound reports a missing application.
	ErrCustomVerificationRequestNotFound = errors.New("custom verification request not found")
	// ErrCustomVerificationRequestExists reports a pending application already
	// occupying the (verifier, peer) pair.
	ErrCustomVerificationRequestExists = errors.New("custom verification request already pending")
	// ErrCustomVerificationRequestInvalid rejects a malformed application.
	ErrCustomVerificationRequestInvalid = errors.New("custom verification request invalid")
	// ErrCustomVerificationVersionConflict reports a lost optimistic-locking race.
	ErrCustomVerificationVersionConflict = errors.New("custom verification changed concurrently")
)

// VerificationIcon is one catalogue entry: a custom emoji document usable as a
// verifier's mark.
type VerificationIcon struct {
	ID         int64
	DocumentID int64
	// OwnerBotID is zero for a shared entry and a bot id when the operator
	// reserved the icon for one verifier.
	OwnerBotID int64
	Name       string
	Active     bool
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

// Validate checks the catalogue entry shape.
func (i VerificationIcon) Validate() error {
	if i.DocumentID <= 0 {
		return ErrVerificationIconInvalid
	}
	name := strings.TrimSpace(i.Name)
	if name == "" || utf8.RuneCountInString(name) > MaxVerificationIconNameLength {
		return ErrVerificationIconInvalid
	}
	if i.OwnerBotID < 0 {
		return ErrVerificationIconInvalid
	}
	return nil
}

// UsableBy reports whether a verifier may mark peers with this icon.
func (i VerificationIcon) UsableBy(botID int64) bool {
	return i.Active && (i.OwnerBotID == 0 || i.OwnerBotID == botID)
}

// BotVerifierSettings is a bot's verifier status, projected onto
// botVerifierSettings#b0cd6617 and consulted by bots.setCustomVerification.
type BotVerifierSettings struct {
	BotID              int64
	IconDocumentID     int64
	CompanyName        string
	DefaultDescription string
	// CanModifyCustomDescription mirrors flags.1 of the TL constructor: when false
	// the verifier may only apply DefaultDescription.
	CanModifyCustomDescription bool
	// Enabled is the operator's kill switch. A disabled verifier keeps its row and
	// its granted marks but can no longer mark anything new, and the settings stop
	// being projected into botInfo.
	Enabled     bool
	GrantedBy   string
	GrantReason string
	CreatedAt   time.Time
	UpdatedAt   time.Time
	Version     int64
}

// Validate checks the verifier configuration.
func (s BotVerifierSettings) Validate() error {
	if s.BotID <= 0 || s.IconDocumentID <= 0 {
		return ErrVerifierSettingsInvalid
	}
	company := strings.TrimSpace(s.CompanyName)
	if company == "" || utf8.RuneCountInString(company) > MaxVerifierCompanyLength {
		return ErrVerifierSettingsInvalid
	}
	if utf8.RuneCountInString(s.DefaultDescription) > MaxCustomVerificationDescriptionLength {
		return ErrVerifierSettingsInvalid
	}
	if len(s.GrantedBy) > 128 || utf8.RuneCountInString(s.GrantReason) > MaxVerifierGrantReasonLength {
		return ErrVerifierSettingsInvalid
	}
	return nil
}

// DescriptionFor resolves the description a mark carries: the verifier-supplied
// one when allowed, the configured default, or the protocol-defined generated
// fallback.
func (s BotVerifierSettings) DescriptionFor(custom string) (string, error) {
	custom = strings.TrimSpace(custom)
	if custom == "" {
		if fallback := strings.TrimSpace(s.DefaultDescription); fallback != "" {
			return fallback, nil
		}
		return fmt.Sprintf(`Was verified by organization "%s"`, strings.TrimSpace(s.CompanyName)), nil
	}
	if !s.CanModifyCustomDescription {
		return "", ErrVerifierDescriptionForbidden
	}
	if utf8.RuneCountInString(custom) > MaxCustomVerificationDescriptionLength {
		return "", ErrCustomVerificationRequestInvalid
	}
	return custom, nil
}

// CustomVerification is one granted third-party mark.
type CustomVerification struct {
	ID            int64
	VerifierBotID int64
	Peer          Peer
	// IconDocumentID is denormalised at grant time so the mark keeps rendering the
	// icon it was granted with even after the verifier changes its own.
	IconDocumentID  int64
	Description     string
	GrantedByUserID int64
	CreatedAt       time.Time
	UpdatedAt       time.Time
	Version         int64
}

// BotVerification is the TL projection (botVerification#f93cd45c).
type BotVerification struct {
	BotID       int64
	Icon        int64
	Description string
}

// Projection returns the botVerification payload for full peer objects.
func (v CustomVerification) Projection() BotVerification {
	return BotVerification{
		BotID:       v.VerifierBotID,
		Icon:        v.IconDocumentID,
		Description: v.Description,
	}
}

// Validate checks the mark shape.
func (v CustomVerification) Validate() error {
	if v.VerifierBotID <= 0 || v.IconDocumentID <= 0 {
		return ErrCustomVerificationTargetInvalid
	}
	if !validCustomVerificationPeer(v.Peer) {
		return ErrCustomVerificationTargetInvalid
	}
	if utf8.RuneCountInString(v.Description) > MaxBotVerificationDescriptionLength {
		return ErrCustomVerificationRequestInvalid
	}
	return nil
}

func validCustomVerificationPeer(peer Peer) bool {
	switch peer.Type {
	case PeerTypeUser, PeerTypeChannel:
		return peer.ID > 0
	default:
		return false
	}
}

// CustomVerificationRequestStatus is the application lifecycle in front of a mark.
type CustomVerificationRequestStatus string

const (
	CustomVerificationPending  CustomVerificationRequestStatus = "pending"
	CustomVerificationApproved CustomVerificationRequestStatus = "approved"
	CustomVerificationRejected CustomVerificationRequestStatus = "rejected"
	CustomVerificationRevoked  CustomVerificationRequestStatus = "revoked"
)

// Valid reports whether the status is modelled.
func (s CustomVerificationRequestStatus) Valid() bool {
	switch s {
	case CustomVerificationPending, CustomVerificationApproved,
		CustomVerificationRejected, CustomVerificationRevoked:
		return true
	default:
		return false
	}
}

// CanTransitionCustomVerificationStatus is the status machine. A revoked mark is
// reached only from approved, which keeps "revoked" meaning "was verified once".
func CanTransitionCustomVerificationStatus(from, to CustomVerificationRequestStatus) bool {
	if !from.Valid() || !to.Valid() || from == to {
		return false
	}
	switch from {
	case CustomVerificationPending:
		return to == CustomVerificationApproved || to == CustomVerificationRejected
	case CustomVerificationApproved:
		return to == CustomVerificationRevoked
	default:
		return false
	}
}

// CustomVerificationRequest is an application filed with a verifier bot.
type CustomVerificationRequest struct {
	ID                   int64
	VerifierBotID        int64
	ApplicantUserID      int64
	Peer                 Peer
	PeerTitle            string
	PeerUsername         string
	Reason               string
	RequestedDescription string
	Status               CustomVerificationRequestStatus
	DecidedBy            string
	DecisionReason       string
	InternalNote         string
	CorrelationID        string
	CreatedAt            time.Time
	UpdatedAt            time.Time
	ApprovedAt           time.Time
	RejectedAt           time.Time
	Version              int64
}

// Validate checks the application shape.
func (r CustomVerificationRequest) Validate() error {
	if r.VerifierBotID <= 0 || r.ApplicantUserID <= 0 || !validCustomVerificationPeer(r.Peer) {
		return ErrCustomVerificationRequestInvalid
	}
	if !r.Status.Valid() {
		return ErrCustomVerificationRequestInvalid
	}
	if utf8.RuneCountInString(r.Reason) > MaxCustomVerificationReasonLength ||
		utf8.RuneCountInString(r.RequestedDescription) > MaxCustomVerificationDescriptionLength ||
		utf8.RuneCountInString(r.InternalNote) > MaxCustomVerificationNoteLength {
		return ErrCustomVerificationRequestInvalid
	}
	if r.Status == CustomVerificationRejected && strings.TrimSpace(r.DecisionReason) == "" {
		return ErrVerificationReasonRequired
	}
	return nil
}

// SetCustomVerificationRequest is the bots.setCustomVerification payload after the
// RPC edge has resolved the caller, the verifier bot and the target peer.
type SetCustomVerificationRequest struct {
	VerifierBotID int64
	Peer          Peer
	// Enabled false revokes the mark this verifier granted; the request then
	// carries no description.
	Enabled bool
	// CustomDescription is the verifier-supplied per-peer text. It is only honoured
	// when the verifier's settings allow it.
	CustomDescription string
	// CallerUserID is the account that invoked the RPC: the bot itself or its owner.
	CallerUserID int64
}

// Validate checks the request shape without consulting stored state.
func (r SetCustomVerificationRequest) Validate() error {
	if r.VerifierBotID <= 0 || !validCustomVerificationPeer(r.Peer) {
		return ErrCustomVerificationTargetInvalid
	}
	if utf8.RuneCountInString(r.CustomDescription) > MaxCustomVerificationDescriptionLength {
		return ErrCustomVerificationRequestInvalid
	}
	if !r.Enabled && strings.TrimSpace(r.CustomDescription) != "" {
		// Revocation with a description is a caller bug worth reporting rather than
		// silently ignoring: it usually means enabled was left unset by mistake.
		return ErrCustomVerificationRequestInvalid
	}
	return nil
}

// CustomVerificationFilter bounds an admin listing query.
type CustomVerificationFilter struct {
	VerifierBotID int64
	PeerType      PeerType
	PeerID        int64
	Query         string
	BeforeID      int64
	Limit         int
}

// CustomVerificationRequestFilter bounds a review-queue query.
type CustomVerificationRequestFilter struct {
	Statuses      []CustomVerificationRequestStatus
	VerifierBotID int64
	PeerType      PeerType
	Query         string
	BeforeID      int64
	Limit         int
}