cyberbot-v2
Version:
cyberbot, 基于napcat-ts, nodejs,轻量qq机器人框架。
705 lines (704 loc) • 28.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.bootstrap = exports.logger = exports.Structs = exports.dayjs = exports.http = exports.Bot = void 0;
const node_napcat_ts_1 = require("node-napcat-ts");
Object.defineProperty(exports, "Structs", { enumerable: true, get: function () { return node_napcat_ts_1.Structs; } });
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const log4js_config_1 = require("./log4js.config.js");
Object.defineProperty(exports, "logger", { enumerable: true, get: function () { return log4js_config_1.logger; } });
const pluginManager_1 = require("./pluginManager.js");
const axios_1 = __importDefault(require("axios"));
exports.http = axios_1.default;
const dayjs_1 = __importDefault(require("dayjs"));
exports.dayjs = dayjs_1.default;
const crypto_1 = require("crypto");
class Bot {
client;
eventHandlers = new Map();
master;
admins;
configPath;
pluginDir;
config_json;
builtInPlugins;
constructor() {
this.configPath = path.resolve(process.cwd(), "bot.config.json");
this.config_json = JSON.parse(fs.readFileSync(this.configPath, "utf8"));
this.pluginDir = path.resolve(process.cwd(), "plugins");
this.builtInPlugins = new Set(['cmds']);
this.client = new node_napcat_ts_1.NCWebsocket({
baseUrl: this.config_json.baseUrl,
accessToken: this.config_json.accessToken,
reconnection: {
enable: true,
attempts: 10,
delay: 5000
}
}, false);
this.master = this.config_json.master;
this.admins = this.config_json.admins;
}
on(event, handler) {
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, new Set());
}
this.eventHandlers.get(event)?.add(handler);
this.client.on(event, handler);
}
off(event, handler) {
this.eventHandlers.get(event)?.delete(handler);
this.client.off(event, handler);
}
async connect() {
await this.client.connect();
}
disconnect() {
this.client.disconnect();
}
getEventHandlers(event) {
return this.eventHandlers.get(event) || new Set();
}
/**
* 发送消息回复给指定的上下文。
*
* @param context - 消息上下文对象,包含消息的详细信息。
* @param message - 要发送的消息内容,可以是字符串、数字或消息段数组。
* @param quote - 是否引用原始消息。默认值为 `false`。
* @returns - 返回发送消息的结果,包含消息ID。
* @throws - 如果发送消息失败,抛出错误。
*/
reply = async (context, message, quote = false) => {
// 处理消息内容,统一转为数组格式
let messageArray = Array.isArray(message) ? message : [message];
// 转换文本和数字为消息段
const processedMessages = messageArray.map(item => {
if (typeof item === 'string' || typeof item === 'number') {
return node_napcat_ts_1.Structs.text(item.toString());
}
return item;
});
// 添加回复消息段(如果需要引用)
if (quote) {
processedMessages.unshift(node_napcat_ts_1.Structs.reply(context.message_id));
}
// 定义发送消息的参数映射,并使用类型保护确保正确访问属性
const sendParams = (() => {
switch (context.message_type) {
case 'group':
return { group_id: context.group_id };
case 'private':
return { user_id: context.user_id };
default:
log4js_config_1.logger.error(`Unsupported message type: ${context}`);
throw new Error(`Unsupported message type: ${context}`);
}
})();
// 发送消息并返回结果
try {
log4js_config_1.logger.debug(`Sending message: ${JSON.stringify(processedMessages)}`);
return await this.client.send_msg({
...sendParams,
message: processedMessages
});
}
catch (error) {
log4js_config_1.logger.error(`Failed to send message: ${JSON.stringify(error)}`);
return await this.client.send_msg({
...sendParams,
message: [node_napcat_ts_1.Structs.text(`Failed to send message: ${JSON.stringify(error)}`)]
});
}
};
/**
* 发送消息到指定的群组。
*
* @param context - 消息上下文对象,包含消息的详细信息。
* @param group_id - 目标群组的ID。
* @param message - 要发送的消息内容,可以是字符串、数字或消息段数组。
* @returns - 返回发送消息的结果,包含消息ID。
* @throws - 如果发送消息失败,抛出错误。
*/
sendGroupMessage = async (group_id, message) => {
// 处理消息内容,统一转为数组格式
let messageArray = Array.isArray(message) ? message : [message];
// 转换文本和数字为消息段
const processedMessages = messageArray.map(item => {
if (typeof item === 'string' || typeof item === 'number') {
return node_napcat_ts_1.Structs.text(item.toString());
}
return item;
});
// 发送消息并返回结果
try {
log4js_config_1.logger.debug(`Sending message: ${JSON.stringify(processedMessages)}`);
return await this.client.send_group_msg({
group_id: group_id,
message: processedMessages
});
}
catch (error) {
log4js_config_1.logger.error(`Failed to send message: ${JSON.stringify(error)}`);
return await this.client.send_group_msg({
group_id: group_id,
message: [node_napcat_ts_1.Structs.text(`Failed to send message: ${JSON.stringify(error)}`)]
});
}
};
/**
* 撤回指定的消息。
*
* @param message_id - 要撤回的消息的ID。
* @throws - 如果撤回消息失败,抛出错误。
*/
delete_msg = async (message_id) => {
try {
await this.client.delete_msg({ message_id });
}
catch (error) {
log4js_config_1.logger.error(`Failed to delete message: ${error}`);
}
};
/**
* 将指定用户从群组中踢出。
*
* @param ctx - 群组消息上下文对象,包含群组的详细信息。
* @param user_id - 要踢出的用户的ID。
* @param reject_add_request - 是否拒绝该用户的加群请求。默认值为 `false`。
* @throws - 如果踢出用户失败,抛出错误。
*/
kick = async (context, user_id, reject_add_request = false) => {
try {
await this.client.set_group_kick({
group_id: context.group_id,
user_id: user_id,
reject_add_request: reject_add_request
});
}
catch (error) {
log4js_config_1.logger.error(`Failed to kick user ${user_id} from group ${context.group_id}: ${error}`);
this.reply(context, `Failed to kick user ${user_id} from group ${context.group_id}: ${error}`);
}
};
/**
* 将指定用户在群组中禁言。
*
* @param ctx - 群组消息上下文对象,包含群组的详细信息。
* @param user_id - 要禁言的用户的ID。
* @param duration - 禁言时长,单位为秒。默认值为 `30` 秒。
* @throws - 如果禁言用户失败,抛出错误。
*/
ban = async (context, user_id, duration = 30) => {
try {
await this.client.set_group_ban({
group_id: context.group_id,
user_id: user_id,
duration: duration
});
}
catch (error) {
log4js_config_1.logger.error(`Failed to ban user ${user_id} in group ${context.group_id} for ${duration} seconds: ${error}`);
this.reply(context, `Failed to ban user ${user_id} in group ${context.group_id} for ${duration} seconds: ${error}`);
}
};
/**
* 设置群组全员禁言状态。
*
* @param ctx - 群组消息上下文对象,包含群组的详细信息。
* @param enable - 是否开启全员禁言。默认值为 `false`,即关闭全员禁言。
* @throws - 如果设置全员禁言状态失败,抛出错误。
*/
banAll = async (context, enable = false) => {
try {
await this.client.set_group_whole_ban({
group_id: context.group_id,
enable: enable
});
}
catch (error) {
log4js_config_1.logger.error(`Failed to set whole ban for group ${context.group_id} to ${enable}: ${error}`);
this.reply(context, `Failed to set whole ban for group ${context.group_id} to ${enable}: ${error}`);
}
};
/**
* 设置群组名称。
*
* @param context - 群组消息上下文对象,包含群组的详细信息。
* @param name - 要设置的群组名称。
* @throws - 如果设置群组名称失败,抛出错误。
*/
setGroupName = async (context, name) => {
try {
await this.client.set_group_name({
group_id: context.group_id,
group_name: name
});
}
catch (error) {
log4js_config_1.logger.error(`Failed to set group name for group ${context.group_id} to ${name}: ${error}`);
this.reply(context, `Failed to set group name for group ${context.group_id} to ${name}: ${error}`);
}
};
/**
* 设置群组管理员。
*
* @param context - 群组消息上下文对象,包含群组的详细信息。
* @param user_id - 要设置或取消管理员权限的用户的ID。
* @param enable - 是否设置为管理员。默认值为 `true`,即设置为管理员。
* @throws - 如果设置管理员权限失败,抛出错误。
*/
setAdmin = async (context, user_id, enable = true) => {
try {
await this.client.set_group_admin({
group_id: context.group_id,
user_id: user_id,
enable: enable
});
}
catch (error) {
log4js_config_1.logger.error(`Failed to set admin status for user ${user_id} in group ${context.group_id} to ${enable}: ${error}`);
this.reply(context, `Failed to set admin status for user ${user_id} in group ${context.group_id} to ${enable}: ${error}`);
}
};
/**
* 设置群组成员的特殊头衔。
*
* @param context - 群组消息上下文对象,包含群组的详细信息。
* @param user_id - 要设置特殊头衔的用户的ID。
* @param title - 要设置的特殊头衔。
* @throws - 如果设置特殊头衔失败,抛出错误。
*/
setTitle = async (context, user_id, title) => {
try {
await this.client.set_group_special_title({
group_id: context.group_id,
user_id: user_id,
special_title: title
});
}
catch (error) {
log4js_config_1.logger.error(`Failed to set special title for user ${user_id} in group ${context.group_id} to ${title}: ${error}`);
this.reply(context, `Failed to set special title for user ${user_id} in group ${context.group_id} to ${title}: ${error}`);
}
};
/**
* 处理群组加入请求,自动同意请求。
*
* @param context - 请求上下文对象,包含请求的详细信息。
* @throws - 如果处理请求失败,抛出错误。
*/
aprroveGroup = async (context) => {
try {
context.quick_action(true);
}
catch (error) {
log4js_config_1.logger.error(`Failed to approve group request: ${error}`);
}
};
/**
* 处理群组加入请求,自动拒绝请求。
*
* @param context - 请求上下文对象,包含请求的详细信息。
* @throws - 如果处理请求失败,抛出错误。
*/
rejectGroup = async (context) => {
try {
context.quick_action(false);
}
catch (error) {
log4js_config_1.logger.error(`Failed to reject group request: ${error}`);
}
};
/**
* 检查用户是否有权限。
*
* @param user_id - 用户的ID。
* @returns 如果用户是管理员或主人,则返回 `true`,否则返回 `false`。
*/
hasRight = (user_id) => {
return this.admins.includes(user_id) || this.master === user_id;
};
/**
* 检查用户是否是群组管理员或群主。
*
* @param context - 群组消息上下文对象,包含群组的详细信息。
* @param user_id - 用户的ID。
* @param group_id - 群组的ID。默认值为 `context.group_id`。
* @returns 如果用户是群组管理员或群主,则返回 `true`,否则返回 `false`。
* @throws - 如果获取群组成员信息失败,抛出错误。
*/
isGroupAdmin = async (context, user_id, group_id = context.group_id) => {
try {
const memberInfo = await this.client.get_group_member_info({ group_id, user_id });
return memberInfo.role === 'admin' || memberInfo.role === 'owner';
}
catch (error) {
log4js_config_1.logger.error(`Failed to get group member info for user ${user_id} in group ${group_id}: ${error}`);
return false;
}
};
/**
* 检查用户是否是群组群主。
*
* @param context - 群组消息上下文对象,包含群组的详细信息。
* @param user_id - 用户的ID。
* @param group_id - 群组的ID。默认值为 `context.group_id`。
* @returns 如果用户是群组群主,则返回 `true`,否则返回 `false`。
* @throws - 如果获取群组成员信息失败,抛出错误。
*/
isGroupOwner = async (context, user_id, group_id = context.group_id) => {
try {
const memberInfo = await this.client.get_group_member_info({ group_id, user_id });
return memberInfo.role === 'owner';
}
catch (error) {
log4js_config_1.logger.error(`Failed to get group member info for user ${user_id} in group ${group_id}: ${error}`);
return false;
}
};
/**
* 异步读取机器人的配置文件。
*
* @returns - 返回解析后的配置对象。
* @throws - 如果读取或解析配置文件失败,抛出错误。
*/
readConfigAsync = async () => {
try {
const configPath = path.resolve(process.cwd(), "bot.config.json");
const content = await fs.promises.readFile(configPath, "utf8");
return JSON.parse(content);
}
catch (error) {
log4js_config_1.logger.error('Async read failed:', error);
}
};
/**
* 异步写入机器人的配置文件。
*
* @param config - 要写入的配置对象。
* @throws - 如果写入配置文件失败,抛出错误。
*/
writeConfigAsync = async (config) => {
try {
const configPath = path.resolve(process.cwd(), "bot.config.json");
await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
}
catch (error) {
log4js_config_1.logger.error('Async write failed:', error);
}
};
/**
* 异步读取所有插件。
* 该方法会读取 `plugins` 目录下的所有子目录,并将每个子目录的名称作为插件名返回。
* @returns - 返回一个包含所有插件名称的字符串数组。
* @throws - 如果读取插件目录失败,抛出错误。
*/
async readAllPlugins() {
try {
const pluginDir = path.resolve(process.cwd(), "src/plugins");
return await fs.promises.readdir(pluginDir);
}
catch (error) {
log4js_config_1.logger.error(`读取插件目录失败: ${JSON.stringify(error)}`);
return [];
}
}
/**
* 异步读取已启用的插件。
* 该方法会读取机器人的配置文件 `bot.config.json`,并返回配置文件中 `plugins` 字段列出的所有已启用插件名称。
* @returns - 返回一个包含所有已启用插件名称的字符串数组。
* @throws - 如果读取或解析配置文件失败,抛出错误。
*/
async readEnabledPlugins() {
try {
const configPath = path.resolve(process.cwd(), "bot.config.json");
const content = await fs.promises.readFile(configPath, "utf8");
const config = JSON.parse(content);
return config.plugins || [];
}
catch (error) {
log4js_config_1.logger.error(`读取或解析配置文件失败: ${JSON.stringify(error)}`);
return [];
}
}
/**
* MD5 加密
* @param {string} text 待 MD5 加密数据
* @return {string} MD5 加密后的 hex 字符串
*/
md5 = (text) => {
const hash = (0, crypto_1.createHash)('md5');
hash.update(text);
return hash.digest('hex');
};
/**
* 生成随机整数
* @param {number} min 最小值
* @param {number} max 最大值
* @return {number} 随机范围内的整数
*/
randomInt = (min, max) => {
if (min > max) {
log4js_config_1.logger.error('最小值不能大于最大值');
}
return Math.floor(Math.random() * (max - min + 1)) + min;
};
/**
* 取数组内随机一项
* @param {Array<T>} array 待操作数组
* @return {T} 数组内的随机一项
*/
randomItem = (array) => {
if (array.length === 0) {
log4js_config_1.logger.error('数组不能为空');
}
const randomIndex = Math.floor(Math.random() * array.length);
return array[randomIndex];
};
/**
* 通过群号获取任意群头像链接
*
* size 可选: 40 | 100 | 640,0 为原图
*/
getGroupAvatarLink = async (group, size) => {
return `https://p.qlogo.cn/gh/111111/${group}/${size || 640}`;
};
/**
* 通过 QQ 号获取任意头像链接
*
* size 可选: 0 | 40 | 100 | 160 | 640,0 为原图
*/
getQQAvatarLink = async (group, size) => {
return `https://q.qlogo.cn/headimg_dl?dst_uin=${group}&spec=${size || 640}`;
};
/**
* 从消息内容中提取图片链接。
* 该方法使用正则表达式从传入的 `raw_message` 中提取 `[CQ:image,...]` 格式的图片链接。
* 如果找到图片链接,则返回该链接;否则,记录警告日志并返回空字符串。
* @param raw_message - 包含图片信息的原始消息字符串。
* @returns 提取的图片链接字符串,如果未找到则返回空字符串。
* @throws 如果在提取过程中发生错误,记录错误日志并返回空字符串。
*/
getImageLink = async (raw_message) => {
try {
const imagePattern = /\[CQ:image,.*?url=(.*?),/g;
const match = imagePattern.exec(raw_message);
if (match && match[1]) {
return match[1];
}
else {
log4js_config_1.logger.warn('未找到图片链接');
return "";
}
}
catch (error) {
log4js_config_1.logger.error('提取图片链接时发生错误:', error);
return "";
}
};
getrKey = async () => {
try {
const rkey = await this.client.nc_get_rkey({});
return rkey[0]?.rkey;
}
catch (error) {
log4js_config_1.logger.error('获取rkey失败:', error);
return "";
}
};
/**
* 替换 URL 中的 rkey 参数
* @param url - 原始 URL
* @returns 替换 rkey 后的新 URL
*/
getDirectLink = async (url) => {
try {
const rKey = await this.getrKey();
if (!rKey) {
log4js_config_1.logger.error('获取 rkey 失败,无法替换');
return "";
}
// 使用正则表达式提取 &rkey= 之前的内容
const regex = /^(.*?)&rkey=/;
const baseUrl = url.match(regex)?.[1];
// 如果匹配到内容,拼接 rKey,否则返回空字符串
return baseUrl ? `${baseUrl}${rKey}` : "";
}
catch (error) {
log4js_config_1.logger.error('获取直链失败:', error);
return "";
}
};
/**
* 从消息内容中提取回复消息的ID。
* 该方法使用正则表达式从传入的 `raw_message` 中提取 `[CQ:reply,id=...]` 格式的回复消息ID。
* 如果找到回复消息ID,则返回该ID;否则,返回空字符串。
*
* @param raw_message - 包含回复消息信息的原始消息字符串。
* @returns 提取的回复消息ID字符串,如果未找到则返回空字符串。
* @throws 如果在提取过程中发生错误,记录错误日志并返回空字符串。
*/
getMessageId = async (raw_message) => {
try {
const regex = /\[CQ:reply,id=(\d+)\]/;
const match = raw_message.match(regex);
if (match && match[1]) {
return match[1];
}
return "";
}
catch (error) {
log4js_config_1.logger.error('提取消息ID时发生错误:', error);
return "";
}
};
/**
* 从消息内容中提取被@的QQ号码。
* 该方法使用正则表达式从传入的 `raw_message` 中提取 `[CQ:at,qq=...]` 格式的QQ号码。
* 如果找到被@的QQ号码,则返回该号码;否则,返回空字符串。
*
* @param raw_message - 包含@信息的原始消息字符串。
* @returns 提取的被@的QQ号码字符串,如果未找到则返回空字符串。
* @throws 如果在提取过程中发生错误,记录错误日志并返回空字符串。
*/
getMessageAt = async (raw_message) => {
try {
const regex = /\[CQ:at,qq=(\d+)\]/;
const match = raw_message.match(regex);
if (match && match[1]) {
return match[1];
}
return "";
}
catch (error) {
log4js_config_1.logger.error('提取消息ID时发生错误:', error);
return "";
}
};
/**
* 从消息内容中提取纯文本内容。
* 该方法使用正则表达式从传入的 `raw_message` 中移除所有的 CQ 码,并返回剩余的纯文本内容。
*
* @param raw_message - 包含 CQ 码的原始消息字符串。
* @returns 提取的纯文本内容字符串。
* @throws 如果在提取过程中发生错误,记录错误日志并抛出错误。
*/
getText = async (raw_message) => {
try {
const cqCodePattern = /\[CQ:[^\]]+\]/g;
// 使用正则表达式替换 CQ 码为空字符串
return raw_message.replace(cqCodePattern, '').trim();
}
catch (error) {
log4js_config_1.logger.error('提取纯文本内容时发生错误:', error);
return "";
}
};
/**
* 发送伪造消息。
*
* @param target_id - 目标用户的ID(如果是私聊)或群组的ID(如果是群聊)。
* @param message - 要发送的消息内容,格式为 `Send['node'][]`。
* @param isGroup - 是否发送到群组。默认值为 `true`。
* @returns - 返回发送消息的结果,包含消息ID和资源ID。
* @throws - 如果发送消息失败,抛出错误。
*/
fakeMessage = async (target_id, message, isGroup = true) => {
try {
// 调用 send_group_forward_msg 函数
/**@ =message例子=
* message: [
* {
* type: 'node',
* data: {
* content: [
* Structs.text(message) // 消息内容,使用 Structs.text 生成文本消息
* ]
* }
* }
* ]
**/
// 动态构建参数对象
const params = isGroup
? { group_id: target_id, message: message } // 群聊消息
: { user_id: target_id, message: message }; // 私聊消息
// 调用转发消息函数
return await this.client.send_forward_msg(params);
}
catch (error) {
log4js_config_1.logger.error(`Failed to send fake message to target ${target_id}: ${error}`);
throw error;
}
};
}
exports.Bot = Bot;
// 启动机器人
const bootstrap = async () => {
try {
const bot = new Bot();
const pluginManager = new pluginManager_1.PluginManager(bot);
// 初始化连接
await bot.connect();
log4js_config_1.logger.info('🤖 机器人已上线');
await bot.client.send_private_msg({
user_id: bot.config_json.master,
message: [node_napcat_ts_1.Structs.text('🤖 机器人已上线')]
});
// 加载插件
await pluginManager.loadAllPlugins();
pluginManager.startWatching();
// 处理关闭信号
process.on('SIGINT', async () => {
try {
await bot.disconnect();
log4js_config_1.logger.info('🤖 机器人已下线');
process.exit();
}
catch (error) {
log4js_config_1.logger.error('机器人断开连接时发生错误:', error);
process.exit(1);
}
});
}
catch (error) {
log4js_config_1.logger.error('机器人启动时发生错误:', error);
process.exit(1);
}
};
exports.bootstrap = bootstrap;