move gotd fork into repo. (#111)

- update to latest telegram layer
- remove some references to fields in tg.Entities that don't exist in
the schema
- originally added here:
https://github.com/beeper/td/commit/820929062a2ba0104397bc01235ab58a9cff780e
  - referenced here
-
https://github.com/mautrix/telegramgo/commit/124f0967ed195b5a380c9bd02e170ada9710dde3
-
https://github.com/mautrix/telegramgo/commit/4205047aab2e0639217148b5d125bfaab668bd8e
This commit is contained in:
Adam Van Ymeren
2025-06-27 20:03:37 -07:00
committed by GitHub
parent 0952df0244
commit 7a04f298d2
19264 changed files with 1539697 additions and 84 deletions
+2
View File
@@ -0,0 +1,2 @@
// Package entity contains message formatting and styling helpers.
package entity
+150
View File
@@ -0,0 +1,150 @@
package entity
import (
"reflect"
"sort"
"strings"
"unicode"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
// SortEntities sorts entities as TDLib does it.
func SortEntities(entity []tg.MessageEntityClass) {
sort.Sort(entitySorter(entity))
}
type entitySorter []tg.MessageEntityClass
func (e entitySorter) Len() int {
return len(e)
}
func (e entitySorter) Less(i, j int) bool {
a, b := e[i], e[j]
return a.GetOffset() < b.GetOffset() ||
a.GetLength() > b.GetLength()
}
func (e entitySorter) Swap(i, j int) {
e[i], e[j] = e[j], e[i]
}
// setLength sets Length field of entity.
func setLength(index, value int, slice []tg.MessageEntityClass) {
reflect.ValueOf(&slice[index]).
Elem().Elem().Elem().
FieldByName("Length").
SetInt(int64(value))
}
// fixEntities trims space, if needed and fixes entities offsets.
func (b *Builder) fixEntities(msg string, entities []tg.MessageEntityClass) (string, []tg.MessageEntityClass) {
// If there are no entities or last text block does not have entities,
// so we just return built message.
if len(b.lengths) == 0 || b.lastFormatIndex >= len(entities) {
return msg, entities
}
// Since Telegram client does not handle space after formatted message
// we should compute length of the last block to trim it.
// Get first entity of last text block.
entity := b.lengths[len(b.lengths)-1]
offset := entity.offset
length := entity.length
// Get last text block.
lastBlock := msg[offset:]
// Trim this block.
trimmed := strings.TrimRightFunc(lastBlock, unicode.IsSpace)
// If there are a difference, we should change length of the all entities.
if length >= len(lastBlock) && len(trimmed) != len(lastBlock) {
length := ComputeLength(trimmed)
for idx := range entities[b.lastFormatIndex:] {
setLength(idx, length, entities[b.lastFormatIndex:])
}
msg = msg[:offset+len(trimmed)]
}
return msg, entities
}
// Raw returns raw result and resets builder without fixing spaces.
func (b *Builder) Raw() (string, []tg.MessageEntityClass) {
msg := b.message.String()
entities := b.entities
b.Reset()
return msg, entities
}
// Complete returns build result and resets builder.
func (b *Builder) Complete() (string, []tg.MessageEntityClass) {
msg, entities := b.Raw()
defer SortEntities(entities)
return b.fixEntities(msg, entities)
}
// ShrinkPreCode merges following <pre> and <code> entities, if needed.
//
// This function is used by formatters to be compliant with TDLib.
func (b *Builder) ShrinkPreCode() {
b.entities = shrinkPreCode(b.entities)
}
// equalRange compares ranges of given entities.
func equalRange(a, b tg.MessageEntityClass) bool {
return a.GetLength() == b.GetLength() && a.GetOffset() == b.GetOffset()
}
// shrinkPreCode merges following <pre> and <code> entities, if needed.
func shrinkPreCode(entities []tg.MessageEntityClass) []tg.MessageEntityClass {
for i, j := 0, len(entities)-1; i < j; i, j = i+1, j-1 {
entities[i], entities[j] = entities[j], entities[i]
}
filter := func(keep func(prev, cur tg.MessageEntityClass) bool) []tg.MessageEntityClass {
n := 0
for i, val := range entities {
if i == 0 || keep(entities[i-1], val) {
entities[n] = val
n++
}
}
return entities[:n]
}
isPreCode := func(class tg.MessageEntityClass) bool {
typeID := class.TypeID()
return typeID == tg.MessageEntityCodeTypeID || typeID == tg.MessageEntityPreTypeID
}
hasLang := func(class tg.MessageEntityClass) bool {
pre, ok := class.(*tg.MessageEntityPre)
return ok && pre.Language != ""
}
resetLang := func(class tg.MessageEntityClass) {
pre, ok := class.(*tg.MessageEntityPre)
if !ok {
return
}
pre.Language = ""
}
return filter(func(prev, cur tg.MessageEntityClass) bool {
if !isPreCode(prev) ||
!isPreCode(cur) ||
prev.TypeID() == cur.TypeID() {
// Keep if not is Pre/Code entities or if they are same.
return true
}
if !equalRange(prev, cur) {
resetLang(prev)
resetLang(cur)
return true
}
return !hasLang(prev)
})
}
@@ -0,0 +1,98 @@
package entity
import (
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestEnsureTrim(t *testing.T) {
a := require.New(t)
prefix := "pre"
expected := "abc\nabc"
b := Builder{}
b.Plain(prefix)
b.Format(expected+"\n\n\n", Bold(), Italic())
msg, ent := b.Complete()
a.Equal(prefix+expected, msg)
a.Equal([]tg.MessageEntityClass{
&tg.MessageEntityBold{
Offset: len(prefix),
Length: ComputeLength(expected),
},
&tg.MessageEntityItalic{
Offset: len(prefix),
Length: ComputeLength(expected),
},
}, ent)
}
func TestComplete(t *testing.T) {
tests := []struct {
name string
format func(e *Builder)
msg string
entities []tg.MessageEntityClass
}{
{"PlainBold", func(e *Builder) {
e.Plain("plain").Bold("bold")
}, "plainbold", []tg.MessageEntityClass{
&tg.MessageEntityBold{
Offset: ComputeLength("plain"),
Length: ComputeLength("bold"),
},
}},
{"PlainBoldAndStrike", func(e *Builder) {
e.Plain("plain").Format("10\n\n\n\n", Bold(), Strike())
}, "plain10", []tg.MessageEntityClass{
&tg.MessageEntityBold{
Offset: ComputeLength("plain"),
Length: ComputeLength("10"),
},
&tg.MessageEntityStrike{
Offset: ComputeLength("plain"),
Length: ComputeLength("10"),
},
}},
{"BoldPlainBold", func(e *Builder) {
e.Bold("bold").Plain("plain").Bold("bold2\n\n\n\n")
}, "boldplainbold2", []tg.MessageEntityClass{
&tg.MessageEntityBold{
Offset: ComputeLength("boldplain"),
Length: ComputeLength("bold2"),
},
&tg.MessageEntityBold{
Offset: 0,
Length: ComputeLength("bold"),
},
}},
{"BoldBold", func(e *Builder) {
e.Bold("bold\n\n\n\n").Bold("bold2\n\n\n\n")
}, "bold\n\n\n\nbold2", []tg.MessageEntityClass{
&tg.MessageEntityBold{
Offset: 0,
Length: ComputeLength("bold\n\n\n\n"),
},
&tg.MessageEntityBold{
Offset: ComputeLength("bold\n\n\n\n"),
Length: ComputeLength("bold2"),
},
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
a := require.New(t)
b := Builder{}
test.format(&b)
msg, entities := b.Complete()
a.Equal(test.msg, msg)
a.Equal(test.entities, entities)
})
}
}
@@ -0,0 +1,80 @@
package entity
import (
"strings"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
type utf8entity struct {
offset int
length int
}
// Builder builds message string and text entities.
type Builder struct {
entities []tg.MessageEntityClass
// lengths stores offset/length of entities too, but in UTF-8 codepoints.
lengths []utf8entity
// We store index of first entity added at last Format call.
// It needed to trim space in all entities of last text block.
lastFormatIndex int
// utf16length stores length in UTF-16 codepoints.
utf16length int
// message is message string builder.
message strings.Builder
}
// GrowText grows internal buffer capacity.
func (b *Builder) GrowText(n int) {
b.message.Grow(n)
}
// GrowEntities grows internal buffer capacity.
func (b *Builder) GrowEntities(n int) {
if n < 0 {
panic("entity.Builder.GrowEntities: negative count")
}
buf := make([]tg.MessageEntityClass, len(b.entities), 2*cap(b.entities)+n)
copy(buf, b.entities)
b.entities = buf
}
// Reset resets the Builder to be empty.
func (b *Builder) Reset() {
b.message.Reset()
b.entities = nil
b.utf16length = 0
}
// UTF8Len returns length of text in bytes.
func (b *Builder) UTF8Len() int {
return b.message.Len()
}
// UTF16Len returns length of text in UTF-16 codepoints.
func (b *Builder) UTF16Len() int {
return b.utf16length
}
// EntitiesLen return length of added entities.
func (b *Builder) EntitiesLen() int {
return len(b.entities)
}
// TextRange returns message text of given byte (UTF-8) range.
//
// If range is invalid, it will panic.
func (b *Builder) TextRange(from, to int) string {
return b.message.String()[from:to]
}
// LastEntity returns last entity if any.
func (b *Builder) LastEntity() (tg.MessageEntityClass, bool) {
l := b.EntitiesLen()
if l < 1 {
return nil, false
}
return b.entities[l-1], true
}
@@ -0,0 +1,71 @@
package entity
import (
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestBuilder_TextRange(t *testing.T) {
var (
a = require.New(t)
b Builder
)
_, _ = b.WriteString("abc")
a.Equal("abc"[1:2], b.TextRange(1, 2))
a.Equal("abc"[0:0], b.TextRange(0, 0))
panicRanges := [][2]int{
{1, 0},
{-1, 0},
{0, -1},
}
for _, r := range panicRanges {
a.Panics(func() {
b.TextRange(r[0], r[1])
})
}
}
func TestBuilder_LastEntity(t *testing.T) {
var (
a = require.New(t)
b Builder
)
e, ok := b.LastEntity()
a.False(ok)
a.Nil(e)
b.Underline("abc")
e, ok = b.LastEntity()
a.True(ok)
a.Equal(&tg.MessageEntityUnderline{
Offset: 0,
Length: 3,
}, e)
}
func TestBuilder_GrowText(t *testing.T) {
var (
a = require.New(t)
b Builder
)
b.GrowText(100)
a.LessOrEqual(100, b.message.Cap())
}
func TestBuilder_GrowEntities(t *testing.T) {
var (
a = require.New(t)
b Builder
)
b.GrowEntities(100)
a.Equal(100, cap(b.entities))
a.Panics(func() {
b.GrowEntities(-1)
})
}
@@ -0,0 +1,398 @@
// Code generated by mkentity, DO NOT EDIT.
package entity
import (
"context"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
var (
_ = tg.Invoker(nil)
_ = context.Context(nil)
)
// Unknown creates Formatter of Unknown message entity.
//
// See https://core.telegram.org/constructor/messageEntityUnknown.
func Unknown() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityUnknown{
Offset: offset,
Length: length,
}
}
}
// Unknown adds and formats message as Unknown message entity.
//
// See https://core.telegram.org/constructor/messageEntityUnknown.
func (b *Builder) Unknown(s string) *Builder {
return b.Format(s, Unknown())
}
// Mention creates Formatter of Mention message entity.
//
// See https://core.telegram.org/constructor/messageEntityMention.
func Mention() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityMention{
Offset: offset,
Length: length,
}
}
}
// Mention adds and formats message as Mention message entity.
//
// See https://core.telegram.org/constructor/messageEntityMention.
func (b *Builder) Mention(s string) *Builder {
return b.Format(s, Mention())
}
// Hashtag creates Formatter of Hashtag message entity.
//
// See https://core.telegram.org/constructor/messageEntityHashtag.
func Hashtag() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityHashtag{
Offset: offset,
Length: length,
}
}
}
// Hashtag adds and formats message as Hashtag message entity.
//
// See https://core.telegram.org/constructor/messageEntityHashtag.
func (b *Builder) Hashtag(s string) *Builder {
return b.Format(s, Hashtag())
}
// BotCommand creates Formatter of BotCommand message entity.
//
// See https://core.telegram.org/constructor/messageEntityBotCommand.
func BotCommand() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityBotCommand{
Offset: offset,
Length: length,
}
}
}
// BotCommand adds and formats message as BotCommand message entity.
//
// See https://core.telegram.org/constructor/messageEntityBotCommand.
func (b *Builder) BotCommand(s string) *Builder {
return b.Format(s, BotCommand())
}
// URL creates Formatter of URL message entity.
//
// See https://core.telegram.org/constructor/messageEntityUrl.
func URL() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityURL{
Offset: offset,
Length: length,
}
}
}
// URL adds and formats message as URL message entity.
//
// See https://core.telegram.org/constructor/messageEntityUrl.
func (b *Builder) URL(s string) *Builder {
return b.Format(s, URL())
}
// Email creates Formatter of Email message entity.
//
// See https://core.telegram.org/constructor/messageEntityEmail.
func Email() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityEmail{
Offset: offset,
Length: length,
}
}
}
// Email adds and formats message as Email message entity.
//
// See https://core.telegram.org/constructor/messageEntityEmail.
func (b *Builder) Email(s string) *Builder {
return b.Format(s, Email())
}
// Bold creates Formatter of Bold message entity.
//
// See https://core.telegram.org/constructor/messageEntityBold.
func Bold() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityBold{
Offset: offset,
Length: length,
}
}
}
// Bold adds and formats message as Bold message entity.
//
// See https://core.telegram.org/constructor/messageEntityBold.
func (b *Builder) Bold(s string) *Builder {
return b.Format(s, Bold())
}
// Italic creates Formatter of Italic message entity.
//
// See https://core.telegram.org/constructor/messageEntityItalic.
func Italic() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityItalic{
Offset: offset,
Length: length,
}
}
}
// Italic adds and formats message as Italic message entity.
//
// See https://core.telegram.org/constructor/messageEntityItalic.
func (b *Builder) Italic(s string) *Builder {
return b.Format(s, Italic())
}
// Code creates Formatter of Code message entity.
//
// See https://core.telegram.org/constructor/messageEntityCode.
func Code() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityCode{
Offset: offset,
Length: length,
}
}
}
// Code adds and formats message as Code message entity.
//
// See https://core.telegram.org/constructor/messageEntityCode.
func (b *Builder) Code(s string) *Builder {
return b.Format(s, Code())
}
// Pre creates Formatter of Pre message entity.
//
// See https://core.telegram.org/constructor/messageEntityPre.
func Pre(language string) Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityPre{
Offset: offset,
Length: length,
Language: language,
}
}
}
// Pre adds and formats message as Pre message entity.
//
// See https://core.telegram.org/constructor/messageEntityPre.
func (b *Builder) Pre(s string, language string) *Builder {
return b.Format(s, Pre(language))
}
// TextURL creates Formatter of TextURL message entity.
//
// See https://core.telegram.org/constructor/messageEntityTextUrl.
func TextURL(uRL string) Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityTextURL{
Offset: offset,
Length: length,
URL: uRL,
}
}
}
// TextURL adds and formats message as TextURL message entity.
//
// See https://core.telegram.org/constructor/messageEntityTextUrl.
func (b *Builder) TextURL(s string, uRL string) *Builder {
return b.Format(s, TextURL(uRL))
}
// MentionName creates Formatter of MentionName message entity.
//
// See https://core.telegram.org/constructor/inputMessageEntityMentionName.
func MentionName(userID tg.InputUserClass) Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.InputMessageEntityMentionName{
Offset: offset,
Length: length,
UserID: userID,
}
}
}
// MentionName adds and formats message as MentionName message entity.
//
// See https://core.telegram.org/constructor/inputMessageEntityMentionName.
func (b *Builder) MentionName(s string, userID tg.InputUserClass) *Builder {
return b.Format(s, MentionName(userID))
}
// Phone creates Formatter of Phone message entity.
//
// See https://core.telegram.org/constructor/messageEntityPhone.
func Phone() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityPhone{
Offset: offset,
Length: length,
}
}
}
// Phone adds and formats message as Phone message entity.
//
// See https://core.telegram.org/constructor/messageEntityPhone.
func (b *Builder) Phone(s string) *Builder {
return b.Format(s, Phone())
}
// Cashtag creates Formatter of Cashtag message entity.
//
// See https://core.telegram.org/constructor/messageEntityCashtag.
func Cashtag() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityCashtag{
Offset: offset,
Length: length,
}
}
}
// Cashtag adds and formats message as Cashtag message entity.
//
// See https://core.telegram.org/constructor/messageEntityCashtag.
func (b *Builder) Cashtag(s string) *Builder {
return b.Format(s, Cashtag())
}
// Underline creates Formatter of Underline message entity.
//
// See https://core.telegram.org/constructor/messageEntityUnderline.
func Underline() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityUnderline{
Offset: offset,
Length: length,
}
}
}
// Underline adds and formats message as Underline message entity.
//
// See https://core.telegram.org/constructor/messageEntityUnderline.
func (b *Builder) Underline(s string) *Builder {
return b.Format(s, Underline())
}
// Strike creates Formatter of Strike message entity.
//
// See https://core.telegram.org/constructor/messageEntityStrike.
func Strike() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityStrike{
Offset: offset,
Length: length,
}
}
}
// Strike adds and formats message as Strike message entity.
//
// See https://core.telegram.org/constructor/messageEntityStrike.
func (b *Builder) Strike(s string) *Builder {
return b.Format(s, Strike())
}
// BankCard creates Formatter of BankCard message entity.
//
// See https://core.telegram.org/constructor/messageEntityBankCard.
func BankCard() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityBankCard{
Offset: offset,
Length: length,
}
}
}
// BankCard adds and formats message as BankCard message entity.
//
// See https://core.telegram.org/constructor/messageEntityBankCard.
func (b *Builder) BankCard(s string) *Builder {
return b.Format(s, BankCard())
}
// Spoiler creates Formatter of Spoiler message entity.
//
// See https://core.telegram.org/constructor/messageEntitySpoiler.
func Spoiler() Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntitySpoiler{
Offset: offset,
Length: length,
}
}
}
// Spoiler adds and formats message as Spoiler message entity.
//
// See https://core.telegram.org/constructor/messageEntitySpoiler.
func (b *Builder) Spoiler(s string) *Builder {
return b.Format(s, Spoiler())
}
// CustomEmoji creates Formatter of CustomEmoji message entity.
//
// See https://core.telegram.org/constructor/messageEntityCustomEmoji.
func CustomEmoji(documentID int64) Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityCustomEmoji{
Offset: offset,
Length: length,
DocumentID: documentID,
}
}
}
// CustomEmoji adds and formats message as CustomEmoji message entity.
//
// See https://core.telegram.org/constructor/messageEntityCustomEmoji.
func (b *Builder) CustomEmoji(s string, documentID int64) *Builder {
return b.Format(s, CustomEmoji(documentID))
}
// Blockquote creates Formatter of Blockquote message entity.
//
// See https://core.telegram.org/constructor/messageEntityBlockquote.
func Blockquote(collapsed bool) Formatter {
return func(offset, length int) tg.MessageEntityClass {
return &tg.MessageEntityBlockquote{
Offset: offset,
Length: length,
Collapsed: collapsed,
}
}
}
// Blockquote adds and formats message as Blockquote message entity.
//
// See https://core.telegram.org/constructor/messageEntityBlockquote.
func (b *Builder) Blockquote(s string, collapsed bool) *Builder {
return b.Format(s, Blockquote(collapsed))
}
@@ -0,0 +1,20 @@
package entity
import "go.mau.fi/mautrix-telegram/pkg/gotd/tg"
// Formatter is a message entity constructor.
type Formatter func(offset, limit int) tg.MessageEntityClass
// Plain formats message as plain text.
func (b *Builder) Plain(s string) *Builder {
_, _ = b.WriteString(s)
b.lastFormatIndex = len(b.entities)
return b
}
// Format formats message using given formatters.
func (b *Builder) Format(s string, formats ...Formatter) *Builder {
return b.appendMessage(s, formats...)
}
//go:generate go run go.mau.fi/mautrix-telegram/pkg/gotd/telegram/message/internal/mkentity -output options.gen.go
@@ -0,0 +1,186 @@
package entity
import (
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestBuilder(t *testing.T) {
b := Builder{}
t.Run("Plain", func(t *testing.T) {
_, ent := b.Plain("abc").Complete()
require.Empty(t, ent)
})
t.Run("EmptyString", func(t *testing.T) {
msg, ent := b.Bold("").Complete()
require.Empty(t, msg)
require.Empty(t, ent)
})
t.Run("Format", func(t *testing.T) {
_, ent := b.Format("abc", Bold(), Italic()).Complete()
require.Equal(t, []tg.MessageEntityClass{
&tg.MessageEntityBold{
Offset: 0,
Length: len("abc"),
},
&tg.MessageEntityItalic{
Offset: 0,
Length: len("abc"),
},
}, ent)
})
t.Run("Unknown", func(t *testing.T) {
_, ent := b.Unknown("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityUnknown{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Mention", func(t *testing.T) {
_, ent := b.Mention("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityMention{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Hashtag", func(t *testing.T) {
_, ent := b.Hashtag("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityHashtag{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("BotCommand", func(t *testing.T) {
_, ent := b.BotCommand("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityBotCommand{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("URL", func(t *testing.T) {
_, ent := b.URL("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityURL{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Email", func(t *testing.T) {
_, ent := b.Email("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityEmail{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Bold", func(t *testing.T) {
_, ent := b.Bold("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityBold{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Italic", func(t *testing.T) {
_, ent := b.Italic("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityItalic{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Code", func(t *testing.T) {
_, ent := b.Code("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityCode{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Pre", func(t *testing.T) {
_, ent := b.Pre("abc", "lang").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityPre{
Offset: 0,
Length: len("abc"),
Language: "lang",
}, r)
})
t.Run("TextURL", func(t *testing.T) {
_, ent := b.TextURL("abc", "url").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityTextURL{
Offset: 0,
Length: len("abc"),
URL: "url",
}, r)
})
t.Run("MentionName", func(t *testing.T) {
user := &tg.InputUser{
UserID: 10,
AccessHash: 10,
}
_, ent := b.MentionName("abc", user).Complete()
r := ent[0]
require.Equal(t, &tg.InputMessageEntityMentionName{
Offset: 0,
Length: len("abc"),
UserID: user,
}, r)
})
t.Run("Phone", func(t *testing.T) {
_, ent := b.Phone("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityPhone{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Cashtag", func(t *testing.T) {
_, ent := b.Cashtag("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityCashtag{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Underline", func(t *testing.T) {
_, ent := b.Underline("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityUnderline{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Strike", func(t *testing.T) {
_, ent := b.Strike("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityStrike{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("Blockquote", func(t *testing.T) {
_, ent := b.Blockquote("abc", false).Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityBlockquote{
Offset: 0,
Length: len("abc"),
}, r)
})
t.Run("BankCard", func(t *testing.T) {
_, ent := b.BankCard("abc").Complete()
r := ent[0]
require.Equal(t, &tg.MessageEntityBankCard{
Offset: 0,
Length: len("abc"),
}, r)
})
}
+48
View File
@@ -0,0 +1,48 @@
package entity
// Token represents raw point in a message string.
type Token struct {
utf8offset int
utf16offset int
}
// UTF8Offset return UTF-8 offset.
func (t Token) UTF8Offset() int {
return t.utf8offset
}
// UTF16Offset returns UTF-16 offset.
func (t Token) UTF16Offset() int {
return t.utf16offset
}
// UTF8Length return UTF-8 length between token start and current state.
func (t Token) UTF8Length(builder *Builder) int {
return builder.UTF8Len() - t.utf8offset
}
// UTF16Length returns UTF-16 length between token start and current state.
func (t Token) UTF16Length(builder *Builder) int {
return builder.UTF16Len() - t.utf16offset
}
// Text message string between token start and current state.
func (t Token) Text(builder *Builder) string {
return builder.TextRange(t.utf8offset, builder.UTF8Len())
}
// Apply formats range between token start and current state using given Formatter slice.
func (t Token) Apply(builder *Builder, f ...Formatter) {
builder.appendEntities(t.utf16offset, t.UTF16Length(builder), utf8entity{
offset: t.utf8offset,
length: t.UTF8Length(builder),
}, f...)
}
// Token creates new Token.
func (b *Builder) Token() Token {
return Token{
utf8offset: b.UTF8Len(),
utf16offset: b.UTF16Len(),
}
}
@@ -0,0 +1,38 @@
package entity
import (
"testing"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)
func TestToken_Apply(t *testing.T) {
var (
a = require.New(t)
b = &Builder{}
)
_ = b.WriteByte('a')
tok := b.Token()
a.Equal(1, tok.UTF8Offset())
a.Equal(1, tok.UTF16Offset())
a.Zero(tok.UTF8Length(b))
a.Zero(tok.UTF16Length(b))
a.Empty(tok.Text(b))
text := "abc🏳"
_, _ = b.WriteString(text)
a.Equal(text, tok.Text(b))
utf16Len := ComputeLength(tok.Text(b))
a.Equal(b.message.Len()-tok.UTF8Offset(), tok.UTF8Length(b))
a.Equal(utf16Len, tok.UTF16Length(b))
tok.Apply(b, Bold())
a.Equal(1, b.EntitiesLen())
e, ok := b.LastEntity()
a.True(ok)
a.Equal(&tg.MessageEntityBold{Offset: 1, Length: utf16Len}, e)
}
@@ -0,0 +1,6 @@
package entity
import "go.mau.fi/mautrix-telegram/pkg/gotd/tg"
// UserResolver is callback for resolving InputUser by ID.
type UserResolver = func(id int64) (tg.InputUserClass, error)
+108
View File
@@ -0,0 +1,108 @@
package entity
import (
"io"
"unicode/utf8"
)
// ComputeLength returns length of s encoded as UTF-16 string.
//
// While Telegram API docs state that they expect the number of UTF-8
// code points, in fact they are talking about UTF-16 code units.
func ComputeLength(s string) int {
// From utf16 package.
n := 0
for _, v := range s {
n += utf16RuneLen(v)
}
return n
}
// ComputeLengthBytes returns length of s encoded as UTF-16 string.
//
// While Telegram API docs state that they expect the number of UTF-8
// code points, in fact they are talking about UTF-16 code units.
func ComputeLengthBytes(s []byte) (n int) {
// From utf16 package.
var i int
for i < len(s) {
v, size := utf8.DecodeRune(s[i:])
i += size
n += utf16RuneLen(v)
}
return n
}
func utf16RuneLen(v rune) int {
const (
surrSelf = 0x10000
maxRune = '\U0010FFFF' // Maximum valid Unicode code point.
)
if surrSelf <= v && v <= maxRune {
return 2
}
return 1
}
func (b *Builder) appendMessage(s string, formats ...Formatter) *Builder {
if s == "" {
return b
}
offset := b.utf16length
length := ComputeLength(s)
b.appendEntities(offset, length, utf8entity{
offset: b.message.Len(),
length: len(s),
}, formats...)
_, _ = b.WriteString(s)
return b
}
func (b *Builder) appendEntities(offset, length int, u utf8entity, formats ...Formatter) *Builder {
b.lastFormatIndex = len(b.entities)
for i := range formats {
b.entities = append(b.entities, formats[i](offset, length))
b.lengths = append(b.lengths, u)
}
return b
}
var _ = []interface {
io.Writer
io.StringWriter
io.ByteWriter
WriteRune(rune) (int, error)
}{
(*Builder)(nil),
}
// Write implements io.Writer.
func (b *Builder) Write(s []byte) (int, error) {
n, err := b.message.Write(s)
b.utf16length += ComputeLengthBytes(s)
return n, err
}
// WriteString implements io.StringWriter.
func (b *Builder) WriteString(s string) (int, error) {
n, err := b.message.WriteString(s)
b.utf16length += ComputeLength(s)
return n, err
}
// WriteByte implements io.ByteWriter.
func (b *Builder) WriteByte(s byte) error {
err := b.message.WriteByte(s)
b.utf16length++
return err
}
// WriteRune implements rune writer.
func (b *Builder) WriteRune(s rune) (int, error) {
n, err := b.message.WriteRune(s)
b.utf16length += utf16RuneLen(s)
return n, err
}
@@ -0,0 +1,55 @@
package entity
import (
"encoding/hex"
"testing"
"unicode/utf8"
"github.com/stretchr/testify/require"
"go.mau.fi/mautrix-telegram/pkg/gotd/testutil"
)
func TestComputeLength(t *testing.T) {
tests := []struct {
s string
want int
}{
{string([]rune{97, 127987, 65039, 8205, 127752}), 7},
{string([]int32{97, 127987, 65039, 8205, 127752, 127987, 65039, 8205, 127752}), 13},
{string([]int32{97, 128104, 8205, 128102, 8205, 128102}), 9},
}
for _, tt := range tests {
r := []byte(tt.s)
testutil.ZeroAlloc(t, func() {
_ = ComputeLength(tt.s)
})
testutil.ZeroAlloc(t, func() {
_ = ComputeLengthBytes(r)
})
t.Run(hex.EncodeToString([]byte(tt.s)), func(t *testing.T) {
require.Equal(t, tt.want, ComputeLength(tt.s))
require.Equal(t, tt.want, ComputeLengthBytes(r))
})
}
}
func TestBuilder_Write(t *testing.T) {
var (
a = require.New(t)
b Builder
)
_, err := b.Write([]byte("abc"))
a.NoError(err)
_, err = b.WriteString("abc")
a.NoError(err)
a.NoError(b.WriteByte('\n'))
a.Equal(3+3+1, b.UTF8Len())
a.Equal(3+3+1, b.UTF16Len())
var r rune = 127987
_, err = b.WriteRune(r)
a.NoError(err)
a.Equal(3+3+1+utf8.RuneLen(r), b.UTF8Len())
a.Equal(3+3+1+utf16RuneLen(r), b.UTF16Len())
}