FILE / ScuroNeko/Laniakea

tgrich/buttons.go

Исходный файл и его история в репозитории.
FILE d78526242bc7aa184ce5e030b95d8afe2d1243c4
Files
Laniakea/tgrich/buttons.go
T
ScuroNeko d78526242b
Golang lint / lint (push) Failing after 1m37s
(new): support Bot API 10.3
(fix): finalize v2 contracts
(tests): cover v2 migration
(doc): prepare release guidance
2026-09-08 23:21:38 +03:00

135 lines
4.3 KiB
Go

package tgrich
import (
"fmt"
"strings"
"unicode/utf8"
"git.scuroneko.dev/scuroneko/laniakea/v2/tgapi"
)
func (v *richValidator) button(b tgapi.RichMessageButton, depth int) error {
actions := 0
for _, set := range []bool{b.URL != "", b.CallbackData != "", b.WebApp != nil, b.LoginURL != nil, b.SwitchInlineQuery != nil, b.SwitchInlineQueryCurrentChat != nil, b.SwitchInlineQueryChosenChat != nil, b.CopyText != nil, b.Disabled != nil} {
if set {
actions++
}
}
if actions != 1 {
return fmt.Errorf("%w: exactly one action is required", ErrRichInvalidButton)
}
switch b.Style {
case "", "danger", "success", "primary":
case "link":
if b.CallbackData == "" {
return fmt.Errorf("%w: link style requires callback data", ErrRichInvalidButton)
}
default:
return fmt.Errorf("%w: unsupported style", ErrRichInvalidButton)
}
if len(b.CallbackData) > 64 {
return fmt.Errorf("%w: callback data exceeds 64 bytes", ErrRichInvalidButton)
}
if b.CopyText != nil && (utf8.RuneCountInString(b.CopyText.Text) < 1 || utf8.RuneCountInString(b.CopyText.Text) > 256) {
return fmt.Errorf("%w: copy text must contain 1-256 characters", ErrRichInvalidButton)
}
if b.LoginURL != nil && b.LoginURL.BotUsername != "" {
return fmt.Errorf("%w: login bot username is unsupported", ErrRichInvalidButton)
}
return v.buttonText(b.Text, depth)
}
func (v *richValidator) buttonText(text tgapi.RichText, depth int) error {
if depth >= maxRichDepth {
return ErrRichNestingTooDeep
}
switch el := text.(type) {
case tgapi.RichTextPlain:
return v.addChars(string(el))
case tgapi.RichTextCustomEmoji:
return v.addChars(el.AlternativeText)
case tgapi.RichTextDateTime:
return v.buttonText(el.Text, depth+1)
case tgapi.RichTextArray:
for _, item := range el {
if err := v.buttonText(item, depth+1); err != nil {
return err
}
}
return nil
default:
return fmt.Errorf("%w: label allows only plain text, custom emoji, and date-time entities", ErrRichInvalidButton)
}
}
func renderButton(b tgapi.RichMessageButton, step int) (string, error) {
if step >= maxRichDepth {
return "", ErrRichNestingTooDeep
}
text, err := renderText(b.Text, step+1)
if err != nil {
return "", err
}
attrs := map[string]string{}
if b.Style != "" {
attrs["style"] = string(b.Style)
}
// Empty query values are significant; render them explicitly below.
var query *string
switch {
case b.URL != "":
attrs["type"], attrs["url"] = "url", b.URL
case b.CallbackData != "":
attrs["type"], attrs["data"] = "callback_data", b.CallbackData
case b.WebApp != nil:
attrs["type"], attrs["url"] = "web_app", b.WebApp.URL
case b.LoginURL != nil:
attrs["type"], attrs["url"] = "login_url", b.LoginURL.URL
if b.LoginURL.ForwardText != "" {
attrs["forward-text"] = b.LoginURL.ForwardText
}
if b.LoginURL.RequestWriteAccess {
attrs["request-write-access"] = ""
}
case b.SwitchInlineQuery != nil:
attrs["type"], query = "switch_inline_query", b.SwitchInlineQuery
case b.SwitchInlineQueryCurrentChat != nil:
attrs["type"], query = "switch_inline_query_current_chat", b.SwitchInlineQueryCurrentChat
case b.SwitchInlineQueryChosenChat != nil:
c := b.SwitchInlineQueryChosenChat
attrs["type"], query = "switch_inline_query_chosen_chat", &c.Query
for key, enabled := range map[string]bool{"allow-user-chats": c.AllowUserChats, "allow-bot-chats": c.AllowBotChats, "allow-group-chats": c.AllowGroupChats, "allow-channel-chats": c.AllowChannelChats} {
if enabled {
attrs[key] = ""
}
}
case b.CopyText != nil:
attrs["type"], attrs["text"] = "copy_text", b.CopyText.Text
case b.Disabled != nil:
attrs["type"] = "disabled"
default:
return "", ErrRichInvalidButton
}
formatted := formatAttrs(attrs)
if query != nil {
formatted = append(formatted, `query="`+escapeHTML(*query)+`"`)
}
return openTag("tg-button", formatted) + text + "</tg-button>", nil
}
func renderButtons(row tgapi.InputRichBlockButtons, step int) (string, error) {
var content strings.Builder
for _, button := range row.Buttons {
html, err := renderButton(button, step+1)
if err != nil {
return "", err
}
content.WriteString(html)
}
attrs := map[string]string{}
if row.Align != "" {
attrs["align"] = string(row.Align)
}
return openTag("tg-button-row", formatAttrs(attrs)) + content.String() + "</tg-button-row>", nil
}