diff --git a/README.md b/README.md index c63e97d..9e4f5b0 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,42 @@ Lightweight Telegram bot framework This framework is a simple wrapper for [go-telegram-bot-api](https://github.com/go-telegram-bot-api/telegram-bot-api), aiming to make the event processing easier. -## Example +## Quick Start +> Here is a plugin-based example +```go +package main -See under `example` folder or below. +import ( + rei "github.com/fumiama/ReiBot" + tgba "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +func main() { + rei.OnMessagePrefix("echo").SetBlock(true).SecondPriority(). + Handle(func(ctx *rei.Ctx) { + args := ctx.State["args"].(string) + if args == "" { + return + } + msg := ctx.Value.(*tgba.Message) + ctx.Caller.Send(tgba.NewMessage(msg.Chat.ID, args)) + }) + rei.Run(rei.Bot{ + Token: "", + Buffer: 256, + UpdateConfig: tgba.UpdateConfig{ + Offset: 0, + Limit: 0, + Timeout: 60, + }, + Debug: true, + }) +} +``` + +## Event-Based + +> If Handler in Bot is implemented, the plugin function will be disabled. ![example](https://user-images.githubusercontent.com/41315874/171180885-c888a031-7797-4b4b-a232-9ff23f031b32.png) @@ -32,7 +65,7 @@ func main() { Timeout: 60, }, Debug: true, - Handler: rei.Handler{ + Handler: &rei.Handler{ OnMessage: func(updateid int, bot *rei.TelegramClient, msg *tgba.Message) { if len(msg.Text) <= len("测试") { return @@ -60,4 +93,4 @@ func main() { }, }) } -``` \ No newline at end of file +``` diff --git a/bot.go b/bot.go index 1e56bee..69914d1 100644 --- a/bot.go +++ b/bot.go @@ -17,7 +17,7 @@ type Bot struct { // Debug 控制调试信息的输出与否 Debug bool `json:"debug"` // Handler 注册对各种事件的处理 - Handler Handler + Handler *Handler // handlers 方便调用的 handler handlers map[string]GeneralHandleType } diff --git a/client.go b/client.go index fad2c48..3102c5d 100644 --- a/client.go +++ b/client.go @@ -18,18 +18,20 @@ type TelegramClient struct { // NewTelegramClient ... func NewTelegramClient(c *Bot) (tc TelegramClient) { tc.b = *c - h := reflect.ValueOf(&tc.b.Handler).Elem() - t := h.Type() - tc.b.handlers = make(map[string]GeneralHandleType, 16) - for i := 0; i < h.NumField(); i++ { - f := h.Field(i) - if f.IsZero() { - continue + if tc.b.Handler != nil { + h := reflect.ValueOf(tc.b.Handler).Elem() + t := h.Type() + tc.b.handlers = make(map[string]GeneralHandleType, 16) + for i := 0; i < h.NumField(); i++ { + f := h.Field(i) + if f.IsZero() { + continue + } + tp := t.Field(i).Name[2:] + log.Println("[INFO] register handler", tp) + handler := f.Interface() + tc.b.handlers[tp] = *(*GeneralHandleType)(unsafe.Add(unsafe.Pointer(&handler), unsafe.Sizeof(uintptr(0)))) } - tp := t.Field(i).Name[2:] - log.Println("[INFO] register handler", tp) - handler := f.Interface() - tc.b.handlers[tp] = *(*GeneralHandleType)(unsafe.Add(unsafe.Pointer(&handler), unsafe.Sizeof(uintptr(0)))) } return } diff --git a/context.go b/context.go new file mode 100644 index 0000000..e304778 --- /dev/null +++ b/context.go @@ -0,0 +1,8 @@ +package rei + +type Ctx struct { + Event + State + Caller *TelegramClient + ma *Matcher +} diff --git a/engine.go b/engine.go new file mode 100644 index 0000000..8ab1d8d --- /dev/null +++ b/engine.go @@ -0,0 +1,176 @@ +package rei + +// New 生成空引擎 +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 +} + +// 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...) +} + +// 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) +} + +// OnMessage 消息触发器 +func (e *Engine) OnMessage(rules ...Rule) *Matcher { return e.On("Message", rules...) } + +// OnMessage 消息触发器 +func OnMessage(rules ...Rule) *Matcher { return On("Message", rules...) } + +// OnEditedMessage 修改消息触发器 +func (e *Engine) OnEditedMessage(rules ...Rule) *Matcher { return e.On("EditedMessage", rules...) } + +// OnEditedMessage 修改消息触发器 +func OnEditedMessage(rules ...Rule) *Matcher { return On("EditedMessage", rules...) } + +// OnChannelPost ... +func (e *Engine) OnChannelPost(rules ...Rule) *Matcher { return e.On("ChannelPost", rules...) } + +// OnChannelPost ... +func OnChannelPost(rules ...Rule) *Matcher { return On("ChannelPost", rules...) } + +// OnEditedChannelPost ... +func (e *Engine) OnEditedChannelPost(rules ...Rule) *Matcher { + return e.On("EditedChannelPost", rules...) +} + +// OnEditedChannelPost ... +func OnEditedChannelPost(rules ...Rule) *Matcher { + return On("EditedChannelPost", rules...) +} + +// OnInlineQuery ... +func (e *Engine) OnInlineQuery(rules ...Rule) *Matcher { return e.On("InlineQuery", rules...) } + +// OnInlineQuery ... +func OnInlineQuery(rules ...Rule) *Matcher { return On("InlineQuery", rules...) } + +// OnChosenInlineResult ... +func (e *Engine) OnChosenInlineResult(rules ...Rule) *Matcher { + return e.On("ChosenInlineResult", rules...) +} + +// OnChosenInlineResult ... +func OnChosenInlineResult(rules ...Rule) *Matcher { return On("ChosenInlineResult", rules...) } + +// OnCallbackQuery ... +func (e *Engine) OnCallbackQuery(rules ...Rule) *Matcher { return e.On("CallbackQuery", rules...) } + +// OnCallbackQuery ... +func OnCallbackQuery(rules ...Rule) *Matcher { return On("CallbackQuery", rules...) } + +// OnShippingQuery ... +func (e *Engine) OnShippingQuery(rules ...Rule) *Matcher { return e.On("ShippingQuery", rules...) } + +// OnShippingQuery ... +func OnShippingQuery(rules ...Rule) *Matcher { return On("ShippingQuery", rules...) } + +// OnPreCheckoutQuery ... +func (e *Engine) OnPreCheckoutQuery(rules ...Rule) *Matcher { + return e.On("PreCheckoutQuery", rules...) +} + +// OnPreCheckoutQuery ... +func OnPreCheckoutQuery(rules ...Rule) *Matcher { return On("PreCheckoutQuery", rules...) } + +// OnPoll ... +func (e *Engine) OnPoll(rules ...Rule) *Matcher { return e.On("Poll", rules...) } + +// OnPoll ... +func OnPoll(rules ...Rule) *Matcher { return On("Poll", rules...) } + +// OnPollAnswer ... +func (e *Engine) OnPollAnswer(rules ...Rule) *Matcher { return e.On("PollAnswer", rules...) } + +// OnPollAnswer ... +func OnPollAnswer(rules ...Rule) *Matcher { return On("PollAnswer", rules...) } + +// OnMyChatMember ... +func (e *Engine) OnMyChatMember(rules ...Rule) *Matcher { return e.On("MyChatMember", rules...) } + +// OnMyChatMember ... +func OnMyChatMember(rules ...Rule) *Matcher { return On("MyChatMember", rules...) } + +// OnChatMember ... +func (e *Engine) OnChatMember(rules ...Rule) *Matcher { return e.On("ChatMember", rules...) } + +// OnChatMember ... +func OnChatMember(rules ...Rule) *Matcher { return On("ChatMember", rules...) } + +// OnChatJoinRequest ... +func (e *Engine) OnChatJoinRequest(rules ...Rule) *Matcher { return e.On("ChatJoinRequest", rules...) } + +// OnChatJoinRequest ... +func OnChatJoinRequest(rules ...Rule) *Matcher { return On("ChatJoinRequest", rules...) } + +// OnPrefix 前缀触发器 +func OnMessagePrefix(prefix string, rules ...Rule) *Matcher { + return defaultEngine.OnMessagePrefix(prefix, rules...) +} + +// OnPrefix 前缀触发器 +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) +} diff --git a/event.go b/event.go index 04fd8e5..f4aad5f 100644 --- a/event.go +++ b/event.go @@ -14,7 +14,7 @@ type Event struct { // UpdateID is the update's unique identifier. UpdateID int // Value is the non-null field value in Update - Value reflect.Value + Value any } func (tc *TelegramClient) processEvent(update tgba.Update) { @@ -26,6 +26,29 @@ func (tc *TelegramClient) processEvent(update tgba.Update) { continue } tp := t.Field(i).Name + if tc.b.Handler == nil { + matcherLock.RLock() + n := len(matcherMap[tp]) + if n == 0 { + matcherLock.RUnlock() + continue + } + log.Println("[INFO] pass", tp, "event to plugins") + matchers := make([]*Matcher, n) + copy(matchers, matcherMap[tp]) + matcherLock.RUnlock() + ctx := &Ctx{ + Event: Event{ + Type: tp, + UpdateID: update.UpdateID, + Value: f.Interface(), + }, + State: State{}, + Caller: tc, + } + match(ctx, matchers) + continue + } h, ok := tc.b.handlers[tp] if !ok { continue @@ -34,3 +57,58 @@ func (tc *TelegramClient) processEvent(update tgba.Update) { go h(update.UpdateID, tc, f.UnsafePointer()) } } + +func match(ctx *Ctx, matchers []*Matcher) { +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 未满足 + continue loop + } + } + } + + for _, rule := range m.Rules { + if rule != nil && !rule(ctx) { // 有 Rule 的条件未满足 + continue loop + } + } + + // mid handler + if m.Engine != nil { + for _, handler := range m.Engine.midHandler { + if !handler(ctx) { // 有 mid handler 未满足 + 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/handler/main.go b/example/handler/main.go new file mode 100644 index 0000000..b702ccf --- /dev/null +++ b/example/handler/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "log" + "strings" + + rei "github.com/fumiama/ReiBot" + tgba "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +func main() { + rei.Run(rei.Bot{ + Token: "", + Buffer: 256, + UpdateConfig: tgba.UpdateConfig{ + Offset: 0, + Limit: 0, + Timeout: 60, + }, + Debug: true, + Handler: &rei.Handler{ + OnMessage: func(updateid int, bot *rei.TelegramClient, msg *tgba.Message) { + if len(msg.Text) <= len("测试") { + return + } + if !strings.HasPrefix(msg.Text, "测试") { + return + } + _, err := bot.Send(tgba.NewMessage(msg.Chat.ID, msg.Text[len("测试"):])) + if err != nil { + log.Println("[ERRO]", err) + } + }, + OnEditedMessage: func(updateid int, bot *rei.TelegramClient, msg *tgba.Message) { + if len(msg.Text) <= len("测试") { + return + } + if !strings.HasPrefix(msg.Text, "测试") { + return + } + _, err := bot.Send(tgba.NewMessage(msg.Chat.ID, "已编辑:"+msg.Text[len("测试"):])) + if err != nil { + log.Println("[ERRO]", err) + } + }, + }, + }) +} diff --git a/example/main.go b/example/main.go index aff9309..e0f205b 100644 --- a/example/main.go +++ b/example/main.go @@ -1,14 +1,20 @@ package main import ( - "log" - "strings" - rei "github.com/fumiama/ReiBot" tgba "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) func main() { + rei.OnMessagePrefix("echo").SetBlock(true).SecondPriority(). + Handle(func(ctx *rei.Ctx) { + args := ctx.State["args"].(string) + if args == "" { + return + } + msg := ctx.Value.(*tgba.Message) + ctx.Caller.Send(tgba.NewMessage(msg.Chat.ID, args)) + }) rei.Run(rei.Bot{ Token: "", Buffer: 256, @@ -18,31 +24,5 @@ func main() { Timeout: 60, }, Debug: true, - Handler: rei.Handler{ - OnMessage: func(updateid int, bot *rei.TelegramClient, msg *tgba.Message) { - if len(msg.Text) <= len("测试") { - return - } - if !strings.HasPrefix(msg.Text, "测试") { - return - } - _, err := bot.Send(tgba.NewMessage(msg.Chat.ID, msg.Text[len("测试"):])) - if err != nil { - log.Println("[ERRO]", err) - } - }, - OnEditedMessage: func(updateid int, bot *rei.TelegramClient, msg *tgba.Message) { - if len(msg.Text) <= len("测试") { - return - } - if !strings.HasPrefix(msg.Text, "测试") { - return - } - _, err := bot.Send(tgba.NewMessage(msg.Chat.ID, "已编辑:"+msg.Text[len("测试"):])) - if err != nil { - log.Println("[ERRO]", err) - } - }, - }, }) } diff --git a/matcher.go b/matcher.go new file mode 100644 index 0000000..d6a2d52 --- /dev/null +++ b/matcher.go @@ -0,0 +1,130 @@ +package rei + +import ( + "sort" + "sync" +) + +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 + // 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]interface{} + +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) +} + +// BindEngine bind the matcher to a engine +func (m *Matcher) BindEngine(e *Engine) *Matcher { + m.Engine = e + 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/rules.go b/rules.go new file mode 100644 index 0000000..6964c75 --- /dev/null +++ b/rules.go @@ -0,0 +1,28 @@ +package rei + +import ( + "strings" + + tgba "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +// PrefixRule check if the message has the prefix and trim the prefix +// +// 检查消息前缀 +func PrefixRule(prefixes ...string) Rule { + return func(ctx *Ctx) bool { + msg, ok := ctx.Value.(*tgba.Message) + if !ok || msg.Text == "" { // 确保无空 + return false + } + for _, prefix := range prefixes { + if strings.HasPrefix(msg.Text, prefix) { + ctx.State["prefix"] = prefix + arg := strings.TrimLeft(msg.Text[len(prefix):], " ") + ctx.State["args"] = arg + return true + } + } + return false + } +}