150 lines
4.6 KiB
Go
150 lines
4.6 KiB
Go
package domain
|
||
|
||
import (
|
||
"errors"
|
||
"net/url"
|
||
"strings"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
const (
|
||
// MaxMarkupRows 限制 inline keyboard 行数。
|
||
MaxMarkupRows = 100
|
||
// MaxMarkupButtonsPerRow 限制单行按钮数。
|
||
MaxMarkupButtonsPerRow = 8
|
||
// MaxMarkupButtonsTotal 限制 inline keyboard 总按钮数(对齐官方约 100)。
|
||
MaxMarkupButtonsTotal = 100
|
||
// MaxCallbackDataLen 是 callback 按钮 data 的字节上限(对齐 Bot API 1-64)。
|
||
MaxCallbackDataLen = 64
|
||
// MaxMarkupButtonTextLen 是按钮文本长度上限(rune 计数)。
|
||
MaxMarkupButtonTextLen = 256
|
||
// MaxBotCallbackAnswerLen 是 callback answer 弹窗/toast 文本上限。
|
||
MaxBotCallbackAnswerLen = 200
|
||
// MaxStartParamLen 是 messages.startBot 深链 payload 上限(对齐官方 64)。
|
||
MaxStartParamLen = 64
|
||
)
|
||
|
||
// markup / callback 业务错误。
|
||
var (
|
||
// ErrButtonDataInvalid 表示 callback data 越界(>64 字节)。
|
||
ErrButtonDataInvalid = errors.New("button data invalid")
|
||
// ErrButtonInvalid 表示键盘结构非法(行/按钮数超限、文本空/过长)。
|
||
ErrButtonInvalid = errors.New("button invalid")
|
||
// ErrButtonURLInvalid 表示 url 按钮的链接非法(非 https)。
|
||
ErrButtonURLInvalid = errors.New("button url invalid")
|
||
// ErrButtonTypeInvalid 表示按钮类型 P3 不支持(webview/game/url_auth/request_* 等)。
|
||
ErrButtonTypeInvalid = errors.New("button type invalid")
|
||
// ErrStartParamInvalid 表示 startBot 的 start_param 越界。
|
||
ErrStartParamInvalid = errors.New("start param invalid")
|
||
)
|
||
|
||
// MarkupButtonType 标识 P3 支持的 inline 按钮类型。
|
||
type MarkupButtonType string
|
||
|
||
const (
|
||
// MarkupButtonCallback 是 keyboardButtonCallback(点击触发 getBotCallbackAnswer)。
|
||
MarkupButtonCallback MarkupButtonType = "callback"
|
||
// MarkupButtonURL 是 keyboardButtonUrl(点击打开链接)。
|
||
MarkupButtonURL MarkupButtonType = "url"
|
||
)
|
||
|
||
// MarkupButton 是一颗 inline keyboard 按钮(P3 仅 callback/url)。
|
||
type MarkupButton struct {
|
||
Type MarkupButtonType `json:"type"`
|
||
Text string `json:"text"`
|
||
// Data 仅 callback 使用:原始字节(含 0x00/非 UTF-8/高位)。json 自动 base64
|
||
// 编解码,保证经 JSONB 列字节级 round-trip(updateBotCallbackQuery.data 须原样)。
|
||
Data []byte `json:"data,omitempty"`
|
||
// URL 仅 url 使用。
|
||
URL string `json:"url,omitempty"`
|
||
// RequiresPassword 仅 callback 使用(keyboardButtonCallback.requires_password,
|
||
// 2FA SRP 校验 P3 stub)。
|
||
RequiresPassword bool `json:"requires_password,omitempty"`
|
||
}
|
||
|
||
// MessageReplyMarkup 是消息携带的 inline keyboard 快照(P3 仅 ReplyInlineMarkup)。
|
||
type MessageReplyMarkup struct {
|
||
Inline [][]MarkupButton `json:"inline,omitempty"`
|
||
}
|
||
|
||
// IsZero 报告 markup 是否为空(无任何按钮)。空 markup 不写 wire flag、不入库。
|
||
func (m *MessageReplyMarkup) IsZero() bool {
|
||
if m == nil {
|
||
return true
|
||
}
|
||
for _, row := range m.Inline {
|
||
if len(row) > 0 {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// ValidateReplyMarkup 校验 inline keyboard 结构与各按钮,校验须先于落库(I9)。
|
||
// 空 markup 合法(视为清空/无键盘)。
|
||
func ValidateReplyMarkup(m *MessageReplyMarkup) error {
|
||
if m == nil {
|
||
return nil
|
||
}
|
||
if len(m.Inline) > MaxMarkupRows {
|
||
return ErrButtonInvalid
|
||
}
|
||
total := 0
|
||
for _, row := range m.Inline {
|
||
if len(row) > MaxMarkupButtonsPerRow {
|
||
return ErrButtonInvalid
|
||
}
|
||
total += len(row)
|
||
if total > MaxMarkupButtonsTotal {
|
||
return ErrButtonInvalid
|
||
}
|
||
for i := range row {
|
||
if err := validateMarkupButton(row[i]); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateMarkupButton(b MarkupButton) error {
|
||
text := strings.TrimSpace(b.Text)
|
||
if text == "" || utf8.RuneCountInString(b.Text) > MaxMarkupButtonTextLen {
|
||
return ErrButtonInvalid
|
||
}
|
||
switch b.Type {
|
||
case MarkupButtonCallback:
|
||
if len(b.Data) > MaxCallbackDataLen {
|
||
return ErrButtonDataInvalid
|
||
}
|
||
case MarkupButtonURL:
|
||
if err := validateButtonURL(b.URL); err != nil {
|
||
return err
|
||
}
|
||
default:
|
||
// webview/game/url_auth/request_* 等 P3 未实现类型:拒绝,绝不半实现下发。
|
||
return ErrButtonTypeInvalid
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateButtonURL(raw string) error {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" || len(raw) > MaxBotMenuButtonURLLen {
|
||
return ErrButtonURLInvalid
|
||
}
|
||
u, err := url.Parse(raw)
|
||
if err != nil || u.Scheme != "https" || u.Host == "" {
|
||
return ErrButtonURLInvalid
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// BotCallbackAnswer 是 bot 对一次 callback query 的应答(setBotCallbackAnswer →
|
||
// 解挂等待中的 getBotCallbackAnswer)。
|
||
type BotCallbackAnswer struct {
|
||
Alert bool
|
||
Message string
|
||
URL string
|
||
CacheTime int
|
||
}
|