354 lines
11 KiB
Markdown
354 lines
11 KiB
Markdown
---
|
||
title: leafbot源码解读
|
||
tags: bot,go
|
||
categories: bot
|
||
date: 2021-12-31 22:05:17
|
||
|
||
---
|
||
|
||
# leafbot简介
|
||
|
||
+ ### leafbot是一个go语言实现的[onebot11](https://github.com/botuniverse/onebot-11)sdk实现之一,是一个免费的开源的go语言库,具有开发简单,持续维护等优点,可对接遵循onebot标准的实现,主要对接[go-cqhttp](https://github.com/Mrs4s/go-cqhttp)
|
||
+ ### leafbot的github仓库地址为:<http://github.con/huoxue1/leafbot>
|
||
+ ### leafbot文档地址为:<https://vtsqr.xyz/leafbot/dist/>
|
||
|
||
# driver介绍
|
||
|
||
driver为和onebot端的通信抽象层实现,目前内置了四种driver,分别对应了go-cqhttp的三种通信模式,还有一个直接连接go-cqhttp进行内置启动,driver包含在形目的driver文件夹下面,每一个driver需要实现如下接口。
|
||
|
||
```go
|
||
type Driver interface {
|
||
// Run
|
||
// @Description: 运行该驱动的接口,该接口应该为阻塞式运行
|
||
//
|
||
Run()
|
||
// GetEvent
|
||
// @Description: 返回一个chan,该chan为事件传递的chan
|
||
// @return chan
|
||
//
|
||
GetEvent() chan []byte
|
||
// 当一个bot连接时的回调
|
||
OnConnect(func(selfId int64, host string, clientRole string))
|
||
// 当bot断开连接时的回调
|
||
OnDisConnect(func(selfId int64))
|
||
|
||
// GetBot
|
||
// @Description: 获取一个实现了APi接口的bot
|
||
// @param int64 bot的id
|
||
// @return interface{}
|
||
//
|
||
GetBot(int64) interface{}
|
||
// GetBots
|
||
// @Description: 获取所有bot
|
||
// @return map[int64]interface{}
|
||
//
|
||
GetBots() map[int64]interface{}
|
||
// 给驱动设置一些运行信息,例如运行地址以及端口之类的
|
||
SetConfig(config map[string]interface{})
|
||
// 给驱动添加一个webhook监听,主要用于cqhttp_http_driver
|
||
AddWebHook(selfID int64, postHost string, postPort int)
|
||
// 给driver设置token,用于onebot端的鉴权
|
||
SetToken(token string)
|
||
}
|
||
```
|
||
|
||
+ ## cqhttp_http_driver
|
||
|
||
该驱动对应go-cqhttp的http连接方式,需要在配置文件中配置listen_address和listen_port,分别为leafbot的监听地址,还需要配置对应的webhook,对应了go-cqhttp端的监听地址。
|
||
|
||
driver通过实现了http包中的handler接口,即实现了ServerHttp方法,在ServerHttp方法中监听来自cqhttp的上报消息,并在在run方法中注册bot对象,每一个bot对象记录了自己上报端的接口地址和自己的selfId,bot通过调用Do方法进行api的调用
|
||
|
||
```go
|
||
|
||
|
||
// 实现了ServerHttp方法
|
||
func (d *Driver) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||
data, err := io.ReadAll(request.Body)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
d.eventChan <- data
|
||
writer.WriteHeader(200)
|
||
}
|
||
|
||
|
||
// 注册Bot对象
|
||
func (d *Driver) Run() {
|
||
log.Infoln("Load the cqhttp_http_driver successful")
|
||
for _, s := range d.webHook {
|
||
b := new(Bot)
|
||
b.selfID = s.selfID
|
||
b.postHost = s.postHost
|
||
b.postPort = s.postPort
|
||
b.responses = sync.Map{}
|
||
b.disConnectHandle = d.disConnectHandle
|
||
b.client = gout.NewWithOpt()
|
||
b.token = d.token
|
||
d.bots.Store(s.selfID, b)
|
||
}
|
||
log.Infoln("Load the cqhttp_http_driver successful")
|
||
log.Infoln(fmt.Sprintf("the cqhttp_http_driver listening in %v:%v", d.listenHost, d.listenPort))
|
||
if err := http.ListenAndServe(fmt.Sprintf("%v:%v", d.listenHost, d.listenPort), d); err != nil {
|
||
log.Errorln("监听webhook失败" + err.Error())
|
||
}
|
||
}
|
||
|
||
|
||
// 进行Api的调用
|
||
func (b *Bot) Do(i interface{}) {
|
||
type userAPi struct {
|
||
Action string `json:"action"`
|
||
Params interface{} `json:"params"`
|
||
Echo string `json:"echo"`
|
||
}
|
||
data := i.(userAPi)
|
||
var resp []byte
|
||
err := b.client.POST(fmt.Sprintf("http://%v:%v/%v", b.postHost, b.postPort, data.Action)).
|
||
SetHeader(gout.H{"Authorization": "Bearer " + b.token}).
|
||
SetJSON(data.Params).
|
||
BindBody(&resp).Do()
|
||
if err != nil {
|
||
log.Errorln("调用api出现错误", err.Error())
|
||
return
|
||
}
|
||
b.responses.Store(data.Echo, resp)
|
||
}
|
||
```
|
||
|
||
+ ## cqhttp_positive_driver
|
||
|
||
该驱动对应了go-cqhttp的正向websocket连接方式,即go-cqhttp为服务的,leafbot为客户端,只需要在配置文件中配置**host**和**port**即可,对应了go-cqhttp的正向ws监听地址。
|
||
|
||
该驱动通过直接在Run方法中主动连接onebot端进行通信,然后再连接成功后从请求头中获取到bot的SelfId。并且创建Bot对象,bot对象中持有ws的conn连接对象,可以通过该对象主动进行Api调用。
|
||
|
||
```go
|
||
func (d *Driver) Run() {
|
||
u := url.URL{Scheme: "ws", Host: d.address + ":" + strconv.Itoa(d.port)}
|
||
header := http.Header{}
|
||
header.Add("Authorization", "Bearer "+d.token)
|
||
conn, _, err := websocket.DefaultDialer.Dial(u.String(), header) //nolint:bodyclose
|
||
if err != nil {
|
||
return
|
||
}
|
||
log.Infoln("Load the cqhttp_positive_driver successful")
|
||
_, data, err := conn.ReadMessage()
|
||
if err != nil {
|
||
return
|
||
}
|
||
selfId := gjson.GetBytes(data, "self_id").Int()
|
||
role := ""
|
||
host := d.address
|
||
|
||
b := new(Bot)
|
||
b.conn = conn
|
||
b.selfId = selfId
|
||
b.responses = sync.Map{}
|
||
|
||
_, ok := d.bots.Load(selfId)
|
||
if ok {
|
||
d.bots.LoadOrStore(selfId, b)
|
||
} else {
|
||
d.bots.Store(selfId, b)
|
||
}
|
||
|
||
d.connectHandle(selfId, host, role)
|
||
b.disConnectHandle = d.disConnectHandle
|
||
log.Infoln(fmt.Sprintf("the bot %v is connected", selfId))
|
||
go func() {
|
||
defer func() {
|
||
i := recover()
|
||
if i != nil {
|
||
log.Errorln("ws链接读取出现错误")
|
||
log.Errorln(i)
|
||
d.disConnectHandle(selfId)
|
||
}
|
||
}()
|
||
for {
|
||
_, data, err := conn.ReadMessage()
|
||
if err != nil {
|
||
b.wsClose()
|
||
}
|
||
|
||
echo := gjson.GetBytes(data, "echo")
|
||
if echo.Exists() {
|
||
b.responses.Store(echo.String(), data)
|
||
} else {
|
||
d.eventChan <- data
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
```
|
||
|
||
```
|
||
|
||
+ ## cqhttp_reverse_driver
|
||
|
||
该驱动对应了cqhttp的反向websocket连接方式,需要配置host和port即可,该驱动leafbot作为服务器,onebot端主动连接leafbot,所以可以实现同时连接多个onebot实现。
|
||
|
||
驱动通过实现http包中的handler接口,再SeverHttp方法中,升级http协议为websocket协议,然后再升级成功后从header中获取selfId,然后创建Bot对象,bot对象分别持有websocket连接对象,可以通过对象赖调用对应的Onebot的api.
|
||
|
||
```go
|
||
func (d *Driver) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||
selfID, err := strconv.ParseInt(request.Header.Get("X-Self-ID"), 10, 64)
|
||
role := request.Header.Get("X-Client-Role")
|
||
host := request.Header.Get("Host")
|
||
if d.token != "" {
|
||
get := request.Header.Get("Authorization")
|
||
auth := strings.Split(get, " ")
|
||
if auth[0] != "Bearer" || auth[1] != d.token {
|
||
log.Errorln("the token is not current!")
|
||
return
|
||
}
|
||
}
|
||
conn, err := upgrade.Upgrade(writer, request, nil)
|
||
if err != nil {
|
||
return
|
||
}
|
||
b := new(Bot)
|
||
b.conn = conn
|
||
b.selfId = selfID
|
||
b.responses = sync.Map{}
|
||
_, ok := d.bots.Load(selfID)
|
||
if ok {
|
||
d.bots.LoadOrStore(selfID, b)
|
||
} else {
|
||
d.bots.Store(selfID, b)
|
||
}
|
||
b.disConnectHandle = d.disConnectHandle
|
||
log.Infoln(fmt.Sprintf("the bot %v is connected", selfID))
|
||
// 执行链接回调
|
||
go d.connectHandle(selfID, host, role)
|
||
go func() {
|
||
defer func() {
|
||
err := recover()
|
||
if err != nil {
|
||
b.wsClose()
|
||
log.Errorln("ws链接读取出现错误")
|
||
log.Errorln(err)
|
||
}
|
||
}()
|
||
for {
|
||
_, data, err := conn.ReadMessage()
|
||
if err != nil {
|
||
b.wsClose()
|
||
}
|
||
|
||
echo := gjson.GetBytes(data, "echo")
|
||
if echo.Exists() {
|
||
b.responses.Store(echo.String(), data)
|
||
} else {
|
||
d.eventChan <- data
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
```
|
||
|
||
## cqhttp_default_driver
|
||
|
||
该驱动为leafbot最新版本添加的驱动,通过调用go-cqhttp的registerServer对象,直接实现内置go-cqhttp来实现铜线,驱动在run方法中调用了gocq.Main()方法,并且注册了一个默认的服务,注册了一个事件回调,使用该驱动控制台同时会输出go-cqhttp的日志和leafbot日志。并且leafbot一些日志配置会被go-cqhttp所覆盖。
|
||
|
||
```go
|
||
|
||
|
||
func (d *Driver) Run() {
|
||
servers.RegisterCustom("leafBot", func(bot *coolq.CQBot) {
|
||
b := new(Bot)
|
||
b.CQBot = bot
|
||
b.call = api.NewCaller(bot)
|
||
d.bot = b
|
||
bot.OnEventPush(func(e *coolq.Event) {
|
||
data := e.JSONString()
|
||
result := gjson.Parse(data)
|
||
if result.Get("message").Exists() {
|
||
m := message.ParseMessageFromString(result.Get("message").String())
|
||
data, _ = sjson.Set(data, "message", m)
|
||
}
|
||
d.EventChan <- []byte(data)
|
||
})
|
||
})
|
||
gocq.Main()
|
||
}
|
||
|
||
```
|
||
|
||
# Plugin介绍
|
||
|
||
leafbot使用命令模式进行运作,先注册插件,然后去回调插件中的方法,其中一个插件包含多个Matcher,一个Matcher为最小的执行者。
|
||
|
||
Plugin结构体中存储了当前插件的插件名、插件帮助以及插件所包含的Matcher,可以通过NewPlugin(name string)方法进行注册一个插件,插件实现了多个注册Matcher的方法。
|
||
|
||
```go
|
||
// Plugin所实现的方法
|
||
basePlugin interface {
|
||
OnCommand(command string, options ...Option) Matcher
|
||
OnMessage(messageType string, options ...Option) Matcher
|
||
OnRequest(requestType string, options ...Option) Matcher
|
||
OnNotice(noticeType string, options ...Option) Matcher
|
||
OnMeta(options ...Option) Matcher
|
||
OnRegex(regexMatcher string, options ...Option) Matcher
|
||
|
||
OnStart(start string, options ...Option) Matcher
|
||
OnEnd(end string, options ...Option) Matcher
|
||
OnFullMatch(content string, options ...Option) Matcher
|
||
OnFullMatchGroup(content string, options ...Option) Matcher
|
||
|
||
OnConnect(options ...Option) Matcher
|
||
OnDisConnect(options ...Option) Matcher
|
||
}
|
||
```
|
||
|
||
其中Matcher接口为如下内容,Matcher接口为为Matcher添加内容
|
||
|
||
```go
|
||
Matcher interface {
|
||
MatcherSet
|
||
Enabled() bool
|
||
GetHandler() Action
|
||
GetRules() []Rule
|
||
GetWeight() int
|
||
IsBlock() bool
|
||
GetDisAbleGroup() []int64
|
||
GetType() string
|
||
GetPluginType() string
|
||
}
|
||
|
||
MatcherSet interface {
|
||
AddRule(rule Rule) Matcher
|
||
SetWeight(weight int) Matcher
|
||
SetBlock(block bool) Matcher
|
||
SetAllies(allies []string) Matcher
|
||
|
||
Handle(action Action)
|
||
}
|
||
```
|
||
|
||
注册一个最小插件的示例为:
|
||
|
||
```go
|
||
|
||
func init() {
|
||
plugin := leafbot.NewPlugin("测试")
|
||
plugin.OnCommand("测试", leafbot.Option{
|
||
Weight: 0,
|
||
Block: false,
|
||
Allies: nil,
|
||
Rules: []leafbot.Rule{func(ctx *leafbot.Context) bool {
|
||
return true
|
||
}},
|
||
}).Handle(func(ctx *leafbot.Context) {
|
||
ctx.Send(message.Text("123"))
|
||
})
|
||
}
|
||
```
|
||
|
||
该插件会匹配命令测试,并且会回应123
|
||
|
||
# 更多
|
||
|
||
## 了解更多leafbot内容,请查看leafbot[文档](https://vtsqr.xyz/leafbot/dist/)
|