Add Tag field to CardNote struct and TaggedNote method to CardBuilder for programmatic note handling. Simplify delete mode toggle logic by returning updated cards instead of nil and using native Feishu form checkers. Improve Discord command description truncation using runes. generated by llmgit Co-Authored-By: Claude <noreply@anthropic.com>
311 lines
8.9 KiB
Go
311 lines
8.9 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Card represents a structured rich message that can be rendered as
|
|
// platform-specific cards (Feishu Interactive Card, Telegram message, etc.)
|
|
// or degraded to plain text for platforms without card support.
|
|
type Card struct {
|
|
Header *CardHeader
|
|
Elements []CardElement
|
|
}
|
|
|
|
// CardHeader is the optional colored title bar of a card.
|
|
type CardHeader struct {
|
|
Title string
|
|
Color string // blue, green, red, orange, purple, grey, turquoise, violet, indigo, wathet, yellow, carmine
|
|
}
|
|
|
|
// CardElement is the interface satisfied by all card content elements.
|
|
type CardElement interface {
|
|
cardElement()
|
|
}
|
|
|
|
// CardMarkdown renders markdown-formatted text.
|
|
type CardMarkdown struct{ Content string }
|
|
|
|
// CardDivider renders a horizontal rule.
|
|
type CardDivider struct{}
|
|
|
|
// CardActions renders a row of clickable buttons.
|
|
type CardActions struct {
|
|
Buttons []CardButton
|
|
Layout CardActionLayout
|
|
}
|
|
|
|
// CardNote renders small footnote text at the bottom.
|
|
// Tag is an optional machine-readable identifier (not displayed) used by
|
|
// platform renderers to recognize and handle specific notes programmatically.
|
|
type CardNote struct {
|
|
Text string
|
|
Tag string
|
|
}
|
|
|
|
// CardListItem renders a row with description text on the left and a button on the right.
|
|
// On Feishu this maps to div+extra; on other platforms it degrades to a text line.
|
|
type CardListItem struct {
|
|
Text string // left-side description
|
|
BtnText string // button label
|
|
BtnType string // "primary", "default", "danger"
|
|
BtnValue string // callback data
|
|
Extra map[string]string // additional key-value pairs carried in the callback
|
|
}
|
|
|
|
// CardSelect renders a dropdown selector.
|
|
// On Feishu this maps to select_static; on other platforms it degrades to text.
|
|
type CardSelect struct {
|
|
Placeholder string
|
|
Options []CardSelectOption
|
|
InitValue string // pre-selected option value (empty = none)
|
|
}
|
|
|
|
// CardSelectOption is one item in a CardSelect dropdown.
|
|
type CardSelectOption struct {
|
|
Text string
|
|
Value string
|
|
}
|
|
|
|
func (CardMarkdown) cardElement() {}
|
|
func (CardDivider) cardElement() {}
|
|
func (CardActions) cardElement() {}
|
|
func (CardNote) cardElement() {}
|
|
func (CardListItem) cardElement() {}
|
|
func (CardSelect) cardElement() {}
|
|
|
|
// CardButton represents a clickable button inside a CardActions element.
|
|
type CardButton struct {
|
|
Text string // display label
|
|
Type string // "primary", "default", "danger"
|
|
Value string // callback data, e.g. "cmd:/new", "cmd:/switch 3"
|
|
Extra map[string]string // additional key-value pairs carried in the callback (platform-specific)
|
|
}
|
|
|
|
// CardActionLayout controls how a CardActions row should be rendered by
|
|
// platforms with richer layout capabilities.
|
|
type CardActionLayout string
|
|
|
|
const (
|
|
CardActionLayoutRow CardActionLayout = "row"
|
|
CardActionLayoutEqualColumns CardActionLayout = "equal_columns"
|
|
)
|
|
|
|
// Btn is a shorthand constructor for CardButton.
|
|
func Btn(text, typ, value string) CardButton {
|
|
return CardButton{Text: text, Type: typ, Value: value}
|
|
}
|
|
|
|
// PrimaryBtn creates a primary-styled button.
|
|
func PrimaryBtn(text, value string) CardButton {
|
|
return CardButton{Text: text, Type: "primary", Value: value}
|
|
}
|
|
|
|
// DefaultBtn creates a default-styled button.
|
|
func DefaultBtn(text, value string) CardButton {
|
|
return CardButton{Text: text, Type: "default", Value: value}
|
|
}
|
|
|
|
// DangerBtn creates a danger-styled button.
|
|
func DangerBtn(text, value string) CardButton {
|
|
return CardButton{Text: text, Type: "danger", Value: value}
|
|
}
|
|
|
|
// --- Builder API ---
|
|
|
|
// CardBuilder provides a fluent API for constructing Card instances.
|
|
type CardBuilder struct {
|
|
card Card
|
|
}
|
|
|
|
// NewCard returns a new CardBuilder.
|
|
func NewCard() *CardBuilder {
|
|
return &CardBuilder{}
|
|
}
|
|
|
|
// Title sets the card header with a title and color.
|
|
func (b *CardBuilder) Title(title, color string) *CardBuilder {
|
|
b.card.Header = &CardHeader{Title: title, Color: color}
|
|
return b
|
|
}
|
|
|
|
// Markdown appends a markdown text element.
|
|
func (b *CardBuilder) Markdown(content string) *CardBuilder {
|
|
if content != "" {
|
|
b.card.Elements = append(b.card.Elements, CardMarkdown{Content: content})
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Markdownf appends a formatted markdown text element.
|
|
func (b *CardBuilder) Markdownf(format string, args ...any) *CardBuilder {
|
|
return b.Markdown(fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// Divider appends a horizontal divider.
|
|
func (b *CardBuilder) Divider() *CardBuilder {
|
|
b.card.Elements = append(b.card.Elements, CardDivider{})
|
|
return b
|
|
}
|
|
|
|
// Buttons appends an action row with the given buttons.
|
|
func (b *CardBuilder) Buttons(buttons ...CardButton) *CardBuilder {
|
|
if len(buttons) > 0 {
|
|
b.card.Elements = append(b.card.Elements, CardActions{Buttons: buttons, Layout: CardActionLayoutRow})
|
|
}
|
|
return b
|
|
}
|
|
|
|
// ButtonsEqual appends an action row where each button should take equal width
|
|
// on platforms that support richer layouts.
|
|
func (b *CardBuilder) ButtonsEqual(buttons ...CardButton) *CardBuilder {
|
|
if len(buttons) > 0 {
|
|
b.card.Elements = append(b.card.Elements, CardActions{Buttons: buttons, Layout: CardActionLayoutEqualColumns})
|
|
}
|
|
return b
|
|
}
|
|
|
|
// ListItem appends a list row: description on the left, button on the right.
|
|
func (b *CardBuilder) ListItem(desc, btnText, btnValue string) *CardBuilder {
|
|
b.card.Elements = append(b.card.Elements, CardListItem{
|
|
Text: desc, BtnText: btnText, BtnType: "default", BtnValue: btnValue,
|
|
})
|
|
return b
|
|
}
|
|
|
|
// ListItemBtn is like ListItem but allows specifying the button type.
|
|
func (b *CardBuilder) ListItemBtn(desc, btnText, btnType, btnValue string) *CardBuilder {
|
|
b.card.Elements = append(b.card.Elements, CardListItem{
|
|
Text: desc, BtnText: btnText, BtnType: btnType, BtnValue: btnValue,
|
|
})
|
|
return b
|
|
}
|
|
|
|
// ListItemBtnExtra is like ListItemBtn but with extra callback data.
|
|
func (b *CardBuilder) ListItemBtnExtra(desc, btnText, btnType, btnValue string, extra map[string]string) *CardBuilder {
|
|
b.card.Elements = append(b.card.Elements, CardListItem{
|
|
Text: desc, BtnText: btnText, BtnType: btnType, BtnValue: btnValue, Extra: extra,
|
|
})
|
|
return b
|
|
}
|
|
|
|
// Select appends a dropdown selector element.
|
|
func (b *CardBuilder) Select(placeholder string, options []CardSelectOption, initValue string) *CardBuilder {
|
|
if len(options) > 0 {
|
|
b.card.Elements = append(b.card.Elements, CardSelect{
|
|
Placeholder: placeholder, Options: options, InitValue: initValue,
|
|
})
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Note appends a footnote element.
|
|
func (b *CardBuilder) Note(text string) *CardBuilder {
|
|
if text != "" {
|
|
b.card.Elements = append(b.card.Elements, CardNote{Text: text})
|
|
}
|
|
return b
|
|
}
|
|
|
|
func (b *CardBuilder) TaggedNote(tag, text string) *CardBuilder {
|
|
if text != "" {
|
|
b.card.Elements = append(b.card.Elements, CardNote{Text: text, Tag: tag})
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Build returns the constructed Card.
|
|
func (b *CardBuilder) Build() *Card {
|
|
c := b.card
|
|
return &c
|
|
}
|
|
|
|
// --- Text Fallback ---
|
|
|
|
// RenderText converts the card to a plain-text representation for platforms
|
|
// that do not support rich cards.
|
|
func (c *Card) RenderText() string {
|
|
var sb strings.Builder
|
|
|
|
if c.Header != nil && c.Header.Title != "" {
|
|
sb.WriteString("**")
|
|
sb.WriteString(c.Header.Title)
|
|
sb.WriteString("**\n\n")
|
|
}
|
|
|
|
for _, elem := range c.Elements {
|
|
switch e := elem.(type) {
|
|
case CardMarkdown:
|
|
sb.WriteString(e.Content)
|
|
sb.WriteString("\n\n")
|
|
case CardDivider:
|
|
sb.WriteString("---\n\n")
|
|
case CardActions:
|
|
// Render buttons as a hint line
|
|
for i, btn := range e.Buttons {
|
|
if i > 0 {
|
|
sb.WriteString(" ")
|
|
}
|
|
sb.WriteString("[")
|
|
sb.WriteString(btn.Text)
|
|
sb.WriteString("]")
|
|
}
|
|
sb.WriteString("\n\n")
|
|
case CardListItem:
|
|
sb.WriteString(e.Text)
|
|
sb.WriteString(" [")
|
|
sb.WriteString(e.BtnText)
|
|
sb.WriteString("]\n")
|
|
case CardSelect:
|
|
sb.WriteString(e.Placeholder)
|
|
sb.WriteString(": ")
|
|
for i, opt := range e.Options {
|
|
if i > 0 {
|
|
sb.WriteString(" | ")
|
|
}
|
|
sb.WriteString(opt.Text)
|
|
}
|
|
sb.WriteString("\n\n")
|
|
case CardNote:
|
|
sb.WriteString(e.Text)
|
|
sb.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
return strings.TrimRight(sb.String(), "\n")
|
|
}
|
|
|
|
// HasButtons returns true if the card contains any interactive elements.
|
|
func (c *Card) HasButtons() bool {
|
|
for _, elem := range c.Elements {
|
|
switch elem.(type) {
|
|
case CardActions, CardListItem, CardSelect:
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CollectButtons extracts all buttons from the card as a 2D slice
|
|
// (one inner slice per CardActions element), suitable for InlineButtonSender.
|
|
// CardListItem elements are collected as single-button rows.
|
|
func (c *Card) CollectButtons() [][]ButtonOption {
|
|
var rows [][]ButtonOption
|
|
for _, elem := range c.Elements {
|
|
switch e := elem.(type) {
|
|
case CardActions:
|
|
var row []ButtonOption
|
|
for _, btn := range e.Buttons {
|
|
row = append(row, ButtonOption{Text: btn.Text, Data: btn.Value})
|
|
}
|
|
if len(row) > 0 {
|
|
rows = append(rows, row)
|
|
}
|
|
case CardListItem:
|
|
rows = append(rows, []ButtonOption{{Text: e.BtnText, Data: e.BtnValue}})
|
|
}
|
|
}
|
|
return rows
|
|
}
|