mirror of
https://github.com/fumiama/NanoBot.git
synced 2026-06-05 18:50:24 +08:00
367 lines
8.5 KiB
Go
367 lines
8.5 KiB
Go
package nano
|
||
|
||
import (
|
||
"encoding/base64"
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"reflect"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
|
||
base14 "github.com/fumiama/go-base16384"
|
||
"github.com/fumiama/imoto"
|
||
)
|
||
|
||
//go:generate go run codegen/context/main.go
|
||
|
||
type Ctx struct {
|
||
Event
|
||
State
|
||
Message *Message
|
||
IsToMe bool
|
||
IsQQ bool
|
||
|
||
caller *Bot
|
||
ma *Matcher
|
||
}
|
||
|
||
// decoder 反射获取的数据
|
||
type decoder []dec
|
||
|
||
type dec struct {
|
||
index int
|
||
key string
|
||
}
|
||
|
||
// decoder 缓存
|
||
var decoderCache = sync.Map{}
|
||
|
||
// Parse 将 Ctx.State 映射到结构体
|
||
func (ctx *Ctx) Parse(model interface{}) (err error) {
|
||
var (
|
||
rv = reflect.ValueOf(model).Elem()
|
||
t = rv.Type()
|
||
modelDec decoder
|
||
)
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
err = fmt.Errorf("parse state error: %v", r)
|
||
}
|
||
}()
|
||
d, ok := decoderCache.Load(t)
|
||
if ok {
|
||
modelDec = d.(decoder)
|
||
} else {
|
||
modelDec = decoder{}
|
||
for i := 0; i < t.NumField(); i++ {
|
||
t1 := t.Field(i)
|
||
if key, ok := t1.Tag.Lookup("zero"); ok {
|
||
modelDec = append(modelDec, dec{
|
||
index: i,
|
||
key: key,
|
||
})
|
||
}
|
||
}
|
||
decoderCache.Store(t, modelDec)
|
||
}
|
||
for _, d := range modelDec { // decoder类型非小内存,无法被编译器优化为快速拷贝
|
||
rv.Field(d.index).Set(reflect.ValueOf(ctx.State[d.key]))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CheckSession 判断会话连续性
|
||
func (ctx *Ctx) CheckSession() Rule {
|
||
msg := ctx.Value.(*Message)
|
||
return func(ctx2 *Ctx) bool {
|
||
msg2, ok := ctx.Value.(*Message)
|
||
if !ok || msg.Author == nil || msg2.Author == nil { // 确保无空
|
||
return false
|
||
}
|
||
return msg.Author.ID == msg2.Author.ID && msg.ChannelID == msg2.ChannelID
|
||
}
|
||
}
|
||
|
||
var imotoken = "f7f06a63b8c111df0d4faa256cd5ba35cb98678ee8274923576d0416b52fe768"
|
||
|
||
// Send 发送一批消息
|
||
func (ctx *Ctx) Send(messages Messages) (m []*Message, err error) {
|
||
isnextreply := false
|
||
textlist := []any{}
|
||
var reply *Message
|
||
for _, msg := range messages {
|
||
switch msg.Type {
|
||
case MessageSegmentTypeText:
|
||
textlist = append(textlist, msg.Data)
|
||
case MessageSegmentTypeImage:
|
||
reply, err = ctx.SendImage(msg.Data, isnextreply, textlist...)
|
||
if isnextreply {
|
||
isnextreply = false
|
||
}
|
||
textlist = textlist[:0]
|
||
m = append(m, reply)
|
||
if err != nil {
|
||
return
|
||
}
|
||
case MessageSegmentTypeImageBytes:
|
||
reply, err = ctx.SendImageBytes(StringToBytes(msg.Data), isnextreply, textlist...)
|
||
if isnextreply {
|
||
isnextreply = false
|
||
}
|
||
textlist = textlist[:0]
|
||
m = append(m, reply)
|
||
if err != nil {
|
||
return
|
||
}
|
||
case MessageSegmentTypeReply:
|
||
isnextreply = true
|
||
case MessageSegmentTypeAudio, MessageSegmentTypeVideo:
|
||
if !ctx.IsQQ {
|
||
continue
|
||
}
|
||
fp := &FilePost{
|
||
URL: msg.Data,
|
||
}
|
||
if msg.Type == MessageSegmentTypeAudio {
|
||
fp.Type = FileTypeAudio
|
||
} else if msg.Type == MessageSegmentTypeVideo {
|
||
fp.Type = FileTypeVideo
|
||
}
|
||
if OnlyQQGroup(ctx) {
|
||
reply, err = ctx.PostFileToQQGroup(ctx.Message.ChannelID, fp)
|
||
} else if OnlyQQPrivate(ctx) {
|
||
reply, err = ctx.PostFileToQQUser(ctx.Message.Author.ID, fp)
|
||
}
|
||
if err != nil {
|
||
return
|
||
}
|
||
reply, err = ctx.Post(isnextreply, &MessagePost{
|
||
Content: " ",
|
||
Media: &MessageMedia{FileInfo: reply.FileInfo},
|
||
})
|
||
m = append(m, reply)
|
||
if err != nil {
|
||
return
|
||
}
|
||
}
|
||
}
|
||
if len(textlist) > 0 {
|
||
reply, err = ctx.SendPlainMessage(isnextreply, textlist...)
|
||
m = append(m, reply)
|
||
}
|
||
return
|
||
}
|
||
|
||
// SendChain 链式发送
|
||
func (ctx *Ctx) SendChain(message ...MessageSegment) (m []*Message, err error) {
|
||
return ctx.Send(message)
|
||
}
|
||
|
||
// Post 发送消息到对方
|
||
func (ctx *Ctx) Post(replytosender bool, post *MessagePost) (reply *Message, err error) {
|
||
msg := ctx.Message
|
||
if msg != nil {
|
||
post.ReplyMessageID = msg.ID
|
||
if OnlyGuild(ctx) && replytosender {
|
||
post.MessageReference = &MessageReference{
|
||
MessageID: msg.ID,
|
||
}
|
||
}
|
||
} else {
|
||
post.ReplyMessageID = "MESSAGE_CREATE"
|
||
}
|
||
|
||
if OnlyDirect(ctx) { // dms
|
||
reply, err = ctx.PostMessageToUser(msg.GuildID, post)
|
||
} else if OnlyChannel(ctx) {
|
||
reply, err = ctx.PostMessageToChannel(msg.ChannelID, post)
|
||
} else { // v2
|
||
switch {
|
||
case post.Markdown != nil:
|
||
post.Type = MessageTypeMarkdown
|
||
case post.Ark != nil:
|
||
post.Type = MessageTypeArk
|
||
case post.Embed != nil:
|
||
post.Type = MessageTypeEmbed
|
||
case post.Media != nil:
|
||
post.Type = MessageTypeMedia
|
||
default:
|
||
post.Type = MessageTypeText
|
||
}
|
||
post.Seq = len(GetTriggeredMessages(msg.ID)) + 1
|
||
if OnlyQQGroup(ctx) {
|
||
reply, err = ctx.PostMessageToQQGroup(msg.ChannelID, post)
|
||
} else if OnlyQQPrivate(ctx) {
|
||
reply, err = ctx.PostMessageToQQUser(msg.ChannelID, post)
|
||
}
|
||
if err == nil {
|
||
logtriggeredmessages(msg.ID, "") // only to log message seq
|
||
}
|
||
return
|
||
}
|
||
if err == nil && msg != nil && reply != nil && reply.ID != "" {
|
||
logtriggeredmessages(msg.ID, reply.ID)
|
||
}
|
||
return
|
||
}
|
||
|
||
// SendPlainMessage 发送纯文本消息到对方
|
||
func (ctx *Ctx) SendPlainMessage(replytosender bool, printable ...any) (*Message, error) {
|
||
return ctx.Post(replytosender, &MessagePost{
|
||
Content: HideURL(fmt.Sprint(printable...)),
|
||
})
|
||
}
|
||
|
||
// SendImage 发送带图片消息到对方
|
||
func (ctx *Ctx) SendImage(file string, replytosender bool, caption ...any) (reply *Message, err error) {
|
||
post := &MessagePost{
|
||
Content: HideURL(fmt.Sprint(caption...)),
|
||
}
|
||
|
||
if OnlyQQ(ctx) {
|
||
if strings.HasPrefix(file, "file:///") {
|
||
data, err := os.ReadFile(file[8:])
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return ctx.SendImageBytes(data, replytosender, caption...)
|
||
}
|
||
if strings.HasPrefix(file, "base64://") {
|
||
data, err := base64.StdEncoding.DecodeString(file[9:])
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return ctx.SendImageBytes(data, replytosender, caption...)
|
||
}
|
||
if strings.HasPrefix(file, "base16384://") {
|
||
data := base14.DecodeFromString(file[12:])
|
||
if len(data) == 0 {
|
||
return nil, errors.New("invalid base16384 image")
|
||
}
|
||
return ctx.SendImageBytes(data, replytosender, caption...)
|
||
}
|
||
fp := &FilePost{
|
||
Type: FileTypeImage,
|
||
URL: file,
|
||
}
|
||
/*if len(caption) > 0 {
|
||
_, _ = ctx.SendPlainMessage(replytosender, caption...)
|
||
}*/
|
||
if post.Content == "" {
|
||
post.Content = " "
|
||
}
|
||
if OnlyQQGroup(ctx) {
|
||
reply, err = ctx.PostFileToQQGroup(ctx.Message.ChannelID, fp)
|
||
} else if OnlyQQPrivate(ctx) {
|
||
reply, err = ctx.PostFileToQQUser(ctx.Message.Author.ID, fp)
|
||
}
|
||
if err != nil {
|
||
return
|
||
}
|
||
post.Media = &MessageMedia{FileInfo: reply.FileInfo}
|
||
} else {
|
||
if strings.HasPrefix(file, "http") {
|
||
post.Image = file
|
||
} else {
|
||
post.ImageFile = file
|
||
}
|
||
}
|
||
|
||
return ctx.Post(replytosender, post)
|
||
}
|
||
|
||
// SendImageBytes 发送带图片消息到对方
|
||
func (ctx *Ctx) SendImageBytes(data []byte, replytosender bool, caption ...any) (*Message, error) {
|
||
if OnlyQQ(ctx) {
|
||
file, _, _, err := imoto.Bed(imotoken, data)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return ctx.SendImage(file, replytosender, caption...)
|
||
}
|
||
|
||
post := &MessagePost{
|
||
Content: HideURL(fmt.Sprint(caption...)),
|
||
}
|
||
|
||
post.ImageBytes = data
|
||
|
||
return ctx.Post(replytosender, post)
|
||
}
|
||
|
||
// Echo 向自身分发虚拟事件
|
||
func (ctx *Ctx) Echo(payload *WebsocketPayload) {
|
||
ctx.caller.processEvent(payload)
|
||
}
|
||
|
||
// FutureEvent ...
|
||
func (ctx *Ctx) FutureEvent(Type string, rule ...Rule) *FutureEvent {
|
||
return ctx.ma.FutureEvent(Type, rule...)
|
||
}
|
||
|
||
// Get 从 promt 获得回复
|
||
func (ctx *Ctx) Get(prompt string) string {
|
||
if prompt != "" {
|
||
_, _ = ctx.SendPlainMessage(false, prompt)
|
||
}
|
||
return (<-ctx.FutureEvent("Message", ctx.CheckSession()).Next()).Event.Value.(*Message).Content
|
||
}
|
||
|
||
// ExtractPlainText 提取消息中的纯文本
|
||
func (ctx *Ctx) ExtractPlainText() string {
|
||
if ctx == nil || ctx.Value == nil {
|
||
return ""
|
||
}
|
||
if msg, ok := ctx.Value.(*Message); ok {
|
||
return msg.Content
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// MessageString 字符串消息便于Regex
|
||
func (ctx *Ctx) MessageString() string {
|
||
return ctx.ExtractPlainText()
|
||
}
|
||
|
||
// Block 匹配成功后阻止后续触发
|
||
func (ctx *Ctx) Block() {
|
||
ctx.ma.SetBlock(true)
|
||
}
|
||
|
||
// Block 在 pre, rules, mid 阶段阻止后续触发
|
||
func (ctx *Ctx) Break() {
|
||
ctx.ma.Break = true
|
||
}
|
||
|
||
// GroupID 唯一的发送者所属组 ID
|
||
func (ctx *Ctx) GroupID() uint64 {
|
||
grp := uint64(0)
|
||
if ctx.IsQQ {
|
||
if OnlyQQGroup(ctx) {
|
||
grp = DigestID(ctx.Message.ChannelID)
|
||
} else if OnlyQQPrivate(ctx) {
|
||
grp = DigestID(ctx.Message.Author.ID)
|
||
} else {
|
||
return 0
|
||
}
|
||
} else {
|
||
var err error
|
||
grp, err = strconv.ParseUint(ctx.Message.ChannelID, 10, 64)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
}
|
||
return grp
|
||
}
|
||
|
||
// GroupID 唯一的发送者 ID
|
||
func (ctx *Ctx) UserID() uint64 {
|
||
if ctx.IsQQ {
|
||
return DigestID(ctx.Message.Author.ID)
|
||
}
|
||
grp, _ := strconv.ParseUint(ctx.Message.Author.ID, 10, 64)
|
||
return grp
|
||
}
|