diff --git a/.github/nano.jpeg b/.github/nano.jpeg
new file mode 100644
index 0000000..884fbbe
Binary files /dev/null and b/.github/nano.jpeg differ
diff --git a/.gitignore b/.gitignore
index 2df58b9..544909b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@ go.work
bot_test.go
/test
+/data
diff --git a/README.md b/README.md
index 18ecf3e..aca00a9 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,120 @@
-# NanoBot
-ZeroBot-like Official QQ Bot Adapter
+
+
+
+
+
+
NanoBot
+ 类 ZeroBot 的官方 QQ 频道适配器
+
+

+
+
+
+## Instructions
+
+> Note: This framework is built mainly for Chinese users thus may display hard-coded Chinese prompts during the interaction.
+
+参见 QQ 官方[文档](https://bot.q.qq.com/wiki/)。
+
+## 快速开始(基于插件)
+> 查看`example`文件夹以获取更多信息
+
+
+
+  |
+  |
+  |
+
+
+ | 开始响应 |
+ 服务列表 |
+ 查看用法 |
+
+
+
+
+
+
+```go
+package main
+
+import (
+ _ "github.com/fumiama/NanoBot/example/echo"
+
+ nano "github.com/fumiama/NanoBot"
+ log "github.com/sirupsen/logrus"
+)
+
+func main() {
+ log.SetLevel(log.DebugLevel)
+ nano.OpenAPI = nano.SandboxAPI
+ nano.OnMessageFullMatch("help").SetBlock(true).
+ Handle(func(ctx *nano.Ctx) {
+ _, _ = ctx.SendPlainMessage(false, "echo string")
+ })
+ nano.Run(&nano.Bot{
+ AppID: "你的AppID",
+ Token: "你的Token",
+ Secret: "你的Secret, 目前没用到, 可以不填",
+ Intents: nano.IntentPublic,
+ SuperUsers: []string{"用户ID1", "用户ID2"},
+ })
+}
+```
+
+## 更多选择(传统的事件驱动)
+
+> 如果声明了 Handler, 所有插件将被禁用
+
+
+
+```go
+package main
+
+import (
+ "strings"
+
+ nano "github.com/fumiama/NanoBot"
+ log "github.com/sirupsen/logrus"
+)
+
+func main() {
+ log.SetLevel(log.DebugLevel)
+ nano.OpenAPI = nano.SandboxAPI
+ nano.Run(&nano.Bot{
+ AppID: "你的AppID",
+ Token: "你的Token",
+ Secret: "你的Secret, 目前没用到, 可以不填",
+ Intents: nano.IntentPublic,
+ Handler: &nano.Handler{
+ OnAtMessageCreate: func(s uint32, bot *nano.Bot, d *nano.Message) {
+ u := ""
+ if len(d.Attachments) > 0 {
+ u = d.Attachments[0].URL
+ if !strings.HasPrefix(u, "http") {
+ u = "http://" + u
+ }
+ }
+ _, err := bot.PostMessageToChannel(d.ChannelID, &nano.MessagePost{
+ Content: "您发送了: " + d.Content,
+ Image: u,
+ ReplyMessageID: d.ID,
+ MessageReference: &nano.MessageReference{
+ MessageID: d.ID,
+ },
+ })
+ if err != nil {
+ bot.PostMessageToChannel(d.ChannelID, &nano.MessagePost{
+ Content: "[ERROR]: " + err.Error(),
+ ReplyMessageID: d.ID,
+ })
+ }
+ },
+ },
+ })
+}
+```
+
+## Thanks
+
+- [ZeroBot](https://github.com/wdvxdr1123/ZeroBot)
diff --git a/bot.go b/bot.go
index 3a96785..b647b1d 100644
--- a/bot.go
+++ b/bot.go
@@ -143,6 +143,11 @@ func (bot *Bot) Authorization() string {
return "Bot " + bot.AppID + "." + bot.Token
}
+// AtMe 返回 "<@!"+bot.ready.User.ID+">"
+func (bot *Bot) AtMe() string {
+ return "<@!" + bot.ready.User.ID + ">"
+}
+
// receive 收一个 payload
func (bot *Bot) reveive() (payload WebsocketPayload, err error) {
err = bot.conn.ReadJSON(&payload)
diff --git a/codegen/engine/engine.yml b/codegen/engine/engine.yml
new file mode 100644
index 0000000..96015fd
--- /dev/null
+++ b/codegen/engine/engine.yml
@@ -0,0 +1,69 @@
+emptyon:
+ - Message
+
+ - GuildCreate
+ - GuildUpdate
+ - GuildDelete
+ - ChannelCreate
+ - ChannelUpdate
+ - ChannelDelete
+
+ - GuildMemberAdd
+ - GuildMemberUpdate
+ - GuildMemberRemove
+
+ - MessageCreate
+ - MessageDelete
+
+ - MessageReactionAdd
+ - MessageReactionRemove
+
+ - DirectMessageCreate
+ - DirectMessageDelete
+
+ - OpenForumThreadCreate
+ - OpenForumThreadUpdate
+ - OpenForumThreadDelete
+ - OpenForumPostCreate
+ - OpenForumPostDelete
+ - OpenForumReplyCreate
+ - OpenForumReplyDelete
+
+ - AudioOrLiveChannelMemberEnter
+ - AudioOrLiveChannelMemberExit
+
+ - MessageAuditPass
+ - MessageAuditReject
+
+ - ForumThreadCreate
+ - ForumThreadUpdate
+ - ForumThreadDelete
+ - ForumPostCreate
+ - ForumPostDelete
+ - ForumReplyCreate
+ - ForumReplyDelete
+ - ForumPublishAuditResult
+
+ - AudioStart
+ - AudioFinish
+ - AudioOnMic
+ - AudioOffMic
+
+ - AtMessageCreate
+ - PublicMessageDelete
+
+ruleon:
+ Message:
+ - Message
+ Rule:
+ Prefix: [prefix, string]
+ Suffix: [suffix, string]
+ Command: [commands, string]
+ Regex: [regexPattern, string]
+ Keyword: [keyword, string]
+ FullMatch: [src, string]
+ FullMatchGroup: [src, "[]string"]
+ KeywordGroup: [keywords, "[]string"]
+ CommandGroup: [commands, "[]string"]
+ PrefixGroup: [prefix, "[]string"]
+ SuffixGroup: [suffix, "[]string"]
diff --git a/codegen/engine/main.go b/codegen/engine/main.go
new file mode 100644
index 0000000..fb6bf20
--- /dev/null
+++ b/codegen/engine/main.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+ "os"
+ "strings"
+
+ "gopkg.in/yaml.v3"
+)
+
+const head = `// Code generated by codegen/engine. DO NOT EDIT.
+
+package nano
+`
+
+const emptyon = `
+// On[Message] ...
+func (e *Engine) On[Message](rules ...Rule) *Matcher { return e.On("[Message]", rules...) }
+
+// On[Message] ...
+func On[Message](rules ...Rule) *Matcher { return On("[Message]", rules...) }
+`
+
+const ruleon = `
+// On[Message][Rule] ...
+func On[Message][Rule]([Name] [Type], rules ...Rule) *Matcher {
+ return defaultEngine.On[Message][Rule]([Name], rules...)
+}
+
+// On[Message][Rule] ...
+func (e *Engine) On[Message][Rule]([Name] [Type], rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "[Message]",
+ Rules: append([]Rule{[Rule]Rule([Name][...])}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+`
+
+const ruleonshell = `
+// On[Message]Shell shell命令触发器
+func On[Message]Shell(command string, model interface{}, rules ...Rule) *Matcher {
+ return defaultEngine.On[Message]Shell(command, model, rules...)
+}
+
+// On[Message]Shell shell命令触发器
+func (e *Engine) On[Message]Shell(command string, model interface{}, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "[Message]",
+ Rules: append([]Rule{ShellRule(command, model)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+`
+
+type config struct {
+ EmptyOn []string `yaml:"emptyon"`
+ RuleOn struct {
+ Message []string `yaml:"Message"`
+ Rule map[string][2]string `yaml:"Rule"`
+ } `yaml:"ruleon"`
+}
+
+func main() {
+ f, err := os.Create("engine_generated.go")
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+ _, err = f.WriteString(head)
+ if err != nil {
+ panic(err)
+ }
+ ef, err := os.Open("codegen/engine/engine.yml")
+ if err != nil {
+ panic(err)
+ }
+ defer ef.Close()
+ cfg := config{}
+ err = yaml.NewDecoder(ef).Decode(&cfg)
+ if err != nil {
+ panic(err)
+ }
+ for _, msg := range cfg.EmptyOn {
+ _, err = f.WriteString(strings.ReplaceAll(emptyon, "[Message]", msg))
+ if err != nil {
+ panic(err)
+ }
+ }
+ for _, msg := range cfg.RuleOn.Message {
+ for rule, x := range cfg.RuleOn.Rule {
+ s := strings.ReplaceAll(ruleon, "[Message]", msg)
+ s = strings.ReplaceAll(s, "[Rule]", rule)
+ s = strings.ReplaceAll(s, "[Name]", x[0])
+ s = strings.ReplaceAll(s, "[Type]", x[1])
+ if strings.Contains(rule, "Group") {
+ s = strings.ReplaceAll(s, "[...]", "...")
+ } else {
+ s = strings.ReplaceAll(s, "[...]", "")
+ }
+ _, err = f.WriteString(s)
+ if err != nil {
+ panic(err)
+ }
+ }
+ _, err = f.WriteString(strings.ReplaceAll(ruleonshell, "[Message]", msg))
+ if err != nil {
+ panic(err)
+ }
+ }
+}
diff --git a/context.go b/context.go
new file mode 100644
index 0000000..fa62851
--- /dev/null
+++ b/context.go
@@ -0,0 +1,131 @@
+package nano
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+ "sync"
+)
+
+type Ctx struct {
+ Event
+ State
+ Caller *Bot
+ Message *Message
+ ma *Matcher
+ IsToMe bool
+}
+
+// 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
+ }
+}
+
+// Send 发送消息到对方
+func (ctx *Ctx) Send(replytosender bool, post *MessagePost) (*Message, error) {
+ msg := ctx.Value.(*Message)
+ post.ReplyMessageID = msg.ID
+ if replytosender {
+ post.MessageReference = &MessageReference{
+ MessageID: msg.ID,
+ }
+ }
+ return ctx.Caller.PostMessageToChannel(msg.ChannelID, post)
+}
+
+// SendPlainMessage 发送纯文本消息到对方
+func (ctx *Ctx) SendPlainMessage(replytosender bool, printable ...any) (*Message, error) {
+ msg := ctx.Value.(*Message)
+ post := &MessagePost{
+ ReplyMessageID: msg.ID,
+ }
+ if replytosender {
+ post.MessageReference = &MessageReference{
+ MessageID: msg.ID,
+ }
+ }
+ post.Content = fmt.Sprint(printable...)
+ return ctx.Caller.PostMessageToChannel(msg.ChannelID, post)
+}
+
+// SendImage 发送带图片消息到对方
+func (ctx *Ctx) SendImage(file string, replytosender bool, caption ...any) (*Message, error) {
+ msg := ctx.Value.(*Message)
+ post := &MessagePost{
+ ReplyMessageID: msg.ID,
+ }
+ if strings.HasPrefix(file, "http") {
+ post.Image = file
+ } else {
+ post.ImageFile = file
+ }
+ if replytosender {
+ post.MessageReference = &MessageReference{
+ MessageID: msg.ID,
+ }
+ }
+ post.Content = fmt.Sprint(caption...)
+ return ctx.Caller.PostMessageToChannel(msg.ChannelID, post)
+}
+
+// Block 匹配成功后阻止后续触发
+func (ctx *Ctx) Block() {
+ ctx.ma.SetBlock(true)
+}
+
+// Block 在 pre, rules, mid 阶段阻止后续触发
+func (ctx *Ctx) Break() {
+ ctx.ma.Break = true
+}
diff --git a/engine.go b/engine.go
new file mode 100644
index 0000000..bbc8f42
--- /dev/null
+++ b/engine.go
@@ -0,0 +1,84 @@
+package nano
+
+//go:generate go run codegen/engine/main.go
+
+// 生成空引擎
+func newEngine() *Engine {
+ return &Engine{
+ preHandler: []Rule{},
+ midHandler: []Rule{},
+ postHandler: []Process{},
+ }
+}
+
+var defaultEngine = newEngine()
+
+// Engine is the pre_handler, mid_handler, post_handler manager
+type Engine struct {
+ preHandler []Rule
+ midHandler []Rule
+ postHandler []Process
+ matchers []*Matcher
+ prio int
+ service string
+ datafolder string
+}
+
+// Delete 移除该 Engine 注册的所有 Matchers
+func (e *Engine) Delete() {
+ for _, m := range e.matchers {
+ m.Delete()
+ }
+}
+
+// UsePreHandler 向该 Engine 添加新 PreHandler(Rule),
+// 会在 Rule 判断前触发,如果 preHandler
+// 没有通过,则 Rule, Matcher 不会触发
+//
+// 可用于分群组管理插件等
+func (e *Engine) UsePreHandler(rules ...Rule) {
+ e.preHandler = append(e.preHandler, rules...)
+}
+
+// UseMidHandler 向该 Engine 添加新 MidHandler(Rule),
+// 会在 Rule 判断后, Matcher 触发前触发,如果 midHandler
+// 没有通过,则 Matcher 不会触发
+//
+// 可用于速率限制等
+func (e *Engine) UseMidHandler(rules ...Rule) {
+ e.midHandler = append(e.midHandler, rules...)
+}
+
+// UsePostHandler 向该 Engine 添加新 PostHandler(Rule),
+// 会在 Matcher 触发后触发,如果 PostHandler 返回 false,
+// 则后续的 post handler 不会触发
+//
+// 可用于速率限制等
+func (e *Engine) UsePostHandler(handler ...Process) {
+ e.postHandler = append(e.postHandler, handler...)
+}
+
+// ApplySingle 应用反并发
+func (e *Engine) ApplySingle(s *Single[int64]) *Engine {
+ s.Apply(e)
+ return e
+}
+
+// DataFolder 本插件数据目录, 默认 data/rbp/
+func (e *Engine) DataFolder() string {
+ return e.datafolder
+}
+
+// On 添加新的指定消息类型的匹配器(默认Engine)
+func On(typ string, rules ...Rule) *Matcher { return defaultEngine.On(typ, rules...) }
+
+// On 添加新的指定消息类型的匹配器
+func (e *Engine) On(typ string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: typ,
+ Rules: rules,
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
diff --git a/engine_generated.go b/engine_generated.go
new file mode 100644
index 0000000..851ad53
--- /dev/null
+++ b/engine_generated.go
@@ -0,0 +1,441 @@
+// Code generated by codegen/engine. DO NOT EDIT.
+
+package nano
+
+// OnMessage ...
+func (e *Engine) OnMessage(rules ...Rule) *Matcher { return e.On("Message", rules...) }
+
+// OnMessage ...
+func OnMessage(rules ...Rule) *Matcher { return On("Message", rules...) }
+
+// OnGuildCreate ...
+func (e *Engine) OnGuildCreate(rules ...Rule) *Matcher { return e.On("GuildCreate", rules...) }
+
+// OnGuildCreate ...
+func OnGuildCreate(rules ...Rule) *Matcher { return On("GuildCreate", rules...) }
+
+// OnGuildUpdate ...
+func (e *Engine) OnGuildUpdate(rules ...Rule) *Matcher { return e.On("GuildUpdate", rules...) }
+
+// OnGuildUpdate ...
+func OnGuildUpdate(rules ...Rule) *Matcher { return On("GuildUpdate", rules...) }
+
+// OnGuildDelete ...
+func (e *Engine) OnGuildDelete(rules ...Rule) *Matcher { return e.On("GuildDelete", rules...) }
+
+// OnGuildDelete ...
+func OnGuildDelete(rules ...Rule) *Matcher { return On("GuildDelete", rules...) }
+
+// OnChannelCreate ...
+func (e *Engine) OnChannelCreate(rules ...Rule) *Matcher { return e.On("ChannelCreate", rules...) }
+
+// OnChannelCreate ...
+func OnChannelCreate(rules ...Rule) *Matcher { return On("ChannelCreate", rules...) }
+
+// OnChannelUpdate ...
+func (e *Engine) OnChannelUpdate(rules ...Rule) *Matcher { return e.On("ChannelUpdate", rules...) }
+
+// OnChannelUpdate ...
+func OnChannelUpdate(rules ...Rule) *Matcher { return On("ChannelUpdate", rules...) }
+
+// OnChannelDelete ...
+func (e *Engine) OnChannelDelete(rules ...Rule) *Matcher { return e.On("ChannelDelete", rules...) }
+
+// OnChannelDelete ...
+func OnChannelDelete(rules ...Rule) *Matcher { return On("ChannelDelete", rules...) }
+
+// OnGuildMemberAdd ...
+func (e *Engine) OnGuildMemberAdd(rules ...Rule) *Matcher { return e.On("GuildMemberAdd", rules...) }
+
+// OnGuildMemberAdd ...
+func OnGuildMemberAdd(rules ...Rule) *Matcher { return On("GuildMemberAdd", rules...) }
+
+// OnGuildMemberUpdate ...
+func (e *Engine) OnGuildMemberUpdate(rules ...Rule) *Matcher { return e.On("GuildMemberUpdate", rules...) }
+
+// OnGuildMemberUpdate ...
+func OnGuildMemberUpdate(rules ...Rule) *Matcher { return On("GuildMemberUpdate", rules...) }
+
+// OnGuildMemberRemove ...
+func (e *Engine) OnGuildMemberRemove(rules ...Rule) *Matcher { return e.On("GuildMemberRemove", rules...) }
+
+// OnGuildMemberRemove ...
+func OnGuildMemberRemove(rules ...Rule) *Matcher { return On("GuildMemberRemove", rules...) }
+
+// OnMessageCreate ...
+func (e *Engine) OnMessageCreate(rules ...Rule) *Matcher { return e.On("MessageCreate", rules...) }
+
+// OnMessageCreate ...
+func OnMessageCreate(rules ...Rule) *Matcher { return On("MessageCreate", rules...) }
+
+// OnMessageDelete ...
+func (e *Engine) OnMessageDelete(rules ...Rule) *Matcher { return e.On("MessageDelete", rules...) }
+
+// OnMessageDelete ...
+func OnMessageDelete(rules ...Rule) *Matcher { return On("MessageDelete", rules...) }
+
+// OnMessageReactionAdd ...
+func (e *Engine) OnMessageReactionAdd(rules ...Rule) *Matcher { return e.On("MessageReactionAdd", rules...) }
+
+// OnMessageReactionAdd ...
+func OnMessageReactionAdd(rules ...Rule) *Matcher { return On("MessageReactionAdd", rules...) }
+
+// OnMessageReactionRemove ...
+func (e *Engine) OnMessageReactionRemove(rules ...Rule) *Matcher { return e.On("MessageReactionRemove", rules...) }
+
+// OnMessageReactionRemove ...
+func OnMessageReactionRemove(rules ...Rule) *Matcher { return On("MessageReactionRemove", rules...) }
+
+// OnDirectMessageCreate ...
+func (e *Engine) OnDirectMessageCreate(rules ...Rule) *Matcher { return e.On("DirectMessageCreate", rules...) }
+
+// OnDirectMessageCreate ...
+func OnDirectMessageCreate(rules ...Rule) *Matcher { return On("DirectMessageCreate", rules...) }
+
+// OnDirectMessageDelete ...
+func (e *Engine) OnDirectMessageDelete(rules ...Rule) *Matcher { return e.On("DirectMessageDelete", rules...) }
+
+// OnDirectMessageDelete ...
+func OnDirectMessageDelete(rules ...Rule) *Matcher { return On("DirectMessageDelete", rules...) }
+
+// OnOpenForumThreadCreate ...
+func (e *Engine) OnOpenForumThreadCreate(rules ...Rule) *Matcher { return e.On("OpenForumThreadCreate", rules...) }
+
+// OnOpenForumThreadCreate ...
+func OnOpenForumThreadCreate(rules ...Rule) *Matcher { return On("OpenForumThreadCreate", rules...) }
+
+// OnOpenForumThreadUpdate ...
+func (e *Engine) OnOpenForumThreadUpdate(rules ...Rule) *Matcher { return e.On("OpenForumThreadUpdate", rules...) }
+
+// OnOpenForumThreadUpdate ...
+func OnOpenForumThreadUpdate(rules ...Rule) *Matcher { return On("OpenForumThreadUpdate", rules...) }
+
+// OnOpenForumThreadDelete ...
+func (e *Engine) OnOpenForumThreadDelete(rules ...Rule) *Matcher { return e.On("OpenForumThreadDelete", rules...) }
+
+// OnOpenForumThreadDelete ...
+func OnOpenForumThreadDelete(rules ...Rule) *Matcher { return On("OpenForumThreadDelete", rules...) }
+
+// OnOpenForumPostCreate ...
+func (e *Engine) OnOpenForumPostCreate(rules ...Rule) *Matcher { return e.On("OpenForumPostCreate", rules...) }
+
+// OnOpenForumPostCreate ...
+func OnOpenForumPostCreate(rules ...Rule) *Matcher { return On("OpenForumPostCreate", rules...) }
+
+// OnOpenForumPostDelete ...
+func (e *Engine) OnOpenForumPostDelete(rules ...Rule) *Matcher { return e.On("OpenForumPostDelete", rules...) }
+
+// OnOpenForumPostDelete ...
+func OnOpenForumPostDelete(rules ...Rule) *Matcher { return On("OpenForumPostDelete", rules...) }
+
+// OnOpenForumReplyCreate ...
+func (e *Engine) OnOpenForumReplyCreate(rules ...Rule) *Matcher { return e.On("OpenForumReplyCreate", rules...) }
+
+// OnOpenForumReplyCreate ...
+func OnOpenForumReplyCreate(rules ...Rule) *Matcher { return On("OpenForumReplyCreate", rules...) }
+
+// OnOpenForumReplyDelete ...
+func (e *Engine) OnOpenForumReplyDelete(rules ...Rule) *Matcher { return e.On("OpenForumReplyDelete", rules...) }
+
+// OnOpenForumReplyDelete ...
+func OnOpenForumReplyDelete(rules ...Rule) *Matcher { return On("OpenForumReplyDelete", rules...) }
+
+// OnAudioOrLiveChannelMemberEnter ...
+func (e *Engine) OnAudioOrLiveChannelMemberEnter(rules ...Rule) *Matcher { return e.On("AudioOrLiveChannelMemberEnter", rules...) }
+
+// OnAudioOrLiveChannelMemberEnter ...
+func OnAudioOrLiveChannelMemberEnter(rules ...Rule) *Matcher { return On("AudioOrLiveChannelMemberEnter", rules...) }
+
+// OnAudioOrLiveChannelMemberExit ...
+func (e *Engine) OnAudioOrLiveChannelMemberExit(rules ...Rule) *Matcher { return e.On("AudioOrLiveChannelMemberExit", rules...) }
+
+// OnAudioOrLiveChannelMemberExit ...
+func OnAudioOrLiveChannelMemberExit(rules ...Rule) *Matcher { return On("AudioOrLiveChannelMemberExit", rules...) }
+
+// OnMessageAuditPass ...
+func (e *Engine) OnMessageAuditPass(rules ...Rule) *Matcher { return e.On("MessageAuditPass", rules...) }
+
+// OnMessageAuditPass ...
+func OnMessageAuditPass(rules ...Rule) *Matcher { return On("MessageAuditPass", rules...) }
+
+// OnMessageAuditReject ...
+func (e *Engine) OnMessageAuditReject(rules ...Rule) *Matcher { return e.On("MessageAuditReject", rules...) }
+
+// OnMessageAuditReject ...
+func OnMessageAuditReject(rules ...Rule) *Matcher { return On("MessageAuditReject", rules...) }
+
+// OnForumThreadCreate ...
+func (e *Engine) OnForumThreadCreate(rules ...Rule) *Matcher { return e.On("ForumThreadCreate", rules...) }
+
+// OnForumThreadCreate ...
+func OnForumThreadCreate(rules ...Rule) *Matcher { return On("ForumThreadCreate", rules...) }
+
+// OnForumThreadUpdate ...
+func (e *Engine) OnForumThreadUpdate(rules ...Rule) *Matcher { return e.On("ForumThreadUpdate", rules...) }
+
+// OnForumThreadUpdate ...
+func OnForumThreadUpdate(rules ...Rule) *Matcher { return On("ForumThreadUpdate", rules...) }
+
+// OnForumThreadDelete ...
+func (e *Engine) OnForumThreadDelete(rules ...Rule) *Matcher { return e.On("ForumThreadDelete", rules...) }
+
+// OnForumThreadDelete ...
+func OnForumThreadDelete(rules ...Rule) *Matcher { return On("ForumThreadDelete", rules...) }
+
+// OnForumPostCreate ...
+func (e *Engine) OnForumPostCreate(rules ...Rule) *Matcher { return e.On("ForumPostCreate", rules...) }
+
+// OnForumPostCreate ...
+func OnForumPostCreate(rules ...Rule) *Matcher { return On("ForumPostCreate", rules...) }
+
+// OnForumPostDelete ...
+func (e *Engine) OnForumPostDelete(rules ...Rule) *Matcher { return e.On("ForumPostDelete", rules...) }
+
+// OnForumPostDelete ...
+func OnForumPostDelete(rules ...Rule) *Matcher { return On("ForumPostDelete", rules...) }
+
+// OnForumReplyCreate ...
+func (e *Engine) OnForumReplyCreate(rules ...Rule) *Matcher { return e.On("ForumReplyCreate", rules...) }
+
+// OnForumReplyCreate ...
+func OnForumReplyCreate(rules ...Rule) *Matcher { return On("ForumReplyCreate", rules...) }
+
+// OnForumReplyDelete ...
+func (e *Engine) OnForumReplyDelete(rules ...Rule) *Matcher { return e.On("ForumReplyDelete", rules...) }
+
+// OnForumReplyDelete ...
+func OnForumReplyDelete(rules ...Rule) *Matcher { return On("ForumReplyDelete", rules...) }
+
+// OnForumPublishAuditResult ...
+func (e *Engine) OnForumPublishAuditResult(rules ...Rule) *Matcher { return e.On("ForumPublishAuditResult", rules...) }
+
+// OnForumPublishAuditResult ...
+func OnForumPublishAuditResult(rules ...Rule) *Matcher { return On("ForumPublishAuditResult", rules...) }
+
+// OnAudioStart ...
+func (e *Engine) OnAudioStart(rules ...Rule) *Matcher { return e.On("AudioStart", rules...) }
+
+// OnAudioStart ...
+func OnAudioStart(rules ...Rule) *Matcher { return On("AudioStart", rules...) }
+
+// OnAudioFinish ...
+func (e *Engine) OnAudioFinish(rules ...Rule) *Matcher { return e.On("AudioFinish", rules...) }
+
+// OnAudioFinish ...
+func OnAudioFinish(rules ...Rule) *Matcher { return On("AudioFinish", rules...) }
+
+// OnAudioOnMic ...
+func (e *Engine) OnAudioOnMic(rules ...Rule) *Matcher { return e.On("AudioOnMic", rules...) }
+
+// OnAudioOnMic ...
+func OnAudioOnMic(rules ...Rule) *Matcher { return On("AudioOnMic", rules...) }
+
+// OnAudioOffMic ...
+func (e *Engine) OnAudioOffMic(rules ...Rule) *Matcher { return e.On("AudioOffMic", rules...) }
+
+// OnAudioOffMic ...
+func OnAudioOffMic(rules ...Rule) *Matcher { return On("AudioOffMic", rules...) }
+
+// OnAtMessageCreate ...
+func (e *Engine) OnAtMessageCreate(rules ...Rule) *Matcher { return e.On("AtMessageCreate", rules...) }
+
+// OnAtMessageCreate ...
+func OnAtMessageCreate(rules ...Rule) *Matcher { return On("AtMessageCreate", rules...) }
+
+// OnPublicMessageDelete ...
+func (e *Engine) OnPublicMessageDelete(rules ...Rule) *Matcher { return e.On("PublicMessageDelete", rules...) }
+
+// OnPublicMessageDelete ...
+func OnPublicMessageDelete(rules ...Rule) *Matcher { return On("PublicMessageDelete", rules...) }
+
+// OnMessageKeyword ...
+func OnMessageKeyword(keyword string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessageKeyword(keyword, rules...)
+}
+
+// OnMessageKeyword ...
+func (e *Engine) OnMessageKeyword(keyword string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{KeywordRule(keyword)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessageCommandGroup ...
+func OnMessageCommandGroup(commands []string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessageCommandGroup(commands, rules...)
+}
+
+// OnMessageCommandGroup ...
+func (e *Engine) OnMessageCommandGroup(commands []string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{CommandGroupRule(commands...)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessageSuffixGroup ...
+func OnMessageSuffixGroup(suffix []string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessageSuffixGroup(suffix, rules...)
+}
+
+// OnMessageSuffixGroup ...
+func (e *Engine) OnMessageSuffixGroup(suffix []string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{SuffixGroupRule(suffix...)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessagePrefix ...
+func OnMessagePrefix(prefix string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessagePrefix(prefix, rules...)
+}
+
+// OnMessagePrefix ...
+func (e *Engine) OnMessagePrefix(prefix string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{PrefixRule(prefix)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessageRegex ...
+func OnMessageRegex(regexPattern string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessageRegex(regexPattern, rules...)
+}
+
+// OnMessageRegex ...
+func (e *Engine) OnMessageRegex(regexPattern string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{RegexRule(regexPattern)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessageFullMatch ...
+func OnMessageFullMatch(src string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessageFullMatch(src, rules...)
+}
+
+// OnMessageFullMatch ...
+func (e *Engine) OnMessageFullMatch(src string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{FullMatchRule(src)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessageFullMatchGroup ...
+func OnMessageFullMatchGroup(src []string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessageFullMatchGroup(src, rules...)
+}
+
+// OnMessageFullMatchGroup ...
+func (e *Engine) OnMessageFullMatchGroup(src []string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{FullMatchGroupRule(src...)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessageKeywordGroup ...
+func OnMessageKeywordGroup(keywords []string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessageKeywordGroup(keywords, rules...)
+}
+
+// OnMessageKeywordGroup ...
+func (e *Engine) OnMessageKeywordGroup(keywords []string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{KeywordGroupRule(keywords...)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessagePrefixGroup ...
+func OnMessagePrefixGroup(prefix []string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessagePrefixGroup(prefix, rules...)
+}
+
+// OnMessagePrefixGroup ...
+func (e *Engine) OnMessagePrefixGroup(prefix []string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{PrefixGroupRule(prefix...)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessageSuffix ...
+func OnMessageSuffix(suffix string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessageSuffix(suffix, rules...)
+}
+
+// OnMessageSuffix ...
+func (e *Engine) OnMessageSuffix(suffix string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{SuffixRule(suffix)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessageCommand ...
+func OnMessageCommand(commands string, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessageCommand(commands, rules...)
+}
+
+// OnMessageCommand ...
+func (e *Engine) OnMessageCommand(commands string, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{CommandRule(commands)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
+
+// OnMessageShell shell命令触发器
+func OnMessageShell(command string, model interface{}, rules ...Rule) *Matcher {
+ return defaultEngine.OnMessageShell(command, model, rules...)
+}
+
+// OnMessageShell shell命令触发器
+func (e *Engine) OnMessageShell(command string, model interface{}, rules ...Rule) *Matcher {
+ matcher := &Matcher{
+ Type: "Message",
+ Rules: append([]Rule{ShellRule(command, model)}, rules...),
+ Engine: e,
+ }
+ e.matchers = append(e.matchers, matcher)
+ return StoreMatcher(matcher)
+}
diff --git a/event.go b/event.go
index a62eb21..58ac060 100644
--- a/event.go
+++ b/event.go
@@ -3,10 +3,23 @@ package nano
import (
"encoding/json"
"reflect"
+ "strings"
log "github.com/sirupsen/logrus"
)
+// Event ...
+type Event struct {
+ // Type is payload.T
+ Type string
+ // Seq 序列号
+ Seq uint32
+ // Value 是 D
+ Value any
+ // value is the reflect value of Value
+ value reflect.Value
+}
+
// processEvent 处理需要关注的业务事件
func (bot *Bot) processEvent(payload *WebsocketPayload) {
tp := UnderlineToCamel(payload.T)
@@ -25,4 +38,129 @@ func (bot *Bot) processEvent(payload *WebsocketPayload) {
go ev.h(payload.S, bot, x.UnsafePointer())
return
}
+ ctx := &Ctx{
+ Event: Event{
+ Type: tp,
+ Seq: payload.S,
+ },
+ State: State{},
+ Caller: bot,
+ }
+ switch tp {
+ case "DirectMessageCreate":
+ ctx.IsToMe = true
+ fallthrough
+ case "MessageCreate", "AtMessageCreate":
+ tp = "Message"
+ }
+ matcherLock.RLock()
+ n := len(matcherMap[tp])
+ if n == 0 {
+ matcherLock.RUnlock()
+ return
+ }
+ log.Debugln(getLogHeader(), "pass", tp, "event to plugins")
+ matchers := make([]*Matcher, n)
+ copy(matchers, matcherMap[tp])
+ matcherLock.RUnlock()
+ x := reflect.New(types[ctx.Type])
+ err := json.Unmarshal(payload.D, x.Interface())
+ if err != nil {
+ log.Warnln(getLogHeader(), "解析", ctx.Type, "事件时出现错误:", err)
+ return
+ }
+ ctx.Value = x.Interface()
+ ctx.value = x
+ switch tp {
+ case "Message":
+ ctx.Message = (*Message)(x.UnsafePointer())
+ log.Infoln(getLogHeader(), "收到 Guild:", ctx.Message.GuildID, ", Channel:", ctx.Message.ChannelID, "消息", ctx.Message.Author.ID, ":", ctx.Message.Content)
+ }
+ go match(ctx, matchers)
+}
+
+func match(ctx *Ctx, matchers []*Matcher) {
+ if ctx.Message != nil && ctx.Message.Content != "" { // 确保无空
+ if !ctx.IsToMe {
+ ctx.IsToMe = func(ctx *Ctx) bool {
+ name := ctx.Caller.ready.User.Username
+ if strings.HasPrefix(ctx.Message.Content, name) {
+ log.Debugln(getLogHeader(), "message before process:", ctx.Message.Content)
+ ctx.Message.Content = strings.TrimLeft(ctx.Message.Content[len(name):], " ")
+ log.Debugln(getLogHeader(), "message after process:", ctx.Message.Content)
+ return true
+ }
+ atme := ctx.Caller.AtMe()
+ if strings.HasPrefix(ctx.Message.Content, atme) {
+ log.Debugln(getLogHeader(), "message before process:", ctx.Message.Content)
+ ctx.Message.Content = strings.TrimLeft(ctx.Message.Content[len(atme):], " ")
+ log.Debugln(getLogHeader(), "message after process:", ctx.Message.Content)
+ return true
+ }
+ return OnlyPrivate(ctx)
+ }(ctx)
+ }
+ }
+ log.Debugln(getLogHeader(), "message is to me:", ctx.IsToMe)
+loop:
+ for _, matcher := range matchers {
+ for k := range ctx.State { // Clear State
+ delete(ctx.State, k)
+ }
+ matcherLock.RLock()
+ m := matcher.copy()
+ matcherLock.RUnlock()
+ ctx.ma = m
+
+ // pre handler
+ if m.Engine != nil {
+ for _, handler := range m.Engine.preHandler {
+ if !handler(ctx) { // 有 pre handler 未满足
+ if m.Break { // 阻断后续
+ break loop
+ }
+ continue loop
+ }
+ }
+ }
+
+ for _, rule := range m.Rules {
+ if rule != nil && !rule(ctx) { // 有 Rule 的条件未满足
+ if m.Break { // 阻断后续
+ break loop
+ }
+ continue loop
+ }
+ }
+
+ // mid handler
+ if m.Engine != nil {
+ for _, handler := range m.Engine.midHandler {
+ if !handler(ctx) { // 有 mid handler 未满足
+ if m.Break { // 阻断后续
+ break loop
+ }
+ continue loop
+ }
+ }
+ }
+
+ if m.Process != nil {
+ m.Process(ctx) // 处理事件
+ }
+ if matcher.Temp { // 临时 Matcher 删除
+ matcher.Delete()
+ }
+
+ if m.Engine != nil {
+ // post handler
+ for _, handler := range m.Engine.postHandler {
+ handler(ctx)
+ }
+ }
+
+ if m.Block { // 阻断后续
+ break loop
+ }
+ }
}
diff --git a/example/echo/main.go b/example/echo/main.go
new file mode 100644
index 0000000..066fabf
--- /dev/null
+++ b/example/echo/main.go
@@ -0,0 +1,20 @@
+package echo
+
+import (
+ ctrl "github.com/FloatTech/zbpctrl"
+ nano "github.com/fumiama/NanoBot"
+)
+
+func init() {
+ nano.Register("echo", &ctrl.Options[*nano.Ctx]{
+ DisableOnDefault: false,
+ Help: "- echo xxx",
+ }).OnMessagePrefix("echo").SetBlock(true).
+ Handle(func(ctx *nano.Ctx) {
+ args := ctx.State["args"].(string)
+ if args == "" {
+ return
+ }
+ _, _ = ctx.SendPlainMessage(false, args)
+ })
+}
diff --git a/example/handler/main.go b/example/handler/main.go
new file mode 100644
index 0000000..b9d81e9
--- /dev/null
+++ b/example/handler/main.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "strings"
+
+ nano "github.com/fumiama/NanoBot"
+ log "github.com/sirupsen/logrus"
+)
+
+func main() {
+ log.SetLevel(log.DebugLevel)
+ nano.OpenAPI = nano.SandboxAPI
+ nano.Run(&nano.Bot{
+ AppID: "你的AppID",
+ Token: "你的Token",
+ Secret: "你的Secret, 目前没用到, 可以不填",
+ Intents: nano.IntentPublic,
+ Handler: &nano.Handler{
+ OnAtMessageCreate: func(s uint32, bot *nano.Bot, d *nano.Message) {
+ u := ""
+ if len(d.Attachments) > 0 {
+ u = d.Attachments[0].URL
+ if !strings.HasPrefix(u, "http") {
+ u = "http://" + u
+ }
+ }
+ _, err := bot.PostMessageToChannel(d.ChannelID, &nano.MessagePost{
+ Content: "您发送了: " + d.Content,
+ Image: u,
+ ReplyMessageID: d.ID,
+ MessageReference: &nano.MessageReference{
+ MessageID: d.ID,
+ },
+ })
+ if err != nil {
+ bot.PostMessageToChannel(d.ChannelID, &nano.MessagePost{
+ Content: "[ERROR]: " + err.Error(),
+ ReplyMessageID: d.ID,
+ })
+ }
+ },
+ },
+ })
+}
diff --git a/example/main.go b/example/main.go
new file mode 100644
index 0000000..b69dffa
--- /dev/null
+++ b/example/main.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+ _ "github.com/fumiama/NanoBot/example/echo"
+
+ nano "github.com/fumiama/NanoBot"
+ log "github.com/sirupsen/logrus"
+)
+
+func main() {
+ log.SetLevel(log.DebugLevel)
+ nano.OpenAPI = nano.SandboxAPI
+ nano.OnMessageFullMatch("help").SetBlock(true).
+ Handle(func(ctx *nano.Ctx) {
+ _, _ = ctx.SendPlainMessage(false, "echo string")
+ })
+ nano.Run(&nano.Bot{
+ AppID: "你的AppID",
+ Token: "你的Token",
+ Secret: "你的Secret, 目前没用到, 可以不填",
+ Intents: nano.IntentPublic,
+ SuperUsers: []string{"用户ID1", "用户ID2"},
+ })
+}
diff --git a/future.go b/future.go
new file mode 100644
index 0000000..0487dd6
--- /dev/null
+++ b/future.go
@@ -0,0 +1,98 @@
+package nano
+
+// FutureEvent 是 ZeroBot 交互式的核心,用于异步获取指定事件
+type FutureEvent struct {
+ Type string
+ Priority int
+ Rule []Rule
+ Block bool
+}
+
+// NewFutureEvent 创建一个FutureEvent, 并返回其指针
+func NewFutureEvent(Type string, Priority int, Block bool, rule ...Rule) *FutureEvent {
+ return &FutureEvent{
+ Type: Type,
+ Priority: Priority,
+ Rule: rule,
+ Block: Block,
+ }
+}
+
+// FutureEvent 返回一个 FutureEvent 实例指针,用于获取满足 Rule 的 未来事件
+func (m *Matcher) FutureEvent(Type string, rule ...Rule) *FutureEvent {
+ return &FutureEvent{
+ Type: Type,
+ Priority: m.priority,
+ Block: m.Block,
+ Rule: rule,
+ }
+}
+
+// Next 返回一个 chan 用于接收下一个指定事件
+//
+// 该 chan 必须接收,如需手动取消监听,请使用 Repeat 方法
+func (n *FutureEvent) Next() <-chan *Ctx {
+ ch := make(chan *Ctx, 1)
+ StoreTempMatcher(&Matcher{
+ Type: n.Type,
+ Block: n.Block,
+ priority: n.Priority,
+ Rules: n.Rule,
+ Engine: defaultEngine,
+ Process: func(ctx *Ctx) {
+ ch <- ctx
+ close(ch)
+ },
+ })
+ return ch
+}
+
+// Repeat 返回一个 chan 用于接收无穷个指定事件,和一个取消监听的函数
+//
+// 如果没有取消监听,将不断监听指定事件
+func (n *FutureEvent) Repeat() (recv <-chan *Ctx, cancel func()) {
+ ch, done := make(chan *Ctx, 1), make(chan struct{})
+ go func() {
+ defer close(ch)
+ in := make(chan *Ctx, 1)
+ matcher := StoreMatcher(&Matcher{
+ Type: n.Type,
+ Block: n.Block,
+ priority: n.Priority,
+ Rules: n.Rule,
+ Engine: defaultEngine,
+ Process: func(ctx *Ctx) {
+ in <- ctx
+ },
+ })
+ for {
+ select {
+ case e := <-in:
+ ch <- e
+ case <-done:
+ matcher.Delete()
+ close(in)
+ return
+ }
+ }
+ }()
+ return ch, func() {
+ close(done)
+ }
+}
+
+// Take 基于 Repeat 封装,返回一个 chan 接收指定数量的事件
+//
+// 该 chan 对象必须接收,否则将有 goroutine 泄漏,如需手动取消请使用 Repeat
+func (n *FutureEvent) Take(num int) <-chan *Ctx {
+ recv, cancel := n.Repeat()
+ ch := make(chan *Ctx, num)
+ go func() {
+ defer close(ch)
+ for i := 0; i < num; i++ {
+ ch <- <-recv
+ }
+ cancel()
+ }()
+ return ch
+}
diff --git a/go.mod b/go.mod
index 5fc54d8..fcdb4fa 100644
--- a/go.mod
+++ b/go.mod
@@ -3,14 +3,35 @@ module github.com/fumiama/NanoBot
go 1.20
require (
+ github.com/FloatTech/floatbox v0.0.0-20230827160415-f0865337a824
+ github.com/FloatTech/zbpctrl v1.5.2
github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e
github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5
github.com/fumiama/go-base16384 v1.7.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.3
+ github.com/stretchr/testify v1.8.1
+ github.com/wdvxdr1123/ZeroBot v1.7.4
+ gopkg.in/yaml.v3 v3.0.1
)
require (
- golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
- golang.org/x/text v0.3.7 // indirect
+ github.com/FloatTech/sqlite v0.5.0 // indirect
+ github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/fumiama/cron v1.3.0 // indirect
+ github.com/fumiama/go-registry v0.2.6 // indirect
+ github.com/fumiama/go-simple-protobuf v0.1.0 // indirect
+ github.com/fumiama/gofastTEA v0.0.10 // indirect
+ github.com/fumiama/sqlite3 v1.14.6 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/mattn/go-isatty v0.0.14 // indirect
+ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
+ golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 // indirect
+ golang.org/x/text v0.4.0 // indirect
+ modernc.org/libc v1.14.6 // indirect
+ modernc.org/mathutil v1.4.1 // indirect
+ modernc.org/memory v1.0.5 // indirect
)
diff --git a/go.sum b/go.sum
index 213fce0..6eaa4b7 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,11 @@
+github.com/FloatTech/floatbox v0.0.0-20230827160415-f0865337a824 h1:w72fzQg1Y9+VLSRl7iKzaZ6fG3myyMJfpOSajcjaMDM=
+github.com/FloatTech/floatbox v0.0.0-20230827160415-f0865337a824/go.mod h1:FwQm6wk+b4wuW54KCKn3zccMX47Q5apnHD/Yakzv0fI=
+github.com/FloatTech/sqlite v0.5.0 h1:U7J5Omc534PqmH6csfu+ypCo3DS8L91l5lTsxUu3b/U=
+github.com/FloatTech/sqlite v0.5.0/go.mod h1:i33d92OtR8jcp5fBUvQtospf27+MkfUxnGwnZ95E/dA=
+github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b h1:tvciXWq2nuvTbFeJGLDNIdRX3BI546D3O7k7vrVueZw=
+github.com/FloatTech/ttl v0.0.0-20220715042055-15612be72f5b/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs=
+github.com/FloatTech/zbpctrl v1.5.2 h1:5ap0t2KgROpfTVHqMd9vHKXLeLmRFGI3ZrTPASgFP6s=
+github.com/FloatTech/zbpctrl v1.5.2/go.mod h1:BVPivMDJCBImPSdwgizb6sqb7rcDaRE65ZjfgthoC7g=
github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e h1:wR3MXQ3VbUlPKOOUwLOYgh/QaJThBTYtsl673O3lqSA=
github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e/go.mod h1:vD7Ra3Q9onRtojoY5sMCLQ7JBgjUsrXDnDKyFxqpf9w=
github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5 h1:bBmmB7he0iVN4m5mcehfheeRUEer/Avo4ujnxI3uCqs=
@@ -5,23 +13,200 @@ github.com/RomiChan/websocket v1.4.3-0.20220227141055-9b2c6168c9c5/go.mod h1:0Uc
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/fumiama/cron v1.3.0 h1:ZWlwuexF+HQHl3cYytEE5HNwD99q+3vNZF1GrEiXCFo=
+github.com/fumiama/cron v1.3.0/go.mod h1:bz5Izvgi/xEUI8tlBN8BI2jr9Moo8N4or0KV8xXuPDY=
github.com/fumiama/go-base16384 v1.7.0 h1:6fep7XPQWxRlh4Hu+KsdH+6+YdUp+w6CwRXtMWSsXCA=
github.com/fumiama/go-base16384 v1.7.0/go.mod h1:OEn+947GV5gsbTAnyuUW/SrfxJYUdYupSIQXOuGOcXM=
+github.com/fumiama/go-registry v0.2.6 h1:+vEeBUwa1+GC87ujW3Km42fi8O/H7QcpVJWu1iuGNh0=
+github.com/fumiama/go-registry v0.2.6/go.mod h1:HjYagPZXzR2xCCxaSQerqX7JRzC0yiv2kslDdBiTq/g=
+github.com/fumiama/go-simple-protobuf v0.1.0 h1:rLzJgNqB6LHNDVMl81yyNt6ZKziWtVfu+ioF0edlEVw=
+github.com/fumiama/go-simple-protobuf v0.1.0/go.mod h1:5yYNapXq1tQMOZg9bOIVhQlZk9pQqpuFIO4DZLbsdy4=
+github.com/fumiama/gofastTEA v0.0.10 h1:JJJ+brWD4kie+mmK2TkspDXKzqq0IjXm89aGYfoGhhQ=
+github.com/fumiama/gofastTEA v0.0.10/go.mod h1:RIdbYZyB4MbH6ZBlPymRaXn3cD6SedlCu5W/HHfMPBk=
+github.com/fumiama/sqlite3 v1.14.6 h1:+e+iygyiDXQJVi7xeXIviBvR7hAc5y20WA9hRwfKn10=
+github.com/fumiama/sqlite3 v1.14.6/go.mod h1:Xx9a2/OtHuy9pBjow0N+bE/RhNeZ7zZz5xh25vqbA5A=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
+github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/wdvxdr1123/ZeroBot v1.7.4 h1:+148rELpf/FCDW2EuvKqpb9bNKcwKRtoh16s2sIb5SE=
+github.com/wdvxdr1123/ZeroBot v1.7.4/go.mod h1:y29UIOy0RD3P+0meDNIWRhcJF3jtWPN9xP9hgt/AJAU=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41 h1:ohgcoMbSofXygzo6AD2I1kz3BFmW1QArPYTtwEM3UXc=
+golang.org/x/sys v0.0.0-20220915200043-7b5979e65e41/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.20/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/cc/v3 v3.35.22/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60=
+modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw=
+modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI=
+modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag=
+modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw=
+modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ=
+modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c=
+modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo=
+modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg=
+modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I=
+modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs=
+modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8=
+modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE=
+modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk=
+modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w=
+modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE=
+modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8=
+modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc=
+modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU=
+modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE=
+modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk=
+modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI=
+modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE=
+modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg=
+modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74=
+modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU=
+modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU=
+modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc=
+modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM=
+modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ=
+modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84=
+modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ=
+modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
+modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
+modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU=
+modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko=
+modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA=
+modernc.org/ccgo/v3 v3.13.1/go.mod h1:aBYVOUfIlcSnrsRVU8VRS35y2DIfpgkmVkYZ0tpIXi4=
+modernc.org/ccgo/v3 v3.15.9/go.mod h1:md59wBwDT2LznX/OTCPoVS6KIsdRgY8xqQwBV+hkTH0=
+modernc.org/ccgo/v3 v3.15.10/go.mod h1:wQKxoFn0ynxMuCLfFD09c8XPUCc8obfchoVR9Cn0fI8=
+modernc.org/ccgo/v3 v3.15.12/go.mod h1:VFePOWoCd8uDGRJpq/zfJ29D0EVzMSyID8LCMWYbX6I=
+modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
+modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
+modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
+modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
+modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg=
+modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M=
+modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU=
+modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE=
+modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso=
+modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8=
+modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8=
+modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I=
+modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk=
+modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY=
+modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE=
+modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg=
+modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM=
+modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg=
+modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo=
+modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8=
+modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ=
+modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA=
+modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM=
+modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg=
+modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE=
+modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM=
+modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU=
+modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw=
+modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M=
+modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18=
+modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8=
+modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw=
+modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0=
+modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI=
+modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE=
+modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY=
+modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ=
+modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c=
+modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI=
+modernc.org/libc v1.12.0/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ=
+modernc.org/libc v1.14.1/go.mod h1:npFeGWjmZTjFeWALQLrvklVmAxv4m80jnG3+xI8FdJk=
+modernc.org/libc v1.14.2/go.mod h1:MX1GBLnRLNdvmK9azU9LCxZ5lMyhrbEMK8rG3X/Fe34=
+modernc.org/libc v1.14.3/go.mod h1:GPIvQVOVPizzlqyRX3l756/3ppsAgg1QgPxjr5Q4agQ=
+modernc.org/libc v1.14.5/go.mod h1:2PJHINagVxO4QW/5OQdRrvMYo+bm5ClpUFfyXCYl9ak=
+modernc.org/libc v1.14.6 h1:SSiZiE5199iYsGM9gtkDj90xqcXVwubWG8CtoYE+Mnk=
+modernc.org/libc v1.14.6/go.mod h1:2PJHINagVxO4QW/5OQdRrvMYo+bm5ClpUFfyXCYl9ak=
+modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8=
+modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
+modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14=
+modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM=
+modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
+modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/handler.go b/handler.go
index d7bf24c..f888f08 100644
--- a/handler.go
+++ b/handler.go
@@ -14,6 +14,18 @@ type eventHandlerType struct {
t reflect.Type
}
+var types map[string]reflect.Type // types 便于反射初始化的 types
+
+func init() {
+ h := reflect.ValueOf(&Handler{}).Elem()
+ t := h.Type()
+ types = make(map[string]reflect.Type, h.NumField()*4)
+ for i := 0; i < h.NumField(); i++ {
+ tp := t.Field(i).Name[2:] // skip On
+ types[tp] = t.Field(i).Type.In(2).Elem()
+ }
+}
+
// Handler 事件订阅
//
// https://bot.q.qq.com/wiki/develop/api/gateway/intents.html
diff --git a/lazy.go b/lazy.go
new file mode 100644
index 0000000..8bac110
--- /dev/null
+++ b/lazy.go
@@ -0,0 +1,20 @@
+package nano
+
+import (
+ "errors"
+ "strings"
+ "unicode"
+
+ "github.com/FloatTech/floatbox/file"
+)
+
+// 下载并获取本 engine 文件夹下的懒加载数据
+func (e *Engine) GetLazyData(filename string, isDataMustEqual bool) ([]byte, error) {
+ if e.datafolder == "" {
+ return nil, errors.New("datafolder is empty")
+ }
+ if !strings.HasSuffix(e.datafolder, "/") || !strings.HasPrefix(e.datafolder, "data/") || !unicode.IsUpper(rune(e.datafolder[5])) {
+ return nil, errors.New("invalid datafolder")
+ }
+ return file.GetLazyData(e.datafolder+filename, "data/control/stor.spb", isDataMustEqual)
+}
diff --git a/manager.go b/manager.go
new file mode 100644
index 0000000..34e80cb
--- /dev/null
+++ b/manager.go
@@ -0,0 +1,106 @@
+package nano
+
+import (
+ "fmt"
+ "os"
+ "sort"
+ "sync/atomic"
+ "unicode"
+
+ "github.com/FloatTech/floatbox/file"
+ ctrl "github.com/FloatTech/zbpctrl"
+ "github.com/sirupsen/logrus"
+)
+
+type Manager ctrl.Manager[*Ctx]
+
+var (
+ enmap = make(map[string]*Engine)
+ priomap = make(map[int]string) // priomap is map[prio]service
+ foldermap = make(map[string]string) // foldermap is map[folder]service
+ prio uint64
+ m = ctrl.NewManager[*Ctx]("data/control/plugins.db")
+)
+
+// Register 注册插件控制器
+func Register(service string, o *ctrl.Options[*Ctx]) *Engine {
+ prio := int(atomic.AddUint64(&prio, 10))
+ e := newEngine()
+ s, ok := priomap[prio]
+ if ok {
+ panic(fmt.Sprint("prio", prio, "is used by", s))
+ }
+ priomap[prio] = service
+ logrus.Debugln("[control]插件", service, "已设置优先级", prio)
+ e.UsePreHandler(newctrl(service, o))
+ e.prio = prio
+ e.service = service
+ switch {
+ case o.PublicDataFolder != "":
+ if unicode.IsLower([]rune(o.PublicDataFolder)[0]) {
+ panic("public data folder " + o.PublicDataFolder + " must start with an upper case letter")
+ }
+ e.datafolder = "data/" + o.PublicDataFolder + "/"
+ case o.PrivateDataFolder != "":
+ if unicode.IsUpper([]rune(o.PrivateDataFolder)[0]) {
+ panic("private data folder " + o.PrivateDataFolder + " must start with an lower case letter")
+ }
+ e.datafolder = "data/" + o.PrivateDataFolder + "/"
+ default:
+ e.datafolder = "data/nano/"
+ }
+ if e.datafolder != "data/nano/" {
+ s, ok := foldermap[e.datafolder]
+ if ok {
+ panic("folder " + e.datafolder + " has been required by service " + s)
+ }
+ foldermap[e.datafolder] = service
+ }
+ if file.IsNotExist(e.datafolder) {
+ err := os.MkdirAll(e.datafolder, 0755)
+ if err != nil {
+ panic(err)
+ }
+ }
+ logrus.Debugln("[control]插件", service, "已设置数据目录", e.datafolder)
+ enmap[service] = e
+ return e
+}
+
+// Delete 删除插件控制器, 不会删除数据
+func Delete(service string) {
+ engine, ok := enmap[service]
+ if ok {
+ engine.Delete()
+ m.RLock()
+ _, ok = m.M[service]
+ m.RUnlock()
+ if ok {
+ m.Lock()
+ delete(m.M, service)
+ m.Unlock()
+ }
+ }
+}
+
+// ForEachByPrio iterates through managers by their priority.
+func ForEachByPrio(iterator func(i int, manager *ctrl.Control[*Ctx]) bool) {
+ for i, v := range cpmp2lstbyprio() {
+ if !iterator(i, v) {
+ return
+ }
+ }
+}
+
+func cpmp2lstbyprio() []*ctrl.Control[*Ctx] {
+ m.RLock()
+ defer m.RUnlock()
+ ret := make([]*ctrl.Control[*Ctx], 0, len(m.M))
+ for _, v := range m.M {
+ ret = append(ret, v)
+ }
+ sort.SliceStable(ret, func(i, j int) bool {
+ return enmap[ret[i].Service].prio < enmap[ret[j].Service].prio
+ })
+ return ret
+}
diff --git a/matcher.go b/matcher.go
new file mode 100644
index 0000000..ff3cb67
--- /dev/null
+++ b/matcher.go
@@ -0,0 +1,150 @@
+package nano
+
+import (
+ "sort"
+ "sync"
+
+ "github.com/wdvxdr1123/ZeroBot/extension/rate"
+)
+
+type (
+ // Rule filter the event
+ Rule func(ctx *Ctx) bool
+ // Process 事件处理函数
+ Process func(ctx *Ctx)
+)
+
+// Matcher 是 ZeroBot 匹配和处理事件的最小单元
+type Matcher struct {
+ // Temp 是否为临时Matcher,临时 Matcher 匹配一次后就会删除当前 Matcher
+ Temp bool
+ // Block 是否阻断后续 Matcher,为 true 时当前Matcher匹配成功后,后续Matcher不参与匹配
+ Block bool
+ // Break 是否退出后续匹配流程, 只有 rule 返回 false 且此值为真才会退出, 且不对 mid handler 以下的 rule 生效
+ Break bool
+ // priority 优先级,越小优先级越高
+ priority int
+ // Event 当前匹配到的事件
+ Event *Event
+ // Type 匹配的事件类型
+ Type string
+ // Rules 匹配规则
+ Rules []Rule
+ // Process 处理事件的函数
+ Process Process
+ // Engine 注册 Matcher 的 Engine,Engine可为一系列 Matcher 添加通用 Rule 和 其他钩子
+ Engine *Engine
+}
+
+var (
+ // 所有主匹配器列表
+ matcherMap = make(map[string][]*Matcher, 0)
+ // Matcher 修改读写锁
+ matcherLock = sync.RWMutex{}
+)
+
+// State store the context of a matcher.
+type State map[string]any
+
+func sortMatcher(typ string) {
+ sort.Slice(matcherMap[typ], func(i, j int) bool { // 按优先级排序
+ return matcherMap[typ][i].priority < matcherMap[typ][j].priority
+ })
+}
+
+// SetBlock 设置是否阻断后面的 Matcher 触发
+func (m *Matcher) SetBlock(block bool) *Matcher {
+ m.Block = block
+ return m
+}
+
+// setPriority 设置当前 Matcher 优先级
+func (m *Matcher) setPriority(priority int) *Matcher {
+ matcherLock.Lock()
+ defer matcherLock.Unlock()
+ m.priority = priority
+ sortMatcher(m.Type)
+ return m
+}
+
+/*
+// firstPriority 设置当前 Matcher 优先级 - 0
+func (m *Matcher) firstPriority() *Matcher {
+ return m.setPriority(0)
+}
+*/
+
+// secondPriority 设置当前 Matcher 优先级 - 1
+func (m *Matcher) secondPriority() *Matcher {
+ return m.setPriority(1)
+}
+
+/*
+// thirdPriority 设置当前 Matcher 优先级 - 2
+func (m *Matcher) thirdPriority() *Matcher {
+ return m.setPriority(2)
+}
+*/
+
+// Limit 限速器
+//
+// postfn 当请求被拒绝时的操作
+func (m *Matcher) Limit(limiterfn func(*Ctx) *rate.Limiter, postfn ...func(*Ctx)) *Matcher {
+ m.Rules = append(m.Rules, func(ctx *Ctx) bool {
+ if limiterfn(ctx).Acquire() {
+ return true
+ }
+ if len(postfn) > 0 {
+ for _, fn := range postfn {
+ fn(ctx)
+ }
+ }
+ return false
+ })
+ return m
+}
+
+// StoreMatcher store a matcher to matcher list.
+func StoreMatcher(m *Matcher) *Matcher {
+ matcherLock.Lock()
+ defer matcherLock.Unlock()
+ matcherMap[m.Type] = append(matcherMap[m.Type], m)
+ sortMatcher(m.Type)
+ return m
+}
+
+// StoreTempMatcher store a matcher only triggered once.
+func StoreTempMatcher(m *Matcher) *Matcher {
+ m.Temp = true
+ StoreMatcher(m)
+ return m
+}
+
+// Delete remove the matcher from list
+func (m *Matcher) Delete() {
+ matcherLock.Lock()
+ defer matcherLock.Unlock()
+ for i, matcher := range matcherMap[m.Type] {
+ if m == matcher {
+ matcherMap[m.Type] = append(matcherMap[m.Type][:i], matcherMap[m.Type][i+1:]...)
+ }
+ }
+}
+
+func (m *Matcher) copy() *Matcher {
+ return &Matcher{
+ Type: m.Type,
+ Rules: m.Rules,
+ Block: m.Block,
+ priority: m.priority,
+ Process: m.Process,
+ Temp: m.Temp,
+ Engine: m.Engine,
+ }
+}
+
+// Handle 直接处理事件
+func (m *Matcher) Handle(handler Process) *Matcher {
+ m.Process = handler
+ return m
+}
diff --git a/rule.go b/rule.go
new file mode 100644
index 0000000..ac73fe3
--- /dev/null
+++ b/rule.go
@@ -0,0 +1,368 @@
+package nano
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+ "unsafe"
+
+ "github.com/FloatTech/floatbox/process"
+ ctrl "github.com/FloatTech/zbpctrl"
+ "github.com/wdvxdr1123/ZeroBot/extension"
+ "github.com/wdvxdr1123/ZeroBot/extension/rate"
+)
+
+func newctrl(service string, o *ctrl.Options[*Ctx]) Rule {
+ c := m.NewControl(service, o)
+ return func(ctx *Ctx) bool {
+ ctx.State["manager"] = c
+ gid, _ := strconv.ParseUint(ctx.Message.ChannelID, 10, 64)
+ uid, _ := strconv.ParseUint(ctx.Message.Author.ID, 10, 64)
+ return c.Handler(uintptr(unsafe.Pointer(ctx)), int64(gid), int64(uid))
+ }
+}
+
+func Lookup(service string) (*ctrl.Control[*Ctx], bool) {
+ return m.Lookup(service)
+}
+
+// respLimiterManager 请求响应限速器管理
+//
+// 每 1d 4次触发
+var respLimiterManager = rate.NewManager[string](time.Hour*24, 4)
+
+func init() {
+ process.NewCustomOnce(&m).Do(func() {
+ OnMessageCommandGroup([]string{
+ "响应", "response", "沉默", "silence",
+ }, UserOrGrpAdmin).SetBlock(true).Limit(func(ctx *Ctx) *rate.Limiter {
+ return respLimiterManager.Load(ctx.Message.ChannelID)
+ }).secondPriority().Handle(func(ctx *Ctx) {
+ grp, _ := strconv.ParseUint(ctx.Message.ChannelID, 10, 64)
+ msg := ""
+ switch ctx.State["command"] {
+ case "响应", "response":
+ if m.CanResponse(int64(grp)) {
+ msg = ctx.Caller.ready.User.Username + "已经在工作了哦~"
+ break
+ }
+ if SuperUserPermission(ctx) {
+ err := m.Response(int64(grp))
+ if err == nil {
+ msg = ctx.Caller.ready.User.Username + "将开始在此工作啦~"
+ } else {
+ msg = "ERROR: " + err.Error()
+ }
+ break
+ }
+ case "沉默", "silence":
+ if !m.CanResponse(int64(grp)) {
+ msg = ctx.Caller.ready.User.Username + "已经在休息了哦~"
+ break
+ }
+ err := m.Silence(int64(grp))
+ if err == nil {
+ msg = ctx.Caller.ready.User.Username + "将开始休息啦~"
+ } else {
+ msg = "ERROR: " + err.Error()
+ }
+ if SuperUserPermission(ctx) {
+ break
+ }
+ default:
+ msg = "ERROR: bad command\"" + fmt.Sprint(ctx.State["command"]) + "\""
+ }
+ _, _ = ctx.SendPlainMessage(false, msg)
+ })
+
+ OnMessageCommandGroup([]string{
+ "全局响应", "allresponse", "全局沉默", "allsilence",
+ }, SuperUserPermission).SetBlock(true).secondPriority().Handle(func(ctx *Ctx) {
+ msg := ""
+ cmd := ctx.State["command"].(string)
+ switch {
+ case strings.Contains(cmd, "响应") || strings.Contains(cmd, "response"):
+ err := m.Response(0)
+ if err == nil {
+ msg = ctx.Caller.ready.User.Username + "将开始在此工作啦~"
+ } else {
+ msg = "ERROR: " + err.Error()
+ }
+ case strings.Contains(cmd, "沉默") || strings.Contains(cmd, "silence"):
+ err := m.Silence(0)
+ if err == nil {
+ msg = ctx.Caller.ready.User.Username + "将开始休息啦~"
+ } else {
+ msg = "ERROR: " + err.Error()
+ }
+ default:
+ msg = "ERROR: bad command\"" + cmd + "\""
+ }
+ _, _ = ctx.SendPlainMessage(false, msg)
+ })
+
+ OnMessageCommandGroup([]string{
+ "启用", "enable", "禁用", "disable",
+ }, UserOrGrpAdmin).SetBlock(true).secondPriority().Handle(func(ctx *Ctx) {
+ grp, _ := strconv.ParseUint(ctx.Message.ChannelID, 10, 64)
+ if !m.CanResponse(int64(grp)) {
+ return
+ }
+ model := extension.CommandModel{}
+ _ = ctx.Parse(&model)
+ service, ok := Lookup(model.Args)
+ if !ok {
+ _, _ = ctx.SendPlainMessage(false, "没有找到指定服务!")
+ return
+ }
+ if strings.Contains(model.Command, "启用") || strings.Contains(model.Command, "enable") {
+ service.Enable(int64(grp))
+ if service.Options.OnEnable != nil {
+ service.Options.OnEnable(ctx)
+ } else {
+ _, _ = ctx.SendPlainMessage(false, "已启用服务: ", model.Args)
+ }
+ } else {
+ service.Disable(int64(grp))
+ if service.Options.OnDisable != nil {
+ service.Options.OnDisable(ctx)
+ } else {
+ _, _ = ctx.SendPlainMessage(false, "已禁用服务: ", model.Args)
+ }
+ }
+ })
+
+ OnMessageCommandGroup([]string{
+ "全局启用", "allenable", "全局禁用", "alldisable",
+ }, OnlyToMe, SuperUserPermission).SetBlock(true).secondPriority().Handle(func(ctx *Ctx) {
+ model := extension.CommandModel{}
+ _ = ctx.Parse(&model)
+ service, ok := Lookup(model.Args)
+ if !ok {
+ _, _ = ctx.SendPlainMessage(false, "没有找到指定服务!")
+ return
+ }
+ if strings.Contains(model.Command, "启用") || strings.Contains(model.Command, "enable") {
+ service.Enable(0)
+ _, _ = ctx.SendPlainMessage(false, "已全局启用服务: ", model.Args)
+ } else {
+ service.Disable(0)
+ _, _ = ctx.SendPlainMessage(false, "已全局禁用服务: ", model.Args)
+ }
+ })
+
+ OnMessageCommandGroup([]string{"还原", "reset"}, UserOrGrpAdmin).SetBlock(true).secondPriority().Handle(func(ctx *Ctx) {
+ grp, _ := strconv.ParseUint(ctx.Message.ChannelID, 10, 64)
+ if !m.CanResponse(int64(grp)) {
+ return
+ }
+ model := extension.CommandModel{}
+ _ = ctx.Parse(&model)
+ service, ok := Lookup(model.Args)
+ if !ok {
+ _, _ = ctx.SendPlainMessage(false, "没有找到指定服务!")
+ return
+ }
+ service.Reset(int64(grp))
+ _, _ = ctx.SendPlainMessage(false, "已还原服务的默认启用状态: ", model.Args)
+ })
+
+ OnMessageCommandGroup([]string{
+ "禁止", "ban", "允许", "permit",
+ }, AdminPermission).SetBlock(true).secondPriority().Handle(func(ctx *Ctx) {
+ grp, _ := strconv.ParseUint(ctx.Message.ChannelID, 10, 64)
+ if !m.CanResponse(int64(grp)) {
+ return
+ }
+ model := extension.CommandModel{}
+ _ = ctx.Parse(&model)
+ args := strings.Split(model.Args, " ")
+ if len(args) >= 2 {
+ service, ok := Lookup(args[0])
+ if !ok {
+ _, _ = ctx.SendPlainMessage(false, "没有找到指定服务!")
+ return
+ }
+ grp, _ := strconv.ParseUint(ctx.Message.ChannelID, 10, 64)
+ msg := "*" + args[0] + "报告*"
+ issu := SuperUserPermission(ctx)
+ if strings.Contains(model.Command, "允许") || strings.Contains(model.Command, "permit") {
+ for _, usr := range args[1:] {
+ uid, err := strconv.ParseInt(usr, 10, 64)
+ if err == nil {
+ if issu {
+ service.Permit(uid, int64(grp))
+ msg += "\n+ 已允许" + usr
+ } else {
+ member, err := ctx.Caller.GetGuildMemberOf(ctx.Message.GuildID, usr)
+ if err == nil && !member.Pending {
+ service.Permit(uid, int64(grp))
+ msg += "\n+ 已允许" + usr
+ } else {
+ msg += "\nx " + usr + " 不在本群"
+ }
+ }
+ }
+ }
+ } else {
+ for _, usr := range args[1:] {
+ uid, err := strconv.ParseInt(usr, 10, 64)
+ if err == nil {
+ if issu {
+ service.Ban(uid, int64(grp))
+ msg += "\n- 已禁止" + usr
+ } else {
+ member, err := ctx.Caller.GetGuildMemberOf(ctx.Message.GuildID, usr)
+ if err == nil && !member.Pending {
+ service.Ban(uid, int64(grp))
+ msg += "\n- 已禁止" + usr
+ } else {
+ msg += "\nx " + usr + " 不在本群"
+ }
+ }
+ }
+ }
+ }
+ _, _ = ctx.SendPlainMessage(false, msg)
+ return
+ }
+ _, _ = ctx.SendPlainMessage(false, "参数错误!")
+ })
+
+ OnMessageCommandGroup([]string{
+ "全局禁止", "allban", "全局允许", "allpermit",
+ }, SuperUserPermission).SetBlock(true).secondPriority().Handle(func(ctx *Ctx) {
+ model := extension.CommandModel{}
+ _ = ctx.Parse(&model)
+ args := strings.Split(model.Args, " ")
+ if len(args) >= 2 {
+ service, ok := Lookup(args[0])
+ if !ok {
+ _, _ = ctx.SendPlainMessage(false, "没有找到指定服务!")
+ return
+ }
+ msg := "*" + args[0] + "全局报告*"
+ if strings.Contains(model.Command, "允许") || strings.Contains(model.Command, "permit") {
+ for _, usr := range args[1:] {
+ uid, err := strconv.ParseInt(usr, 10, 64)
+ if err == nil {
+ service.Permit(uid, 0)
+ msg += "\n+ 已允许" + usr
+ }
+ }
+ } else {
+ for _, usr := range args[1:] {
+ uid, err := strconv.ParseInt(usr, 10, 64)
+ if err == nil {
+ service.Ban(uid, 0)
+ msg += "\n- 已禁止" + usr
+ }
+ }
+ }
+ _, _ = ctx.SendPlainMessage(false, msg)
+ return
+ }
+ _, _ = ctx.SendPlainMessage(false, "参数错误!")
+ })
+
+ OnMessageCommandGroup([]string{
+ "封禁", "block", "解封", "unblock",
+ }, SuperUserPermission).SetBlock(true).secondPriority().Handle(func(ctx *Ctx) {
+ model := extension.CommandModel{}
+ _ = ctx.Parse(&model)
+ args := strings.Split(model.Args, " ")
+ if len(args) >= 1 {
+ msg := "*报告*"
+ if strings.Contains(model.Command, "解") || strings.Contains(model.Command, "un") {
+ for _, usr := range args {
+ uid, err := strconv.ParseInt(usr, 10, 64)
+ if err == nil {
+ if m.DoUnblock(uid) == nil {
+ msg += "\n- 已解封" + usr
+ }
+ }
+ }
+ } else {
+ for _, usr := range args {
+ uid, err := strconv.ParseInt(usr, 10, 64)
+ if err == nil {
+ if m.DoBlock(uid) == nil {
+ msg += "\n+ 已封禁" + usr
+ }
+ }
+ }
+ }
+ _, _ = ctx.SendPlainMessage(false, msg)
+ return
+ }
+ _, _ = ctx.SendPlainMessage(false, "参数错误!")
+ })
+
+ OnMessageCommandGroup([]string{
+ "改变默认启用状态", "allflip",
+ }, SuperUserPermission).SetBlock(true).secondPriority().Handle(func(ctx *Ctx) {
+ model := extension.CommandModel{}
+ _ = ctx.Parse(&model)
+ service, ok := Lookup(model.Args)
+ if !ok {
+ _, _ = ctx.SendPlainMessage(false, "没有找到指定服务!")
+ return
+ }
+ err := service.Flip()
+ if err != nil {
+ _, _ = ctx.SendPlainMessage(false, "ERROR: ", err)
+ return
+ }
+ _, _ = ctx.SendPlainMessage(false, "已改变全局默认启用状态: ", model.Args)
+ })
+
+ OnMessageCommandGroup([]string{"用法", "usage"}, UserOrGrpAdmin).SetBlock(true).secondPriority().
+ Handle(func(ctx *Ctx) {
+ model := extension.CommandModel{}
+ _ = ctx.Parse(&model)
+ service, ok := Lookup(model.Args)
+ if !ok {
+ _, _ = ctx.SendPlainMessage(false, "没有找到指定服务!")
+ return
+ }
+ if service.Options.Help != "" {
+ gid := ctx.Message.ChannelID
+ grp, _ := strconv.ParseUint(gid, 10, 64)
+ _, _ = ctx.SendPlainMessage(false, service.EnableMarkIn(int64(grp)), " ", service)
+ } else {
+ _, _ = ctx.SendPlainMessage(false, "该服务无帮助!")
+ }
+ })
+
+ OnMessageCommandGroup([]string{"服务列表", "service_list"}, UserOrGrpAdmin).SetBlock(true).secondPriority().
+ Handle(func(ctx *Ctx) {
+ gid := ctx.Message.ChannelID
+ m.RLock()
+ msg := make([]any, 1, len(m.M)*4+1)
+ m.RUnlock()
+ msg[0] = "--------服务列表--------\n发送\"/用法 name\"查看详情\n发送\"/响应\"启用会话"
+ ForEachByPrio(func(i int, service *ctrl.Control[*Ctx]) bool {
+ grp, _ := strconv.ParseUint(gid, 10, 64)
+ msg = append(msg, "\n", i+1, ": ", service.EnableMarkIn(int64(grp)), service.Service)
+ return true
+ })
+ _, _ = ctx.SendPlainMessage(false, msg...)
+ })
+
+ OnMessageCommandGroup([]string{"服务详情", "service_detail"}, UserOrGrpAdmin).SetBlock(true).secondPriority().
+ Handle(func(ctx *Ctx) {
+ gid := ctx.Message.ChannelID
+ m.RLock()
+ msgs := make([]any, 1, len(m.M)*7+1)
+ m.RUnlock()
+ msgs[0] = "---服务详情---\n"
+ ForEachByPrio(func(i int, service *ctrl.Control[*Ctx]) bool {
+ grp, _ := strconv.ParseUint(gid, 10, 64)
+ msgs = append(msgs, i+1, ": ", service.EnableMarkIn(int64(grp)), service.Service, "\n", service, "\n\n")
+ return true
+ })
+ _, _ = ctx.SendPlainMessage(false, msgs...)
+ })
+ })
+}
diff --git a/rules.go b/rules.go
new file mode 100644
index 0000000..7a2f9db
--- /dev/null
+++ b/rules.go
@@ -0,0 +1,450 @@
+package nano
+
+import (
+ "reflect"
+ "regexp"
+ "strings"
+ "time"
+)
+
+// PrefixRule check if the text message has the prefix and trim the prefix
+//
+// 检查消息前缀
+func PrefixRule(prefix string) Rule {
+ return PrefixGroupRule(prefix)
+}
+
+// PrefixGroupRule check if the text message has the prefix and trim the prefix
+//
+// 检查消息前缀
+func PrefixGroupRule(prefixes ...string) Rule {
+ return func(ctx *Ctx) bool {
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.Content == "" { // 确保无空
+ return false
+ }
+ for _, prefix := range prefixes {
+ if strings.HasPrefix(msg.Content, prefix) {
+ ctx.State["prefix"] = prefix
+ arg := strings.TrimLeft(msg.Content[len(prefix):], " ")
+ ctx.State["args"] = arg
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+ }
+}
+
+// SuffixRule check if the text message has the suffix and trim the suffix
+//
+// 检查消息后缀
+func SuffixRule(suffix string) Rule {
+ return SuffixGroupRule(suffix)
+}
+
+// SuffixGroupRule check if the text message has the suffix and trim the suffix
+//
+// 检查消息后缀
+func SuffixGroupRule(suffixes ...string) Rule {
+ return func(ctx *Ctx) bool {
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.Content == "" { // 确保无空
+ return false
+ }
+ for _, suffix := range suffixes {
+ if strings.HasSuffix(msg.Content, suffix) {
+ ctx.State["suffix"] = suffix
+ arg := strings.TrimRight(msg.Content[:len(msg.Content)-len(suffix)], " ")
+ ctx.State["args"] = arg
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+ }
+}
+
+// CommandRule check if the message is a command and trim the command name
+//
+// this rule only supports Message
+func CommandRule(command string) Rule {
+ return CommandGroupRule(command)
+}
+
+// CommandGroupRule check if the message is a command and trim the command name
+//
+// this rule only supports Message
+func CommandGroupRule(commands ...string) Rule {
+ return func(ctx *Ctx) bool {
+ msg, ok := ctx.Value.(*Message)
+ if !ok || msg.Content == "" { // 确保无空
+ return false
+ }
+ msg.Content = strings.TrimSpace(msg.Content)
+ if msg.Content == "" { // 确保无空
+ return false
+ }
+ cmdMessage := ""
+ args := ""
+ switch {
+ case strings.HasPrefix(msg.Content, "/"):
+ cmdMessage, args, _ = strings.Cut(msg.Content, " ")
+ cmdMessage, _, _ = strings.Cut(cmdMessage, "@")
+ cmdMessage = cmdMessage[1:]
+ default:
+ return false
+ }
+ for _, command := range commands {
+ if strings.HasPrefix(cmdMessage, command) {
+ ctx.State["command"] = command
+ ctx.State["args"] = args
+ return true
+ }
+ }
+ return false
+ }
+}
+
+// RegexRule check if the message can be matched by the regex pattern
+func RegexRule(regexPattern string) Rule {
+ regex := regexp.MustCompile(regexPattern)
+ return func(ctx *Ctx) bool {
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.Content == "" { // 确保无空
+ return false
+ }
+ if matched := regex.FindStringSubmatch(msg.Content); matched != nil {
+ ctx.State["regex_matched"] = matched
+ return true
+ }
+ return false
+ default:
+ return false
+ }
+ }
+}
+
+// ReplyRule check if the message is replying some message
+//
+// this rule only supports Message
+func ReplyRule(messageID string) Rule {
+ return func(ctx *Ctx) bool {
+ msg, ok := ctx.Value.(*Message)
+ if !ok || msg.MessageReference == nil { // 确保无空
+ return false
+ }
+ return messageID == msg.MessageReference.MessageID
+ }
+}
+
+func KeywordRule(src string) Rule {
+ return KeywordGroupRule(src)
+}
+
+// KeywordGroupRule check if the message has a keyword or keywords
+func KeywordGroupRule(src ...string) Rule {
+ return func(ctx *Ctx) bool {
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.Content == "" { // 确保无空
+ return false
+ }
+ for _, str := range src {
+ if strings.Contains(msg.Content, str) {
+ ctx.State["keyword"] = str
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+ }
+}
+
+// FullMatchRule check if src has the same copy of the message
+func FullMatchRule(src string) Rule {
+ return FullMatchGroupRule(src)
+}
+
+// FullMatchGroupRule check if src has the same copy of the message
+func FullMatchGroupRule(src ...string) Rule {
+ return func(ctx *Ctx) bool {
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.Content == "" { // 确保无空
+ return false
+ }
+ for _, str := range src {
+ if str == msg.Content {
+ ctx.State["matched"] = msg.Content
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+ }
+}
+
+// ShellRule 定义shell-like规则
+//
+// this rule only supports Message
+func ShellRule(cmd string, model interface{}) Rule {
+ cmdRule := CommandRule(cmd)
+ t := reflect.TypeOf(model)
+ return func(ctx *Ctx) bool {
+ if !cmdRule(ctx) {
+ return false
+ }
+ // bind flag to struct
+ args := ParseShell(ctx.State["args"].(string))
+ val := reflect.New(t)
+ fs := registerFlag(t, val)
+ err := fs.Parse(args)
+ if err != nil {
+ return false
+ }
+ ctx.State["args"] = fs.Args()
+ ctx.State["flag"] = val.Interface()
+ return true
+ }
+}
+
+// OnlyToMe only triggered in conditions of @bot or begin with the nicknames
+//
+// this rule only supports Message
+func OnlyToMe(ctx *Ctx) bool {
+ return ctx.IsToMe
+}
+
+// CheckUser only triggered by specific person
+func CheckUser(userID ...string) Rule {
+ return func(ctx *Ctx) bool {
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.Author == nil { // 确保无空
+ return false
+ }
+ for _, uid := range userID {
+ if msg.Author.ID == uid {
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+ }
+}
+
+// CheckChannel only triggered in specific channel
+func CheckChannel(channelID ...string) Rule {
+ return func(ctx *Ctx) bool {
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.ChannelID == "" { // 确保无空
+ return false
+ }
+ for _, cid := range channelID {
+ if msg.ChannelID == cid {
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+ }
+}
+
+// CheckGuild only triggered in specific guild
+func CheckGuild(guildID ...string) Rule {
+ return func(ctx *Ctx) bool {
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.GuildID == "" { // 确保无空
+ return false
+ }
+ for _, gid := range guildID {
+ if msg.GuildID == gid {
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+ }
+}
+
+// OnlyPrivate requires that the ctx.Event is direct message
+func OnlyPrivate(ctx *Ctx) bool {
+ if ctx.Type == "" { // 确保无空
+ return false
+ }
+ return strings.HasPrefix(ctx.Type, "Direct")
+}
+
+// OnlyPublic requires that the ctx.Event is channel message
+func OnlyPublic(ctx *Ctx) bool {
+ if ctx.Type == "" { // 确保无空
+ return false
+ }
+ return !strings.HasPrefix(ctx.Type, "Direct")
+}
+
+// OnlyChannel requires that the ctx.Event is channel message
+func OnlyChannel(ctx *Ctx) bool {
+ return OnlyPublic(ctx)
+}
+
+// SuperUserPermission only triggered by the bot's owner
+func SuperUserPermission(ctx *Ctx) bool {
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.Author == nil { // 确保无空
+ return false
+ }
+ for _, su := range ctx.Caller.SuperUsers {
+ if su == msg.Author.ID {
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+}
+
+// CreaterPermission only triggered by the creater or higher permission
+func CreaterPermission(ctx *Ctx) bool {
+ if SuperUserPermission(ctx) {
+ return true
+ }
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.Author == nil || msg.Member == nil { // 确保无空
+ return false
+ }
+ for _, role := range msg.Member.Roles {
+ if role == RoleIDCreater {
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+}
+
+// AdminPermission only triggered by the admins or higher permission
+func AdminPermission(ctx *Ctx) bool {
+ if SuperUserPermission(ctx) {
+ return true
+ }
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.Author == nil || msg.Member == nil { // 确保无空
+ return false
+ }
+ for _, role := range msg.Member.Roles {
+ if role == RoleIDCreater || role == RoleIDAdmin {
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+}
+
+// ChannelAdminPermission only triggered by the channel admins or higher permission
+func ChannelAdminPermission(ctx *Ctx) bool {
+ if SuperUserPermission(ctx) {
+ return true
+ }
+ switch msg := ctx.Value.(type) {
+ case *Message:
+ if msg.Author == nil || msg.Member == nil { // 确保无空
+ return false
+ }
+ for _, role := range msg.Member.Roles {
+ if role == RoleIDCreater || role == RoleIDAdmin || role == RoleIDChannelAdmin {
+ return true
+ }
+ }
+ return false
+ default:
+ return false
+ }
+}
+
+// UserOrGrpAdmin 允许用户单独使用或群管使用
+func UserOrGrpAdmin(ctx *Ctx) bool {
+ if OnlyPublic(ctx) {
+ return AdminPermission(ctx)
+ }
+ return OnlyToMe(ctx)
+}
+
+// UserOrChannelAdmin 允许用户单独使用或频道管理使用
+func UserOrChannelAdmin(ctx *Ctx) bool {
+ if OnlyPublic(ctx) {
+ return ChannelAdminPermission(ctx)
+ }
+ return OnlyToMe(ctx)
+}
+
+// HasAttachments 消息包含 Attachments (典型: 图片) 返回 true
+func HasAttachments(ctx *Ctx) bool {
+ msg, ok := ctx.Value.(*Message)
+ if !ok || len(msg.Attachments) == 0 { // 确保无空
+ return false
+ }
+ ctx.State["attachments"] = msg.Attachments
+ return true
+}
+
+// MustProvidePhoto 消息不存在图片阻塞120秒至有图片,超时返回 false
+func MustProvidePhoto(onmessage string, needphohint, failhint string) Rule {
+ return func(ctx *Ctx) bool {
+ msg, ok := ctx.Value.(*Message)
+ if ok && len(msg.Attachments) > 0 { // 确保无空
+ ctx.State["attachments"] = msg.Attachments
+ return true
+ }
+ // 没有图片就索取
+ if needphohint != "" {
+ _, err := ctx.Caller.PostMessageToChannel(msg.ChannelID, &MessagePost{
+ Content: needphohint,
+ MessageReference: &MessageReference{MessageID: msg.ID},
+ ReplyMessageID: msg.ID,
+ })
+ if err != nil {
+ return false
+ }
+ }
+ next := NewFutureEvent(onmessage, 999, false, ctx.CheckSession(), HasAttachments).Next()
+ select {
+ case <-time.After(time.Second * 120):
+ if failhint != "" {
+ _, _ = ctx.SendPlainMessage(true, failhint)
+ }
+ return false
+ case newCtx := <-next:
+ ctx.State["photos"] = newCtx.State["photos"]
+ ctx.Event = newCtx.Event
+ return true
+ }
+ }
+}
diff --git a/shell.go b/shell.go
new file mode 100644
index 0000000..3b7eccc
--- /dev/null
+++ b/shell.go
@@ -0,0 +1,131 @@
+package nano
+
+import (
+ "flag"
+ "reflect"
+ "strings"
+)
+
+func isSpace(r rune) bool {
+ switch r {
+ case ' ', '\t', '\r', '\n':
+ return true
+ }
+ return false
+}
+
+type argType int
+
+const (
+ argNo argType = iota
+ argSingle
+ argQuoted
+)
+
+// ParseShell 将指令转换为指令参数.
+// modified from https://github.com/mattn/go-shellwords
+func ParseShell(s string) []string {
+ var args []string
+ buf := strings.Builder{}
+ var escaped, doubleQuoted, singleQuoted, backQuote bool
+ backtick := ""
+
+ got := argNo
+
+ for _, r := range s {
+ if escaped {
+ buf.WriteRune(r)
+ escaped = false
+ got = argSingle
+ continue
+ }
+
+ if r == '\\' {
+ if singleQuoted {
+ buf.WriteRune(r)
+ } else {
+ escaped = true
+ }
+ continue
+ }
+
+ if isSpace(r) {
+ if singleQuoted || doubleQuoted || backQuote {
+ buf.WriteRune(r)
+ backtick += string(r)
+ } else if got != argNo {
+ args = append(args, buf.String())
+ buf.Reset()
+ got = argNo
+ }
+ continue
+ }
+
+ switch r {
+ case '`':
+ if !singleQuoted && !doubleQuoted {
+ backtick = ""
+ backQuote = !backQuote
+ }
+ case '"':
+ if !singleQuoted {
+ if doubleQuoted {
+ got = argQuoted
+ }
+ doubleQuoted = !doubleQuoted
+ }
+ case '\'':
+ if !doubleQuoted {
+ if singleQuoted {
+ got = argSingle
+ }
+ singleQuoted = !singleQuoted
+ }
+ default:
+ got = argSingle
+ buf.WriteRune(r)
+ if backQuote {
+ backtick += string(r)
+ }
+ }
+ }
+
+ if got != argNo {
+ args = append(args, buf.String())
+ }
+
+ return args
+}
+
+var (
+ boolType = reflect.TypeOf(false)
+ intType = reflect.TypeOf(0)
+ stringType = reflect.TypeOf("")
+ float64Type = reflect.TypeOf(float64(0))
+)
+
+func registerFlag(t reflect.Type, v reflect.Value) *flag.FlagSet {
+ v = v.Elem()
+ fs := flag.NewFlagSet("", flag.ContinueOnError)
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+ name := field.Tag.Get("flag")
+ if name == "" {
+ continue
+ }
+ help := field.Tag.Get("help")
+ switch field.Type {
+ case boolType:
+ fs.BoolVar(v.Field(i).Addr().Interface().(*bool), name, false, help)
+ case intType:
+ fs.IntVar(v.Field(i).Addr().Interface().(*int), name, 0, help)
+ case stringType:
+ fs.StringVar(v.Field(i).Addr().Interface().(*string), name, "", help)
+ case float64Type:
+ fs.Float64Var(v.Field(i).Addr().Interface().(*float64), name, 0, help)
+ default:
+ panic("unsupported type")
+ }
+ }
+ return fs
+}
diff --git a/shell_test.go b/shell_test.go
new file mode 100644
index 0000000..b1044c4
--- /dev/null
+++ b/shell_test.go
@@ -0,0 +1,45 @@
+package nano
+
+import (
+ "reflect"
+ "strconv"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_parse(t *testing.T) {
+ shellTests := [...]struct {
+ shell string
+ expected []string
+ }{
+ {`rm -rf /*`, []string{"rm", "-rf", "/*"}},
+ {`echo "cat cat" -n`, []string{"echo", "cat cat", "-n"}},
+ {`shutdown halt init`, []string{"shutdown", "halt", "init"}},
+ {`test test2`, []string{"test", "test2"}},
+ }
+ for i, v := range shellTests {
+ t.Run(strconv.Itoa(i), func(t *testing.T) {
+ out := ParseShell(v.shell)
+ assert.Equal(t, v.expected, out)
+ })
+ }
+}
+
+func Test_registerFlag(t *testing.T) {
+ type args struct {
+ RF bool `flag:"rf"`
+ File string `flag:"file"`
+ Count int `flag:"count"`
+ }
+ got := args{}
+ expected := args{
+ RF: true,
+ File: "123",
+ Count: 10,
+ }
+ fs := registerFlag(reflect.TypeOf(args{}), reflect.ValueOf(&got))
+ err := fs.Parse([]string{"-rf", "-file=123", "-count", "10"})
+ assert.NoError(t, err)
+ assert.Equal(t, expected, got)
+}
diff --git a/single.go b/single.go
new file mode 100644
index 0000000..88c999f
--- /dev/null
+++ b/single.go
@@ -0,0 +1,61 @@
+package nano
+
+import (
+ "github.com/RomiChan/syncx"
+)
+
+// Option 配置项
+type Option[K comparable] func(*Single[K])
+
+// Single 反并发
+type Single[K comparable] struct {
+ group syncx.Map[K, struct{}]
+ key func(ctx *Ctx) K
+ post func(ctx *Ctx)
+}
+
+// WithKeyFn 指定反并发的 Key
+func WithKeyFn[K comparable](fn func(ctx *Ctx) K) Option[K] {
+ return func(s *Single[K]) {
+ s.key = fn
+ }
+}
+
+// WithPostFn 指定反并发拦截后的操作
+func WithPostFn[K comparable](fn func(ctx *Ctx)) Option[K] {
+ return func(s *Single[K]) {
+ s.post = fn
+ }
+}
+
+// NewSingle 创建反并发中间件
+func NewSingle[K comparable](op ...Option[K]) *Single[K] {
+ s := Single[K]{}
+ for _, option := range op {
+ option(&s)
+ }
+ return &s
+}
+
+// Apply 为指定 Engine 添加反并发功能
+func (s *Single[K]) Apply(engine *Engine) {
+ engine.UseMidHandler(func(ctx *Ctx) bool {
+ if s.key == nil {
+ return true
+ }
+ key := s.key(ctx)
+ if _, ok := s.group.Load(key); ok {
+ if s.post != nil {
+ defer s.post(ctx)
+ }
+ return false
+ }
+ s.group.Store(key, struct{}{})
+ ctx.State["__single-key__"] = key
+ return true
+ })
+
+ engine.UsePostHandler(func(ctx *Ctx) {
+ s.group.Delete(ctx.State["__single-key__"].(K))
+ })
+}