QQ机器人是怎么回事呢?QQ相信大家都很熟悉,但是QQ机器人是怎么回事呢?QQ机器人,其实就是能够接收QQ消息并自动回复的机器人程序,这就是关于QQ机器人的事情了。
好了,今天要介绍的就是一个QQ机器人(bot)框架——mirai。mirai 是一个在全平台下运行,提供 QQ Android 和 TIM PC 协议支持的高效率机器人库。项目地址:https://github.com/mamoe/mirai
目前mirai支持通过大部分主流语言调用,本文将介绍如何使用Golang搭建属于你自己的bot。另外Python也可以写但是我懒得写了(要用到asyncio库,解释起来又要一堆篇幅了),感兴趣可以自行研究。
1. 好玩
2. 真的好玩
3. 可以用来实践go语言,毕竟大家小学期不是刚学完吗(什么你没选?现在开始自学还来得及。你问为什么要学?看看下面这张图就懂了)
1. 在电脑上下载miraiOK一键启动器(https://github.com/LXY1226/miraiOK),并且装上mirai-api-http插件(https://github.com/project-mirai/mirai-api-http,在右边的releases下载jar文件)
2. 去看README配置好mirai-api-http
3. 运行miraiOK,输入login QQ号 密码进行登录(我这里Windows Defender会报毒,应该是误报,信不过的话就把源码克隆下来自己编译)
4. 执行命令go get github.com/Logiase/gomirai,用来为go下载gomirai库(https://github.com/Logiase/gomirai)
这是一个最简单的bot模板,适用于一问一答的方式。使用时把const一栏里的东西改成自己的东西,main函数内基本不需要变动,在onReceiveMessage函数里写机器人的应答代码。
这种直接通过看Demo上手的学习方式我愿称之为Demo式学习
package main
import (
"github.com/Logiase/gomirai"
"github.com/Logiase/gomirai/message"
"io/ioutil"
"math/rand"
"os"
"os/signal"
)
const (
qq = 123456
url = "http://127.0.0.1:3399"
authKey = "qwerty"
)
var client *gomirai.Client
var bot *gomirai.Bot
func main() {
interrupt := make(chan os.signal, 1)
signal.Notify(interrupt, os.Interrupt)
// 初始化Bot部分
client = gomirai.NewClient("default", url, authKey)
key, err := client.Auth()
if err != nil {
client.Logger.Fatal(err)
}
bot, err = client.Verify(qq, key)
if err != nil {
client.Logger.Fatal(err)
}
defer func() {
err := client.Release(qq)
if err != nil {
client.Logger.Warn(err)
}
}()
// 启动一个goroutine用于接收消息
go func() {
err := bot.FetchMessages()
if err != nil {
client.Logger.Error(err)
}
}()
// 开始监听消息
for true {
select {
case <-interrupt:
return
case e := <-bot.Chan:
switch e.Type {
case message.EventReceiveFriendMessage:// 收到好友消息
go onReceiveMessage("friend", e)
case message.EventReceiveGroupMessage: // 收到群组消息
go onReceiveMessage("group", e)
case message.EventReceiveTempMessage:// 收到临时会话消息
go onReceiveMessage("temp", e)
}
}
}
}
func onReceiveMessage(senderType string, e message.Event) {
// 在这里写应答代码
}
当然我们的标题是“手把手教你”,所以肯定不能只扔个Demo就完事了。下面就该手把手教了。
作为例子,让我们现在实现一个色图(误)bot。
首先看到onReceiveMessage里的两个参数:senderType是一个string类型,表示发送者是好友、群组还是临时会话;e是一个message.Event,包含了这条消息的信息。
让我们去看看message.Event的定义:
type Event struct {
Type string `json:"type"` //事件类型
MessageChain [ ]Message `json:"messageChain"` //(ReceiveMessage)消息链
Sender Sender `json:"sender"` //(ReceiveMessage)发送者信息
EventId uint `json:"eventId"` //事件ID
FromId uint `json:"fromId"` //操作人
GroupId uint `json:"groupId"` //群号
}
这里有一个“消息链”的概念。我们看到一条QQ消息,可能是由不同的各个组件构成的,比如纯文本、图片、QQ表情、At、……这些不同的组件串在一起形成一条消息,在mirai中就叫做“消息链”。在gomirai里的实现就是一个Message类型的切片。
有一点需要注意的是,MessageChain[0]永远都是Source类型的。Source并不是一个真实的消息组件,它只是提供了一个序号用于定位这条消息。我们能看到的其他消息组件其实是从MessageChain[1]开始的。
而Sender表示这条消息的具体的发送者,定义如下:
type Sender struct {
Id uint `json:"id,omitempty"` //发送者QQ号
NickName string `json:"memberName,omitempty"` //(FriendMessage)发送者昵称
Remark string `json:"remark,omitempty"` //(FriendMessage)发送者备注
MemberName string `json:"memberName,omitempty"` //(GroupMessage)发送者群昵称
Permission string `json:"permission,omitempty"` //(GroupMessage)发送者在群中的角色
Group Group `json:"group,omitempty"` //(GroupMessage)消息来源群信息
}
type Group struct {
Id uint `json:"id,omitempty"` //消息来源群号
Name string ` json:"name,omitempty"` //消息来源群名
Permisson string ` json:"permisson,omitempty"` //bot在群中的角色
}
回到我们的bot来。首先我们准备一些图片,保存在img目录里。(别问,问就是蓝色p站)
我们希望收到特定的消息才应答,其他消息直接忽视。所以:
// 如果没检测到关键词就直接结束
if e.MessageChain[1].Text != "来张色图" {
return
}
然后再从img目录随机抽选一张图片:
// 从img目录里随机抽一张图片
dir, err := ioutil.ReadDir("img")
if err != nil {
client.Logger.Error(err)
}
var name string
var filepath string
if l := len(dir); l != 0 {
ran := rand.Intn(l)
name = dir[ran].Name()
filepath = "img/" + name
} else {
return
}
之后就是上传图片然后发送消息:
// 发送消息
switch senderType {
case "friend":
_, err = bot.SendFriendMessage(e.Sender.Id, 0,
message.ImageMessage("id", imgId), message.PlainMessage(name))
case "group":
_, err = bot.SendGroupMessage(e.Sender.Group.Id, 0,
message.ImageMessage("id", imgId), message.PlainMessage(name))
case "temp":
_, err = bot.SendTempMessage(e.Sender.Group.Id, e.Sender.Id,
message.ImageMessage("id", imgId), message.PlainMessage(name))
}
if err != nil {
client.Logger.Error(err)
}
整个程序是这样的:
package main
import (
"github.com/Logiase/gomirai"
"github.com/Logiase/gomirai/message"
"io/ioutil"
"math/rand"
"os"
"os/signal"
)
const (
qq = 123456
url = "http://127.0.0.1:3399"
authKey = "qwerty"
)
var client *gomirai.Client
var bot *gomirai.Bot
func main() {
// 用于从键盘监听结束信号(win下是Ctrl+C)
interrupt := make(chan os.signal, 1)
signal.Notify(interrupt, os.Interrupt)
// 初始化Bot部分
client = gomirai.NewClient("default", url, authKey)
key, err := client.Auth()
if err != nil {
client.Logger.Fatal(err)
}
bot, err = client.Verify(qq, key)
if err != nil {
client.Logger.Fatal(err)
}
defer func() {
err := client.Release(qq)
if err != nil {
client.Logger.Warn(err)
}
}()
// 启动一个goroutine用于接收消息
go func() {
err := bot.FetchMessages()
if err != nil {
client.Logger.Error(err)
}
}()
// 开始监听消息
for true{
select {
case<-interrupt:
return
case e := <-bot.Chan:
switch e.Type {
case message.EventReceiveFriendMessage: // 收到好友消息
go onReceiveMessage("friend", e)
case message.EventReceiveGroupMessage: // 收到群组消息
go onReceiveMessage("group", e)
case message.EventReceiveTempMessage: // 收到临时会话消息
go onReceiveMessage("temp", e)
}
}
}
}
func onReceiveMessage(senderType string, e message.Event) {
// 如果没检测到关键词就直接结束
if e.MessageChain[1].Text != "来张色图" {
return
}
// 从img目录里随机抽一张图片
dir, err := ioutil.ReadDir("img")
if err != nil {
client.Logger.Error(err)
}
var name string
var filepath string
if l := len(dir); l != 0 {
ran := rand.Intn(l)
name = dir[ran].Name()
filepath = "img/" + name
} else {
return
}
// 上传图片
imgId, err := bot.UploadImage(senderType, filepath)
if err != nil {
client.Logger.Error(err)
}
// 发送消息
switch senderType {
case "friend":
_, err = bot.SendFriendMessage(e.Sender.Id, 0,
message.ImageMessage("id", imgId), message.PlainMessage(name))
case "group":
_, err = bot.SendGroupMessage(e.Sender.Group.Id, 0,
message.ImageMessage("id", imgId), message.PlainMessage(name))
case "temp":
_, err = bot.SendTempMessage(e.Sender.Group.Id, e.Sender.Id,
message.ImageMessage("id", imgId), message.PlainMessage(name))
}
if err != nil {
client.Logger.Error(err)
}
}
一个色图bot就写好了!编译运行测试一下(确保已经运行mirai-console并且已经登录了):
首先介绍守护协程的概念,我们定义一个无限循环的函数daemon():
func daemon() {
for true {
// do something
}
}
然后在main函数里作为goroutine启动:go daemon()
这个goroutine永远不会结束,因此叫做守护协程。(在别的语言里如果是线程的话就叫守护线程)当然我们不希望里面的语句执行得太频繁,所以通常会用到time.Sleep(Duration)函数让协程休眠一会儿再继续。
介绍这个有什么用呢?思考一下,我想让bot在某一时刻主动发送消息,那么就让这个守护协程睡到那个时候不就行了。举个例子,假如我要实现一个整分钟报时bot,那就可以这么写:
func daemon() {
for true {
now := time.Now() // 现在的时间
nextMinute := time.Date(now.Year(), now.Month(), now.Day(),
now.Hour(), now.Minute(), 0,
0, now.Location()).Add(time.Minute) // 一分钟之后的时间
delta := nextMinute.Unix() - now.Unix() // 两个时间相差了多少秒
time.Sleep(time.Duration(delta) * time.Second)
_, err := bot.SendFriendMessage(targetQQ, 0,
message.PlainMessage(time.Now().String()))
if err != nil {
client.Logger.Error(err)
}
}
}()
整个程序代码如下(因为这个bot只发不收,所以忽略了接收消息部分的代码):
package main
import (
"github.com/Logiase/gomirai"
"github.com/Logiase/gomirai/message"
"github.com/sirupsen/logrus"
"os"
"os/signal"
"time"
)
const (
qq = 123456
targetQQ = 987654
url = "http://127.0.0.1:3399"
authKey = "qwerty"
)
var client *gomirai.Client
var bot *gomirai.Bot
func main() {
// 用于从键盘监听结束信号(win下是Ctrl+C)
interrupt := make(chan os.signal, 1)
signal.Notify(interrupt, os.Interrupt)
// 初始化Bot部分
client = gomirai.NewClient("default", url, authKey)
client.Logger.Level = logrus.TraceLevel
key, err := client.Auth()
if err != nil {
client.Logger.Fatal(err)
}
bot, err = client.Verify(qq, key)
if err != nil {
client.Logger.Fatal(err)
}
defer func() {
err := client.Release(qq)
if err != nil {
client.Logger.Warn(err)
}
}()
// 启动守护协程
go daemon()
// 等待结束
<-interrupt
}
func daemon() {
for true {
now := time.Now() // 现在的时间
nextMinute := time.Date(now.Year(), now.Month(), now.Day(),
now.Hour(), now.Minute(), 0,
0, now.Location()).Add(time.Minute) // 一分钟之后的时间
delta := nextMinute.Unix() - now.Unix() // 两个时间相差了多少秒
time.Sleep(time.Duration(delta) * time.Second)
_, err := bot.SendFriendMessage(targetQQ, 0,
message.PlainMessage(time.Now().String()))
if err != nil {
client.Logger.Error(err)
}
}
}
效果如下:
虽然这个例子很简单而且没什么用,但是基于这个原理,结合你自己的想法,就可以做出各种功能的bot了。
写完自己的bot以后,尝试思考一下这些问题:
• 你的bot能够长时间运行吗?能够运行多久?
• 你的bot能够同时处理多少消息?1个?10个?100个?
• 你的bot有多少代码?当随着功能增多、代码越来越多的时候应该怎么组织项目?
不要小看toy project,哪怕是toy project也是有很多值得挑战的方面的。