blog/leafbot源码解读.md

11 KiB
Raw Permalink Blame History

title tags categories date
leafbot源码解读 bot,go bot 2021-12-31 22:05:17

leafbot简介

driver介绍

driver为和onebot端的通信抽象层实现目前内置了四种driver分别对应了go-cqhttp的三种通信模式还有一个直接连接go-cqhttp进行内置启动,driver包含在形目的driver文件夹下面每一个driver需要实现如下接口。

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的调用



// 实现了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为客户端只需要在配置文件中配置hostport即可对应了go-cqhttp的正向ws监听地址。

    该驱动通过直接在Run方法中主动连接onebot端进行通信然后再连接成功后从请求头中获取到bot的SelfId。并且创建Bot对象bot对象中持有ws的conn连接对象可以通过该对象主动进行Api调用。

    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所覆盖。



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的方法。

    // 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添加内容
    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)
    }
    注册一个最小插件的示例为:

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文档