3/27:拟定将Aya更换成酷Q Pro的版本,追加如下功能:推特转发(带图)、模拟抽卡(兼带自定义概率)、活动信息查询、卡面图鉴查询、谱面查询、每日占卜

前言

除了ハロハピCiRCLE放送局之外,最大最新的消息来源莫过于官方推特了。每一个邦邦人多多少少都会有看官推的需求,但由于某些原因看推不能像看微博那么方便。
爬推可能第一时间想到的是利用Twitter的API,可Twitter Developer已经改成了这个样子。。。

你没看错,这不是入境单,这是开发者申请。
放弃。

技术路线

后来我发现了这个:Astalaseven/twitter-rss 这是基于RSS-Bridge项目搭建的,你可以按照Astalaseven的参考文档搭建自己的RSS-Bridge,这样就可以实现信息的爬取了。
这样信息获取的部分就搞定了。

接下来是通信的部分。我花了很多时间找这方面的解决方案,貌似目前还有在维护的、有一定社区规模的也只剩下酷Q一家了。酷Q分为Air版本和Pro版本,其中Pro版本是收费的,相较于免费的Air多了能够发送图片之类的功能。
但收费不是阻止我使用Pro的原因。Air/Pro都是针对Win平台开发的(没错,用的易语言),对于Air官方释出了docker的版本(基于wine)。由于Windows的主机对内存要求较高,1G以下的服务器大部分是不提供Win安装选项的,所以我也只能选择Air-docker。

然后是交互的部分,我选择的是Richard Chien编写的Nonebot机器人。不因为什么,纯粹是因为我参考的教程里有一篇使用的是集成的方案,我就照着做了。Nonebot的原理是利用了CoolQ HTTP API 插件,将酷Q的信息交接给前台,然后通过Nonebot编写的脚本实现交互。

安装过程

安装前请先确保你的服务器没有安装Nginx/Apache之类的服务,或者说你的端口没有被Web程序监听。酷Q是需要开启VNC进行配置的,如果你不确定这些因素是否存在,建议使用一个空白的服务器进行搭建。

首先检查你的服务器Python版本是否高于3.6.1,如果不是的话请升级到以上版本。
大部分镜像预装的版本都是3.5.1或者2.7(OSX Catalina是2.7),无论是yum还是apt-get,都需要修改源才能通过命令安装。所以我们不如直接:

wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tgz

然后

./configure --prefix=/usr/local/python3
make && make install

安装完成了做软链接

ln -s /usr/local/python3/bin/python3.7 /usr/bin/python3
ln -s /usr/local/python3/bin/pip3.7 /usr/bin/pip3

然后运行一下python3验证一下是否安装成功。当然pip升级完成后还是需要--upgrade pip到20的版本的。
然后安装docker,这里尽量不要使用yum之类的原件源,如果你出现了问题,不妨试试官方的安装脚本:

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

然后docker vision验证是否成功安装。
安装成功后sudo systemctl start docker或者sudo systemctl start docker开启Docker服务

然后可以安装Nonebot:pip3 install nonebot

然后拖取docker镜像:docker pull richardchien/cqhttp:latest

创建容器开启酷Q:

docker run -ti --rm --name cqhttp-test  -v $(pwd)/coolq:/home/user/coolq  -p 9001:9000  -p 5701:5700  -e COOLQ_ACCOUNT=123456   -e CQHTTP_POST_URL=http://example.com:8080  -e CQHTTP_SERVE_DATA_FILES=yes  richardchien/cqhttp:latest

其实我也是第一次接触docker,好在这东西不算太复杂。
如果你想改动,大概有这么几个地方是可以更改的:
--rm是前台开启docker,如果你想以服务的形式打开可以换成-d
--name cqhttp-test相比就不用多说了
$(pwd)/coolq:/home/user/coolq这里是很“docker”的地方,$(pwd)是你当前所在的目录,就是把当前目录的/coolq映射到你container的/home/user/coolq目录。也就是说,你想修改container里的文件,可以对应到物理机的这个目录找到文件。
-p 9001:9000-p 5701:5700同理,也是把container的port映射到物理机的port,此处的9001一会儿是vnc的入口
COOLQ_ACCOUNT=123456默认的QQ号配置,可改可不改,因为你一会儿进去vnc也是要登陆的,此处改了酷Q里就会直接显示Q号
别的暂且不用改动了。

进入{ip}:9001后可以通过vnc看到酷Q,如果没有问题的话你给机器人账号发送“新手教程”是会有回复的。vnc的默认密码是MAX8char
然后你就可以关掉窗口了。

接下来配置CoolQ HTTP API。因为安装的方式不同,配置文件可能在data/app/io.github.richardchien.coolqhttpapi/config/或者app/io.github.richardchien.coolqhttpapi/config/。找到{机器人Q号}.json文件,对里面的内容进行修改:

{
    "ws_reverse_api_url": "ws://{你的服务器ip}:9999/ws/api/",
    "ws_reverse_event_url": "ws://{你的服务器ip}:9999/ws/event/",
    "use_ws_reverse": true
}

如果你是在本地运行的话,应该写成127.0.0.1,具体的可以参照官方文档的说明

创建一个bot.py文件:

import nonebot

if __name__ == '__main__':
    nonebot.init()
    nonebot.load_builtin_plugins()
    nonebot.run(host='0.0.0.0', port=9999)

此处的host和port,是根据你的配置填写的。执行python3 not.py,应该会有以下信息:
一个是CoolQ HTTP API传回的信息,如果有说明你的CoolQ HTTP API插件配置成功了。随便和机器人账号进行对话console是能收到echo的。

[2019-01-26 16:23:17,159] 172.29.84.18:50639 GET /ws/api/ 1.1 101 - 986
[2019-01-26 16:23:17,201] 172.29.84.18:53839 GET /ws/event/ 1.1 101 - 551

另一个是Nonebot的输出信息:

[2019-01-26 14:24:15,984 nonebot] INFO: Succeeded to import "nonebot.plugins.base"
[2019-01-26 14:24:15,987 nonebot] INFO: Running on 127.0.0.1:9999
Running on https://127.0.0.1:9999 (CTRL + C to quit)

此处的nonebot.plugins.base是官方提供的初始插件,用于测试,我们刚才在代码中有nonebot.load_builtin_plugins(),意为导入了插件。
如果给你的机器人账号发送“/echo 你好,世界”,机器人账号就会对应的回复“你好,世界”

至此基本的配置就已经完成了。

功能实现

现在我们对自定义的内容进行编写。
如果你想详细的学习nonebot的知识,可以参考官方文档
如果你觉得废话太多,可以看一下的凝练版本。
首先是确定项目的结构,以官方例程为参考,最简单的目录可以是这样的:

awesome-bot
├── plugins
│   └── weather.py //存放实现功能的插件
├── bot.py  //程序主入口
└── config.py //配置文件,如果不需要对起始字符、超级用户之类的进行配置也可以不要

如果没有特殊要求,config.py这样配置就可以了:

from nonebot.default_config import *
SUPERUSERS = {215930974}
COMMAND_START = {'', '/', '!', '/', '!'}
HOST = '0.0.0.0'
PORT = 9999
#HOST和PORT一定要大写!
#命令起始字符,其实有个''就可以了,就是不用前置字符即可调用

然后是bot.py的配置:

import nonebot
import config
from os import path
if __name__ == "__main__":
    nonebot.init(config)
    nonebot.load_plugins(path.join(path.dirname(__file__), 'plugins'), 'plugins')
    nonebot.run()

然后就是官方给出的交互式例程,我觉得写得很好一看就能明白:

from nonebot import on_command, CommandSession

# on_command 装饰器将函数声明为一个命令处理器
# 这里 weather 为命令的名字,同时允许使用别名「天气」「天气预报」「查天气」
@on_command('weather', aliases=('天气', '天气预报', '查天气'))
async def weather(session: CommandSession):
    # 从会话状态(session.state)中获取城市名称(city),如果当前不存在,则询问用户
    city = session.get('city', prompt='你想查询哪个城市的天气呢?')
    # 获取城市的天气预报
    weather_report = await get_weather_of_city(city)
    # 向用户发送天气预报
    await session.send(weather_report)

# weather.args_parser 装饰器将函数声明为 weather 命令的参数解析器
# 命令解析器用于将用户输入的参数解析成命令真正需要的数据
@weather.args_parser
async def _(session: CommandSession):
    # 去掉消息首尾的空白符
    stripped_arg = session.current_arg_text.strip()
    if session.is_first_run:
        # 该命令第一次运行(第一次进入命令会话)
        if stripped_arg:
            # 第一次运行参数不为空,意味着用户直接将城市名跟在命令名后面,作为参数传入
            # 例如用户可能发送了:天气 南京
            session.state['city'] = stripped_arg
        return
    if not stripped_arg:
        # 用户没有发送有效的城市名称(而是发送了空白字符),则提示重新输入
        # 这里 session.pause() 将会发送消息并暂停当前会话(该行后面的代码不会被运行)
        session.pause('要查询的城市名称不能为空呢,请重新输入')
    # 如果当前正在向用户询问更多信息(例如本例中的要查询的城市),且用户输入有效,则放入会话状态
    session.state[session.current_key] = stripped_arg

async def get_weather_of_city(city: str) -> str:
    # 这里简单返回一个字符串
    # 实际应用中,这里应该调用返回真实数据的天气 API,并拼接成天气预报内容
    return f'{city}的天气是……'

交互式的命令编写可以分成三段,一是对传入信息的接收,二是对语言的处理,即解释器,三是生成返回的值

但我并不打算让推特内容进行交互式的获取,因为这样风险太大了。官方文档对自然语言处理,还有对类似新成员入群或者自动同意加群请求这样处理通知和请求的命令有介绍,我就不再赘述了。

如果需要对应信息回复(查询)之类的方法,可以参照一下模版:

@on_command('正面',aliases=('彩彩可爱', '老婆', '可爱'))
async def auto_reply(session:CommandSession):
    await session.send('o(*////▽////*)q',at_sender=True)

比如说我想实现对机器人对话时特定关键字回复(私聊是直接发送,群聊则是at机器人账号):我说“可爱”,机器人回复“o(////▽////)q”
此处的正面是函数方法名称,后面的是别称。也就是说我回复这四个任意一个,机器人就会回复下面的内容。

下面给出一个手动查询推文的模板:

from nonebot import on_command, CommandSession
from nonebot import on_notice, NoticeSession
import nonebot
import sys
import os
import requests
import re
import json
import arrow

@on_command('twitter1', aliases=('twitter1', '推特更新', '推文更新'))
#async def twitter(session: CommandSession):
async def auto_reply(session:CommandSession):
    bot = nonebot.get_bot()
    time_now = arrow.now() # 获取当前时间
    #获取推的RSS 格式为json
    res = requests.get('{此处填入获得的json源头}')
    res.raise_for_status()
    jsonfile = open('./data.json','wb')
    for chuck in res.iter_content(10000):
        jsonfile.write(chuck)
    jsonfile.close()
    # 写入 JSON 数据
    with open('./data.json', 'r') as f:
        load_data = json.load(f)
    # 读取数据
    json_data = load_data.get("items")
    time_result = []
    title_result = []
    for i in json_data:
        time_result.append(i.get("date_modified"))
        title_result.append(i.get("title"))
    # 检查是否有置顶
    if arrow.get(time_result[0]) > arrow.get(time_result[1]):
        print("没有置顶推文")
    else:
        print("存在置顶推文")
    # 近3小时内的推文
    tweetcontent_send = ''
    for i in range(0,len(time_result)-1):
        if arrow.get(time_result[i]) > arrow.now().shift(hours=-3) :
            tweetcontent_send+=title_result[i]
    #print(tweetcontent_send)
    if tweetcontent_send != '':
        await session.send('最近三小时的推文有:',at_sender=True)
        temp_send=''
        for i in range(0,len(time_result)-1):
            if arrow.get(time_result[i]) > arrow.now().shift(hours=-3) :
                temp_send+=title_result[i]
                await session.send(temp_send,at_sender=True)
                temp_send=''
    else:
        await session.send('最近三小时内没有推文更新哦~'+'\n'+'------'+'\n'+'彩彩机器人持续为你服务~')

代码逻辑应该幼儿园都能看得明白的水平,你可能会对tweetcontent_send这部分写的这么别扭会有些疑惑:
一开始我也是将几条推文合并在一起发送,但后期出现了异步处理超时的问题,经过排查发现只要发送的内容稍微短一点就可以成功,所以临时做的修改。

要定时进行推送,先要安装schedulerpip install "nonebot[scheduler]"

import nonebot
from aiocqhttp.exceptions import Error as CQHttpError

@nonebot.scheduler.scheduled_job('cron', hour='*')
async def _():
    bot = nonebot.get_bot()
    try:
        {在此处填入内容}
    except CQHttpError:
        pass

因为推送大多都是对于群聊而言的,所以用await session.send就不合适了,对于群聊可以用:

await bot.send_group_msg(group_id={此处填入群号},message={此处填入发送内容,type必须是str})

上面scheduler的触发器是整点触发,你也可以采用别的。
比如说想被封号的你可以尝试一下nonebot.scheduler.scheduled_job('interval', seconds=5),即5s触发一次,如果你这么做了,记得邮件告诉我多久被封的,我好提醒提醒自己。

接下来就是调戏机器人的时间了~

后记

1.我知道很多音游群是需要复读机的,你可以在nonebot里对对话进行监听编写服务规则,也可以直接尝试这个酷Q插件:[免费(开源)]人的本质是复读机
下载的cpk文件放在/coolq/app文件夹下,在酷Q的应用管理中重载程序启用即可。不过插件的规则是写死的,即固定时间内监听到3个或以上不同账号同一句话时启用复读
2.昨天应群里的成员要求,补充了翻译的功能。如果你有Google Cloud账号,尽量调用谷歌翻译。如果你现在没有双币卡的话,百度翻译api是最容易申请的。进入此处下方的立即使用,填写信息即可。信息别的可以忽略,IP一定要准确的填写,否则无法调用程序。各个语言的调用demo和post方式可以在这里找到。

参考文献

[1] 友人C.30分钟写一个简单QQ自动回复机器人[EB/OL].https://www.ihewro.com/archives/979/, 2019–09–19.
[2] Richard Chien.Nonebot 参考文档[EB/OL].https://nonebot.cqp.moe/, 2019.