FILE / ScuroNeko/Laniakea
tgfmt/message_builder.go
Исходный файл и его история в репозитории.
334 lines
8.0 KiB
Go
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()
|
|
}
|
|
}
|