feat: v1.1.4

This commit is contained in:
mol
2023-05-31 14:21:33 +08:00
commit 9b8dafefb1
17 changed files with 596 additions and 0 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
.env
.prettierignore
.prettierrc.js

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
.env

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
# Ignore artifacts:
build
coverage
# Ignore all HTML files:
*.html

26
.prettierrc.js Normal file
View File

@ -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
// 来源:稀土掘金
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

13
Dockerfile Normal file
View File

@ -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" ]

54
app.js Normal file
View File

@ -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);

56
bot/index.js Normal file
View File

@ -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;

10
config/index.js Normal file
View File

@ -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(',')
: [],
},
};

View File

@ -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);
});
};

22
handlers/sendMessage.js Normal file
View File

@ -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);
});
};

49
handlers/sendMessage2.js Normal file
View File

@ -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);
});
};

60
http/index.js Normal file
View File

@ -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;

149
package-lock.json generated Normal file
View File

@ -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
}
}
}
}
}

18
package.json Normal file
View File

@ -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"
}
}

4
start.js Normal file
View File

@ -0,0 +1,4 @@
const dotenv = require('dotenv');
dotenv.config();
require('./app');

25
utils/logger.js Normal file
View File

@ -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;

34
utils/queue.js Normal file
View File

@ -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);
}
}
};