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
|
|
}
|