From 9b8dafefb1b95f9a1b4d66b0b0fdcf28b37a20e5 Mon Sep 17 00:00:00 2001 From: mol Date: Wed, 31 May 2023 14:21:33 +0800 Subject: [PATCH] feat: v1.1.4 --- .dockerignore | 4 + .gitignore | 2 + .prettierignore | 6 ++ .prettierrc.js | 26 ++++++ Dockerfile | 13 +++ app.js | 54 ++++++++++++ bot/index.js | 56 +++++++++++++ config/index.js | 10 +++ handlers/sendForwardMessage.js | 64 ++++++++++++++ handlers/sendMessage.js | 22 +++++ handlers/sendMessage2.js | 49 +++++++++++ http/index.js | 60 +++++++++++++ package-lock.json | 149 +++++++++++++++++++++++++++++++++ package.json | 18 ++++ start.js | 4 + utils/logger.js | 25 ++++++ utils/queue.js | 34 ++++++++ 17 files changed, 596 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.js create mode 100644 Dockerfile create mode 100644 app.js create mode 100644 bot/index.js create mode 100644 config/index.js create mode 100644 handlers/sendForwardMessage.js create mode 100644 handlers/sendMessage.js create mode 100644 handlers/sendMessage2.js create mode 100644 http/index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 start.js create mode 100644 utils/logger.js create mode 100644 utils/queue.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..adc5cb5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.env +.prettierignore +.prettierrc.js \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1dcef2d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.env \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..10f9d57 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +# Ignore artifacts: +build +coverage + +# Ignore all HTML files: +*.html \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..c67cf81 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,26 @@ +//此处的规则供参考,其中多半其实都是默认值,可以根据个人习惯改写 +module.exports = { + printWidth: 80, //单行长度 + tabWidth: 2, //缩进长度 + useTabs: false, //使用空格代替tab缩进 + semi: true, //句末使用分号 + singleQuote: true, //使用单引号 + quoteProps: 'as-needed', //仅在必需时为对象的key添加引号 + jsxSingleQuote: true, // jsx中使用单引号 + trailingComma: 'all', //多行时尽可能打印尾随逗号 + bracketSpacing: true, //在对象前后添加空格-eg: { foo: bar } + jsxBracketSameLine: true, //多属性html标签的‘>’折行放置 + arrowParens: 'always', //单参数箭头函数参数周围使用圆括号-eg: (x) => x + requirePragma: false, //无需顶部注释即可格式化 + insertPragma: false, //在已被preitter格式化的文件顶部加上标注 + proseWrap: 'preserve', //不知道怎么翻译 + htmlWhitespaceSensitivity: 'ignore', //对HTML全局空白不敏感 + vueIndentScriptAndStyle: false, //不对vue中的script及style标签缩进 + endOfLine: 'lf', //结束行形式 + embeddedLanguageFormatting: 'auto', //对引用代码进行格式化 +}; + +// 作者:星始流年 +// 链接:https://juejin.cn/post/6938687606687432740 +// 来源:稀土掘金 +// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..620fe57 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:18-bullseye as dep-builder + +WORKDIR /app + +COPY package*.json ./ + +RUN npm install + +COPY . . + +EXPOSE 8080 + +CMD [ "node", "app.js" ] diff --git a/app.js b/app.js new file mode 100644 index 0000000..dea172a --- /dev/null +++ b/app.js @@ -0,0 +1,54 @@ +const fs = require('fs'); +const { join } = require('path'); +const CreateBot = require('./bot'); +const CreateWebhookServer = require('./http'); +const logger = require('./utils/logger'); + +let retryCount = 0; + +(async function start(port) { + if (retryCount >= 5) { + logger.warning('重试次数已达上限,请联系管理员重新启动本服务'); + return; + } + + logger('start server!!'); + const bot = new CreateBot(); + + logger('bot 连接中...'); + await bot + .open({ + baseUrl: process.env.MIRAI_HTTP_API_HOST, + verifyKey: process.env.MIRAI_HTTP_API_VERIFY_KEY, + qq: process.env.QQ, + }) + .catch((e) => { + retryCount++; + logger.error(`bot 连接失败,${e}。正在重试, 重试次数 ${retryCount}`); + + setTimeout(() => { + start(); + }, 5000); + return Promise.reject(); + }); + logger('bot 连接成功!!'); + + logger('开启 webhook 服务器'); + const webhook = new CreateWebhookServer(); + + logger('开始引入handler函数'); + const files = fs.readdirSync('./handlers'); + const handlers = files + .filter((it) => /\.js$/.test(it)) + .map((it) => { + let fPath = join(__dirname, './handlers', it); + return { + event: it.split('.').slice(0, -1).join('.'), + handler: require(fPath).bind(this, bot), + }; + }); + webhook.registerHanlder(handlers); + + webhook.startListen(port); + logger(`开始监听端口: ${port}`); +})(process.env.WEB_HOOK_PORT); diff --git a/bot/index.js b/bot/index.js new file mode 100644 index 0000000..a6e77c3 --- /dev/null +++ b/bot/index.js @@ -0,0 +1,56 @@ +const { Bot } = require('mirai-js'); +const Queue = require('../utils/queue'); +const logger = require('../utils/logger'); + +class CreateBot { + constructor() { + this.bot = new Bot(); + this.queue = new Queue(); + this.running = false; + } + + async open(config) { + logger(`开始连接 mirai: host: ${config.baseUrl}, qq: ${config.qq}`); + await this.bot.open({ + baseUrl: config.baseUrl, + verifyKey: config.verifyKey, + qq: config.qq, + }); + } + + async sendMessageToFriend(qq, message) { + logger(`发送好友[${qq}]消息进入消息队列`); + return this.queue + .addMethod(this.bot.sendMessage.bind(this.bot, { friend: qq, message })) + .then( + (res) => { + logger(`发送好友[${qq}]消息成功 ${res}`); + return res; + }, + (e) => { + logger.warning(`发送好友[${qq}]消息失败,错误信息${e}`); + return Promise.reject(e); + }, + ); + } + + async sendMessageToGroup(groupId, message) { + logger(`发送群[${groupId}]消息进入消息队列`); + return this.queue + .addMethod( + this.bot.sendMessage.bind(this.bot, { group: groupId, message }), + ) + .then( + (res) => { + logger(`发送群[${groupId}]消息成功 ${res}`); + return res; + }, + (e) => { + logger.warning(`发送群[${groupId}]消息失败,错误信息${e}`); + return Promise.reject(e); + }, + ); + } +} + +module.exports = CreateBot; diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..5877c25 --- /dev/null +++ b/config/index.js @@ -0,0 +1,10 @@ +module.exports = { + defaultSubs: { + groups: process.env.DEFAULT_SUB_GROUPS + ? process.env.DEFAULT_SUB_GROUPS.split(',') + : [], + users: process.env.DEFAULT_SUB_USERS + ? process.env.DEFAULT_SUB_USERS.split(',') + : [], + }, +}; diff --git a/handlers/sendForwardMessage.js b/handlers/sendForwardMessage.js new file mode 100644 index 0000000..28d8853 --- /dev/null +++ b/handlers/sendForwardMessage.js @@ -0,0 +1,64 @@ +const { Message } = require('mirai-js'); +const { defaultSubs } = require('../config'); +const logger = require('../utils/logger'); + +module.exports = function sendForwardMessage(bot, data) { + logger('开始执行handler: sendForwardMessage'); + const { from, messages, subs } = data; + const subscriptions = subs || defaultSubs; + const { groups, users } = subscriptions; + + // 创建转发信息 + const forwardMsgContent = Message.createForwardMessage(); + for (let i = 0; i < messages.length; i++) { + const { message, imgUrls, originUrl } = messages[i]; + const msgContent = new Message(); + // 依次添加文本消息 + if (message) { + Array.isArray(message) + ? message.forEach((message) => msgContent.addText(`${message}\n`)) + : msgContent.addText(`${message}\n`); + } + // 添加图片消息 + if (imgUrls) { + Array.isArray(imgUrls) + ? imgUrls + .slice(0, process.env.IMG_NUMBER_IN_ONE_MESSAGE || 1) + .forEach((url) => msgContent.addImageUrl(url)) + : msgContent.addImageUrl(imgUrls); + } + // 添加来源地址 + if (originUrl) { + msgContent.addText(`\n点击查看详情:${originUrl}`); + } + forwardMsgContent.addForwardNode({ + senderId: process.env.QQ, + time: 0, + senderName: process.env.FORWARD_SENDER_NAME || '莉娜·模儿', + messageChain: msgContent, + }); + } + + Array.isArray(groups) && + groups.forEach((groupId) => { + // 添加消息来源 + if (from) { + bot.sendMessageToGroup( + groupId, + new Message().addText(`${from}更新啦!快来看吧。`), + ); + } + bot.sendMessageToGroup(groupId, forwardMsgContent); + }); + Array.isArray(users) && + users.forEach((qq) => { + // 添加消息来源 + if (from) { + bot.sendMessageToFriend( + qq, + new Message().addText(`${from}更新啦!快来看吧。`), + ); + } + bot.sendMessageToFriend(qq, forwardMsgContent); + }); +}; diff --git a/handlers/sendMessage.js b/handlers/sendMessage.js new file mode 100644 index 0000000..e923758 --- /dev/null +++ b/handlers/sendMessage.js @@ -0,0 +1,22 @@ +const { Message } = require('mirai-js'); +const { defaultSubs } = require('../config'); +const logger = require('../utils/logger'); + +// 方法已废弃 +module.exports = function sendMessage(bot, data) { + logger('开始执行handler: sendMessage'); + const { message, imgUrl } = data; + const subscriptions = defaultSubs; + const { groups, users } = subscriptions; + const msg = new Message().addText(message); + if (imgUrl) { + msg.addImageUrl(imgUrl); + } + + groups.forEach((groupId) => { + bot.sendMessageToGroup(groupId, msg); + }); + users.forEach((qq) => { + bot.sendMessageToFriend(qq, msg); + }); +}; diff --git a/handlers/sendMessage2.js b/handlers/sendMessage2.js new file mode 100644 index 0000000..4d108ba --- /dev/null +++ b/handlers/sendMessage2.js @@ -0,0 +1,49 @@ +const { Message } = require('mirai-js'); +const logger = require('../utils/logger'); + +module.exports = function sendMessage2(bot, data) { + logger('开始执行handler: sendMessage2'); + const { from, messages, imgUrls, subs, originUrl, at } = data; + const subscriptions = subs || defaultSubs; + const { groups, users } = subscriptions; + const msgContent = new Message(); + // 添加@信息 + if (at) { + if (at === 'all') { + msgContent.addAtAll(); + } else if (Array.isArray(at)) { + at.forEach((qq) => msgContent.addAt(qq)); + } + } + // 添加消息来源 + if (from) { + msgContent.addText(`${from}\n`); + } + // 依次添加文本消息 + if (messages) { + Array.isArray(messages) + ? messages.forEach((message) => msgContent.addText(`${message}\n`)) + : msgContent.addText(messages); + } + // 添加图片消息 + if (imgUrls) { + Array.isArray(imgUrls) + ? imgUrls + .slice(0, process.env.IMG_NUMBER_IN_ONE_MESSAGE || 1) + .forEach((url) => msgContent.addImageUrl(url)) + : msgContent.addImageUrl(imgUrls); + } + // 添加来源地址 + if (originUrl) { + msgContent.addText(`点击查看详情:${originUrl}`); + } + + Array.isArray(groups) && + groups.forEach((groupId) => { + bot.sendMessageToGroup(groupId, msgContent); + }); + Array.isArray(users) && + users.forEach((qq) => { + bot.sendMessageToFriend(qq, msgContent); + }); +}; diff --git a/http/index.js b/http/index.js new file mode 100644 index 0000000..bda895e --- /dev/null +++ b/http/index.js @@ -0,0 +1,60 @@ +const http = require('http'); +const logger = require('../utils/logger'); + +class CreateWebhookServer { + constructor() { + this.http = null; + this.handlers = []; + } + + startListen(port) { + this.http = http + .createServer((request, response) => { + const { url, method } = request; + logger(`监听到请求:${url}, 方法: ${method}`); + if (method.toLowerCase() !== 'post') return; + let body = ''; + request + .on('error', (err) => { + console.error(err); + }) + .on('data', (chunk) => { + body += chunk; + }) + .on('end', () => { + try { + const jsonData = JSON.parse(body); + Promise.all( + this.handlers.map((val) => + jsonData.event === val.event + ? val.handler(jsonData) + : Promise.resolve(), + ), + ) + .then(() => { + response.writeHead(200, { 'Content-Type': 'text/plain' }); + response.write('Hello World'); + response.end(); + }) + .catch((e) => { + response.writeHead(405, { 'Content-Type': 'text/plain' }); + response.write(e); + response.end(); + }); + } catch (e) { + response.writeHead(405, { 'Content-Type': 'text/plain' }); + response.write(e); + response.end(); + } + }); + }) + .listen(port || 8080); + } + + registerHanlder(handlers) { + logger(`${handlers.map((item) => item.event).join('、')}函数已引入`); + this.handlers.push(...handlers); + } +} + +module.exports = CreateWebhookServer; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5c4a26e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,149 @@ +{ + "name": "mirai-middle-server", + "version": "1.1.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mirai-middle-server", + "version": "1.1.4", + "dependencies": { + "dayjs": "^1.11.7", + "mirai-js": "^2.8.11" + }, + "devDependencies": { + "dotenv": "^16.1.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "dependencies": { + "follow-redirects": "^1.14.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.1.1.tgz", + "integrity": "sha512-UGmzIqXU/4b6Vb3R1Vrfd/4vGgVlB+mO+vEixOdfRhLeppkyW2BMhuK7TL8d0el+q9c4lW9qK2wZYhNLFhXYLA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mirai-js": { + "version": "2.8.11", + "resolved": "https://registry.npmjs.org/mirai-js/-/mirai-js-2.8.11.tgz", + "integrity": "sha512-oon2wxi3EEFyGG45ss/Zhx24wv+6WcFQJ3Hx0MAQFj8dZTrf+v39Nm4+KCvjDAllogPJmRVmF21laCh/ISdLmw==", + "dependencies": { + "axios": "^0.24.0", + "form-data": "^3.0.1", + "ws": "^8.8.1" + } + }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2f44c39 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "mirai-middle-server", + "version": "1.1.4", + "description": "使用miraijs开发,用于连接kuginn等外部服务与mirai通信的中转服务器", + "author": "mol", + "main": "app.js", + "scripts": { + "dev": "node start.js", + "start": "node app.js" + }, + "dependencies": { + "dayjs": "^1.11.7", + "mirai-js": "^2.8.11" + }, + "devDependencies": { + "dotenv": "^16.1.1" + } +} diff --git a/start.js b/start.js new file mode 100644 index 0000000..86ac505 --- /dev/null +++ b/start.js @@ -0,0 +1,4 @@ +const dotenv = require('dotenv'); +dotenv.config(); + +require('./app'); diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 0000000..c7af3f4 --- /dev/null +++ b/utils/logger.js @@ -0,0 +1,25 @@ +const dayjs = require('dayjs'); + +function info(message) { + console.log(`${dayjs().format('YYYY-MM-DD hh:mm:ss')}【消息】${message}`); +} + +function warning(message) { + console.log(`${dayjs().format('YYYY-MM-DD hh:mm:ss')}【警告】${message}`); +} + +function error(message) { + console.log(`${dayjs().format('YYYY-MM-DD hh:mm:ss')}【错误】${message}`); +} + +function logger(message) { + info(message); +} + +logger.prototype.warning = warning; + +logger.prototype.info = info; + +logger.prototype.error = error; + +module.exports = logger; diff --git a/utils/queue.js b/utils/queue.js new file mode 100644 index 0000000..7f7739a --- /dev/null +++ b/utils/queue.js @@ -0,0 +1,34 @@ +module.exports = class Queue { + constructor() { + this.queue = []; + } + + addMethod(method) { + return new Promise((resolve, reject) => { + this.queue.push({ method, resolve, reject }); + + if (this.queue.length === 1) { + this.executeNext(); + } + }); + } + + async executeNext() { + const { method, resolve, reject } = this.queue[0]; + + try { + const result = await method(); + resolve(result); + } catch (e) { + reject(e); + } + + this.queue.shift(); + + if (this.queue.length > 0) { + setTimeout(() => { + this.executeNext(); + }, Math.random() * 1000 + 500); + } + } +};