Project RC

一种标准阿西的设计与实现。

情人节要到了,手把手教你写个 QQ 机器人给女朋友玩

创建于
分类:Dev
标签:酷QQQ BotMojo-WebqqHTTP API

其实 QQ 机器人写起来很简单的,对于本教程中使用的 QQ 机器人框架,你需要一点点 C/C++ 基础,或者了解其它任何语言的 HTTP 库(开启 HTTP 服务端、发起 HTTP 请求两种)的使用。

QQ 机器人框架简介

也就是对 QQ 协议分析之后封装出来的一个完整的 QQ 客户端及开发框架,通过对这个客户端的事件的处理和接口的调用,即可实现诸如自动回复、主动通知等功能。

目前网上各种机器人框架很多,本教程主要以 酷 QMojo-Webqq 为例。

这两个框架差别还是蛮大的:酷 Q 是闭源的,使用 Android QQ 协议,似乎是易语言写的,只能在 Windows 上跑(最新版本也支持在 Wine 里面跑),且必须在图形界面操作,官方提供易语言和 C++ 的 SDK;Mojo-Webqq 是开源的,使用 SmartQQ 协议,用 Perl 写的,全平台基本都能跑,比较方便用脚本来自动化,官方提供 Perl 的接口以及 HTTP 接口。

另外,酷 Q 由于是 Android QQ 协议,要比 SmartQQ 稳定很多,且可以接收图片、语音等。

这两者的差别还有很多,可以分别去看它们的文档。

下面分别讲如何使用这两个客户端框架来编写简易的 QQ 机器人。

使用酷 Q 开发(C/C++)

酷 Q 这玩意官方似乎是比较建议用易语言来写插件的,然而易语言这坑,我反正是不想进……

所以这里用它的 C++ SDK 来写,如果你不会 C/C++,可以直接跳到下个标题,通过 HTTP 接口来开发。

安装酷 Q

好现在开始,首先在 https://cqp.cc/ 下载最新版的酷 Q Air,下载下来是 zip 包,直接解压就能运行。其实酷 Q 的图灵版和小 i 版都是自带了 AI 的插件的,可以去 图灵机器人小 i 机器人 注册获得一个 API Key,就可以直接自动 AI 回复了。这里主要讲如何自己开发。

如果没用过酷 Q,可以先走一遍它的首次启动教程。

写点 C/C++

官方的 C++ SDK 是直接给了一个 demo 工程,直接在 这里 下载完整的工程目录,然后用 Visual Studio 打开,一般情况下基本上可以直接编译成功的。

然后就可以开始写了,其实这个 demo 工程里面注释给的也基本上很清楚。

对于一个自动回复的机器人来说,我们主要需要关注的是 appmain.cpp 文件,这个文件就是用来处理接收到的酷 Q 事件的(比如私聊消息、群组消息、群成员变动等等),主要视需求修改 __eventGroupMsg__eventGroupMsg__eventDiscussMsg 这三个事件的处理代码。

然后还需要看一下 cqp.h 这个文件,这里声明了酷 Q 的 C++ 接口,发送消息、处理请求等就是通过调用这里声明的函数来实现。

通过这些就可以开始实现一些简单的功能了,比如要让机器人直接重复对方发的内容,代码如下:

/*
* Type=21 私聊消息
* subType 子类型,11/来自好友 1/来自在线状态 2/来自群 3/来自讨论组
*/
CQEVENT(int32_t, __eventPrivateMsg, 24)(int32_t subType, int32_t sendTime, int64_t fromQQ, const char* msg, int32_t font)
{
    //如果要回复消息,请调用酷Q方法发送,并且这里 return EVENT_BLOCK - 截断本条消息,不再继续处理  注意:应用优先级设置为"最高"(10000)时,不得使用本返回值
    //如果不回复消息,交由之后的应用/过滤器处理,这里 return EVENT_IGNORE - 忽略本条消息
    CQ_sendPrivateMsg(ac, fromQQ, msg);
    return EVENT_IGNORE;
}

__eventPrivateMsg 事件的几个参数含义分别是:

  • subType:消息子类型,见注释;
  • sendTime:消息发送时间的 Unix 时间戳;
  • fromQQ:发送者的 QQ 号;
  • msg:收到的消息内容;
  • font:字体……我也不知道这有啥用;

上面代码通过调用 CQ_sendPrivateMsg(ac, fromQQ, msg); 即可把消息原样发送出去了,其中 ac 是酷 Q 授权码,在 Initialize 事件里获得,每个接口都需要传入这个授权码才能调用。

编译成功后会得到一个 .dll 文件,把它和工程目录下面那个 .json 文件一起放到酷 Q 的 app 文件夹里面,并且在 conf 文件夹的 CQP.cfg 文件里加上如下配置:

[Debug]
DeveloperMode=1

这时候重启酷 Q 就可以看到刚刚自己的插件了。

限于篇幅这里只给出这个示例了,如果你有一些 C/C++ 基础的话就可以对消息内容字符串进行匹配、请求网络数据、构造回复内容等等。但是我突然觉得就这么一个简单的示例可能也没啥用,你可以看一下这几个文档:Pro/开发/快速入门Pro/开发酷Q机器人开发笔记(C++)。注意一个坑,就是事件参数中的 msg 是 GBK 编码的,你可能需要转换到 UTF-8 才能正常处理,可以看一下 这里 的实现。

使用酷 Q 开发(任意语言)

说实话用 C++ 开发插件,还是太累了(当然如果你说你会易语言,那当我没说……),而且必要性也不是很大,你说一个 QQ 机器人要啥性能呢……

然后我写了个 CoolQ HTTP API 插件,于是就可以用其它语言来开发插件了。如果你喜欢酷 Q 的稳定以及更多的功能,又不想用易语言或 C++ 开发插件,那么你可以尝试用用这个 HTTP API 插件。

安装 HTTP API 插件

这里 下载最新的 cpk 文件放到酷 Q 的 app 文件夹,然后重启酷 Q,启用插件,然后插件会在 app\io.github.richardchien.coolqhttpapi 目录下生成一个默认的 config.cfg 配置文件,然后开了一个 HTTP 服务端监听 0.0.0.0:5700

编写实际的插件功能

启用插件之后你就可以通过请求 http://192.168.1.100:5700/send_private_msg 来调用酷 Q 的发送私聊消息的功能,其它功能也都和这个类似。

你还需要用你想用的语言开启一个 HTTP 服务端,用来接收 HTTP API 插件的消息、事件上报(POST 请求),然后把 URL 填到 config.cfg 配置文件的 post_url 配置项,比如 post_url=http://192.168.1.200:8888

然后就可以写逻辑了。

我们这里以 Python 为例(我只会 Python……)给出一个重复消息内容的 demo:

import requests
from flask import Flask
from flask import request as flask_req

app = Flask(__name__)


@app.route('/', methods=['POST'])
def index():
    data = flask_req.json or {}
    if data.get('post_type') == 'message' and data.get('message_type') == 'private':
        requests.post('http://192.168.1.100:5700/send_private_msg', json={
            'user_id': data.get('user_id'),
            'message': data.get('message')
        })
    return '', 204


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8888)

这里从收到的上报消息(JSON 格式)里取出 post_typemessage_type 判断消息类型为私聊消息,然后发送 POST 请求到 http://192.168.1.100:5700/send_private_msg,以 JSON 传入参数,其实也可以用 GET 请求通过 URL 参数传入,或者 POST 请求通过 url-encoded-form 传入。

另外,所有传递的数据全都是 UTF-8 编码。

总之通过这个插件可以直接使用其它几乎任何语言来编写插件了,要写更复杂的功能你可能还需要阅读 CoolQ HTTP API 插件文档

使用 Mojo-Webqq 开发(任意语言)

Mojo-Webqq 是用 Perl 编写的,你可以直接用 Perl 写插件,同时它也支持通过 HTTP 来上报消息事件和调用接口,因此也可以用几乎任意语言来写。

另外这个是运行在命令行的,因此相比酷 Q 更方便进行自动化部署(不过它需要扫码登录,毕竟改变不了是 SmartQQ 的事实……)。

运行 Mojo-Webqq

安装和运行 Mojo-Webqq 的方法可以看它的 文档,写的非常清晰(这也是 Mojo-Webqq 的一个很大的优点之一,另外遇到问题还可以加入它的交流群提问,作者人还是很不错的)。

一般情况下如果只是简单的跑个小机器人,Windows 上直接用它官方给的打包好的 Mojo-StrawberryPerl 就行了,修改一下它的 mojo_webqq.pl 里的 $post_api 即可,其它配置基本上不用改。使用 Docker 来运行也是一种比较建议的办法,基本上可以一条命令运行起来。

编写实际的插件功能

同样还是用 Python 为例,同样写一个重复消息内容的 demo:

import requests
from flask import Flask
from flask import request as flask_req

app = Flask(__name__)


@app.route('/', methods=['POST'])
def index():
    data = flask_req.json or {}
    if data.get('post_type') == 'receive_message' and data.get('type') == 'friend_message':
        requests.post('http://192.168.1.100:5000/openqq/send_friend_message', data={
            'uid': data.get('sender_uid'),
            'content': data.get('content')
        })
    return '', 204


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8888)

实际上这跟上面的「使用酷 Q 开发(任意语言)」标题下的代码几乎一样,基本上只有数据的字段名不太一样。

总的来说还是很简单的,要深入了解的话,可以看它的 API 文档。

最后

好了,简单的示例都给完了,就看各位老司机的发挥了。现在问题来了,我的女朋友呢?

情人节要到了,手把手教你写个 QQ 机器人给女朋友玩

写给新的一年

创建于
分类:Misc
标签:年度总结

因为需要重新换宽带套餐,今天把网关了,于是也有了时间来写这个年终总结暨新年展望。

2016

2016 年对我来说是进步最多的一年。我希望每年都是这样,最好进步幅度更大。

Android

其实回头翻看 Projects 文件夹里的东西、看这一年的日记,才发现自己居然学了好久的 Android 开发,可是还是欠缺很多,demo 的东西一大堆,却没有一个像样的完整的 app,那个所谓的清心天气也早已觉得烂到不行。勉强能算还行的项目可能只有 DBox 和 FusionCache 吧,后者其实也并没有什么技术含量。最终没能坚持学 Android,上一次写 Android 代码已经是 9 月份了,一个烂尾的 app。

就这种水平我居然还尝试投了几个简历,自然全是石沉大海。

Python

BlogA - Created: Friday, February 5, 2016 at 20:58

1 月 28 号创建了 Test,2 月 5 号创建了上面这个 BlogA 项目,这个时间开始尝试写 Python。现在回忆起来,这时候应该是,刚学了一些 Android,然后寒假就没有继续学,转而学了点 Python,之后下一学期开始后的某个时间又开始继续学 Android。

总之,刚学了几天 Python 之后写的这个 BlogA 竟然成了可能是目前为止自己写的最好的一个项目之一,也可能是维护时间最长的之一,而且自己的博客以及 Fenkipedia 也正在用它跑。之后又写了 BlogNG 和 BlogT,分别是前端、主题集,共同构成 BlogTANG 系列,你可以轻松地看出,这个命名是故意的。

后来也写了个学校学生门户网站的爬虫+重新封装的 API,当时还想着后续再重新写个前端、移动端,但是结果因为学校网站请求实在太慢,调用一个 API 要 10 秒多,就放弃了。其实部分也是因为懒,写完 API 之后,激情已经不在这上面了,其实也有其它方案获取信息,比如抓掌上校园 app 的包,反正是没有动力继续了。也许以后会重新来做吧。

Python 也尝试投了一个简历,DaoCloud,面试没过,十分糟糕,这次面试也让我知道了自己的能力还差多少。

C

C 其实,能说的很少,这玩意我觉得自己还是欠很多火候,这是一个高深的东西。上半年的大一下学期的课程设计,也算是第一次写有用的 C 程序(尽管还非常简陋)——用纯 C 模仿 OpenCV 写了一点基础的图像处理功能,只能读写特定格式的 bmp 文件,程序其实扩展性也不强,不算好代码,但姑且拿来一说。之后下半年某个时间,跟一个 MOOC 写了个所谓的 rcsh,其实非常简单,但这里拿出来说是想立个 flag,以后想把它好好写一次。

CCZU-DEV

常大开发者协会,这是一个不存在的存在……往回翻说说发现是 10 月 27 号发的招新说说,那大概是那个前几天决定要开这么一个协会的,跟张宇泽决定弄个这玩意来找找学校里面其他有趣的人,我然后临时赶了个注册页面,后来还出各种 bug,最后也并没有发现几个有趣的人,不过,也够了,这是一次奇妙的体验,另外,也许以后会继续发展呢。

XiaoKai Bot

翻 GitHub 发现 XiaoKai Bot 的 Initial commit 在 12 月 2 号,那大概是在那前几天开始写的,然后基本上能运行了才发到 GitHub 的。

这其实是用 Python 写的,但是单独放一个标题,是因为最近一直在更新它,精力投入比较多。这可能是目前为止,架构最清晰的一个程序,我自认为扩展性是非常强的。

但是没有什么人用,这其实也是我一开始就有所预期的,因为使用方式略微有点 geek 了,不是程序员可能很难对它产生兴趣。但是我也开始尝试做一些让它能识别自然语言的工作了,不过重点仍然会在高级功能上,毕竟我自己才是使用最频繁的用户。

休学

这是一个大胆的决定,当时是做了非常多的考虑才最终做了决定。当然到现在已经过去半年了,对于这件事,并没有什么深的感触了,不想多说。

活活把总结写成了简历

其实本来想写一点社会、互联网的变化,可是这些东西网上各个媒体也都有盘点吧,而且我也并没有关注太多。就写自己吧,给以后的自己看。

新的一年

我希望新的一年里,人们都不再迷惑、都能在重要的时刻做出最深思熟虑的决定。

而我自己呢,还是希望自己能精通一些东西,以及能够做出一些更完整的项目,这样于人于己都更有实际的意义。

写给新的一年

BlogA 添加 Webhook 回调的支持

创建于
分类:Misc
标签:BlogAWebhookBackend

刚刚给 BlogA 支持了 Webhook 回调,其实也就是开了一个支持 POST 的 URL 然后去执行自定义脚本而已。

因为之前把博客的内容和 BlogA 框架本身分离了,内容放在 richardchien/blog-content,然后 BlogA 跑在 docker,后台用脚本每 100 秒 pull 一次,不过这样还是有一种很不爽的感觉,于是这次直接加了 Webhook 回调,在 GitHub 添加一个 Webhook 然后在发生 push 时通知 BlogA,从而执行我的更新博客内容的脚本,这样看起来要清爽很多。

所以这篇就算水完了……主要算是测试一下效果。

BlogA 添加 Webhook 回调的支持

利用 JSONP 实现跨站请求的方法及原理

创建于
分类:Dev
标签:JSONPJavaScriptFrontendBackend跨站请求

因为之前想解决博客前后端对接时候跨站请求的问题,于是看到可以用 JSONP 来实现,研究了一番之后做一下笔记。

实现方法

要实现跨站请求是需要同时牵扯到前后两端的代码的。

后端

首先需要修改后端,这边以 Flask 为例:

from functools import wraps
from flask import Flask, request, current_app, jsonify

app = Flask(__name__)


def support_jsonp(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        callback = request.args.get('callback', False)
        if callback:
            content = str(callback) + '(' + f(*args, **kwargs).data.decode('utf-8') + ')'
            return current_app.response_class(content, mimetype='application/json')
        else:
            return f(*args, **kwargs)

    return decorated_function


@app.route('/test')
@support_jsonp
def test():
    return jsonify({'abc': 1, 'def': 2})

这边 support_jsonp 装饰器会判断是否是 JSONP 请求,如果是,则返回相应的「特殊」数据(具体在下面原理的地方详细讲),否则就按正常的请求处理。

前端

后端改完之后,前端就可以来请求了,这边先以 jQuery 的 ajax 函数为例:

$.ajax({
    url: 'http://api.example.com/test',
    dataType: 'jsonp',
    type: 'get',
    success: function(data) {
        console.log(JSON.parse(data));
    },
    error: function(XMLHttpRequest, textStatus, errorThrown) {
        console.log('failed');
    }
});

这样就可以成功请求到跨站的 JSON 了。

原理

其实原理非常简单,就是利用了 script 标签可以获取其它站点的 JavaScript 代码并执行这一特点。

前面的示例中,前端发起请求后,观察后端的日志就会发现,请求时候的地址 /test 被修改成了形如 /test?callback=jsonp1470561184396 的地址,也就是加上了一个 callback 参数,这时候去看之前修改的后端代码的 support_jsonp 装饰器就会发现,后端这里实际上先检查请求参数里有没有 callback 这个参数,如果有的话,就会通过 content = str(callback) + '(' + f(*args, **kwargs).data.decode('utf-8') + ')' 这一句来把 callbackf(*args, **kwargs) 返回的 JSON 数据拼接起来形成形如 jsonp1470561184396({"abc": 1, "def": 2}) 这样的「数据」,然后把这个「数据」返回给浏览器。这里的「数据」之所以加引号,是因为其实这里拼接出来的 content 已经不再是单纯的 JSON 数据,而是一个 JavaScript 函数调用,而这里面形如 jsonp1470561184396 的东西就是 ajax 自动给回调函数编的号。另外,这里被修改后的 /test?callback=jsonp1470561184396 这个链接实际上被放在了一个 script 标签的 src 属性里,于是:script 标签从 http://api.example.com/test?callback=jsonp1470561184396 获取到 JavaScript 代码 jsonp1470561184396({"abc": 1, "def": 2}) 然后执行,成功调用回调函数。

理解了这个之后就可以比较随心所欲的使用了,无论代码怎么写只要最终后端拼接出了相应的函数调用代码就行,你甚至可以在 HTML 里面这么写:

<script src="http://api.example.com/test?callback=console.log"></script>

参考资料

利用 JSONP 实现跨站请求的方法及原理

给博客换了新的前端和域名

创建于
分类:Misc
标签:BlogBlogNGBlogAFrontendBackend

前几天学长 ntzyz 也开始写一个博客框架,然后他是前后端分开的,后端用 Node.js,前端用 Vue.js,源码在 这里,然后我看了也心痒了,于是决定把之前的 BlogA 改一改,让它支持作为独立的后端,然后再写个前端,于是就有了 BlogA 的 API 模式和 BlogNG。

BlogA API 模式

这个其实是得益于 Flask 里面使用模板的方式,BlogA 在处理请求时是把数据全部收集到一个字典对象,然后传给 render_template 函数来渲染模板,因此我直接在配置项里面加了一个模式选项,可以选择「web-app」「api」「mixed」三种模式,然后在原来调用 render_template 的前面判断当前的模式,如果是开启了「api」模式,那就不调用 render_template,而是调用 jsonify 来把字典转换成 JSON 返回,而如果是「web-app」模式那就像以前那样返回 HTML,如果是「mixed」模式就通过 HTTP 请求头的「Accept」选项或者 URL 里面的「format」参数来确定是返回 JSON 还是 HTML,于是基本上在不修改以前的使用方式和 URL 路径的情况下,做到了完美的 API 支持。

BlogNG

然后后端弄好就弄前端了,看学长那个博客前端用 Vue.js,然后去了解了一下,感觉挺不错的,配合 vue-router 可以比较轻松地做到整站无刷新。然后页面的整体框架肯定是用 Bootstrap 了,先是用 Pingendo 拖了个大致的模板,然后去把左边的主要内容和右边的侧边栏抽成模板,这样基本的结构就出来了。因为用了 vue-router 和 vue-resource,处理页面切换很方便,直接在 BaseComponent 里面让它在访问每个 URL 的时候都直接请求相对应的 API 地址,然后具体的不同内容比如博文、自定义页面、分类和标签归档去基于 BaseComponent 分别写模板。因为其实每个页面的处理逻辑都是一样的:请求 API、渲染模板,所以这些基础性的东西都抽离在 BaseComponent 来做会清楚很多。然后这么一下整体样子就基本出来了。

但是在写侧边栏时遇到坑,因为想弄一个音乐播放器,尝试网易云音乐的外链,不好用,不仅没有完全 HTTPS,而且很多歌因为版权原因会导致播放器停住点播放没反应,于是另外找了一个播放器,但是播放器初始化显然是需要跑 JavaScript 代码的,Vue 的模板里面插入 <script> 标签是没有效果的,于是折腾了不少时间找到一个 workaround:在 Component 的 readybeforeDestroy 回调里面调用三个固定的方法 handleReady()handleLoadedAll()handleBeforeDestroy(),分别在页面切换完成、请求 API 完成、即将销毁时候调用,于是就在 index.html 里面包含一个 custom.js 在这里面去实现那三个函数就好了。

前后配合

JavaScript 里面不能直接跨站请求,但是 BlogA 的 API 模式跟普通模式的 URL 是一样的(这可能是个问题,也许要去改一下让 API 模式处理 /api/ 下的 URL),所以得在 Web 服务器的配置文件里面去把 /api/ 开头的请求反代到 BlogA 的地址。并且,因为是用 vue-router 做成了单页应用,所以还需要改一下 rewrite 规则,来把所有除 /api/ 以外的链接全部引导到 index.html 来处理。全部弄完,就大功告成了。

这里给一下 GitHub 上的项目链接:BlogABlogNG,觉得不错的话,求个 star。

新域名

全写完,一兴奋,就买了个新域名 stdrc.cc(其实一年才 16 块钱),这域名配合博客副标题 #include <stdrc.h> 简直完美哈哈(cc 是 C++ 的一种文件后缀)。

给博客换了新的前端和域名