实现MUD游戏和QQ消息互通教程[更新到2.x版]

提示:本教程针对fluffos v2019驱动的MUD。

前言

现在MUD游戏中的玩家聊天越来越少,很多游戏也都有QQ交流群,为了保持更活跃的交流氛围,打通MUD和QQ群游戏是比较好的。比如这种:

file

有很多MUD已经实现了QQ消息和MUD消息互通,本教程是针对fluffos v2019驱动的MUD的一种更好的解决方案。

QQ官方是没有提供消息接口的,要实现消息互通,我们需要使用第三方API,本教程使用的是一个非常好用工具:mirai 。这是一款开源的工具,而且提供的有HTTP和websocket接口,我们自己下载后在服务器上配置后,在MUD中通过websocket读取消息到MUD,通过http发送游戏消息到QQ。


QQ 消息服务配置

Mirai 是一个在全平台下运行,提供 QQ Android 和 TIM PC 协议支持的高效率机器人框架。我们需要通过Mirai HTTP API (console) plugin来在MUD中通信。

安装Mirai Console Loader

iTX Technologies Mirai Console Loader(下简称MCL)采用模块化设计,包含以下几个基础模块:

  • Script 脚本执行模块,用于加载和执行脚本,MCL的主要功能均由脚本实现。脚本执行有各个阶段,详见注释。
  • Config 配置文件模块,用于配置的持久化。
  • Downloader 下载器模块,用于下载文件,并实时返回进度。
  • Logger 日志模块,用于向控制台输出日志。

一键安装

iTXTech MCL Installer 能在所有操作系统上一键安装 iTXTech MCL

手动安装

  1. 安装 Java 运行时(版本必须 >= 11)
  2. Releases 下载最新版本的MCL
  3. 解压到某处
  4. 在命令行中启动MCL
    • Windows: 运行mcl.cmd
    • linuxmac: 运行./mcl

安装mirai-api-http

注意:mirai-api-http分1.x版本和2.x版本,这里分别做说明。

使用 Mirai Console Loader 安装mirai-api-http

  • MCL 支持自动更新插件,支持设置插件更新频道等功能

./mcl --update-package net.mamoe:mirai-api-http --channel stable --type plugin

目前此方式只能安装1.x版本,暂不推荐使用这种方式。

手动安装mirai-api-http

  1. 运行 Mirai Console 生成plugins文件夹
  2. Releases 下载jar并将其放入plugins文件夹中

启动QQ API服务

  1. 编辑config/MiraiApiHttp/setting.yml配置文件 (没有则自行创建)
  2. 需要注意的是mirai-api-http的1.x版本和2.x版本配置不同,本教程只提供2.x版

2.x版本setting.yml模板

## 配置文件中的值,全为默认值

## 启用的 adapter, 内置有 http, ws, reverse-ws, webhook
adapters:
  - http
  - ws

## 是否开启认证流程, 若为 true 则建立连接时需要验证 verifyKey
## 建议公网连接时开启
enableVerify: true
verifyKey: 1234567890

## 开启一些调式信息
debug: false

## 是否开启单 session 模式, 若为 true,则自动创建 session 绑定 console 中登录的 bot
## 开启后,接口中任何 sessionKey 不需要传递参数
## 若 console 中有多个 bot 登录,则行为未定义
## 确保 console 中只有一个 bot 登陆时启用
singleMode: false

## 历史消息的缓存大小
## 同时,也是 http adapter 的消息队列容量
cacheSize: 4096

## adapter 的单独配置,键名与 adapters 项配置相同
adapterSettings:
  ## 详情看 http adapter 使用说明 配置
  http:
    host: localhost
    port: 8080
    cors: [*]

  ## 详情看 websocket adapter 使用说明 配置
  ws:
    host: localhost
    port: 8080
    reservedSyncId: -1

启动 Mirai Console后,使用login指令登录你的消息同步QQ即可。需要注意的是,现在QQ登录安全验证比较严,解决方法如下:

  1. 下载MiraiAndroid
  2. 下载之后安装到你的手机上,并且完成登录;登录方法在主屏幕右上角的自动登录里,登录过程中需要验证请在通知栏内点击通知完成验证
  3. 登录成功后到左边菜单内找到高级功能,选择你得账号之后导出设备文件(device.json)发送到电脑,并覆盖电脑版文件,再次login QQ 密码即可成功登陆

获取QQ消息到MUD

先看看API文档

从文档可知除了交易相关功能不支持外,其它所有QQ和群消息都可以支持,我们可以通过API实现所有你想实现的功能。

我们MUD中只需要实现QQ群的消息自动同步到MUD和在MUD中发消息同步到QQ群。群消息同步到MUD这里直接使用API的websocket模式,简单好实现。

这里功能实现主要是使用了socket,如果你对socket编程不熟悉,请先看这里补充知识点:socket 介绍及游戏开发实战

先上示例,2.x版本示例代码:

/** QQ_D QQ群消息转发机器人
 *
 * 机器人服务端使用mirai开源框架,需要自己配置QQ API服务器
 * 服务器配置文档:https://github.com/project-mirai/mirai-api-http
 *
 */
#include <ansi.h>

#define STREAM 1
#define EESUCCESS 1
// QQ消息API服务器配置
nosave string host = "mud.ren";
nosave string addr = "118.190.104.241 8006";
nosave string mirai_verifyKey = "QQ7300637-6427";
nosave string mirai_qq = "21791131";
// 游戏消息转发到指定的QQ群
nosave string group = "285533476";
nosave mapping status = ([]);
nosave string session;

private void bind();
private void websocket();

private void http(int fd)
{
    socket_write(fd, status[fd]["http"]);
}

private void receive_verify(int fd, mixed result)
{
    int n = strsrch(result, "{");
    // debug_message(result);
    if (n > 0)
    {
        mixed json;
        json = json_decode(trim(result[n..]));
        // debug_message(sprintf("%O", json));
        session = json["session"];
    }

    if (stringp(session))
    {
        socket_close(fd);
        debug_message("QQ_D 开始认证!");
        bind();
    }
}

private void receive_bind(int fd, mixed result)
{
    int n = strsrch(result, "{");
    // debug_message(result);
    if (n > 0)
    {
        mixed json;
        json = json_decode(trim(result[n..]));
        // debug_message(sprintf("%O", json));
        if (!json["code"])
        {
            socket_close(fd);
            debug_message("QQ_D 认证完成!");
            websocket();
        }
    }
}
private void receive_msg(int fd, mixed result)
{
    // debug_message(sprintf("QQ_D receive_msg(fd = %d, result = %O)", fd, result));
    socket_close(fd);
}

private void receive_data(int fd, mixed result)
{
    string res;
    int n = strsrch(result, "{");
    // debug_message("n = " + n + "\n" + result);
    if (n == 4 && strsrch(result, "}}}") > 0)
    {
        mixed json;
        mapping sender, messageChain;
        string type;
        res = trim(result[n..]);
        json = json_decode(res);
        debug_message(sprintf("%O", json));
        json = json["data"];
        sender = json["sender"];
        type = json["type"];
        // 这里只做最傻瓜的处理
        messageChain = json["messageChain"][1];
        if (type == "GroupMessage")
        {
            string msg ="[其它类型消息]";
            if (messageChain["type"] == "Plain")
            {
                msg = messageChain["text"];
            }
            else if (messageChain["type"] == "Face")
            {
                msg = "[表情]" + messageChain["name"];
            }

            message("chat", HIG"【QQ群】"HIC + sender["memberName"] + "@" + sender["group"]["name"] + ":" + msg, users());
        }
    }
}

private void receive_callback(int fd, mixed result)
{
    // debug_message(sprintf("QQ_D receive_callback(fd = %d, result = %O)", fd, result));
}

private void socket_shutdown(int fd)
{
    // debug_message(sprintf("QQ_D socket_shutdown(fd = %d)", fd));
    socket_close(fd);
}

/* 游戏消息转发QQ群调用此方法 */
void msg(string msg)
{
    int fd;
    int ret;
    string path = "/sendGroupMessage";
    // string body = "{\"sessionKey\":\"" + session + "\",\"target\":" + group + ",\"messageChain\":[{\"type\":\"Plain\",\"text\":\"" + msg + "\"}]}";
    // 美化格式,不用转义
    string body = @RAW
{
    "sessionKey": "%^session%^",
    "target": %^group%^,
    "messageChain": [
        {
            "type": "Plain",
            "text": "%^msg%^"
        }
    ]
}
RAW;
    body = terminal_colour(body, ([
        "session":session,
        "group":group,
        "msg":msg,
    ]));

    fd = socket_create(STREAM, "receive_callback", "socket_shutdown");
    status[fd] = ([]);
    status[fd]["http"] = "POST " + path + " HTTP/1.1\nHost: " + host + "\nContent-Type: application/json\nContent-Length: " + sizeof(string_encode(body, "utf-8")) + "\r\n\r\n" + body;
    debug_message(status[fd]["http"]);
    ret = socket_connect(fd, addr, "receive_msg", "http");
    if (ret != EESUCCESS)
    {
        debug_message("消息服务器连接失败。\n");
        socket_close(fd);
    }
}
// 连接websocket
private void websocket()
{
    int fd;
    int ret;
    string path = "/all?verifyKey=" + mirai_verifyKey + "&sessionKey=" + session;

    fd = socket_create(STREAM, "receive_callback", "socket_shutdown");
    status[fd] = ([]);
    status[fd]["http"] = "GET " + path + " HTTP/1.1\nHost: " + host + "\nUpgrade: websocket\nConnection: Upgrade\r\n\r\n";

    ret = socket_connect(fd, addr, "receive_data", "http");
    if (ret != EESUCCESS)
    {
        debug_message("WebSocket服务器连接失败。\n");
        socket_close(fd);
    }
}
// 绑定session到QQ
private void bind()
{
    int fd;
    int ret;
    string path = "/bind";
    string body = "{\"sessionKey\":\"" + session + "\",\"qq\":\"" + mirai_qq + "\"}";

    fd = socket_create(STREAM, "receive_callback", "socket_shutdown");
    status[fd] = ([]);
    status[fd]["http"] = "POST " + path + " HTTP/1.1\nHost: " + host + "\nContent-Type: application/json\nContent-Length: " + strlen(body) + "\r\n\r\n" + body;
    // debug_message(status[fd]["http"]);
    ret = socket_connect(fd, addr, "receive_bind", "http");
    if (ret != EESUCCESS)
    {
        debug_message("激活服务器连接失败。\n");
        socket_close(fd);
    }
}
// 认证获取session
private void verify()
{
    int fd;
    int ret;
    string path = "/verify";
    string body = "{\"verifyKey\":\"" + mirai_verifyKey + "\"}";

    fd = socket_create(STREAM, "receive_callback", "socket_shutdown");
    status[fd] = ([]);
    status[fd]["http"] = "POST " + path + " HTTP/1.1\nHost: " + host + "\nContent-Type: application/json\nContent-Length: " + strlen(body) + "\r\n\r\n" + body;
    // debug_message(status[fd]["http"]);
    ret = socket_connect(fd, addr, "receive_verify", "http");
    if (ret != EESUCCESS)
    {
        debug_message("认证服务器连接失败。\n");
        socket_close(fd);
    }
}

void create()
{
    verify();
}

以上代码实现了自动验证,并通过websocket获取QQ群的消息并同步到MUD。2.x版可以取消验证,并直接开启singleMode,代码会减少很多,不过教程保持和1.x版本一致要求验证。

这里示例的API服务器是我自己配置的,如果你要在你的LIB中使用,需要自己配置自己的服务器,并登录你的QQ,QQ推荐新注册一个机器人消息专用的,可以减少不必要的代码判断。

代码中的private void receive_data(int fd, mixed result)方法负责接收QQ消息并发送到MUD中,这个方法可以根据自己的需求改造。

代码中void msg(string msg)方法负责转发MUD消息到指定的QQ群,如果你要把MUD消息同步到QQ群,在MUD的CHANNEL_D中调用msg(string msg)方法即可。

mudcore框架调用

以上示例代码是完全从零实现所有http通信,代码比较复杂,如果你的项目中集成了mudcore框架,可以用很少的代码实现自己的需求。需要注意的是,框架对代码做了优化,和以上示例并不一至,主要是msg()方法为接受QQ消息,且可根据自己的需求覆盖,而发送消息改为send()方法。

新建一个/adm/daemons/qq_d.c守护进程,代码示例如下:

/** QQ_D QQ群消息转发机器人
 *
 * 机器人服务端使用mirai开源框架,需要自己配置QQ API服务器
 * 服务器配置文档:https://project-mirai.github.io/mirai-api-http/
 *
 */
inherit CORE_DIR "system/daemons/http/qq_d";

#include <ansi.h>

protected void msg(mapping data)
{
    mapping sender, messageChain;
    string type;

    if (!data)
    {
        return;
    }

    sender = data["sender"];
    type = data["type"];

    if (type == "GroupMessage")
    {
        string msg = "";
        foreach(messageChain in data["messageChain"])
        {
            if (messageChain["type"] == "Source")
            {
                continue;
            }
            else if (messageChain["type"] == "Plain")
            {
                msg += messageChain["text"];
            }
            else if (messageChain["type"] == "Face")
            {
                msg += "[" + messageChain["name"] + "]";
            }
            else
            {
                msg += "[" + messageChain["type"] + " 类型消息]";
            }
        }
        // 发送消息到MUD
        message("QQ", HIG "【QQ群】" HIC + sender["memberName"] + "@" + sender["group"]["name"] + ":" + msg + NOR "\n", users());
        // 转发消息到其它MUD
        if (sender["group"]["id"] == Group)
            "/adm/daemons/network/services/gchannel.c"->send_msg("ic", sender["group"]["name"], sender["memberName"], msg);
    }
}

void create()
{
    if (env("MIRAI_HOST"))
    {
        // Debug = 1;
        verify();
    }
}

通过inherit CORE_DIR "system/daemons/http/qq_d";继承框架提供的QQ_D守护进程,在create()方法中调用verify()方法完成认证初始化,需要在/data/.env环境配置文件中增加以下配置:

# QQ群机器人mirai配置(请修改为自己的配置)
MIRAI_HOST : http://mud.ren:8006
MIRAI_KEY : QQ7300637-****
MIRAI_QQ : 21791131
MIRAI_GROUP : 9783836

通过重写protected void msg(mapping data)自己处理收到的QQ消息。

通过调用/adm/daemons/qq_d->send("消息")发送消息到QQ群,send()方法可以接受第2个参数发消息到非.env配置的群,函数原型为varargs void send(string msg, int qun)


答记者问

  1. 不用域名,只用IP怎么配置?

如果QQAPI服务和MUD是在同一台服务器上,直接把配置文件和代码中的host配置为127.0.0.1,代码中的addr改为127.0.0.1 端口

京ICP备13031296号-4