fix(messages): sync recognize configured app link entities
This commit is contained in:
parent
35d3908660
commit
c9371c6048
12 changed files with 327 additions and 72 deletions
|
|
@ -4,8 +4,31 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
"telesrv/internal/links"
|
||||
)
|
||||
|
||||
var testDefaultAppLinks = func() links.AppLinkBuilder {
|
||||
appLinks, err := links.NewAppLinkBuilder("telesrv", "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return appLinks
|
||||
}()
|
||||
|
||||
func testAugmentAutoEntities(message string, entities []tg.MessageEntityClass) []tg.MessageEntityClass {
|
||||
return augmentAutoEntities(message, entities, testDefaultAppLinks)
|
||||
}
|
||||
|
||||
func testAugmentAutoEntitiesWithAppLinks(t *testing.T, message string, entities []tg.MessageEntityClass, scheme, base string) []tg.MessageEntityClass {
|
||||
t.Helper()
|
||||
appLinks, err := links.NewAppLinkBuilder(scheme, base)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return augmentAutoEntities(message, entities, appLinks)
|
||||
}
|
||||
|
||||
func TestFirstPreviewableURL(t *testing.T) {
|
||||
t.Run("text-url-entity", func(t *testing.T) {
|
||||
got, ok := firstPreviewableURL("click here", []tg.MessageEntityClass{
|
||||
|
|
@ -74,6 +97,26 @@ func TestFirstPreviewableURL(t *testing.T) {
|
|||
t.Fatalf("ftp URL should be rejected")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("custom-scheme-entity-does-not-preview", func(t *testing.T) {
|
||||
message := "telesrv://resolve?domain=Alice"
|
||||
entities := testAugmentAutoEntities(message, nil)
|
||||
if len(entities) != 1 {
|
||||
t.Fatalf("entities = %d, want 1", len(entities))
|
||||
}
|
||||
if got, ok := firstPreviewableURL(message, entities); ok {
|
||||
t.Fatalf("custom app-link must not become a webpage preview, got %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("custom-scheme-before-http-still-previews-http", func(t *testing.T) {
|
||||
message := "telesrv://resolve?domain=Alice then https://example.com/x"
|
||||
entities := testAugmentAutoEntities(message, nil)
|
||||
got, ok := firstPreviewableURL(message, entities)
|
||||
if !ok || got != "https://example.com/x" {
|
||||
t.Fatalf("got (%q,%v), want https://example.com/x", got, ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func urlEntity(t *testing.T, e tg.MessageEntityClass) *tg.MessageEntityURL {
|
||||
|
|
@ -89,7 +132,7 @@ func urlEntity(t *testing.T, e tg.MessageEntityClass) *tg.MessageEntityURL {
|
|||
// (高亮),UTF-16 偏移正确,多链接全检测,客户端已带 url 实体则不重复。
|
||||
func TestAugmentAutoEntitiesURL(t *testing.T) {
|
||||
t.Run("detect-when-no-client-entities", func(t *testing.T) {
|
||||
got := augmentAutoEntities("see https://example.com/x now", nil)
|
||||
got := testAugmentAutoEntities("see https://example.com/x now", nil)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("entities = %d, want 1", len(got))
|
||||
}
|
||||
|
|
@ -99,7 +142,7 @@ func TestAugmentAutoEntitiesURL(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("utf16-offset-with-emoji", func(t *testing.T) {
|
||||
got := augmentAutoEntities("\U0001f44d https://x", nil) // 👍=2 units, space=1 → url at 3
|
||||
got := testAugmentAutoEntities("\U0001f44d https://x", nil) // 👍=2 units, space=1 → url at 3
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("entities = %d, want 1", len(got))
|
||||
}
|
||||
|
|
@ -108,28 +151,64 @@ func TestAugmentAutoEntitiesURL(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("multiple-urls", func(t *testing.T) {
|
||||
if got := augmentAutoEntities("https://a.com and https://b.com", nil); len(got) != 2 {
|
||||
if got := testAugmentAutoEntities("https://a.com and https://b.com", nil); len(got) != 2 {
|
||||
t.Fatalf("entities = %d, want 2", len(got))
|
||||
}
|
||||
})
|
||||
t.Run("respect-client-url-entity", func(t *testing.T) {
|
||||
ents := []tg.MessageEntityClass{&tg.MessageEntityURL{Offset: 0, Length: 9}}
|
||||
if got := augmentAutoEntities("https://x more https://y", ents); len(got) != 1 {
|
||||
t.Fatalf("client url entity present → no server detection, got %d", len(got))
|
||||
if got := testAugmentAutoEntities("https://x more https://y", ents); len(got) != 2 {
|
||||
t.Fatalf("server should fill the missing URL span, got %d", len(got))
|
||||
}
|
||||
})
|
||||
t.Run("trailing-punct-not-in-entity", func(t *testing.T) {
|
||||
got := augmentAutoEntities("见 https://example.com。", nil) // 见 ...。
|
||||
got := testAugmentAutoEntities("见 https://example.com。", nil) // 见 ...。
|
||||
e := urlEntity(t, got[0])
|
||||
if e.Length != utf16CodeUnitLen("https://example.com") {
|
||||
t.Errorf("length = %d, want %d (trailing 。 excluded)", e.Length, utf16CodeUnitLen("https://example.com"))
|
||||
}
|
||||
})
|
||||
t.Run("no-url-no-entities", func(t *testing.T) {
|
||||
if got := augmentAutoEntities("plain text", nil); len(got) != 0 {
|
||||
if got := testAugmentAutoEntities("plain text", nil); len(got) != 0 {
|
||||
t.Fatalf("entities = %d, want 0", len(got))
|
||||
}
|
||||
})
|
||||
t.Run("configured-custom-scheme", func(t *testing.T) {
|
||||
message := "👍 TELESRV://resolve?domain=Alice。"
|
||||
got := testAugmentAutoEntities(message, nil)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("entities = %d, want 1", len(got))
|
||||
}
|
||||
e := urlEntity(t, got[0])
|
||||
wantURL := "TELESRV://resolve?domain=Alice"
|
||||
if e.Offset != 3 || e.Length != utf16CodeUnitLen(wantURL) {
|
||||
t.Fatalf("offset/length = %d/%d, want 3/%d", e.Offset, e.Length, utf16CodeUnitLen(wantURL))
|
||||
}
|
||||
})
|
||||
t.Run("mixed-client-http-and-missing-custom-scheme", func(t *testing.T) {
|
||||
message := "https://x telesrv://resolve?domain=Alice"
|
||||
client := []tg.MessageEntityClass{&tg.MessageEntityURL{Offset: 0, Length: utf16CodeUnitLen("https://x")}}
|
||||
got := testAugmentAutoEntities(message, client)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("entities = %d, want client HTTP plus server app-link", len(got))
|
||||
}
|
||||
e := urlEntity(t, got[1])
|
||||
if e.Offset != utf16CodeUnitLen("https://x ") || e.Length != utf16CodeUnitLen("telesrv://resolve?domain=Alice") {
|
||||
t.Fatalf("custom entity offset/length = %d/%d", e.Offset, e.Length)
|
||||
}
|
||||
})
|
||||
t.Run("unconfigured-scheme-rejected", func(t *testing.T) {
|
||||
if got := testAugmentAutoEntities("foo://resolve telesrv://resolve", nil); len(got) != 1 {
|
||||
t.Fatalf("entities = %d, want only configured telesrv link", len(got))
|
||||
}
|
||||
})
|
||||
t.Run("host-base-is-exact", func(t *testing.T) {
|
||||
message := "owpg://links.example.test/Alice owpg://other.example.test/Bob telesrv://resolve?domain=Carol"
|
||||
got := testAugmentAutoEntitiesWithAppLinks(t, message, nil, "telesrv", "owpg://links.example.test")
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("entities = %d, want configured host-base and legacy link", len(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkAugmentAutoEntities 量化发送热路径:纯文本(无触发字符)应零分配走快路径短路;
|
||||
|
|
@ -139,14 +218,14 @@ func BenchmarkAugmentAutoEntities(b *testing.B) {
|
|||
msg := "hey everyone, just wanted to share some thoughts about the meeting today and tomorrow"
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = augmentAutoEntities(msg, nil)
|
||||
_ = testAugmentAutoEntities(msg, nil)
|
||||
}
|
||||
})
|
||||
b.Run("with-mention-hashtag-url", func(b *testing.B) {
|
||||
msg := "hi @alice please check #golang docs at https://example.com/x thanks"
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = augmentAutoEntities(msg, nil)
|
||||
_ = testAugmentAutoEntities(msg, nil)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -167,16 +246,16 @@ func mentionAt(t *testing.T, got []tg.MessageEntityClass, off, ln int) {
|
|||
func TestAugmentAutoEntitiesMention(t *testing.T) {
|
||||
t.Run("bare-mentions", func(t *testing.T) {
|
||||
// 对齐官方抓包:纯 "@G0ldenMods\n@NGame_Official" → 两个裸 messageEntityMention,含前导 @。
|
||||
got := augmentAutoEntities("@G0ldenMods\n@NGame_Official", nil)
|
||||
got := testAugmentAutoEntities("@G0ldenMods\n@NGame_Official", nil)
|
||||
mentionAt(t, got, 0, 11) // @G0ldenMods = 10+1
|
||||
mentionAt(t, got, 12, 15) // @NGame_Official = 14+1
|
||||
})
|
||||
t.Run("mention-mid-text", func(t *testing.T) {
|
||||
got := augmentAutoEntities("hi @alice see you", nil)
|
||||
got := testAugmentAutoEntities("hi @alice see you", nil)
|
||||
mentionAt(t, got, 3, 6) // @alice
|
||||
})
|
||||
t.Run("email-not-a-mention", func(t *testing.T) {
|
||||
for _, e := range augmentAutoEntities("mail me at bob@example.com please", nil) {
|
||||
for _, e := range testAugmentAutoEntities("mail me at bob@example.com please", nil) {
|
||||
if _, ok := e.(*tg.MessageEntityMention); ok {
|
||||
t.Fatalf("email local@domain must not yield a mention: %#v", e)
|
||||
}
|
||||
|
|
@ -184,13 +263,13 @@ func TestAugmentAutoEntitiesMention(t *testing.T) {
|
|||
})
|
||||
t.Run("utf16-offset-with-emoji", func(t *testing.T) {
|
||||
// 👍(2 units) + space(1) → @bob 起于 offset 3。
|
||||
got := augmentAutoEntities("\U0001f44d @bob", nil)
|
||||
got := testAugmentAutoEntities("\U0001f44d @bob", nil)
|
||||
mentionAt(t, got, 3, 4)
|
||||
})
|
||||
t.Run("no-overlap-with-client-entity", func(t *testing.T) {
|
||||
// 客户端把 "@bob" 区间标成 textUrl([offset 0,len 4)),服务端不得再补 mention。
|
||||
ents := []tg.MessageEntityClass{&tg.MessageEntityTextURL{Offset: 0, Length: 4, URL: "https://x"}}
|
||||
for _, e := range augmentAutoEntities("@bob", ents) {
|
||||
for _, e := range testAugmentAutoEntities("@bob", ents) {
|
||||
if _, ok := e.(*tg.MessageEntityMention); ok {
|
||||
t.Fatalf("mention must not overlap client entity: %#v", e)
|
||||
}
|
||||
|
|
@ -201,7 +280,7 @@ func TestAugmentAutoEntitiesMention(t *testing.T) {
|
|||
// 裸 URL 路径里的 @scam / #frag 不得被误标成 mention/hashtag(钓鱼风险)。
|
||||
msg := "Docs see https://t.me/@scam and #promo"
|
||||
ents := []tg.MessageEntityClass{&tg.MessageEntityTextURL{Offset: 0, Length: 4, URL: "https://x"}}
|
||||
got := augmentAutoEntities(msg, ents)
|
||||
got := testAugmentAutoEntities(msg, ents)
|
||||
for _, e := range got {
|
||||
if m, ok := e.(*tg.MessageEntityMention); ok {
|
||||
t.Fatalf("mention must not be synthesised inside a raw URL: offset=%d len=%d", m.Offset, m.Length)
|
||||
|
|
@ -220,7 +299,7 @@ func TestAugmentAutoEntitiesMention(t *testing.T) {
|
|||
})
|
||||
t.Run("hashtag", func(t *testing.T) {
|
||||
var found bool
|
||||
for _, e := range augmentAutoEntities("love #golang here", nil) {
|
||||
for _, e := range testAugmentAutoEntities("love #golang here", nil) {
|
||||
if h, ok := e.(*tg.MessageEntityHashtag); ok && h.Offset == 5 && h.Length == 7 {
|
||||
found = true
|
||||
}
|
||||
|
|
@ -230,7 +309,7 @@ func TestAugmentAutoEntitiesMention(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("hashtag-all-digits-skipped", func(t *testing.T) {
|
||||
for _, e := range augmentAutoEntities("number #123 here", nil) {
|
||||
for _, e := range testAugmentAutoEntities("number #123 here", nil) {
|
||||
if _, ok := e.(*tg.MessageEntityHashtag); ok {
|
||||
t.Fatalf("leading-digit hashtag must be skipped: %#v", e)
|
||||
}
|
||||
|
|
@ -238,7 +317,7 @@ func TestAugmentAutoEntitiesMention(t *testing.T) {
|
|||
})
|
||||
t.Run("bot-command", func(t *testing.T) {
|
||||
var found bool
|
||||
for _, e := range augmentAutoEntities("/start@MyBot now", nil) {
|
||||
for _, e := range testAugmentAutoEntities("/start@MyBot now", nil) {
|
||||
if c, ok := e.(*tg.MessageEntityBotCommand); ok && c.Offset == 0 && c.Length == 12 {
|
||||
found = true
|
||||
}
|
||||
|
|
@ -248,7 +327,7 @@ func TestAugmentAutoEntitiesMention(t *testing.T) {
|
|||
}
|
||||
})
|
||||
t.Run("slash-in-path-not-command", func(t *testing.T) {
|
||||
for _, e := range augmentAutoEntities("see and/or maybe", nil) {
|
||||
for _, e := range testAugmentAutoEntities("see and/or maybe", nil) {
|
||||
if _, ok := e.(*tg.MessageEntityBotCommand); ok {
|
||||
t.Fatalf("and/or must not be a bot command: %#v", e)
|
||||
}
|
||||
|
|
@ -256,7 +335,7 @@ func TestAugmentAutoEntitiesMention(t *testing.T) {
|
|||
})
|
||||
t.Run("cashtag", func(t *testing.T) {
|
||||
var found bool
|
||||
for _, e := range augmentAutoEntities("buy $USD now", nil) {
|
||||
for _, e := range testAugmentAutoEntities("buy $USD now", nil) {
|
||||
if c, ok := e.(*tg.MessageEntityCashtag); ok && c.Offset == 4 && c.Length == 4 {
|
||||
found = true
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue