代码先锋网 代码片段及技术文章聚合

leafbot源码解读

技术标签: golang  开发语言  后端

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.

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

版权声明:本文为m15082717021原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m15082717021/article/details/122263464

智能推荐

ArrayList源码解读

本文基于jdk1.8解读ArrayList关键代码 ArrayList是非线程安全的,在多线程环境下要使用CopyOnWriteArrayList 存储结构 ArrayList内部存储结构是数组,ArrayList的容量就是数组的长度 初始化 刚new出来的ArrayList容量是0,在首次add的时候,容量会变成默认初始大小10,当然可以在new的时候手动传入初始大小 大小及容量涨幅 size属...

HashMap的源码解读

最近看到几篇精彩的文章: 存取之美 —— HashMap原理、源码、实践 Hash碰撞与拒绝服务攻击 这些文章让我收获良多, 但是有些地方说的不够详细, 在此写下本人对上述文章的总结和理解, 希望可以给需要的朋友带来一些帮助. 1. 概述 HashMap在底层采用数组+链表的形式存储键值对. 在HashMap中定义了一个内部类Entry<K, V>, 该内部类是...

源码解读之Spliterator

以下源码摘自JDK8...

Hibernate源码解读——查询

  本文主要通过源码,概述一下session的查询过程。   主要api有get、load。他们基本过程是类似的;   以get为例:   Session public Object get(String entityName, Serializable id) throws HibernateException;   调用SessionImpl:...

Hibernate源码解读——启动

 作为javar的必学框架,我相信Hibernate源码也是被研究过无数次了。不过,别人研究过不代表我就不需要看了。   这里我以我的视野简单的过一遍。各位iteye的朋友有时间可以看看。如何已经很熟悉这个了就不用看了,否则看也是浪费时间。我研究的方法非常简单,就是反复读源码,一遍不行,二遍,或者三遍。。。   从Hibernate源码看它的启动过程:   ...

猜你喜欢

LinkedHashMap源码解读

LinkedHashMap继承了HashMap,采用双向链表结构,数据顺序是可预知的  以及  来实现双向链表。   取数据时,可以选择是按存放顺序取还是访问顺序取:  ...

HashMap源码解读

HashMap保存数据的结构是数组+单向链表,它集成AbstractMap类以及实现Map接口:       HashMap有四个构造方法: 1、带初始化大小和加载因子的构造方法  2、带初始化大小的构造方法  3、无参构造方法  4、带Map类型参数的构造方法   在HashMap中,通过key的hash值来计算元素在数组ta...

ArrayList源码解读

ArrayList继承了AbstractList类记忆实现了List接口,使用数组保存元素 当使用无参的构造器时,数组大小默认10个。   添加数据时会检查数组容量大小,如果数组容量比数据个数小,那么数组容量会以原来的1.5倍增加,并且会重排数据。    ...

LinkedList源码解读

LinkedList 存放数据通过双向链表实现: 写道 private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.nex...