From ce34c996ef31e66c8721cdb9dde60a534a0090ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BA=90=E6=96=87=E9=9B=A8?= <41315874+fumiama@users.noreply.github.com> Date: Mon, 16 Oct 2023 00:26:04 +0900 Subject: [PATCH] finish --- .github/nano.jpeg | Bin 0 -> 47871 bytes .gitignore | 1 + README.md | 122 ++++++++++- bot.go | 5 + codegen/engine/engine.yml | 69 ++++++ codegen/engine/main.go | 114 ++++++++++ context.go | 131 +++++++++++ engine.go | 84 +++++++ engine_generated.go | 441 +++++++++++++++++++++++++++++++++++++ event.go | 138 ++++++++++++ example/echo/main.go | 20 ++ example/handler/main.go | 44 ++++ example/main.go | 24 ++ future.go | 98 +++++++++ go.mod | 25 ++- go.sum | 193 +++++++++++++++- handler.go | 12 + lazy.go | 20 ++ manager.go | 106 +++++++++ matcher.go | 150 +++++++++++++ rule.go | 368 +++++++++++++++++++++++++++++++ rules.go | 450 ++++++++++++++++++++++++++++++++++++++ shell.go | 131 +++++++++++ shell_test.go | 45 ++++ single.go | 61 ++++++ 25 files changed, 2844 insertions(+), 8 deletions(-) create mode 100644 .github/nano.jpeg create mode 100644 codegen/engine/engine.yml create mode 100644 codegen/engine/main.go create mode 100644 context.go create mode 100644 engine.go create mode 100644 engine_generated.go create mode 100644 example/echo/main.go create mode 100644 example/handler/main.go create mode 100644 example/main.go create mode 100644 future.go create mode 100644 lazy.go create mode 100644 manager.go create mode 100644 matcher.go create mode 100644 rule.go create mode 100644 rules.go create mode 100644 shell.go create mode 100644 shell_test.go create mode 100644 single.go diff --git a/.github/nano.jpeg b/.github/nano.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..884fbbe0eb8c727dab4a74d293a7996649ed96a8 GIT binary patch literal 47871 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 p?SFG;c3 + + 东云名乃 +
+ +

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`文件夹以获取更多信息 + + + + + + + + + + + + +
开始响应服务列表查看用法
+ +![启用禁用](https://github.com/fumiama/NanoBot/assets/41315874/fc7f4774-f64b-44c5-9575-b9483bf3a455) + + +```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, 所有插件将被禁用 + +![event-based example](https://github.com/fumiama/NanoBot/assets/41315874/414ef9a6-1da2-49ff-b28e-9e3009cdb41c) + +```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)) + }) +}