201 lines
5.8 KiB
Go
201 lines
5.8 KiB
Go
package napcat
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"strconv"
|
||
)
|
||
|
||
// Segment 是 OneBot 11 的消息段:{"type": "...", "data": {...}}。
|
||
// 收到的事件里 data 值可能是数字或字符串,因此统一用 map 承载,
|
||
// 发送时用本包的构造函数生成,避免字段名拼错。
|
||
type Segment struct {
|
||
Type string `json:"type"`
|
||
Data map[string]any `json:"data"`
|
||
}
|
||
|
||
// With 为消息段附加一个 data 字段,可链式调用:
|
||
//
|
||
// napcat.Image("1.jpg").With("summary", "看图")
|
||
func (s Segment) With(key string, value any) Segment {
|
||
if s.Data == nil {
|
||
s.Data = map[string]any{}
|
||
}
|
||
s.Data[key] = value
|
||
return s
|
||
}
|
||
|
||
func seg(t string, kv ...any) Segment {
|
||
data := map[string]any{}
|
||
for i := 0; i+1 < len(kv); i += 2 {
|
||
key, ok := kv[i].(string)
|
||
if !ok {
|
||
panic(fmt.Sprintf("napcat: segment key must be string, got %T", kv[i]))
|
||
}
|
||
data[key] = kv[i+1]
|
||
}
|
||
return Segment{Type: t, Data: data}
|
||
}
|
||
|
||
// Text 构造纯文本消息段。
|
||
func Text(text string) Segment { return seg("text", "text", text) }
|
||
|
||
// Face 构造 QQ 系统表情消息段。
|
||
func Face(id int) Segment { return seg("face", "id", id) }
|
||
|
||
// Image 构造图片消息段。file 支持本地路径、http(s) URL、base64://。
|
||
func Image(file string) Segment { return seg("image", "file", file) }
|
||
|
||
// Record 构造语音消息段。
|
||
func Record(file string) Segment { return seg("record", "file", file) }
|
||
|
||
// Video 构造视频消息段。
|
||
func Video(file string) Segment { return seg("video", "file", file) }
|
||
|
||
// At 构造 @ 消息段。
|
||
func At(qq int64) Segment { return seg("at", "qq", strconv.FormatInt(qq, 10)) }
|
||
|
||
// AtAll 构造 @全体成员 消息段。
|
||
func AtAll() Segment { return seg("at", "qq", "all") }
|
||
|
||
// Reply 构造回复消息段。
|
||
func Reply(messageID int64) Segment { return seg("reply", "id", messageID) }
|
||
|
||
// Forward 构造合并转发消息段(引用一条已存在的合并转发)。
|
||
func Forward(id string) Segment { return seg("forward", "id", id) }
|
||
|
||
// NodeSeg 引用一条已有消息作为合并转发节点。
|
||
// 注意:与 api.go 里的 Node 结构体(构造转发节点参数)区分开。
|
||
func NodeSeg(messageID int64) Segment {
|
||
return seg("node", "id", messageID)
|
||
}
|
||
|
||
// CustomNode 构造伪造发送者的合并转发节点。
|
||
func CustomNode(userID int64, nickname string, content []Segment) Segment {
|
||
return seg("node",
|
||
"user_id", strconv.FormatInt(userID, 10),
|
||
"nickname", nickname,
|
||
"content", content,
|
||
)
|
||
}
|
||
|
||
// JSON 构造 JSON 卡片消息段(json/lightapp 卡片)。
|
||
func JSON(payload string) Segment { return seg("json", "data", payload) }
|
||
|
||
// Music 构造音乐分享消息段,musicType 取值 qq/163/kugou/migu/kuwo。
|
||
func Music(musicType, id string) Segment {
|
||
return seg("music", "type", musicType, "id", id)
|
||
}
|
||
|
||
// CustomMusic 构造自定义音乐分享消息段。
|
||
func CustomMusic(url, audio, title string) Segment {
|
||
return seg("music", "type", "custom", "url", url, "audio", audio, "title", title)
|
||
}
|
||
|
||
// Contact 构造好友/群推荐卡片,contactType 取值 qq/group。
|
||
func Contact(contactType, id string) Segment {
|
||
return seg("contact", "type", contactType, "id", id)
|
||
}
|
||
|
||
// Markdown 构造 markdown 消息段(NapCat 下需置于合并转发内发送)。
|
||
func Markdown(content string) Segment { return seg("markdown", "content", content) }
|
||
|
||
// Poke 构造戳一戳消息段。
|
||
func Poke(qq int64) Segment { return seg("poke", "qq", strconv.FormatInt(qq, 10)) }
|
||
|
||
// Dice 构造随机骰子消息段。
|
||
func Dice() Segment { return Segment{Type: "dice", Data: map[string]any{}} }
|
||
|
||
// RPS 构造猜拳消息段。
|
||
func RPS() Segment { return Segment{Type: "rps", Data: map[string]any{}} }
|
||
|
||
// Shake 构造窗口抖动消息段(仅私聊可用)。
|
||
func Shake() Segment { return Segment{Type: "shake", Data: map[string]any{}} }
|
||
|
||
// FileSegment 构造文件消息段(一般推荐使用 UploadGroupFile/UploadPrivateFile)。
|
||
func FileSegment(file, name string) Segment {
|
||
return seg("file", "file", file, "name", name)
|
||
}
|
||
|
||
// DecodeMessage 将事件或 API 返回中的 message 字段解码为消息段数组,
|
||
// 兼容段数组和 CQ 码字符串两种形态。
|
||
func DecodeMessage(raw json.RawMessage) ([]Segment, error) {
|
||
if len(raw) == 0 {
|
||
return nil, nil
|
||
}
|
||
trimmed := trimSpaceBytes(raw)
|
||
if len(trimmed) > 0 && trimmed[0] == '[' {
|
||
var segs []Segment
|
||
if err := json.Unmarshal(trimmed, &segs); err != nil {
|
||
return nil, fmt.Errorf("napcat: decode message segments: %w", err)
|
||
}
|
||
return segs, nil
|
||
}
|
||
var s string
|
||
if err := json.Unmarshal(trimmed, &s); err != nil {
|
||
return nil, fmt.Errorf("napcat: message is neither array nor string: %w", err)
|
||
}
|
||
return ParseCQ(s), nil
|
||
}
|
||
|
||
// SegmentsText 拼接所有 text 段的文本内容。
|
||
func SegmentsText(segs []Segment) string {
|
||
out := ""
|
||
for _, s := range segs {
|
||
if s.Type == "text" {
|
||
out += strOf(s.Data["text"])
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func trimSpaceBytes(b []byte) []byte {
|
||
start, end := 0, len(b)
|
||
for start < end && (b[start] == ' ' || b[start] == '\t' || b[start] == '\r' || b[start] == '\n') {
|
||
start++
|
||
}
|
||
for end > start && (b[end-1] == ' ' || b[end-1] == '\t' || b[end-1] == '\r' || b[end-1] == '\n') {
|
||
end--
|
||
}
|
||
return b[start:end]
|
||
}
|
||
|
||
// strOf 把 data 里的值统一转成字符串(兼容 float64/string/json.Number 等)。
|
||
func strOf(v any) string {
|
||
switch t := v.(type) {
|
||
case nil:
|
||
return ""
|
||
case string:
|
||
return t
|
||
case float64:
|
||
if t == float64(int64(t)) {
|
||
return strconv.FormatInt(int64(t), 10)
|
||
}
|
||
return strconv.FormatFloat(t, 'f', -1, 64)
|
||
case json.Number:
|
||
return t.String()
|
||
case bool:
|
||
return strconv.FormatBool(t)
|
||
default:
|
||
return fmt.Sprint(t)
|
||
}
|
||
}
|
||
|
||
// intOf 把 data 里的值尽量转成 int64(兼容 float64/string)。
|
||
func intOf(v any) int64 {
|
||
switch t := v.(type) {
|
||
case nil:
|
||
return 0
|
||
case float64:
|
||
return int64(t)
|
||
case string:
|
||
n, _ := strconv.ParseInt(t, 10, 64)
|
||
return n
|
||
case json.Number:
|
||
n, _ := t.Int64()
|
||
return n
|
||
default:
|
||
return 0
|
||
}
|
||
}
|