目录

1 文章简介

2 项目介绍

3 qq机器人的登录部分

3.1 模块的调用

3.2 登录配置文件

3.3 登录部分

4. 普通非指令功能

4.1 自动复读

4.2 自助禁言

4.3 来点颜色

4.4 回复功能

5. 指令功能

5.1 删除图片

 5.2 禁言

5.3 解除禁言

5.4 查看帮助

5.5 群白名单

5.6 管理员

5.7 回复


 

1 文章简介

ociq是基于node.js的一个模块,内含绝大多数qq的api,用户可以很简单的通过调用模块并调用里面的方法来使用qq的api,自定义自己的qq机器人

我自己从从未接触node.js到写完这个简单的机器人,是从21年11月16日开始,一直写到21年11月22日暂时告一段落。如今想对当时边学习边写机器人的这1300行代码进行分析,回顾一下当初学习到的东西,做成笔记避免遗忘,也能方便以后回顾,所以写了这篇文章。

2 项目介绍

  • 控制指令

    • 删除图片

      • 描述:从数据库中删除指定的图片 id 对应的记录

      • 使用方法:@bot 删除图片 [pid]

      • 创建日期:2021-11-16

      • 需要权限:admin

    • 禁言

      • 描述:禁言指定用户

      • 使用方法:@bot 禁言 @target [时间(分钟)]

      • 创建日期:2021-11-17

      • 需要权限:admin

    • 解除禁言

      • 描述:解除指定用户的禁言

      • 使用方法:@bot 解除禁言 @target

      • 创建日期:2021-11-17

      • 需要权限:admin

    • 查看帮助

      • 描述:查看指定的可用的代码,command为控制指令,interaction为互动指令,不填则表示两种都显示

      • 使用方法:@bot 查看帮助[type? : command | interaction]

      • 创建日期:2021-11-18

      • 需要权限:customer

    • 群白名单

      • 描述:-ls为list列出全部 -a 为 add 增加,后跟 args 内容为 [gid] 新群号 -rm 为 remove 删除,后跟 agrs 内容为 [gid] 删除的群号 -update 为修改,后跟 args 内容为 [old_gid new_gid ]修改 old_gid 为新的 new_gid

      • 使用方法:@bot 群白名单 [option] [args?]

      • 创建日期:2021-11-19

      • 需要权限:admin

    • 管理员

      • 描述:对管理员名单进行增删查

        -ls为list列出全部 ​ -a 为 add 增加,将target加为管理 ​ -rm 为 remove 删除,将target的管理删除

      • 使用方法:@bot 管理员 [-ls | -a | -rm] @target?

      • 创建日期:2021-11-19

    • 回复

      • 描述:对消息回复功能做出增删查

        -find查看msg1对应的回复信息

        -a添加msg1的回复信息为msg2,msg2可以为图片、表情、文字

        -rm删除msg1对应回复信息为msg2的记录,用户仅可删除自己创建的,管理员可以删除指定的

      • 使用方法:@bot 回复 [option : -find | -a | -rm] [msg1] [msg2?]

      • 创建日期:2021-11-19

  • 交互指令

    • 来点颜色

      • 描述:随机发送一个图片

      • 使用方法:来点颜色

      • 创建日期:2021-11-15

      • 需要权限:customer

    • 自助禁言

      • 描述:禁言自己并指定禁言的时间

      • 使用方法:自助禁言 [时间(分钟)]

      • 创建日期:2021-11-17

      • 需要权限:customer

  • 其他功能:

    • 自动复读

      • 当有人复读的时候自动进行复读+1

    • 对线(已移除)

      • 当在黑名单的用户输入了 “?” 或者 “啊对对对” 的时候,会重复那个人所说的话

    • 获取命令的使用方法

      • 在想要知道使用方法的命令后面加入-h参数,或者直接使用help命令查看所有命令的使用方法

    • 自动回复消息

      • 允许对某一个文本消息指定一个回复内容,在创建记录之后,下次当bot收到这个文本消息时,会自动回复设置好的回复内容,回复内容仅支持图片、文字、表情,指定被回复的文本消息仅支持文本

  • 日志

    • 2021年11月19日

      • 修改 help 命令为 查看帮助

      • 将所有的名单或者列表类型变量从代码层面移动到数据库

      • 修改了禁言和解除禁言加入 -h 参数的时候不会显示命令用法的 bug

      • 新增群白名单指令,可以增删查改允许bot复读的群

      • 新增管理员指令,可以增删查bot的管理员列表

      • 新增消息回复功能,bot会根据预设的消息,回复对应的预设消息,当一个触发消息对应多个回复消息的时候,随机选一个回复。触发消息仅支持文本,回复消息支持文本、图片、表情

      • 新增消息回复指令,允许对消息回复功能的预设记录进行增删查,管理员允许增删查的全部操作,无障碍。而普通用户只允许增加,删除只允许删除自己创建的,不允许查询(可能日后会改成允许查询自己创建的,看使用效果而定)

    • 2021年11月20日

      • 修改回复功能创建回复时无法识别图片和表情的bug

      • 优化了回复功能的查找指令返回的内容的可读性

      • 优化代码结构,减少无用代码

      • 允许普通用户使用回复的find指令查找指定的回复内容,但只能查找到自己创建的

    • 2021年11月21日

      • 修改了群白名单指令的ls选项不会输出信息的bug

      • 修改了删除图片时如果图片pid不存在的时候,依旧会显示删除成功的bug

    • 2021年11月22日

      • 修复了回复功能删除image表情时发生错误导致无法继续删除的问题

      • 修复了连接数据库时如果数据库返回错误会导致代码直接终止的问题

      • 优化了涉及数据库的操作时,数据库发生错误的时候的返回信息

3 qq机器人的登录部分

3.1 模块的调用

// 库的调用,oicq 为 qq 的 api 库,mysql 为 js 连接 mysql 的库,string-format 为字符串的 format 库
// string-format 库的存在意义已经被反单引号所替代,可以全部将 format 替换为反单引号表达
const bot = require("oicq").createClient(account, MyConf)
var mysql = require('mysql');
var format = require("string-format");

3.2 登录配置文件

let MyConf = {
    /** 日志等级,默认info (往屏幕打印日志会降低性能,若消息量巨大建议修改此参数或重定向) */
    log_level: "info",
    /** 1:安卓手机(默认) 2:aPad 3:安卓手表 4:MacOS 5:iPad */
    platform: 5
}

platform 是比较重要的设置项,因为设置 iPad 登录的时候,当前账号可以同时登陆手机和电脑端 qq 而不受影响

3.3 登录部分

// 登录监听
bot.on("system.login.qrcode", function(e) {
        this.logger.mark("扫码后按Enter完成登录") //通过扫码二维码登录
        process.stdin.once("data", () => {
            this.login()
        })
    })
    .on("system.login.error", function(e) {
        if (e.code < 0)
            this.login()
    })
    .login()

exports.bot = bot

对 bot 对象绑定一个监听,内容是监听 qr 码登录,通过调用 api 进行登录。

4. 普通非指令功能

指令功能和非指令功能的区别在于,指令功能需要 @bot 并在后面跟指令内容,才能使用的功能;而非指令内功能是指只需要直接通过文字消息触发,而不需要特意 @bot 就能使用的功能。

4.1 自动复读

自动复读通过对当前接收到的消息和上个接收到的消息是否重复来进行判断,如果重复,则机器人自动发送一次重复消息,也就是复读一次。

通过绑定一个消息的监听,并设置允许复读的群的群号。每次接收消息的时候进行比对,如果是可以复读的群,而且复读的消息不是机器人刚刚复读过的(避免重复触发),并且将要复读的消息不是被列入不可复读的消息内的部分,则进行复读。

function is_same(msg1, msg2) {
    // 判断两个 MessageElem 是否相同,用于判断复读条件
    var flag = true;
    for (let index = 0; index < Math.max(msg1.length, msg2.length); index++) {
        // 以两个 MessageElem 中长度最大的长度作为遍历长度,当某个长度比另一个长度小的时候会取到 msg[index] 为 undefined
        if (msg1[index] != undefined && msg2[index] != undefined && msg1[index]['type'] === msg2[index]['type']) {
            // 使用短路判断避免取到 undefined['type'] 而导致程序报错,也可以使用 undefined?.type 避免这种情况,但是此处没有必要
            switch (msg1[index]['type']) {
                case 'text':
                    flag = msg1[index]['text'] === msg2[index]['text'] ? true : false;
                    break;
                case 'image':
                    flag = msg1[index]['file'] === msg2[index]['file'] ? true : false;
                    break;
                case 'json':
                    flag = msg1[index]['data'] === msg2[index]['data'] ? true : false;
                    break;
                case 'face':
                    flag = msg1[index]['id'] === msg2[index]['id'] ? true : false;
                    break;
                default:
                    flag = false;
                    break;
            }
        } else {
            flag = false;
            return flag;
        }
        if (flag === false) {
            return flag;
        }
    }
    return flag;
}

首先创建了一个函数 is_same,专门用来判断两个 MessageElem 消息对象是否相同。函数中定义了一个 flag 变量表示是否相同的判断结果,是一个布尔值。

用 for 循环遍历,并用 switch 对每个元素分别判断的原因是因为,qq 的消息中可能不只包含一种消息,比如 “哈哈(后面跟一个图片)” 的情况下。MessageElem 的内容是 :

[{
    type: 'text',
    text: '哈哈'
},
{
    type: 'image',
    file: "https://i.pximg.net/img-original/img/2021/11/13/00/00/03/94091879_p0.png",
    url: "https://i.pximg.net/img-original/img/2021/11/13/00/00/03/94091879_p0.png",
    asface: false
}]

所以需要先遍历,才能知道这个消息里各个部分的细节内容,才能比对是否相同

其中 MessageElem 和部分常用元素的定义如下:

export type MessageElem = TextElem | AtElem | FaceElem | BfaceElem | MfaceElem |
    ImgPttElem | LocationElem | MusicElem | ShareElem | JsonElem | XmlElem |
    AnonymousElem | ReplyElem | NodeElem | ShakeElem | PokeElem | FileElem | VideoElem | MiraiElem;

export interface TextElem {
    type: "text",
    text: string
}

export interface AtElem {
    type: "at",
    qq: number | "all",
    text?: string, //at失败时显示的文本
    dummy?: boolean, //假at
}

export interface FaceElem {
    type: "face" | "sface",
    id: number,
    text?: string
}

export interface ImgPttElem {
    type: "image" | "flash" | "record",
    file: MediaFile,
    cache?: boolean, //file为网络资源时是否使用缓存
    timeout?: number, //file为网络资源时请求超时时间
    headers?: OutgoingHttpHeaders, //file为网络资源时请求头
    url?: string, //仅接收
    type?: "image" | "flash" | "face"
}

export interface JsonElem {
    type: "json",
    data: any, // a json string or a json object
    text?: string
}


接下来我们剖析一下监听主体部分

bot.on('message', function(e) {
    // 复读的判断监听
    if (e.message_type === 'group' && repeater_group_id.includes(e.group_id)) {
        // 当接收到的消息为群组消息,并且对应群组在群白名单内进行消息复读的判断
        if (group_whitelist[e.group_id]['is_repeating'] === false) {
            group_whitelist[e.group_id]['first_message'] = e.message;
            if (e.message != group_whitelist[e.group_id]['unable_message']) {
                group_whitelist[e.group_id]['is_repeating'] = true;
            }
        } else if (group_whitelist[e.group_id]['is_repeating'] === true && !ban_word.includes(e.raw_message) && !reply_msg.includes(e.raw_message)) {
            bot.emit('repeater', e);
        }
    }
})
  • 如果当消息类型是群消息的时候,并且群是允许复读的群,则开始进行复读的判断。
  • 如果可复读的群里,对应群号的 is_repeating 是否,也就是目前并不处于复读状态(避免单次重复触发复读),开始记录当前消息为该群的消息的 first_message。如果处于复读状态,并且复读内容不是屏蔽词,并且复读的内容不是回复功能内的触发词,则触发复读事件 bot.emit('repeater', e)。
  • 当记录了当前消息之后,判断如果当前消息不是该群的不可复读消息的话,将是否正在处于复读状态的标志 is_repeating 变成 true。

然后是我们绑定的复读事件:

bot.on('repeater', function(e) {
    // 自动复读功能的监听,监听是否满足自动复读条件,当满足时触发该事件
    if (is_same(e.message, group_whitelist[e.group_id]['first_message']) && !(is_same(e.message, group_whitelist[e.group_id]['unable_message']))) {
        e.reply(e.message);
        group_whitelist[e.group_id]['unable_message'] = group_whitelist[e.group_id]['first_message'];
        group_whitelist[e.group_id]['first_message'] = '';
    } else {
        group_whitelist[e.group_id]['first_message'] = e.message;
    }
})
  • 如果消息和该群上次消息相同,并且不是不可复读的消息的时候,进行消息的复读“e.reply(e.message)”,将这次复读的消息改为不可复读的消息,并且将上一次接收到的消息设置为空(初始化)。否则设置上一个消息为本次接收到的消息。

自此就实现了我们的复读功能。

4.2 自助禁言

自助禁言功能的使用格式是:自助禁言 [时间(分钟)],需要的权限等级是 customer,效果是直接禁言发送自助禁言的人指定的时间,但是由于使用了禁言功能,必须拥有群的管理权限。

下面是指令分割和判断的伪代码:

msg_command = e.raw_message.split(' ');
switch (msg_command[0]) {
    case '自助禁言':
        switch (msg_command[1]) {
            case undefined:
                当 msg_command[1] 为空时,反馈使用方式错误
                break;
            case '-h':
                当选项为 -h 的时候,反馈命令使用格式
                break;
            default:
                进行自助禁言的判断和实现
                break;

使用方法错误的时候,反馈使用方式错误的提示:

case undefined:
    e.reply("不是这样用滴~ 使用:自助禁言 -h,查看使用方法");
    break;

当选项为 '-h' 的时候,反馈该指令的提示信息:

case '-h':
    e.reply(format("使用方法:{}", interaction_command['自助禁言']['help']));
    break;

自助禁言的主体部分的代码:

// 获取当前机器人在该群的成员信息
bot.getGroupMemberInfo(e.group_id, account).then(function(info) {
    // 当成员信息不是普通成员的时候 
    if (info['role'] != 'member') {
        // 获取使用自助禁言功能的用户的成员信息
        bot.getGroupMemberInfo(e.group_id, e.user_id).then(function(target) {
            // 如果禁言时间是数字
            if (/^ *\d+(\.\d+)?$/.test(msg_command[1])) {
                // 进行禁言对象的成员身份判断,是管理员群主则反馈失败信息,否则执行禁言主体
                switch (target['role']) {
                    case 'owner':    // 群主
                        e.reply("?!我可不敢禁言群主 !!!∑(゚Д゚ノ)ノ");
                        break;
                    case 'admin':    // 管理员
                        e.reply(format("管理员之间可没法相互禁言 (눈‸눈)"));
                        break;
                    default:
                        // 如果时间小于 1
                        if (parseInt(msg_command[1]) < 1) {
                            e.reply("自助禁言单次下限时间为1分钟 (。-ω-)zzz")
                        } else {
                            // 如果时间不大于 720
                            if (parseInt(msg_command[1]) <= 720) {
                                // 禁言函数
                                bot.setGroupBan(e.group_id, e.user_id, parseInt(msg_command[1]) * 60);
                                e.reply(format("成功领取 {} 分钟禁言", msg_command[1]));
                            } else {
                                e.reply("自助禁言单次上限时间为12小时(720分钟)(。-ω-)zzz");
                            }
                        }
                        break;
                }
            // 否则反馈错误
            } else {
                e.reply("格式错误!格式为:自助禁言 时间(分钟)o(´^`)o");
            }
        })
    // 否则反馈错误
    } else {
        e.reply("机器人权限不足,当前权限等级为" + info['role'] + " (〃>_<;〃)");
    }
})
        

其中,关于获取指定群成员的成员信息的时候,返回的对象结构为:

export interface MemberInfo extends MemberBaseInfo {
    readonly group_id: number,
    // readonly user_id: number,
    // readonly nickname: string,
    // readonly card: string,
    // readonly sex: Gender,
    // readonly age: number,
    // readonly area: string,
    readonly join_time: number, //入群时间
    readonly last_sent_time: number, //最后发言时间
    // readonly level: number,
    readonly rank: string,
    readonly role: GroupRole,
    /** @deprecated */
    readonly unfriendly: boolean,
    // readonly title: string,
    readonly title_expire_time: number, //头衔过期时间
    /** @deprecated */
    readonly card_changeable: boolean,
    readonly shutup_time: number, //禁言到期时间
    readonly update_time: number, //此群员资料的最后更新时间
}

在这个禁言功能中,代码里值得学习的地方是对数字使用了正则表达式匹配

/^ *\d+(\.\d+)?$/

这段正则表达式意思是匹配所有数字,包括整数和浮点数,并忽略字符串前的空格

4.3 来点颜色

来点颜色功能触发方式是直接发送消息:来点颜色。当 bot 检测到消息的时候,会从后台数据库中随机选择一个图片发送出来,并附带图片的信息,包括画师、p站id、作品id。

来点颜色功能中,后台存储数据采用了 mysql 数据库,图片数据是预先使用 python 在p站上爬取的图片地址,然后经过下载并保存在服务器上,并将图片信息存储到 mysql 便于调取。

功能的伪代码和上一个相似,因为都是交互式非指令功能。

case '来点颜色':
    switch (msg_command[1]) {
        case undefined:
            从数据库获取信息并返回,然后由 bot 发送
            break;
        case '-h':
            返回指令的帮助信息
            break;
        default:
            反馈指令格式错误,并提示使用 -h 选项来获取帮助
            break;

其中返回指令的帮助信息和格式错误的错误信息及提示就不再赘述,之后也不会讲,除非遇到值得一谈的部分。

数据库获取数据,使用了 mysql 模块来连接到 mysql,连接 mysql 的时候有一种可以直接套用的固定格式:

let connection = mysql.createConnection({
    host: 'mysql 所在的设备的 ip 地址',
    user: 'mysql 用户',
    password: 'mysql 密码',
    database: '使用的数据库',
    multipleStatements: true    // 允许每个mysql语句有多条查询
});    
connection.connect();
var sql_command = "sql 语句"
connection.query(sql_command, function(err, result) {
    if (err) {
        e.reply(err);
    } else {
        设置成功的时候的行为
    }
});
connection.end();

于是再这个基础上,只需要对查询到的信息进行简单的处理并直接发送,唯一难点反而在于 sql 语句的编写

let connection = mysql.createConnection({
    host: '121.199.62.144',
    user: 'root',
    password: 'Sh123456!',
    database: 'pixiv',
    multipleStatements: true
});    
connection.connect();
var sql_command = 'SELECT t1.* FROM `info` AS t1 JOIN (SELECT ROUND(RAND() * ((SELECT MAX(id) FROM `info`)-(SELECT MIN(id) FROM `info`))+(SELECT MIN(id) FROM `info`)) AS id) AS t2 WHERE t1.id >= t2.id ORDER BY t1.id LIMIT 1;'
connection.query(sql_command, function(err, result) {
    if (err) {
        e.reply(err);
    } else {
        var pixiv_image = result[0];
        e.reply([{
                type: 'image',
                file: pixiv_image['local'],
                url: pixiv_image['original'],
                asface: false
            },
            {
                type: 'text',
                text: '图片pid:' + pixiv_image['pid'] + '\n' + '画师:' + pixiv_image['author'] + '\n' + '画师uid:' + pixiv_image['uid']
            }
        ])
    }
});
connection.end();

首先先看这里,mysql 查询成功的时候执行的行为,直接获取数据库返回的信息,然后根据这个信息进行一个可发送消息的构建。oicq 内可发送消息的声明如下:

export type Sendable = string | MessageElem | Iterable<MessageElem | string>

其中 MessageElem 可以翻看上面的内容。这里构建了一个包含了两个 MessageElem 的 Sendable 对象,先输出图片,然后紧跟对图片的描述文字(画师id,作品id,画师名称)。

这里的重点值得学习的地方在于这个随机从 sql 表中获取一个条目的 sql 语句的编写!

SELECT t1.* 
    FROM `info` AS t1 
    JOIN (
        SELECT ROUND(RAND() * ((
            SELECT MAX(id) FROM `info`
        )-(
            SELECT MIN(id) FROM `info`
        ))+(
            SELECT MIN(id) FROM `info`
        )) AS id
    )
    AS t2 
    WHERE t1.id >= t2.id 
    ORDER BY t1.id LIMIT 1;

其中存放了图片信息的表叫做 info 表。这一部分相当于,假设从  gif.latex?%5Ba%2Cb%5D 区间内随机取一个整数,其中 gif.latex?b%3Ea%3E%3D0 且 gif.latex?b%20%5Cin%20Z%5E&plus;%20%5Ccup%20a%20%5Cin%20Z%5E&plus;,并且随机数 gif.latex?r%20%5Cin%20%5B0%2C%201%5D,则可以使用 gif.latex?r%20%5Ccdot%20%28b-a%29%20-a 来将随机数 r 映射到区间 gif.latex?%5Ba%2C%20b%5D 当中。由于我们选出来需要是整数,所以使用 round 函数进行舍入。

使用这种方法选择一个 sql 表中的随机条目的时候,必须保证表中有一列数据是行号,因为生成的随机数是映射到行号的区间内的,如果没有行号,则没有办法实现。

4.4 回复功能

回复功能允许对某一个文本消息指定一个回复内容,在创建记录之后,下次当bot收到这个触发词文本消息时,会自动回复设置好的回复内容,回复内容仅支持图片、文字、表情,指定触发词文本消息仅支持文本。

该功能使用 mysql 数据库作为存储,一个触发词支持绑定多个回复词。

该功能属于非指令功能,功能只有当触发回复词不属于已定义的非指令功能的触发词的情况话,才会生效。但是回复词的设置属于指令功能,允许权限 admin 和 customer 进行设置,但部分功能稍有差异。

该功能的伪代码如下:

if (e.message[0]['type'] === 'text') {
    if (reply_msg.includes(e.message[0]['text'])) {
        使用数据库连接模板,从数据库根据触发词查找对应的所有条目,并随机输出一个
    }
}

代码中并没有什么值得需要注意的地方,数据库模板和之前是一样的,只不过处理的部分稍有不同,处理方式是,根据数据库存储的,回复内容的格式(文字、图片、表情)来设定不同格式的 MessageElem 进行发送。

唯一可以学习的地方是 sql 随机的写法,这个随机写法不同于来点颜色中的随机写法,因为这里找到的条目数据很少,可以使用简单的方法进行查找而不会损耗太多时间。

select answer, msg_type 
    from 
    (select * from reply 
        where question='${e.message[0]['text']}' 
        order by rand() 
        limit 1)

重点在于嵌套的 select 部分,可以看到这里使用了 order by rand() limit 1 的方法进行随机选取一个条目的效果。

order by rand() 的作用是随机重新排序表内的内容,然后通过 limit 1 取第一个,则出现了随机选取一个的效果。

5. 指令功能

所有指令功能在使用的时候,需要先 @bot 才可以使用,因为 bot 会根据收到消息的第一段是不是 @bot 来判断该消息是否是指令消息。

指令功能大部分会根据权限进行区分,权限分为三类,分别是:拥有者 owner,管理员 admin,普通用户 customer。

bot 添加了一个消息监听,用于监听所有的消息,在监听中加入了指令的判断和运行功能。一般情况下,指令的大致格式为:@bot [指令名称]  [可添加的选项] [可添加的指令参数]

在 oicq 中,接收到的这种格式的指令消息中会包含两个 MessageElem,第一个是 at 类型,第二个是 text 文本类型。其中 text 文本类型包含了指令名称、可添加选项、可添加的指令参数,它们之间使用空格分隔,所以对于指令,可以使用 split 函数进行分割。

于是,指令部分的实现方式就十分清晰了:

if (e.message[0]['type'] === 'at' && e.message[0]['qq'] === account) {
    if (e.message[1] === undefined) {
        e.reply("不要乱@我qwq,我的@是有用的 (•́へ•́╬)");
    } else {
    switch (e.message[1]['type']) {
        case 'text':    // 判断消息为命令格式,进行命令的功能实现
            var msg_command = e.message[1]['text'].split(' ');
            if (msg_command[0] === '') {
                msg_command = msg_command.slice(1);    // 去除 at 后可能存在的空格符号
            }
            if (admin_list[0].includes(e.user_id)) {
                以管理员身份执行命令
            } else {
                以普通用户身份执行命令
            }
            break;
        default:
            e.reply("无法识别命令⊙(・◇・)? 使用:@bot 查看帮助 [type? command | interaction],查看命令的使用方法");
            break;
    }
}
            

5.1 删除图片

  • 描述:从数据库中删除指定的图片 id 对应的记录

  • 使用方法:@bot 删除图片 [pid]

  • 创建日期:2021-11-16

  • 需要权限:admin

删除图片功能是为了方便管理员对于来点颜色功能中一些不受欢迎的图片进行非常方便的,在 qq 中就可以进行删除图片操作而制作的功能。

删除图片支持选项 -h 获取帮助。删除图片的实现方法是,对参数 pid 进行判断,如果是数字,则对数据库进行查找,找到后直接删除 pid 对应的记录,如果未找到或者数据库返回错误,则直接回复对应的错误提示,如果找到并成功删除,则直接返回删除成功的消息。

switch (msg_command[0]) {
    case "删除图片":
        switch (msg_command[1]) {
            case undefined:
                e.reply("不是这样用滴~ 使用:@bot 删除图片 -h,查看使用方法");
                break;
            case '-h':
                e.reply(format("使用方法:{}", ctl_command['删除图片']['help']));
                break;
            default:
                if (/^ *\d+$/.test(msg_command[1])) {    // 如果是数字
                    连接数据库并执行删除
                } else {
                    e.reply("指定的图片pid必须为数字");
                }
                break;
        }
    }
}

其中值得注意的地方在于 sql 语句,因为数据库中所有记录都有一列数据是行号,所以为了保证删除之后行号依旧保持连续且单调递增,需要进行特殊的处理

delete from info where pid=' + msg_command[1] + ';
alter table info drop id;
alter table info add id int unsigned not null first;
alter table info modify column id int unsigned not null auto_increment, add primary key( id );
  •  先执行删除操作。
  • 然后修改表结构,丢掉 id 这一列。
  • 重新添加 id 列,并将其置于第一列。
  • 对 id 列设置自增,并且将其修改为主键。

对于没有拥有管理员权限的用户,返回提示信息。

e.reply(`删除图片的权限等级要求是${ctl_command['删除图片']['permit']},而您的权限等级为:customer`);

 5.2 禁言

  • 描述:禁言指定用户

  • 使用方法:@bot 禁言 @target [时间(分钟)]

  • 创建日期:2021-11-17

  • 需要权限:admin

禁言功能算是自助禁言和指令结合后的产物,总体实现十分简单。

case "禁言":
    switch (e.message[2]?.type) {
        case 'at':
            try {
                if (/^ *\d+(\.\d+)? *$/.test(parseInt(e.message[3]['text'].replace(/ /g, '')))) {
                    if (parseInt(e.message[3]['text'].replace(/ /g, '')) >= 1) {
                        禁言功能部分,同自助禁言一样
                } else {
                    e.reply("时间必须是正数而且要大于1哦~");
                }
            } catch (TypeError) {
                e.reply(format("格式错误!格式为:{} o(´^`)o", ctl_command['禁言']['help']));
            }
            break;
        case undefined:
            if (msg_command[1] === '-h') {
                e.reply(format("使用方法:{}", ctl_command['禁言']['help']));
            }
        default:
            e.reply("不是这样用滴~ 使用:@bot 禁言 -h,查看使用方法");
            break;
    }
break;

判断数字的正则表达式后面有一段 .replace(/ /g, ''),这部分的效果是删除字符串里所有的空格。

下面 case undefined 内的 e.reply 内的字符串使用了跟以往不同的 format 函数的方式进行填充,我个人并不是很喜欢这种方式,这里只是为了尝试这种方式的可行性而这么做的,我更偏向于使用我另一篇文章内说的这种方式进行字符串的 format 处理。

python、js中更好的format实现(个人认为)_虚叶的博客-CSDN博客_python格式化js文件python中在python中,字符串的format形式可以通过字符串的 string.format() 方法对字符串内填值name, age, sex = "mike", 18, "female"print("name: {}, age: {}, sex: {}".format(name, age, sex))# [OUT]: name: mike, age: 18, sex: female但是这样做有个缺点在于,当需要填入的变量内容非常多的时候,会导致整个变得十分不好看,哪怕是查找这https://blog.csdn.net/qq_45638941/article/details/123655210?spm=1001.2014.3001.5501

这部分值得注意的地方是第二行 e.message[2]?.type 中的 .? 这个操作符

由于 e.message 是一个列表,其中包含一个或多个 MessageElem 元素,但是由于 e.message 中不一定存在第三个元素,则 e.message[2]['type'] 可能会出现报错:undefined 没有键 'type' 之类的报错,所以这部分需要使用 ?. 来进行特殊的处理。具体解释部分可以去查看我另一篇博客:nodejs访问字典中不确定是否存在的元素_虚叶的博客-CSDN博客遇到的问题在使用nodejs访问字典或列表内嵌套的字典或列表的某个内容的时候,对于其中某个键值对是否存在不能确定,直接进行访问会产生报错比如,现有如下一个列表,列表内有三个字典。当访问列表第三项的键 'v' 的内容时,如下代码可以正常输出var ls = [{'v':1}, {'v':2}, {'v':3}]console.log(ls[2]['v']) // [OUT]: 3当访问列表第四个元素(假定是字典)的键 'v' 的时候会直接抛出如下错误信息var ls = [{'https://blog.csdn.net/qq_45638941/article/details/123649731?spm=1001.2014.3001.5501

5.3 解除禁言

  • 描述:解除指定用户的禁言

  • 使用方法:@bot 解除禁言 @target

  • 创建日期:2021-11-17

  • 需要权限:admin

接触禁言是将禁言操作的时间设置为 0,就可以做到解除的效果。所以解除禁言功能的代码部分和禁言的代码部分几乎一模一样,只不过少了个参数时间。总的来说并没有什么值得注意的地方

case "解除禁言":
    switch (e.message[2]?.type) {
        case 'at':
            bot.setGroupBan(e.group_id, e.message[2]['qq'], 0);
            e.reply(format("成功解除 {}({}) 的禁言 (*^▽^*)", e.message[2]['text'], e.message[2]['qq']));
            break;
        case undefined:
            if (msg_command[1] === '-h') {
                e.reply(format("使用方法:{}", ctl_command['解除禁言']['help']));
            }
            break;
        default:
            e.reply("不是这样用滴~ 使用:@bot 解除禁言 -h,查看使用方法");
            break;
    }
break;

5.4 查看帮助

  • 描述:查看指定的可用的代码,command为控制指令,interaction为互动指令,不填则表示两种都显示

  • 使用方法:@bot 查看帮助[type? : command | interaction]

  • 创建日期:2021-11-18

  • 需要权限:customer

查看机器人支持的指令功能之类的东西

case "查看帮助":
    let show_msg = ''
    switch (msg_command[1]) {
        case 'command':
            show_msg = "控制命令:"
            for (let key in ctl_command) {
                show_msg += format("\n--{}\n----命令描述:{}\n----命令格式:{}\n----命令创建日期:{}\n----使用权限等级:{}\n", ctl_command[key]['title'], ctl_command[key]['describe'], ctl_command[key]['help'], ctl_command[key]['create_time'], ctl_command[key]['permit']);
            }
            e.reply(show_msg);
            break;
        case 'interaction':
            show_msg = "交互命令:"
            for (let key in interaction_command) {
                show_msg += format("\n--{}\n----命令描述:{}\n----命令格式:{}\n----命令创建日期:{}\n----使用权限等级:{}\n", interaction_command[key]['title'], interaction_command[key]['describe'], interaction_command[key]['help'], interaction_command[key]['create_time'], interaction_command[key]['permit']);
            }
            e.reply(show_msg);
            break;
        case undefined:
            show_msg = "交互命令:";
            for (let key in interaction_command) {
show_msg += format("\n--{}\n----命令描述:{}\n----命令格式:{}\n----命令创建日期:{}\n----使用权限等级:{}:\n", interaction_command[key]['title'], interaction_command[key]['describe'], interaction_command[key]['help'], interaction_command[key]['create_time'], interaction_command[key]['permit']);
            }
            show_msg += "\n控制命令:"
            for (let key in ctl_command) {
show_msg += format("\n--{}\n----命令描述:{}\n----命令格式:{}\n----命令创建日期:{}\n----使用权限等级:{}:\n", ctl_command[key]['title'], ctl_command[key]['describe'], ctl_command[key]['help'], ctl_command[key]['create_time'], ctl_command[key]['permit']);
            }
            e.reply(show_msg);
            break;
        case '-h':
            e.reply(format("使用方法:{}", ctl_command['查看帮助']['help']));
            break;
        default:
            e.reply("不是这样用滴~使用:@bot 查看帮助 -h,查看使用方法");
            break;
    }
break;

5.5 群白名单

  • 描述:-ls为list列出全部 -a 为 add 增加,后跟 args 内容为 [gid] 新群号 -rm 为 remove 删除,后跟 agrs 内容为 [gid] 删除的群号 -update 为修改,后跟 args 内容为 [old_gid new_gid ]修改 old_gid 为新的 new_gid

  • 使用方法:@bot 群白名单 [option] [args?]

  • 创建日期:2021-11-19

  • 需要权限:admin

添加白名单之后,被添加的群内将可以使用 bot 的各种功能,类似于授权。白名单支持增删查改功能,通过添加选项的方式实现不同的功能。白名单数据存储在 mysql 中。

case '群白名单':
    switch (msg_command[1]) {
        case undefined:
            指令格式错误提示
            break;
        case '-h':
            指令使用方法提示
            break;
        case '-ls':
            显示当前白名单内存在的所有群
            break;
        case '-a':
            if (/^\d*$/.test(msg_command[2])){
                添加群号对应的群到数据库中的白名单内,并更新 node.js 中的白名单内容
            } else {
                返回群号不全为数字的错误提示
            }
            break;
        case '-rm':
            if (/^\d*$/.test(msg_command[2])){
                从数据库和 node.js 中删除对应的群
            } else {
                返回群号不全为数字的错误提示
            }
            break;
        case '-update':
            if (/^\d*$/.test(msg_command[2]) && /^\d*$/.test(msg_command[3])){
                将前者群号更新为后者群号
            } else {
                返回群号不全为数字或者只填写了更改前的群号的错误提示
            }
            break;
        default:
            指令格式错误提示
            break;
    }
break;

当使用者权限不足的时候会返回权限不足的错误提示,这里不再赘述,整个代码部分没有什么值得注意的地方。

5.6 管理员

  • 描述:对管理员名单进行增删查

    -ls为list列出全部 ​ -a 为 add 增加,将target加为管理 ​ -rm 为 remove 删除,将target的管理删除

  • 使用方法:@bot 管理员 [-ls | -a | -rm] @target?

  • 创建日期:2021-11-19

这里添加的管理员名单只是相对于 bot 来说,而不是相对于群来说。两者不要混为一谈。

整个代码部分,包括结构,与群白名单功能一样,本质上没有太大改变。群管理员名单存在数据库中,修改的时候直接对数据库和本地 node.js 缓存进行修改。 

case '管理员':
    switch (msg_command[1]) {
        case undefined:
            e.reply(`不是这样用滴~ 添加-h选项查看一下怎么用这个命令吧(*^▽^*)`);
            break;
        case '-h':
            e.reply(`使用方法:${ctl_command['管理员']['help']}\n${ctl_command['管理员']['describe']}`);
            break;
        case '-ls':
            let show_msg = `管理员名单:`;
            for (let i = 0; i < admin_list[0].length; i++) {
                show_msg += `\n--${admin_list[1][i]}(${admin_list[0][i]})`;
            }
            e.reply(show_msg);
            break;
        case '-a':
            if (e.message[2]?.type === 'at'){
                对数据库和本地 node.js 缓存进行添加
            } else {
                e.reply(`不是这样用滴~ 添加-h选项查看一下怎么用这个命令吧(*^▽^*)`);
            }
            break;
        case '-rm':
            if (e.message[2]?.type === 'at'){
                对数据库和本地 node.js 缓存进行删除
            } else {
                e.reply(`不是这样用滴~ 添加-h选项查看一下怎么用这个命令吧(*^▽^*)`);
            }
            break;
        default:
            e.reply("不是这样用滴~使用:@bot 管理员 -h,查看使用方法");
            break;
    }
break;

5.7 回复

  • 描述:对消息回复功能做出增删查

    -find查看msg1对应的回复信息

    -a添加msg1的回复信息为msg2,msg2可以为图片、表情、文字

    -rm删除msg1对应回复信息为msg2的记录,用户仅可删除自己创建的,管理员可以删除指定的

  • 使用方法:@bot 回复 [option : -find | -a | -rm] [msg1] [msg2?]

  • 创建日期:2021-11-19

支持 -h 选项查看指令使用方法。

当选项为 -a 的时候,表示添加一条回复,当机器人检测到消息 msg1 的时候,自动发送 msg2 作为回复。可支持的消息有文字、图片、表情。

case '-a':
    if (msg1存在且不为空){
        if (msg2存在且不为空){
            向数据库添加文本回复消息
        } else {
            switch (e.message[2]?.type) {
                case 'image':
                    添加图片作为回复
                    break;
                case 'face':
                    添加表情作为回复
                    break;
                default:
                    e.reply("格式错误或者回复消息的类型不支持(只支持图片、文字、表情)٩( 'ω' )و ");
                    break;
            }
        }
    }
    break;

当消息为图片的时候,由于qq会自动在服务器上生成一个发送的图片的文件,所以只需要保存该图片的网络地址,并将该地址添加到数据库中,即可做到保存回复图片的效果。

源码链接:https://github.com/ky0ha/ky0ha.github.io/blob/main/download/server_1842562759.js

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐