手把手教你搭建一个自己的QQ机器人 in Golang

作者:ssttkkl

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语言,毕竟大家小学期不是刚学完吗(什么你没选?现在开始自学还来得及。你问为什么要学?看看下面这张图就懂了)

 

                  

Get Started

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并且已经登录了):

进阶——怎么让bot主动发送消息?

首先介绍守护协程的概念,我们定义一个无限循环的函数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也是有很多值得挑战的方面的。



本文为我原创

本文禁止转载或摘编

-- --
  • 投诉或建议
评论