added messages templates
This commit is contained in:
parent
e8dc967e6a
commit
7cd1f64d0d
29 changed files with 1266 additions and 84 deletions
|
|
@ -12,7 +12,7 @@ func TestServiceIdentityAndLoginMessageUseOwpenGramBrand(t *testing.T) {
|
|||
if serviceUser.FirstName != "OwpenGram" || serviceUser.Username != "" {
|
||||
t.Fatalf("service user = %+v, want OwpenGram identity with no username", serviceUser)
|
||||
}
|
||||
message, err := OfficialLoginCodeMessage(42, "12345", 1)
|
||||
message, err := OfficialLoginCodeMessage(42, "", "12345", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("build login message: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,87 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"telesrv/internal/branding"
|
||||
)
|
||||
|
||||
func officialLoginCodeMessageTemplate() string {
|
||||
return `Login code: %s. Do not give this code to anyone, even if they say they are from ` + branding.ProductName + `!
|
||||
// loginCodeTemplateCodePlaceholder marks where the actual OTP code is
|
||||
// substituted into a (possibly admin-edited) login-code message template.
|
||||
// Unlike the old hardcoded "Login code: %s" format, the template body is no
|
||||
// longer fixed, so the substituted code's bold MessageEntity offset/length
|
||||
// must be computed dynamically from wherever the placeholder actually lands
|
||||
// -- see OfficialLoginCodeMessage. It must appear exactly once in any
|
||||
// template that reaches OfficialLoginCodeMessage (see
|
||||
// ValidateLoginCodeMessageTemplate): zero occurrences would silently drop
|
||||
// the code from the message entirely, and two-or-more is ambiguous about
|
||||
// which occurrence is "the" code.
|
||||
const loginCodeTemplateCodePlaceholder = "{{code}}"
|
||||
|
||||
This code can be used to log in to your ` + branding.ProductName + ` account. We never ask it for anything else.
|
||||
// DefaultLoginCodeMessageTemplate is the built-in, final-fallback copy for
|
||||
// the 777000 login-code delivery message. It is sent for every login code
|
||||
// regardless of delivery channel (phone SMS or email) -- see the
|
||||
// LoginCodeDeliveryStore implementations in internal/store/{postgres,memory},
|
||||
// which all pass the same code through unconditionally, and
|
||||
// internal/app/auth's recordLoginMessage (the new-account bootstrap path).
|
||||
// Unlike DefaultWelcomeMessage{Phone,Email}Template there is only one
|
||||
// template: the message never varies by channel. Supports {{server_name}}
|
||||
// (see RenderWelcomeMessageTemplate) and requires the {{code}} placeholder
|
||||
// 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 is returned when a candidate
|
||||
// login-code message template does not contain the {{code}} placeholder
|
||||
// exactly once -- see ValidateLoginCodeMessageTemplate. The admin-API layer
|
||||
// (cmd/telesrv-admin) must reject a save with this error outright rather
|
||||
// than silently accepting it: a template with zero {{code}} occurrences
|
||||
// would never deliver the actual OTP to the user at all.
|
||||
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 occurrences is a functional break (the OTP
|
||||
// itself would never reach the user), and two-or-more is ambiguous (which
|
||||
// occurrence gets the bold entity and the substitution?) -- both are
|
||||
// rejected outright, never silently patched around by e.g. appending the
|
||||
// code somewhere the admin didn't put it.
|
||||
func ValidateLoginCodeMessageTemplate(template string) error {
|
||||
if strings.Count(template, loginCodeTemplateCodePlaceholder) != 1 {
|
||||
return ErrLoginCodeMessageTemplateMissingCode
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolveLoginCodeMessageTemplate picks the final template body, in order:
|
||||
// an explicit admin-panel override (panelOverride, as stored raw in
|
||||
// identity.Info -- empty means "not configured"), then an explicit env-var
|
||||
// default (envDefault, empty means "not configured"), then the compiled-in
|
||||
// DefaultLoginCodeMessageTemplate. It is a pure function so the precedence
|
||||
// logic can be unit-tested without touching the identity store or config --
|
||||
// those live in internal/app/auth, which resolves this fresh on every
|
||||
// login-code delivery (never cached) so an admin-panel edit takes effect
|
||||
// immediately, mirroring ResolveWelcomeMessageTemplate. Unlike that
|
||||
// resolver there is no per-method branching: every login code, regardless
|
||||
// of delivery channel, uses the same template.
|
||||
//
|
||||
// This does not itself validate the {{code}} placeholder -- callers that
|
||||
// persist an override (the admin API) must call
|
||||
// ValidateLoginCodeMessageTemplate before saving. OfficialLoginCodeMessage
|
||||
// re-validates whatever it resolves to anyway, as defense in depth against
|
||||
// an invalid value that reached here some other way (a hand-edited
|
||||
// identity.json, an out-of-band env var change).
|
||||
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.
|
||||
|
|
@ -23,7 +91,15 @@ type LoginCodeDeliveryRequest struct {
|
|||
UserID int64
|
||||
PhoneCodeHash string
|
||||
Code string
|
||||
Date int
|
||||
// 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
|
||||
|
|
@ -39,12 +115,33 @@ type LoginCodeDeliveryResult struct {
|
|||
// OfficialLoginCodeMessage builds the account-visible incoming message from
|
||||
// Telegram's official notification account. Persistence assigns ID, UID and
|
||||
// Pts atomically.
|
||||
func OfficialLoginCodeMessage(userID int64, code string, date int) (Message, error) {
|
||||
//
|
||||
// template is rendered ({{server_name}} substituted, then {{code}} replaced
|
||||
// with the actual code) and the resulting bold MessageEntity is positioned
|
||||
// dynamically from wherever {{code}} actually landed after substitution --
|
||||
// never assumed from a fixed prefix, since template is admin-editable (see
|
||||
// ValidateLoginCodeMessageTemplate). A template that is empty or fails
|
||||
// validation falls back to DefaultLoginCodeMessageTemplate instead of ever
|
||||
// shipping a message with no code in it.
|
||||
func OfficialLoginCodeMessage(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)
|
||||
}
|
||||
body := fmt.Sprintf(officialLoginCodeMessageTemplate(), code)
|
||||
codeOffset := len("Login code: ")
|
||||
if strings.TrimSpace(template) == "" || ValidateLoginCodeMessageTemplate(template) != nil {
|
||||
template = DefaultLoginCodeMessageTemplate
|
||||
}
|
||||
rendered := RenderWelcomeMessageTemplate(template)
|
||||
idx := strings.Index(rendered, loginCodeTemplateCodePlaceholder)
|
||||
if idx < 0 {
|
||||
// Unreachable in practice: template was just validated (or is the
|
||||
// compiled-in default) to contain the placeholder exactly once, and
|
||||
// {{server_name}} substitution cannot remove or relocate an
|
||||
// unrelated placeholder. Guarded anyway rather than ever ship a
|
||||
// message silently missing its 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},
|
||||
|
|
@ -52,8 +149,7 @@ func OfficialLoginCodeMessage(userID int64, code string, date int) (Message, err
|
|||
Date: date,
|
||||
Body: body,
|
||||
Entities: []MessageEntity{
|
||||
{Type: MessageEntityBold, Offset: 0, Length: len("Login code:")},
|
||||
{Type: MessageEntityBold, Offset: codeOffset, Length: len(code)},
|
||||
{Type: MessageEntityBold, Offset: automaticEntityUTF16Length(rendered[:idx]), Length: automaticEntityUTF16Length(code)},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
132
internal/domain/login_code_delivery_test.go
Normal file
132
internal/domain/login_code_delivery_test.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateLoginCodeMessageTemplate(t *testing.T) {
|
||||
if err := ValidateLoginCodeMessageTemplate("Your code is {{code}}."); err != nil {
|
||||
t.Fatalf("exactly one {{code}} should be valid, got %v", err)
|
||||
}
|
||||
if err := ValidateLoginCodeMessageTemplate("No placeholder here."); !errors.Is(err, ErrLoginCodeMessageTemplateMissingCode) {
|
||||
t.Fatalf("zero occurrences should be rejected, got %v", err)
|
||||
}
|
||||
if err := ValidateLoginCodeMessageTemplate("{{code}} and again {{code}}."); !errors.Is(err, ErrLoginCodeMessageTemplateMissingCode) {
|
||||
t.Fatalf("two occurrences should be rejected, got %v", err)
|
||||
}
|
||||
if err := ValidateLoginCodeMessageTemplate(""); !errors.Is(err, ErrLoginCodeMessageTemplateMissingCode) {
|
||||
t.Fatalf("empty template should be rejected, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveLoginCodeMessageTemplatePrecedence(t *testing.T) {
|
||||
const panel = "panel override {{code}}"
|
||||
const env = "env default {{code}}"
|
||||
|
||||
if got := ResolveLoginCodeMessageTemplate(panel, env); got != panel {
|
||||
t.Fatalf("panel override should win, got %q", got)
|
||||
}
|
||||
if got := ResolveLoginCodeMessageTemplate("", env); got != env {
|
||||
t.Fatalf("env default should win when panel unset, got %q", got)
|
||||
}
|
||||
if got := ResolveLoginCodeMessageTemplate(" ", env); got != env {
|
||||
t.Fatalf("whitespace-only panel override should be treated as unset, got %q", got)
|
||||
}
|
||||
if got := ResolveLoginCodeMessageTemplate("", ""); got != DefaultLoginCodeMessageTemplate {
|
||||
t.Fatalf("built-in default should be the final fallback, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfficialLoginCodeMessageDynamicEntityOffset(t *testing.T) {
|
||||
SetOfficialSystemUserDisplayName("")
|
||||
defer SetOfficialSystemUserDisplayName("")
|
||||
|
||||
// {{code}} is nowhere near a fixed prefix here -- it sits at the end of
|
||||
// a sentence, after other text -- proving the entity offset is computed
|
||||
// from where the placeholder actually landed, not assumed from a
|
||||
// hardcoded "Login code: " prefix the way the old %s-based
|
||||
// implementation did.
|
||||
template := "Please do not share your one-time code, which is: {{code}} -- thanks!"
|
||||
msg, err := OfficialLoginCodeMessage(7, template, "998877", 1700000000)
|
||||
if err != nil {
|
||||
t.Fatalf("OfficialLoginCodeMessage: %v", err)
|
||||
}
|
||||
wantBody := "Please do not share your one-time code, which is: 998877 -- thanks!"
|
||||
if msg.Body != wantBody {
|
||||
t.Fatalf("body = %q, want %q", msg.Body, wantBody)
|
||||
}
|
||||
if len(msg.Entities) != 1 {
|
||||
t.Fatalf("expected exactly one entity, got %d: %+v", len(msg.Entities), msg.Entities)
|
||||
}
|
||||
entity := msg.Entities[0]
|
||||
if entity.Type != MessageEntityBold {
|
||||
t.Fatalf("expected bold entity, got %v", entity.Type)
|
||||
}
|
||||
wantOffset := automaticEntityUTF16Length("Please do not share your one-time code, which is: ")
|
||||
if entity.Offset != wantOffset {
|
||||
t.Fatalf("offset = %d, want %d", entity.Offset, wantOffset)
|
||||
}
|
||||
if entity.Length != automaticEntityUTF16Length("998877") {
|
||||
t.Fatalf("length = %d, want %d", entity.Length, automaticEntityUTF16Length("998877"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfficialLoginCodeMessageOffsetShiftsWithServerNameSubstitution(t *testing.T) {
|
||||
SetOfficialSystemUserDisplayName("A Much Longer Custom Server Name")
|
||||
defer SetOfficialSystemUserDisplayName("")
|
||||
|
||||
// {{server_name}} is substituted BEFORE {{code}}'s position is located,
|
||||
// so a longer server name shifts the code's offset. If the offset math
|
||||
// were still relying on a fixed/original position (e.g. computed
|
||||
// against the raw un-substituted template), this would land on the
|
||||
// wrong text.
|
||||
template := "Server {{server_name}} says your code is {{code}}."
|
||||
msg, err := OfficialLoginCodeMessage(7, template, "42", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("OfficialLoginCodeMessage: %v", err)
|
||||
}
|
||||
wantBody := "Server A Much Longer Custom Server Name says your code is 42."
|
||||
if msg.Body != wantBody {
|
||||
t.Fatalf("body = %q, want %q", msg.Body, wantBody)
|
||||
}
|
||||
wantOffset := automaticEntityUTF16Length("Server A Much Longer Custom Server Name says your code is ")
|
||||
if len(msg.Entities) != 1 || msg.Entities[0].Offset != wantOffset {
|
||||
t.Fatalf("entities = %+v, want single bold entity at offset %d", msg.Entities, wantOffset)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfficialLoginCodeMessageFallsBackWhenTemplateInvalid(t *testing.T) {
|
||||
SetOfficialSystemUserDisplayName("")
|
||||
defer SetOfficialSystemUserDisplayName("")
|
||||
|
||||
// Defense in depth: a template that somehow reaches here without
|
||||
// {{code}} (or with it more than once) must never ship a message
|
||||
// silently missing the actual OTP -- it falls back to the compiled-in
|
||||
// default instead.
|
||||
for _, template := range []string{"", "no placeholder", "{{code}} twice {{code}}"} {
|
||||
msg, err := OfficialLoginCodeMessage(7, template, "13579", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("template %q: OfficialLoginCodeMessage: %v", template, err)
|
||||
}
|
||||
if !strings.Contains(msg.Body, "13579") {
|
||||
t.Fatalf("template %q: fallback body missing code: %q", template, msg.Body)
|
||||
}
|
||||
if len(msg.Entities) != 1 || msg.Entities[0].Length != automaticEntityUTF16Length("13579") {
|
||||
t.Fatalf("template %q: unexpected entities: %+v", template, msg.Entities)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfficialLoginCodeMessageValidation(t *testing.T) {
|
||||
if _, err := OfficialLoginCodeMessage(0, "{{code}}", "12345", 1); !errors.Is(err, ErrLoginCodeDeliveryInvalid) {
|
||||
t.Fatalf("expected invalid user id to be rejected, got %v", err)
|
||||
}
|
||||
if _, err := OfficialLoginCodeMessage(7, "{{code}}", "", 1); !errors.Is(err, ErrLoginCodeDeliveryInvalid) {
|
||||
t.Fatalf("expected empty code to be rejected, got %v", err)
|
||||
}
|
||||
if _, err := OfficialLoginCodeMessage(OfficialSystemUserID, "{{code}}", "12345", 1); !errors.Is(err, ErrLoginCodeDeliveryInvalid) {
|
||||
t.Fatalf("expected system user id to be rejected, got %v", err)
|
||||
}
|
||||
}
|
||||
70
internal/domain/login_welcome_template.go
Normal file
70
internal/domain/login_welcome_template.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package domain
|
||||
|
||||
import "strings"
|
||||
|
||||
// LoginMethod distinguishes the two sign-in channels the login-welcome
|
||||
// message template can be customized per: phone (SMS/app-code) and email
|
||||
// (email-signup accounts, see SignInMethodLabel). There is no third axis --
|
||||
// no signup-vs-signin distinction, no 2FA-vs-not distinction -- because
|
||||
// recordWelcomeMessage's callers never carry more than this.
|
||||
type LoginMethod string
|
||||
|
||||
const (
|
||||
LoginMethodPhone LoginMethod = "phone"
|
||||
LoginMethodEmail LoginMethod = "email"
|
||||
)
|
||||
|
||||
// LoginMethodFromLabel maps SignInMethodLabel's human-readable string back
|
||||
// to a LoginMethod, so callers that already computed the label (for the
|
||||
// {{...}} template's own historical "via %s" wording) don't need to
|
||||
// recompute it from the User a second time.
|
||||
func LoginMethodFromLabel(label string) LoginMethod {
|
||||
if label == "email" {
|
||||
return LoginMethodEmail
|
||||
}
|
||||
return LoginMethodPhone
|
||||
}
|
||||
|
||||
// DefaultWelcomeMessagePhoneTemplate and DefaultWelcomeMessageEmailTemplate
|
||||
// are the built-in, final-fallback copy for the login-notification message
|
||||
// sent from the official system account (777000) on every completed
|
||||
// sign-in. They are deliberately separate strings (not one template with a
|
||||
// substituted method name) so each reads naturally in its own channel.
|
||||
//
|
||||
// {{server_name}} is replaced with the server's current effective display
|
||||
// name (see ResolveWelcomeMessageTemplate / RenderWelcomeMessageTemplate).
|
||||
const (
|
||||
DefaultWelcomeMessagePhoneTemplate = "👋 Welcome to {{server_name}}!\n\nA new sign-in to your account was just completed using your phone number.\n\nIf this was you, no action is needed. If it wasn't, please revoke this session immediately from Settings → Privacy and Security → Active Sessions."
|
||||
|
||||
DefaultWelcomeMessageEmailTemplate = "👋 Welcome to {{server_name}}!\n\nA new sign-in to your account was just completed using your email address.\n\nIf this was you, no action is needed. If it wasn't, please revoke this session immediately from Settings → Privacy and Security → Active Sessions."
|
||||
)
|
||||
|
||||
// ResolveWelcomeMessageTemplate picks the final template body for the given
|
||||
// login method, in order: an explicit admin-panel override (panelOverride,
|
||||
// as stored raw in identity.Info -- empty means "not configured"), then an
|
||||
// explicit env-var default (envDefault, empty means "not configured"), then
|
||||
// the compiled-in default for that method. It is a pure function so the
|
||||
// precedence logic can be unit-tested without touching the identity store
|
||||
// or config -- those live in internal/app/auth, which calls this on every
|
||||
// recordWelcomeMessage invocation (never cached) so an admin-panel edit
|
||||
// takes effect immediately.
|
||||
func ResolveWelcomeMessageTemplate(method LoginMethod, panelOverride, envDefault string) string {
|
||||
if t := strings.TrimSpace(panelOverride); t != "" {
|
||||
return panelOverride
|
||||
}
|
||||
if t := strings.TrimSpace(envDefault); t != "" {
|
||||
return envDefault
|
||||
}
|
||||
if method == LoginMethodEmail {
|
||||
return DefaultWelcomeMessageEmailTemplate
|
||||
}
|
||||
return DefaultWelcomeMessagePhoneTemplate
|
||||
}
|
||||
|
||||
// RenderWelcomeMessageTemplate substitutes the {{server_name}} placeholder
|
||||
// in template with the server's current effective display name. It is a
|
||||
// literal, single-placeholder replacement -- no templating engine, since
|
||||
// there's exactly one substitution to make.
|
||||
func RenderWelcomeMessageTemplate(template string) string {
|
||||
return strings.ReplaceAll(template, "{{server_name}}", officialSystemDisplayName())
|
||||
}
|
||||
57
internal/domain/login_welcome_template_test.go
Normal file
57
internal/domain/login_welcome_template_test.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package domain
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveWelcomeMessageTemplatePrecedence(t *testing.T) {
|
||||
const panel = "panel override"
|
||||
const env = "env default"
|
||||
|
||||
if got := ResolveWelcomeMessageTemplate(LoginMethodPhone, panel, env); got != panel {
|
||||
t.Fatalf("panel override should win, got %q", got)
|
||||
}
|
||||
if got := ResolveWelcomeMessageTemplate(LoginMethodPhone, "", env); got != env {
|
||||
t.Fatalf("env default should win when panel unset, got %q", got)
|
||||
}
|
||||
if got := ResolveWelcomeMessageTemplate(LoginMethodPhone, " ", env); got != env {
|
||||
t.Fatalf("whitespace-only panel override should be treated as unset, got %q", got)
|
||||
}
|
||||
if got := ResolveWelcomeMessageTemplate(LoginMethodPhone, "", ""); got != DefaultWelcomeMessagePhoneTemplate {
|
||||
t.Fatalf("built-in phone default should be the final fallback, got %q", got)
|
||||
}
|
||||
if got := ResolveWelcomeMessageTemplate(LoginMethodEmail, "", ""); got != DefaultWelcomeMessageEmailTemplate {
|
||||
t.Fatalf("built-in email default should be the final fallback, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginMethodFromLabel(t *testing.T) {
|
||||
if LoginMethodFromLabel("email") != LoginMethodEmail {
|
||||
t.Fatal("expected email label to map to LoginMethodEmail")
|
||||
}
|
||||
if LoginMethodFromLabel("phone number") != LoginMethodPhone {
|
||||
t.Fatal("expected phone label to map to LoginMethodPhone")
|
||||
}
|
||||
if LoginMethodFromLabel("") != LoginMethodPhone {
|
||||
t.Fatal("expected unknown label to default to LoginMethodPhone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderWelcomeMessageTemplateSubstitutesServerName(t *testing.T) {
|
||||
SetOfficialSystemUserDisplayName("")
|
||||
defer SetOfficialSystemUserDisplayName("")
|
||||
|
||||
got := RenderWelcomeMessageTemplate("Hello from {{server_name}}!")
|
||||
if got != "Hello from OwpenGram!" {
|
||||
t.Fatalf("expected default branding.ProductName substitution, got %q", got)
|
||||
}
|
||||
|
||||
SetOfficialSystemUserDisplayName("Custom Server")
|
||||
got = RenderWelcomeMessageTemplate("Hello from {{server_name}}!")
|
||||
if got != "Hello from Custom Server!" {
|
||||
t.Fatalf("expected custom display name substitution, got %q", got)
|
||||
}
|
||||
|
||||
// No placeholder present -- must be a no-op.
|
||||
if got := RenderWelcomeMessageTemplate("no placeholder here"); got != "no placeholder here" {
|
||||
t.Fatalf("expected no-op when placeholder absent, got %q", got)
|
||||
}
|
||||
}
|
||||
|
|
@ -124,6 +124,19 @@ func SetOfficialSystemUserDisplayName(name string) {
|
|||
officialSystemUserDisplayName = strings.TrimSpace(name)
|
||||
}
|
||||
|
||||
// officialSystemDisplayName returns the official system account's current
|
||||
// effective display name: the operator's custom override if set via
|
||||
// SetOfficialSystemUserDisplayName, else branding.ProductName. Shared by
|
||||
// OfficialSystemUser (777000's FirstName) and the login-welcome-message
|
||||
// {{server_name}} placeholder (see login_welcome_template.go) so both stay
|
||||
// consistent with each other.
|
||||
func officialSystemDisplayName() string {
|
||||
if officialSystemUserDisplayName != "" {
|
||||
return officialSystemUserDisplayName
|
||||
}
|
||||
return branding.ProductName
|
||||
}
|
||||
|
||||
// botFatherPhotoDCID/Stripped 由 files.Service.SeedBotFatherAvatar 在启动时
|
||||
// 通过 SetBotFatherAvatar 写入一次;写入前 BotFatherUser() 不带头像(PhotoID==0)。
|
||||
var (
|
||||
|
|
@ -189,15 +202,11 @@ func SetVerifyBotAvatar(dcID int, stripped []byte) {
|
|||
// config.ReservedUsernames (which it now is, by default, precisely because
|
||||
// nothing keeps another account from claiming it once this one has none).
|
||||
func OfficialSystemUser() User {
|
||||
name := branding.ProductName
|
||||
if officialSystemUserDisplayName != "" {
|
||||
name = officialSystemUserDisplayName
|
||||
}
|
||||
u := User{
|
||||
ID: OfficialSystemUserID,
|
||||
AccessHash: 6599886787491911851,
|
||||
Phone: "42777",
|
||||
FirstName: name,
|
||||
FirstName: officialSystemDisplayName(),
|
||||
Verified: true,
|
||||
Support: true,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const officialWelcomeMessageTemplate = "👋 Welcome to OwpenGram!\n\nYou just signed in via %s.\n\nIf this wasn't you, revoke this session from \"Settings > Privacy and Security > Active sessions\" immediately."
|
||||
|
||||
// OfficialWelcomeMessage builds the account-visible incoming message sent
|
||||
// from the official system account on every completed sign-in (SignUp and
|
||||
// every subsequent SignIn/SignInWithEmail), regardless of delivery channel.
|
||||
|
|
@ -19,17 +17,23 @@ const officialWelcomeMessageTemplate = "👋 Welcome to OwpenGram!\n\nYou just s
|
|||
// to send unconditionally — it exists to give the account owner (and, on a
|
||||
// self-hosted single-admin server, that's usually also "the admin") a
|
||||
// visible record of every session start.
|
||||
func OfficialWelcomeMessage(userID int64, method string, date int) (Message, error) {
|
||||
method = strings.TrimSpace(method)
|
||||
if userID <= 0 || IsSystemUserID(userID) || method == "" || date < 0 || date > math.MaxInt32 {
|
||||
return Message{}, fmt.Errorf("%w: user=%d method=%q date=%d", ErrLoginCodeDeliveryInvalid, userID, method, date)
|
||||
//
|
||||
// body is the already-resolved, already-{{server_name}}-substituted message
|
||||
// text (see ResolveWelcomeMessageTemplate / RenderWelcomeMessageTemplate in
|
||||
// login_welcome_template.go) -- resolving it requires the identity store and
|
||||
// config, both of which live above this package, so callers (internal/app/auth)
|
||||
// do that and pass the final text in here.
|
||||
func OfficialWelcomeMessage(userID int64, body string, date int) (Message, error) {
|
||||
body = strings.TrimSpace(body)
|
||||
if userID <= 0 || IsSystemUserID(userID) || body == "" || date < 0 || date > math.MaxInt32 {
|
||||
return Message{}, fmt.Errorf("%w: user=%d date=%d", ErrLoginCodeDeliveryInvalid, userID, date)
|
||||
}
|
||||
return Message{
|
||||
OwnerUserID: userID,
|
||||
Peer: Peer{Type: PeerTypeUser, ID: OfficialSystemUserID},
|
||||
From: Peer{Type: PeerTypeUser, ID: OfficialSystemUserID},
|
||||
Date: date,
|
||||
Body: fmt.Sprintf(officialWelcomeMessageTemplate, method),
|
||||
Body: body,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue