Files
napcat/segment.go

201 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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