From 233a2399e95bcf0f3ae59aac96989f11ab30e80b Mon Sep 17 00:00:00 2001 From: Astra Date: Tue, 8 Sep 2026 16:29:37 +0100 Subject: [PATCH] welcome message: link @zio with a mention entity --- internal/domain/welcome_message.go | 43 ++++++++++++++++++++++--- internal/domain/welcome_message_test.go | 29 +++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 internal/domain/welcome_message_test.go diff --git a/internal/domain/welcome_message.go b/internal/domain/welcome_message.go index 9fea9acc..b66ac942 100644 --- a/internal/domain/welcome_message.go +++ b/internal/domain/welcome_message.go @@ -6,7 +6,12 @@ import ( "strings" ) -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.\n\nIf you haven't already, feel free to join @zio for all the latest updates!" +// officialUpdatesChannelMention is the public @username of the updates channel +// linked from the welcome message. It is carried both in the template text and +// in a MessageEntityMention so clients render it as a tappable link. +const officialUpdatesChannelMention = "@zio" + +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.\n\nIf you haven't already, feel free to join " + officialUpdatesChannelMention + " for all the latest updates!" // OfficialWelcomeMessage builds the account-visible incoming message sent // from the official system account on every completed sign-in (SignUp and @@ -20,13 +25,43 @@ func OfficialWelcomeMessage(userID int64, method string, date int) (Message, err 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) } - return Message{ + body := fmt.Sprintf(officialWelcomeMessageTemplate, method) + msg := Message{ OwnerUserID: userID, Peer: Peer{Type: PeerTypeUser, ID: OfficialSystemUserID}, From: Peer{Type: PeerTypeUser, ID: OfficialSystemUserID}, Date: date, - Body: fmt.Sprintf(officialWelcomeMessageTemplate, method), - }, nil + Body: body, + } + if ent, ok := usernameMentionEntity(body, officialUpdatesChannelMention); ok { + msg.Entities = []MessageEntity{ent} + } + return msg, nil +} + +// usernameMentionEntity locates mention ("@name") in text and returns a +// MessageEntityMention spanning it, with the UTF-16 offset/length clients expect. +func usernameMentionEntity(text, mention string) (MessageEntity, bool) { + before, _, found := strings.Cut(text, mention) + if !found { + return MessageEntity{}, false + } + return MessageEntity{ + Type: MessageEntityMention, + Offset: utf16Len(before), + Length: utf16Len(mention), + }, true +} + +func utf16Len(s string) int { + n := 0 + for _, r := range s { + n++ + if r > 0xffff { + n++ + } + } + return n } // SignInMethodLabel returns the human-readable method name embedded in diff --git a/internal/domain/welcome_message_test.go b/internal/domain/welcome_message_test.go new file mode 100644 index 00000000..ea5c0d87 --- /dev/null +++ b/internal/domain/welcome_message_test.go @@ -0,0 +1,29 @@ +package domain + +import ( + "testing" + "unicode/utf16" +) + +func TestOfficialWelcomeMessageLinksUpdatesChannel(t *testing.T) { + for _, method := range []string{"phone number", "email"} { + msg, err := OfficialWelcomeMessage(1780243200, method, 1_700_000_000) + if err != nil { + t.Fatalf("method %q: %v", method, err) + } + if len(msg.Entities) != 1 { + t.Fatalf("method %q: entities = %+v, want one mention", method, msg.Entities) + } + ent := msg.Entities[0] + if ent.Type != MessageEntityMention { + t.Fatalf("entity type = %q, want mention", ent.Type) + } + units := utf16.Encode([]rune(msg.Body)) + if ent.Offset < 0 || ent.Length <= 0 || ent.Offset+ent.Length > len(units) { + t.Fatalf("entity %+v out of bounds for body of %d utf16 units", ent, len(units)) + } + if got := string(utf16.Decode(units[ent.Offset : ent.Offset+ent.Length])); got != officialUpdatesChannelMention { + t.Fatalf("entity spans %q, want %q", got, officialUpdatesChannelMention) + } + } +}