github.com/fumiama/NanoBot@v0.0.0-20231122134259-c22d8183efca/context.go (about) 1 package nano 2 3 import ( 4 "encoding/base64" 5 "errors" 6 "fmt" 7 "os" 8 "reflect" 9 "strconv" 10 "strings" 11 "sync" 12 13 base14 "github.com/fumiama/go-base16384" 14 "github.com/fumiama/imoto" 15 "github.com/sirupsen/logrus" 16 ) 17 18 //go:generate go run codegen/context/main.go 19 20 type Ctx struct { 21 Event 22 State 23 Message *Message 24 IsToMe bool 25 IsQQ bool 26 27 caller *Bot 28 ma *Matcher 29 } 30 31 // decoder 反射获取的数据 32 type decoder []dec 33 34 type dec struct { 35 index int 36 key string 37 } 38 39 // decoder 缓存 40 var decoderCache = sync.Map{} 41 42 // Parse 将 Ctx.State 映射到结构体 43 func (ctx *Ctx) Parse(model interface{}) (err error) { 44 var ( 45 rv = reflect.ValueOf(model).Elem() 46 t = rv.Type() 47 modelDec decoder 48 ) 49 defer func() { 50 if r := recover(); r != nil { 51 err = fmt.Errorf("parse state error: %v", r) 52 } 53 }() 54 d, ok := decoderCache.Load(t) 55 if ok { 56 modelDec = d.(decoder) 57 } else { 58 modelDec = decoder{} 59 for i := 0; i < t.NumField(); i++ { 60 t1 := t.Field(i) 61 if key, ok := t1.Tag.Lookup("zero"); ok { 62 modelDec = append(modelDec, dec{ 63 index: i, 64 key: key, 65 }) 66 } 67 } 68 decoderCache.Store(t, modelDec) 69 } 70 for _, d := range modelDec { // decoder类型非小内存,无法被编译器优化为快速拷贝 71 rv.Field(d.index).Set(reflect.ValueOf(ctx.State[d.key])) 72 } 73 return nil 74 } 75 76 // CheckSession 判断会话连续性 77 func (ctx *Ctx) CheckSession() Rule { 78 msg := ctx.Value.(*Message) 79 return func(ctx2 *Ctx) bool { 80 msg2, ok := ctx.Value.(*Message) 81 if !ok || msg.Author == nil || msg2.Author == nil { // 确保无空 82 return false 83 } 84 return msg.Author.ID == msg2.Author.ID && msg.ChannelID == msg2.ChannelID 85 } 86 } 87 88 var imotoken = "f7f06a63b8c111df0d4faa256cd5ba35cb98678ee8274923576d0416b52fe768" 89 90 // Send 发送一批消息 91 func (ctx *Ctx) Send(messages Messages) (m []*Message, err error) { 92 isnextreply := false 93 textlist := []any{} 94 var reply *Message 95 for _, msg := range messages { 96 switch msg.Type { 97 case MessageSegmentTypeText: 98 textlist = append(textlist, msg.Data) 99 case MessageSegmentTypeImage: 100 reply, err = ctx.SendImage(msg.Data, isnextreply, textlist...) 101 if isnextreply { 102 isnextreply = false 103 } 104 textlist = textlist[:0] 105 m = append(m, reply) 106 if err != nil { 107 return 108 } 109 case MessageSegmentTypeImageBytes: 110 reply, err = ctx.SendImageBytes(StringToBytes(msg.Data), isnextreply, textlist...) 111 if isnextreply { 112 isnextreply = false 113 } 114 textlist = textlist[:0] 115 m = append(m, reply) 116 if err != nil { 117 return 118 } 119 case MessageSegmentTypeReply: 120 isnextreply = true 121 case MessageSegmentTypeAudio, MessageSegmentTypeVideo: 122 if !ctx.IsQQ { 123 continue 124 } 125 fp := &FilePost{ 126 URL: msg.Data, 127 } 128 if msg.Type == MessageSegmentTypeAudio { 129 fp.Type = FileTypeAudio 130 } else if msg.Type == MessageSegmentTypeVideo { 131 fp.Type = FileTypeVideo 132 } 133 if OnlyQQGroup(ctx) { 134 reply, err = ctx.PostFileToQQGroup(ctx.Message.ChannelID, fp) 135 } else if OnlyQQPrivate(ctx) { 136 reply, err = ctx.PostFileToQQUser(ctx.Message.Author.ID, fp) 137 } 138 if err != nil { 139 return 140 } 141 logrus.Infoln(getLogHeader(), "=> 上传:", reply) 142 reply, err = ctx.Post(isnextreply, &MessagePost{ 143 Content: " ", 144 Media: &MessageMedia{FileInfo: reply.FileInfo}, 145 }) 146 m = append(m, reply) 147 if err != nil { 148 return 149 } 150 } 151 } 152 if len(textlist) > 0 { 153 reply, err = ctx.SendPlainMessage(isnextreply, textlist...) 154 m = append(m, reply) 155 } 156 return 157 } 158 159 // SendChain 链式发送 160 func (ctx *Ctx) SendChain(message ...MessageSegment) (m []*Message, err error) { 161 return ctx.Send(message) 162 } 163 164 // Post 发送消息到对方 165 func (ctx *Ctx) Post(replytosender bool, post *MessagePost) (reply *Message, err error) { 166 msg := ctx.Message 167 if msg != nil { 168 post.ReplyMessageID = msg.ID 169 if OnlyGuild(ctx) && replytosender { 170 post.MessageReference = &MessageReference{ 171 MessageID: msg.ID, 172 } 173 } 174 } else { 175 post.ReplyMessageID = "MESSAGE_CREATE" 176 } 177 178 if OnlyDirect(ctx) { // dms 179 reply, err = ctx.PostMessageToUser(msg.GuildID, post) 180 } else if OnlyChannel(ctx) { 181 reply, err = ctx.PostMessageToChannel(msg.ChannelID, post) 182 } else { // v2 183 switch { 184 case post.Markdown != nil: 185 post.Type = MessageTypeMarkdown 186 case post.Ark != nil: 187 post.Type = MessageTypeArk 188 case post.Embed != nil: 189 post.Type = MessageTypeEmbed 190 case post.Media != nil: 191 post.Type = MessageTypeMedia 192 default: 193 post.Type = MessageTypeText 194 } 195 post.Seq = len(GetTriggeredMessages(msg.ID)) + 1 196 if OnlyQQGroup(ctx) { 197 reply, err = ctx.PostMessageToQQGroup(msg.ChannelID, post) 198 } else if OnlyQQPrivate(ctx) { 199 reply, err = ctx.PostMessageToQQUser(msg.ChannelID, post) 200 } 201 if err == nil { 202 logtriggeredmessages(msg.ID, "") // only to log message seq 203 } 204 return 205 } 206 if err == nil && msg != nil && reply != nil && reply.ID != "" { 207 logtriggeredmessages(msg.ID, reply.ID) 208 } 209 return 210 } 211 212 // SendPlainMessage 发送纯文本消息到对方 213 func (ctx *Ctx) SendPlainMessage(replytosender bool, printable ...any) (*Message, error) { 214 return ctx.Post(replytosender, &MessagePost{ 215 Content: HideURL(fmt.Sprint(printable...)), 216 }) 217 } 218 219 // SendImage 发送带图片消息到对方 220 func (ctx *Ctx) SendImage(file string, replytosender bool, caption ...any) (reply *Message, err error) { 221 post := &MessagePost{ 222 Content: HideURL(fmt.Sprint(caption...)), 223 } 224 225 if OnlyQQ(ctx) { 226 if strings.HasPrefix(file, "file:///") { 227 data, err := os.ReadFile(file[8:]) 228 if err != nil { 229 return nil, err 230 } 231 return ctx.SendImageBytes(data, replytosender, caption...) 232 } 233 if strings.HasPrefix(file, "base64://") { 234 data, err := base64.StdEncoding.DecodeString(file[9:]) 235 if err != nil { 236 return nil, err 237 } 238 return ctx.SendImageBytes(data, replytosender, caption...) 239 } 240 if strings.HasPrefix(file, "base16384://") { 241 data := base14.DecodeFromString(file[12:]) 242 if len(data) == 0 { 243 return nil, errors.New("invalid base16384 image") 244 } 245 return ctx.SendImageBytes(data, replytosender, caption...) 246 } 247 fp := &FilePost{ 248 Type: FileTypeImage, 249 URL: file, 250 } 251 /*if len(caption) > 0 { 252 _, _ = ctx.SendPlainMessage(replytosender, caption...) 253 }*/ 254 if post.Content == "" { 255 post.Content = " " 256 } 257 if OnlyQQGroup(ctx) { 258 reply, err = ctx.PostFileToQQGroup(ctx.Message.ChannelID, fp) 259 } else if OnlyQQPrivate(ctx) { 260 reply, err = ctx.PostFileToQQUser(ctx.Message.Author.ID, fp) 261 } 262 if err != nil { 263 return 264 } 265 logrus.Infoln(getLogHeader(), "=> 上传:", reply) 266 post.Media = &MessageMedia{FileInfo: reply.FileInfo} 267 } else { 268 if strings.HasPrefix(file, "http") { 269 post.Image = file 270 } else { 271 post.ImageFile = file 272 } 273 } 274 275 return ctx.Post(replytosender, post) 276 } 277 278 // SendImageBytes 发送带图片消息到对方 279 func (ctx *Ctx) SendImageBytes(data []byte, replytosender bool, caption ...any) (*Message, error) { 280 if OnlyQQ(ctx) { 281 file, _, _, err := imoto.Bed(imotoken, data) 282 if err != nil { 283 return nil, err 284 } 285 return ctx.SendImage(file, replytosender, caption...) 286 } 287 288 post := &MessagePost{ 289 Content: HideURL(fmt.Sprint(caption...)), 290 } 291 292 post.ImageBytes = data 293 294 return ctx.Post(replytosender, post) 295 } 296 297 // Echo 向自身分发虚拟事件 298 func (ctx *Ctx) Echo(payload *WebsocketPayload) { 299 ctx.caller.processEvent(payload) 300 } 301 302 // FutureEvent ... 303 func (ctx *Ctx) FutureEvent(Type string, rule ...Rule) *FutureEvent { 304 return ctx.ma.FutureEvent(Type, rule...) 305 } 306 307 // Get 从 promt 获得回复 308 func (ctx *Ctx) Get(prompt string) string { 309 if prompt != "" { 310 _, _ = ctx.SendPlainMessage(false, prompt) 311 } 312 return (<-ctx.FutureEvent("Message", ctx.CheckSession()).Next()).Event.Value.(*Message).Content 313 } 314 315 // ExtractPlainText 提取消息中的纯文本 316 func (ctx *Ctx) ExtractPlainText() string { 317 if ctx == nil || ctx.Value == nil { 318 return "" 319 } 320 if msg, ok := ctx.Value.(*Message); ok { 321 return msg.Content 322 } 323 return "" 324 } 325 326 // MessageString 字符串消息便于Regex 327 func (ctx *Ctx) MessageString() string { 328 return ctx.ExtractPlainText() 329 } 330 331 // Block 匹配成功后阻止后续触发 332 func (ctx *Ctx) Block() { 333 ctx.ma.SetBlock(true) 334 } 335 336 // Block 在 pre, rules, mid 阶段阻止后续触发 337 func (ctx *Ctx) Break() { 338 ctx.ma.Break = true 339 } 340 341 // GroupID 唯一的发送者所属组 ID 342 func (ctx *Ctx) GroupID() uint64 { 343 grp := uint64(0) 344 if ctx.IsQQ { 345 if OnlyQQGroup(ctx) { 346 grp = DigestID(ctx.Message.ChannelID) 347 } else if OnlyQQPrivate(ctx) { 348 grp = DigestID(ctx.Message.Author.ID) 349 } else { 350 return 0 351 } 352 } else { 353 var err error 354 grp, err = strconv.ParseUint(ctx.Message.ChannelID, 10, 64) 355 if err != nil { 356 return 0 357 } 358 } 359 return grp 360 } 361 362 // GroupID 唯一的发送者 ID 363 func (ctx *Ctx) UserID() uint64 { 364 if ctx.IsQQ { 365 return DigestID(ctx.Message.Author.ID) 366 } 367 grp, _ := strconv.ParseUint(ctx.Message.Author.ID, 10, 64) 368 return grp 369 }