Ctrl K
ComunidadGuías…

internal/domain/login_code_delivery.go

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

Archivo original ↗Preguntar a KiBot
package domain

import (
	"errors"
	"fmt"
	"math"
	"strings"
)

// loginCodeTemplateCodePlaceholder marks the position in a login-code message template that is replaced with the real verification code.
// The template body is no longer the old hard-coded "Login code: %s", so the bold MessageEntity
// offset/length of the code must be computed dynamically from the placeholder's actual position (see OfficialLoginCodeMessage).
// Any template entering OfficialLoginCodeMessage must contain this placeholder exactly once
// (see ValidateLoginCodeMessageTemplate): zero occurrences silently lose the code, and two or more
// make it impossible to tell which one is the "real verification code".
const loginCodeTemplateCodePlaceholder = "{{code}}"

// DefaultLoginCodeMessageTemplate is the built-in final fallback text for the 777000 login-code delivery message.
// All login codes (SMS/email) share the same template, which does not vary by delivery channel. It supports the {{server_name}}
// placeholder (see RenderWelcomeMessageTemplate) and must contain {{code}} exactly once
// (see ValidateLoginCodeMessageTemplate).
const DefaultLoginCodeMessageTemplate = `Login code: {{code}}. Do not give this code to anyone, even if they say they are from {{server_name}}!

This code can be used to log in to your {{server_name}} account. We never ask it for anything else.

If you didn't request this code by trying to log in on another device, simply ignore this message.`

// ErrLoginCodeMessageTemplateMissingCode means a candidate login-code template does not contain
// the {{code}} placeholder exactly once. The admin API layer must reject such a save outright rather than accept it silently:
// zero occurrences mean the real verification code would never be delivered to the user.
var ErrLoginCodeMessageTemplateMissingCode = errors.New("login code message template must contain the {{code}} placeholder exactly once")

// ValidateLoginCodeMessageTemplate requires the {{code}} placeholder to appear exactly once.
// Zero is functionally broken (the code itself never reaches the user), and two or more is ambiguous
// (which one is replaced and bolded?) — both are rejected outright, never silently patched.
func ValidateLoginCodeMessageTemplate(template string) error {
	if strings.Count(template, loginCodeTemplateCodePlaceholder) != 1 {
		return ErrLoginCodeMessageTemplateMissingCode
	}
	return nil
}

// ResolveLoginCodeMessageTemplate picks the final template body by priority: an explicit admin-panel override
// (panelOverride, stored verbatim in identity.Info, empty means unset) → an explicit environment-variable default
// (envDefault, empty means unset) → the built-in DefaultLoginCodeMessageTemplate. A pure function,
// for dependency-free testing of its priority without depending on the identity store / config. The caller (internal/app/auth)
// resolves it live before each delivery (never cached), so admin-panel edits take effect immediately — consistent with
// ResolveWelcomeMessageTemplate's semantics. The login-code template does not branch by channel.
//
// This function itself does not validate the {{code}} placeholder — the caller that persists an override (the admin API) must
// call ValidateLoginCodeMessageTemplate before saving. OfficialLoginCodeMessage validates the
// resolved result again, as defense in depth against illegal values arriving by other routes (hand-edited identity.json,
// out-of-range environment variables).
func ResolveLoginCodeMessageTemplate(panelOverride, envDefault string) string {
	if t := strings.TrimSpace(panelOverride); t != "" {
		return panelOverride
	}
	if t := strings.TrimSpace(envDefault); t != "" {
		return envDefault
	}
	return DefaultLoginCodeMessageTemplate
}

// LoginCodeDeliveryRequest describes one durable 777000 login-code delivery.
// PhoneCodeHash is an opaque idempotency token and must never be persisted in
// plaintext; store implementations persist only its SHA-256 digest.
type LoginCodeDeliveryRequest struct {
	UserID        int64
	PhoneCodeHash string
	Code          string
	// Template is the already-resolved login-code message template (see
	// ResolveLoginCodeMessageTemplate) -- resolving it requires the identity
	// store and config, both of which live above internal/store, so callers
	// (internal/app/auth) do that and pass the final template text in here,
	// the same division of responsibility OfficialWelcomeMessage's body
	// parameter uses. Empty falls back to DefaultLoginCodeMessageTemplate
	// (see OfficialLoginCodeMessage).
	Template string
	Date     int
	// ExpiresAt is the unix second after which the compact idempotency receipt
	// may be reclaimed. It must cover the corresponding code's usable lifetime.
	ExpiresAt int64
}

// LoginCodeDeliveryResult returns the immutable first delivery. Created is
// false when the same phone_code_hash was already committed and replayed.
type LoginCodeDeliveryResult struct {
	Message Message
	Created bool
}

// OfficialLoginCodeMessage builds the account-visible incoming message from
// Telegram's official notification account. Persistence assigns ID, UID and
// Pts atomically.
//
// The template is rendered first ({{server_name}} replaced, then {{code}} replaced with the real code),
// and the bold MessageEntity is located dynamically from the actual position of {{code}} after replacement — never assuming a fixed prefix,
// because the template can be edited by an admin (see ValidateLoginCodeMessageTemplate). When the template is empty or
// fails validation it falls back to DefaultLoginCodeMessageTemplate and never delivers a message with no code.
func OfficialLoginCodeMessage(userID int64, template, code string, date int) (Message, error) {
	return OfficialLoginCodeMessageWithTemplate(userID, template, code, date)
}

// SnapshotLoginCodeMessageTemplate resolves the brand and fallback at the first
// delivery. The returned template still contains {{code}}, so a compact
// receipt can reproduce the original body without storing the secret code.
func SnapshotLoginCodeMessageTemplate(template string) string {
	if strings.TrimSpace(template) == "" || ValidateLoginCodeMessageTemplate(template) != nil {
		template = DefaultLoginCodeMessageTemplate
	}
	rendered := RenderWelcomeMessageTemplate(template)
	if ValidateLoginCodeMessageTemplate(rendered) != nil {
		rendered = RenderWelcomeMessageTemplate(DefaultLoginCodeMessageTemplate)
		if ValidateLoginCodeMessageTemplate(rendered) != nil {
			return strings.ReplaceAll(DefaultLoginCodeMessageTemplate, "{{server_name}}", "Telesrv")
		}
	}
	return rendered
}

func OfficialLoginCodeMessageWithTemplate(userID int64, template, code string, date int) (Message, error) {
	if userID <= 0 || IsSystemUserID(userID) || strings.TrimSpace(code) == "" || len(code) > 64 || date < 0 || date > math.MaxInt32 {
		return Message{}, fmt.Errorf("%w: user=%d code_length=%d date=%d", ErrLoginCodeDeliveryInvalid, userID, len(code), date)
	}
	rendered := SnapshotLoginCodeMessageTemplate(template)
	idx := strings.Index(rendered, loginCodeTemplateCodePlaceholder)
	if idx < 0 {
		// Actually unreachable: the template was just validated (or is the built-in default) guaranteeing the placeholder exactly once,
		// and the {{server_name}} replacement never removes or moves an unrelated placeholder. Still defensive,
		// never delivering a message that silently lost the code.
		rendered = DefaultLoginCodeMessageTemplate
		idx = strings.Index(rendered, loginCodeTemplateCodePlaceholder)
	}
	body := rendered[:idx] + code + rendered[idx+len(loginCodeTemplateCodePlaceholder):]
	return Message{
		OwnerUserID: userID,
		Peer:        Peer{Type: PeerTypeUser, ID: OfficialSystemUserID},
		From:        Peer{Type: PeerTypeUser, ID: OfficialSystemUserID},
		Date:        date,
		Body:        body,
		Entities: []MessageEntity{
			{Type: MessageEntityBold, Offset: automaticEntityUTF16Length(rendered[:idx]), Length: automaticEntityUTF16Length(code)},
		},
	}, nil
}