2022-08-01 13:59:44
其实很早之前就有接触过qq机器人,不过那个时候,我只会使用别人搭建起来的程序客户端,并且搜索别人做好的插件来实现想要的效果。
然而,个人肯定想要更加自由的管理qq机器人,那么就需要更接近核心组件的去了解如何自定义的发送想要的内容,比如发送自己服务器群的状态信息到常用qq、定时发送每天的百度热搜来查看热点新闻、发送自己b站的各项数据、亦或者是结合图灵等和群友的激情互动\( ̄︶ ̄*\))。
凭借着个人的主观能动性,找到了一个符合我预期的核心,go-cqhttp 是基于onebot标准原生实现,而且支持反向HTTP post,这样我们只需要将数据转移到我们自己的接口上处理再返回即可实现高自由度的自定义qq机器人制作了。
前往 go-cqhttp ,可以看到有详细的文档介绍以及下载的各个服务器版本。找到自己服务器对应的版本下载即可。
我这里就以centos服务器配置方式为例,进行如下的介绍:
0. 解压安装rmp包到系统
rpm -ivh go-cqhttp.rpm
1. 在自己用户下面新建一个目录,用来专门管理该机器人功能的各项资源。
2. 进入该目录(cd go-cqhttp),并且运行 go-cqhttp 指令,之后会有如下提示,并且当前目录下出现了config.yml的配置文件
[WARNING]: 尝试加载配置文件 config.yml 失败: 文件不存在
[INFO]: 默认配置文件已生成,请编辑 config.yml 后重启程序.
3. 我们使用 vim config.yml (或者自己习惯的linux编辑程序)来编辑,目前我们只需要编辑account.uin为自己机器人的qq号,密码如果不设置会启用扫码登录的方式。默认会开启5700端口,也可以阅读文档进行相应的设置。
4. 再次运行go-cqhttp指令,此时会出现登录成功并且让你认证(二维码或者滚动条或者手机号验证码验证),认证成功后,当前目录下会多出来多个相关的文件夹。之后我们可以ctrl+c终止该程序运行了。
这个部分其文档中并没有提到,是自己摸索出来的,可能部分描述不准确,不过最后测试是有效的。基本流程如下
发送消息 ===> cqhttp接受到消息 ===> 包装成json串 ===> 自己编写的服务处理 ===> 带有reply的json串或者null ===> cqhttp将json串发送给目标群或用户
包装成的json串会有各种情况,其中常见的有反向服务(自己的处理服务)心跳检测、以及包含message的消息json串(如下)
{
"post_type":"message",
"message_type":"group",
"time":1659101824,"self_id":3350214881,"sub_type":"normal",
"sender":{"age":0,"area":"","card":"","level":"","nickname":"(*˘︶˘*).。.:*♡","role":"owner","sex":"unknown","title":"","user_id":1981669259},
"group_id":860092799,
"message":"1","message_seq":52,"user_id":1981669259,
"message_id":680371521,"anonymous":null,"font":0,"raw_message":"1"
}
心跳检测不会有message这个key,所以为了剔除心跳检测,我们只需要看有没有message即可知道是否为消息了。
对于收到的消息我通过摸索总结出来了三种处理方式:
其一:通过如下json串来返回消息[适合被动应答类的消息]
{
"reply": "the msg which want to be send back"
}
其二,通过cqhttp提供的api来处理消息(api文档)[适合主动发送类的消息]
例如发送一个私聊信息,我们可以通过发送如下请求来实现,这样就会发现成功向指定的用户发送了消息
http://127.0.0.1:5700/send_private_msg?user_id=1981669259&message=test
其三,通过返回null或者不符合【其一】格式的串处理 [适合无需反馈,只做处理类的消息]
例如我们想要通过私聊更改服务器的一些配置等等(当然也可以反馈操作是否成功啦)
我这里处理业务采用的是springboot,老实说,不是非常合适,奈何专业对口,就用了。
首先推荐使用端口号5701,这样方便了解业务是qq机器人的,一眼便知。然后我们需要一个controller类,用来接受由go-cqhttp发来的所有json串并且处理调用指定的业务逻辑:
@RestController
public class TestController {
@Resource
public TestService service;
@RequestMapping("/test")
public String test(@RequestBody MessageEntity messageEntity) {
String message = messageEntity.getMessage();
if (message == null){ return null; }
// Func中,我定义了若干个字符串常量,代表各个不同的业务,只要发送了相同的常量,那么就会调用相应的业务。
// 如果没有匹配,默认为开启聊天功能,聊天可以接入图灵机器人,或者编写自己的AI
switch (message){
case Func.MENU:return service.menu();
case Func.BILI_VIDEO_DATA:return service.getBiliVideoData();
case Func.ACG_PIC:return service.randomAcgPic();
case Func.FLATTERER_DIARY:return service.getFlattererDiary();
default:return service.chat(messageEntity);
}
}
}
在service中,为了方便编写reply串,我写了一个函数:
private String reply(String src){ return "{\"reply\":\""+src+"\"}"; }
如果调用了菜单业务,由于菜单项是绑定Func的属性的,为了可扩展性和解耦性提高,我的menu获取采用的是java的反射机制,而且为了减少重复反射性能降低,使用tmpOfMenu变量用来存储第一次反射的结果,后续直接读取即可:
@Override
public String menu() {
if (tmpOfMenu!=null) return reply(tmpOfMenu);
Class<Func> clazz = Func.class;
StringBuilder sb = new StringBuilder();
try {
Object obj = clazz.newInstance();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
sb.append(field.get(obj)).append("\\n");
//注意,reply方式,如果消息要换行,用\\n来转义!
}
tmpOfMenu = sb.toString();
return reply(tmpOfMenu);
} catch (InstantiationException | IllegalAccessException e) {
System.out.println(e.getMessage());
return null;
}
}
private String tmpOfMenu;
当然还是利用的springboot的@sheduled来实现的,其它语言如果没有的话,可能要自己写一个定时器进行到点发送。
我这里实现的是每天早八发送百度热搜到群列表(向多个群发送)。
@Component
public class MySchedule {
@Resource
public RestTemplate restTemplate;
@Resource
private QChatProperties properties;
@Scheduled(cron = "0 0 8 * * ?")
public void dailyHotNews(){
System.out.println("[schedule] daily_hot_news running ...");
String url = "http://api.tianapi.com/nethot/index?key={APIKEY}";
String API_KEY = "xxxxxxx";
String str = restTemplate.getForObject(url, String.class, API_KEY);
try {
JSONObject jsonObject = new JSONObject(str);
int code = jsonObject.getInt("code");
if (code == 200){
StringBuilder sb = new StringBuilder();
JSONArray newslist = jsonObject.getJSONArray("newslist");
sb.append("今日百度热搜").append("\n==============\n");
for (int i = 0; i < newslist.length(); i++) {
JSONObject obj = newslist.getJSONObject(i);
sb.append(i).append(". ").append(obj.get("keyword")).append("\n");
}
for (String s : properties.getGroup()) {
String urlOfSend = "http://{server}:5700/send_group_msg?group_id={gid}&message={msg}";
Map<String,String> map = new HashMap<>();
map.put("server", properties.getServer());
map.put("gid", s);
map.put("msg",sb.toString());
restTemplate.getForObject(urlOfSend,String.class,map);
}
}
} catch (JSONException e) {
System.out.println(e.getMessage());
}
}
}
上面我们发的消息都是简单的文本消息,但是和qq支持的复杂消息相差甚远,其实只需要加入CQ码即可实现复杂的逻辑实现,官方文档。
非常的简单,我这里也只举一个简单的例子(如果被@到,并且被骂了sb,那么就返回 【@那个人 你才是人 [糊脸.emoji]】):
@Override
public String chat(MessageEntity entity) {
String msg = entity.getMessage();
if (msg.contains("谁最帅")){
return reply("♥当然是小戴啦!♥");
}
if (isAt(entity)&&msg.contains("傻逼")){
return reply("[CQ:at,qq=" + entity.getUser_id() + "] 你才是呢[CQ:face,id=215]");
}
return null;
}
private boolean isAt(MessageEntity entity){
return entity.getMessage().contains("[CQ:at,qq=" + ConfigConst.ROBOT_QQ + "]");
}
等我们业务都写完后,可以将这个服务打成jar包,在服务端进行部署运行,那么这个服务就一直在监听了。我们只需要修改cqhttp的config.yml配置即可
servers[0].http.post[0].url = 'http://127.0.0.1:5701/test'
再次启动go-cqhttp即可启动成功,发送消息,也将成功实现!
当然也许还有更多的隐藏东西没有研究透,不过大体上已经可以满足,通过这个机器人,无论什么需求,只要自己的业务能力够硬,都能做出来,(●ˇ∀ˇ●)。
当然了为了避免封号,最好将机器人账号等级升高一点,并且不要发送违法犯罪的东西哦!