FILE / ScuroNeko/Laniakea

tgfmt/message_builder.go

Исходный файл и его история в репозитории.
FILE 9e3450df3106572ee3feecca2a53fe4411633724
Files
Laniakea/tgfmt/message_builder.go
T
ScuroNeko 7205b21fa2
Golang lint / lint (pull_request) Successful in 7m10s
Golang lint / lint (push) Successful in 7m13s
(new): tgfmt package
(fix): formatting helpers
(ci/cd): go checks
(tests): tgfmt coverage
2026-05-04 11:20:09 +03:00

334 lines
8.0 KiB
Go

package tgfmt
import (
"strings"
"time"
"git.scuroneko.dev/scuroneko/extypes"
"git.scuroneko.dev/scuroneko/laniakea/tgapi"
)
// MessageBuilder builds Telegram message text with explicit message entities.
// MessageBuilder is not safe for concurrent use.
type MessageBuilder struct {
str string
offset int
entities extypes.Slice[tgapi.MessageEntity]
entries extypes.Slice[*MessageBuilderEntry]
isDirty bool
}
// NewMessageBuilder returns an empty MessageBuilder.
func NewMessageBuilder() *MessageBuilder {
return &MessageBuilder{
entities: make([]tgapi.MessageEntity, 0),
entries: make(extypes.Slice[*MessageBuilderEntry], 0),
isDirty: false,
}
}
// String returns the built message text.
func (b *MessageBuilder) String() string {
if b.isDirty {
b.update()
}
return b.str
}
// Entities returns a copy of the built message entities.
func (b *MessageBuilder) Entities() []tgapi.MessageEntity {
if b.isDirty {
b.update()
}
return append([]tgapi.MessageEntity(nil), b.entities...)
}
// Build returns the built message text and a copy of its entities.
func (b *MessageBuilder) Build() (string, []tgapi.MessageEntity) {
if b.isDirty {
b.update()
}
return b.str, append([]tgapi.MessageEntity(nil), b.entities...)
}
// Reset clears the builder and keeps it ready for reuse.
func (b *MessageBuilder) Reset() {
b.str = ""
b.offset = 0
b.entities = b.entities[:0]
b.entries = b.entries[:0]
b.isDirty = false
}
func (b *MessageBuilder) update() *MessageBuilder {
b.offset = 0
var textLen int
var entitiesLen int
for _, e := range b.entries {
textLen += len(e.text)
entitiesLen += len(e.entities)
}
b.entities = make(extypes.Slice[tgapi.MessageEntity], 0, entitiesLen)
var sb strings.Builder
sb.Grow(textLen)
for _, e := range b.entries {
sb.WriteString(e.text)
for _, entity := range e.entities {
entity.Offset += b.offset
b.entities = append(b.entities, entity)
}
b.offset += e.length
}
b.str = sb.String()
b.isDirty = false
return b
}
func (b *MessageBuilder) markDirty() {
b.isDirty = true
}
// MessageBuilderEntry represents text appended to a MessageBuilder.
type MessageBuilderEntry struct {
text string
length int
b *MessageBuilder
entities extypes.Slice[tgapi.MessageEntity]
}
// Add appends plain text to the message and returns its entry for formatting.
func (b *MessageBuilder) Add(text string) *MessageBuilderEntry {
e := &MessageBuilderEntry{
b: b,
entities: make(extypes.Slice[tgapi.MessageEntity], 0),
text: text,
length: telegramTextLen(text),
}
b.entries = b.entries.Push(e)
b.markDirty()
return e
}
// Mention marks the entry as a Telegram mention.
func (e *MessageBuilderEntry) Mention() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityMention,
Offset: 0, Length: e.length,
})
return e
}
// Hashtag marks the entry as a Telegram hashtag.
func (e *MessageBuilderEntry) Hashtag() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityHashtag,
Offset: 0, Length: e.length,
})
return e
}
// Cashtag marks the entry as a Telegram cashtag.
func (e *MessageBuilderEntry) Cashtag() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityCashtag,
Offset: 0, Length: e.length,
})
return e
}
// BotCommand marks the entry as a Telegram bot command.
func (e *MessageBuilderEntry) BotCommand() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityBotCommand,
Offset: 0, Length: e.length,
})
return e
}
// Email marks the entry as an email address.
func (e *MessageBuilderEntry) Email() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityEmail,
Offset: 0, Length: e.length,
})
return e
}
// Phone marks the entry as a phone number.
func (e *MessageBuilderEntry) Phone() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityPhoneNumber,
Offset: 0, Length: e.length,
})
return e
}
// Bold marks the entry as bold text.
func (e *MessageBuilderEntry) Bold() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityBold,
Offset: 0, Length: e.length,
})
return e
}
// Italic marks the entry as italic text.
func (e *MessageBuilderEntry) Italic() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityItalic,
Offset: 0, Length: e.length,
})
return e
}
// Underline marks the entry as underlined text.
func (e *MessageBuilderEntry) Underline() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityUnderline,
Offset: 0, Length: e.length,
})
return e
}
// Strikethrough marks the entry as strikethrough text.
func (e *MessageBuilderEntry) Strikethrough() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityStrike,
Offset: 0, Length: e.length,
})
return e
}
// Spoiler marks the entry as spoiler text.
func (e *MessageBuilderEntry) Spoiler() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntitySpoiler,
Offset: 0, Length: e.length,
})
return e
}
// Quote marks the entry as a blockquote.
func (e *MessageBuilderEntry) Quote() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityBlockquote,
Offset: 0, Length: e.length,
})
return e
}
// ExpandableQuote marks the entry as an expandable blockquote.
func (e *MessageBuilderEntry) ExpandableQuote() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityExpandableBlockquote,
Offset: 0, Length: e.length,
})
return e
}
// InlineCode marks the entry as inline code.
func (e *MessageBuilderEntry) InlineCode() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityCode,
Offset: 0, Length: e.length,
})
return e
}
// CodeBlock marks the entry as a preformatted code block.
func (e *MessageBuilderEntry) CodeBlock() *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityPre,
Offset: 0, Length: e.length,
})
return e
}
// CodeBlockWithLanguage marks the entry as a preformatted code block with language.
func (e *MessageBuilderEntry) CodeBlockWithLanguage(lang string) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityPre,
Offset: 0, Length: e.length, Language: lang,
})
return e
}
// Link marks the entry as a text link.
func (e *MessageBuilderEntry) Link(url string) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityTextLink,
Offset: 0, Length: e.length, URL: url,
})
return e
}
// TextMention marks the entry as a mention of user.
func (e *MessageBuilderEntry) TextMention(user *tgapi.User) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityTextMention,
Offset: 0, Length: e.length, User: user,
})
return e
}
// CustomEmoji marks the entry as a custom emoji.
func (e *MessageBuilderEntry) CustomEmoji(emojiID string) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityCustomEmoji,
Offset: 0, Length: e.length, CustomEmojiID: emojiID,
})
return e
}
// DateTime marks the entry as a localized timestamp.
func (e *MessageBuilderEntry) DateTime(time time.Time) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityDateTime,
Offset: 0, Length: e.length, UnixTime: time.Unix(),
})
return e
}
// DateTimeFormat marks the entry as a localized timestamp with format.
func (e *MessageBuilderEntry) DateTimeFormat(time time.Time, format string) *MessageBuilderEntry {
e.addEntity(tgapi.MessageEntity{
Type: tgapi.MessageEntityDateTime,
Offset: 0, Length: e.length,
UnixTime: time.Unix(), DateTimeFormat: format,
})
return e
}
func telegramTextLen(text string) int {
n := 0
for _, r := range text {
if r <= 0xFFFF {
n++
} else {
n += 2
}
}
return n
}
func (e *MessageBuilderEntry) addEntity(entity tgapi.MessageEntity) {
if entity.Length <= 0 {
return
}
e.entities = append(e.entities, entity)
if e.b != nil {
e.b.markDirty()
}
}