1
0
mirror of https://github.com/fumiama/NanoBot.git synced 2026-06-05 02:30:23 +08:00
This commit is contained in:
源文雨
2023-10-16 00:26:04 +09:00
parent 6a1019d201
commit ce34c996ef
25 changed files with 2844 additions and 8 deletions

BIN
.github/nano.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

1
.gitignore vendored
View File

@@ -22,3 +22,4 @@ go.work
bot_test.go
/test
/data

122
README.md
View File

@@ -1,2 +1,120 @@
# NanoBot
ZeroBot-like Official QQ Bot Adapter
<div align="center">
<a href="https://crypko.ai/crypko/GtWYDpVMx5GYm/">
<img src=".github/nano.jpeg" alt="东云名乃" width = "256">
</a><br>
<h1>NanoBot</h1>
类 ZeroBot 的官方 QQ 频道适配器<br><br>
<img src="https://counter.seku.su/cmoe?name=NanoBot&theme=r34" /><br>
</div>
## 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`文件夹以获取更多信息
<table>
<tr>
<td align="center"><img src="https://github.com/fumiama/NanoBot/assets/41315874/6ef9fd95-ae99-449e-85e1-25797271e088"></td>
<td align="center"><img src="https://github.com/fumiama/NanoBot/assets/41315874/edd374e4-b8a5-4cff-a463-8c3b30e537c4"></td>
<td align="center"><img src="https://github.com/fumiama/NanoBot/assets/41315874/ed1b063f-44b0-4950-ac35-1e72745cf3f4"></td>
</tr>
<tr>
<td align="center">开始响应</td>
<td align="center">服务列表</td>
<td align="center">查看用法</td>
</tr>
</table>
![启用禁用](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)

5
bot.go
View File

@@ -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)

69
codegen/engine/engine.yml Normal file
View File

@@ -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"]

114
codegen/engine/main.go Normal file
View File

@@ -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)
}
}
}

131
context.go Normal file
View File

@@ -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
}

84
engine.go Normal file
View File

@@ -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)
}

441
engine_generated.go Normal file
View File

@@ -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)
}

138
event.go
View File

@@ -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
}
}
}

20
example/echo/main.go Normal file
View File

@@ -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)
})
}

44
example/handler/main.go Normal file
View File

@@ -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,
})
}
},
},
})
}

24
example/main.go Normal file
View File

@@ -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"},
})
}

98
future.go Normal file
View File

@@ -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
}

25
go.mod
View File

@@ -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
)

193
go.sum
View File

@@ -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=

View File

@@ -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

20
lazy.go Normal file
View File

@@ -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)
}

106
manager.go Normal file
View File

@@ -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
}

150
matcher.go Normal file
View File

@@ -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 的 EngineEngine可为一系列 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
}

368
rule.go Normal file
View File

@@ -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...)
})
})
}

450
rules.go Normal file
View File

@@ -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
}
}
}

131
shell.go Normal file
View File

@@ -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
}

45
shell_test.go Normal file
View File

@@ -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)
}

61
single.go Normal file
View File

@@ -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))
})
}