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`文件夹以获取更多信息 + + + + + + + + + + + + +
开始响应服务列表查看用法
+ +![启用禁用](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)) + }) +}