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 + "", 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() + "", nil }