first version
This commit is contained in:
Generated
+83
@@ -14,6 +14,7 @@
|
||||
"@koa/router": "^10.1.1",
|
||||
"commander": "^8.3.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"formidable": "^2.0.1",
|
||||
"inquirer": "^8.2.0",
|
||||
"koa": "^2.13.4",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
@@ -233,6 +234,11 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -597,6 +603,15 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
|
||||
"integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=",
|
||||
"dependencies": {
|
||||
"asap": "^2.0.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -736,6 +751,20 @@
|
||||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/formidable": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz",
|
||||
"integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==",
|
||||
"dependencies": {
|
||||
"dezalgo": "1.0.3",
|
||||
"hexoid": "1.0.0",
|
||||
"once": "1.4.0",
|
||||
"qs": "6.9.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/tunnckoCore/commissions"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
@@ -836,6 +865,14 @@
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
|
||||
},
|
||||
"node_modules/hexoid": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
|
||||
"integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/http-assert": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz",
|
||||
@@ -1503,6 +1540,17 @@
|
||||
"qrcode-terminal": "bin/qrcode-terminal.js"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.9.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz",
|
||||
"integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
@@ -2094,6 +2142,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -2357,6 +2410,15 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
||||
},
|
||||
"dezalgo": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
|
||||
"integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=",
|
||||
"requires": {
|
||||
"asap": "^2.0.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -2463,6 +2525,17 @@
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"formidable": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz",
|
||||
"integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==",
|
||||
"requires": {
|
||||
"dezalgo": "1.0.3",
|
||||
"hexoid": "1.0.0",
|
||||
"once": "1.4.0",
|
||||
"qs": "6.9.3"
|
||||
}
|
||||
},
|
||||
"fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
@@ -2533,6 +2606,11 @@
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
|
||||
},
|
||||
"hexoid": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
|
||||
"integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g=="
|
||||
},
|
||||
"http-assert": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz",
|
||||
@@ -3018,6 +3096,11 @@
|
||||
"resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz",
|
||||
"integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ=="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.9.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz",
|
||||
"integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw=="
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@koa/router": "^10.1.1",
|
||||
"commander": "^8.3.0",
|
||||
"dayjs": "^1.10.7",
|
||||
"formidable": "^2.0.1",
|
||||
"inquirer": "^8.2.0",
|
||||
"koa": "^2.13.4",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
|
||||
@@ -50,9 +50,12 @@ function getNickName(usernameOrContact) {
|
||||
|
||||
/** 获取群聊消息的发言人 */
|
||||
function getGroupMsgSpeaker(msg) {
|
||||
if (!isGroupMessage(msg) || msg.isSendBySelf) {
|
||||
if (!isGroupMessage(msg)) {
|
||||
return null;
|
||||
}
|
||||
if (msg.isSendBySelf) {
|
||||
return bot.contacts[bot.user.UserName];
|
||||
}
|
||||
const speakerID = msg["OriginalContent"].split(":", 1)[0];
|
||||
return bot.contacts[speakerID];
|
||||
}
|
||||
|
||||
@@ -75,10 +75,22 @@ async function initDB(dbFile) {
|
||||
|
||||
/** 将消息存储到数据库 */
|
||||
async function pushMessage(message) {
|
||||
const m = {};
|
||||
for (const key of Object.keys(message)) {
|
||||
m["$" + key] = message[key];
|
||||
}
|
||||
// NOTICE: sql的参数对象必须和sql语句中的占位符一一对应,不能多也不能少
|
||||
const m = {
|
||||
$messageID: message.messageID,
|
||||
$createTimestamp: message.createTimestamp,
|
||||
$type: message.type,
|
||||
$messageType: message.messageType,
|
||||
$fromName: message.fromName,
|
||||
$fromNickName: message.fromNickName,
|
||||
$toName: message.toName,
|
||||
$toNickName: message.toNickName,
|
||||
$isGroupMessage: message.isGroupMessage,
|
||||
$speakingGroupMemberName: message.speakingGroupMemberName,
|
||||
$content: message.content,
|
||||
$filename: message.filename,
|
||||
$displayFilename: message.displayFilename,
|
||||
};
|
||||
await pushStatement.run(m);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,3 +61,34 @@ async function start() {
|
||||
}
|
||||
|
||||
start();
|
||||
|
||||
// let sessionData = JSON.parse(
|
||||
// fs.readFileSync("/home/master/desktop/session.json"),
|
||||
// {
|
||||
// encoding: "utf-8",
|
||||
// }
|
||||
// );
|
||||
// const W = require("wechat4u");
|
||||
// const bot = new W(sessionData);
|
||||
// bot.on("uuid", (uuid) => {
|
||||
// qrCode.generate("https://login.weixin.qq.com/l/" + uuid, {
|
||||
// small: true,
|
||||
// });
|
||||
// });
|
||||
|
||||
// bot.on("login", async () => {
|
||||
// console.log("logged in");
|
||||
// fs.writeFileSync(
|
||||
// "/home/master/desktop/session.json",
|
||||
// JSON.stringify(bot.botData)
|
||||
// );
|
||||
// const { mediaId } = await bot.uploadMedia(
|
||||
// fs.createReadStream("/home/master/desktop/q.jpg"),
|
||||
// undefined,
|
||||
// "filehelper"
|
||||
// );
|
||||
// bot.sendPic(mediaId, "filehelper").then((rse) => {
|
||||
// console.log(rse);
|
||||
// });
|
||||
// });
|
||||
// bot.restart();
|
||||
|
||||
@@ -6,6 +6,7 @@ const {
|
||||
bot,
|
||||
getDisplayName,
|
||||
getGroupMsgSpeaker,
|
||||
getGroupMsgContent,
|
||||
getNickName,
|
||||
} = require("./bot.js");
|
||||
const {
|
||||
@@ -58,7 +59,9 @@ async function processMessage(msg) {
|
||||
speakingGroupMember: null,
|
||||
filename: null,
|
||||
displayFilename: msg["FileName"] || "",
|
||||
fileSize: msg.FileSize | 0,
|
||||
isGroupMessage: bot.Contact.isRoomContact(peerContact),
|
||||
isSentBySelf: msg.isSendBySelf,
|
||||
};
|
||||
|
||||
if (m.isGroupMessage) {
|
||||
@@ -71,18 +74,18 @@ async function processMessage(msg) {
|
||||
case bot.CONF.MSGTYPE_TEXT:
|
||||
// 文本消息
|
||||
m.type = "text";
|
||||
m.content = msg["Content"];
|
||||
m.content = m.isGroupMessage ? getGroupMsgContent(msg) : msg["Content"];
|
||||
break;
|
||||
case bot.CONF.MSGTYPE_EMOTICON:
|
||||
case bot.CONF.MSGTYPE_IMAGE: {
|
||||
const suffix = msgType === bot.CONF.MSGTYPE_IMAGE ? ".jpg" : ".gif";
|
||||
m.type = "img";
|
||||
m.content =
|
||||
`【${msgType === bot.CONF.MSGTYPE_IMAGE ? "图片" : "表情"}】` +
|
||||
`[${msgType === bot.CONF.MSGTYPE_IMAGE ? "图片" : "表情"}]` +
|
||||
(msg["FileName"] || "");
|
||||
if (msg["HasProductId"]) {
|
||||
m.type = "unknown";
|
||||
m.content = "【表情贴图】请在手机上查看";
|
||||
m.content = "[表情贴图]请在手机上查看";
|
||||
break;
|
||||
}
|
||||
const cachePath = composeCacheFilePath(msg, suffix);
|
||||
@@ -101,7 +104,7 @@ async function processMessage(msg) {
|
||||
}
|
||||
case bot.CONF.MSGTYPE_VOICE: {
|
||||
m.type = "voice";
|
||||
m.content = "【语音消息】";
|
||||
m.content = "[语音消息]";
|
||||
m.filename = composeFilename(msg, ".mp3");
|
||||
const { data } = await bot.getVoice(msg.MsgId);
|
||||
fs.writeFileSync(composeMediaFilePath(msg, m.filename), data);
|
||||
@@ -110,6 +113,7 @@ async function processMessage(msg) {
|
||||
case bot.CONF.MSGTYPE_VIDEO:
|
||||
case bot.CONF.MSGTYPE_MICROVIDEO: {
|
||||
m.type = "video";
|
||||
m.content = "[视频]";
|
||||
const suffix = ".mp4";
|
||||
const cachePath = composeCacheFilePath(msg, suffix);
|
||||
const res = await bot.getVideo(msg.MsgId);
|
||||
@@ -125,16 +129,16 @@ async function processMessage(msg) {
|
||||
break;
|
||||
}
|
||||
case bot.CONF.MSGTYPE_APP:
|
||||
m.type = "file";
|
||||
if (msg.AppMsgType === 5) {
|
||||
m.type = "text";
|
||||
m.content = "【链接】" + msg["FileName"] + ": " + msg["Url"];
|
||||
m.content = "[链接]" + msg["FileName"] + ": " + msg["Url"];
|
||||
break;
|
||||
} else if (msg.AppMsgType == 6) {
|
||||
m.type = "file";
|
||||
m.content = "[文件]" + m.displayFilename;
|
||||
if (!(msg.FileSize < 10 * 1024 * 1024)) {
|
||||
// 忽略10M以上的大文件
|
||||
m.type = "unknown";
|
||||
m.content = "【文件】" + m.displayFilename;
|
||||
m.filename = null;
|
||||
break;
|
||||
}
|
||||
@@ -178,11 +182,11 @@ async function processMessage(msg) {
|
||||
(m.speakingGroupMember && getDisplayName(m.speakingGroupMember)) || null,
|
||||
content: m.content,
|
||||
filename: m.filename,
|
||||
fileSize: m.fileSize,
|
||||
displayFilename: m.displayFilename,
|
||||
};
|
||||
const clientMsg = {
|
||||
msgID: m.messageID,
|
||||
composedID: m.composedID,
|
||||
messageID: m.composedID,
|
||||
createTimestamp: m.createTimestamp,
|
||||
type: m.type,
|
||||
fromID: m.fromID,
|
||||
@@ -192,15 +196,19 @@ async function processMessage(msg) {
|
||||
toName: m.toName,
|
||||
toNickName: m.toNickName,
|
||||
isGroupMessage: m.isGroupMessage,
|
||||
isSentBySelf: m.isSentBySelf,
|
||||
speakingGroupMemberID:
|
||||
m.speakingGroupMember && m.speakingGroupMember.UserName,
|
||||
speakingGroupMemberName:
|
||||
(m.speakingGroupMember && getDisplayName(m.speakingGroupMember)) || null,
|
||||
content: m.content,
|
||||
filename: m.filename,
|
||||
fileSize: m.fileSize,
|
||||
displayFilename: m.displayFilename,
|
||||
};
|
||||
await db.pushMessage(dbMsg);
|
||||
if (!bot.Contact.isSpContact(peerContact)) {
|
||||
await db.pushMessage(dbMsg);
|
||||
}
|
||||
socket.pushMessage(clientMsg);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"dbFile": "/home/master/desktop/wxbot.db",
|
||||
"dataDir": "/home/master/desktop",
|
||||
"serve": true,
|
||||
"httpHost": "127.0.0.1",
|
||||
"httpHost": "localhost",
|
||||
"httpPort": 9200,
|
||||
"debugMode": false,
|
||||
"baseUrl": "http:127.0.0.1",
|
||||
"baseUrl": "http://localhost:9200",
|
||||
"appCode": {
|
||||
"mattuy-laptop": "iasdjfkwe3ds-qw3rjkwjfda8"
|
||||
},
|
||||
|
||||
+155
-83
@@ -1,19 +1,21 @@
|
||||
const Koa = require("koa");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { promisify } = require("util");
|
||||
const Router = require("@koa/router");
|
||||
const cors = require("@koa/cors");
|
||||
const formidable = require("formidable");
|
||||
const dayjs = require("dayjs");
|
||||
const { Server } = require("socket.io");
|
||||
const { createServer } = require("http");
|
||||
const { bot, getDisplayName } = require("./bot.js");
|
||||
const { bot, getDisplayName, cacheAvatars } = require("./bot.js");
|
||||
const { config } = require("./config.js");
|
||||
const {
|
||||
textSHA1,
|
||||
ensureAvatarDir,
|
||||
ensureUploadDir,
|
||||
composeAvatarFileName,
|
||||
composeMediaFilePathFromID,
|
||||
getOriginalMsgID,
|
||||
} = require("./util.js");
|
||||
|
||||
const DEFAULT_ROOM = "default_room";
|
||||
@@ -26,67 +28,65 @@ const io = new Server(httpServer, {
|
||||
path: "/socket.io",
|
||||
});
|
||||
const activeSessions = new Set();
|
||||
const socketIDToSessionID = new Map();
|
||||
|
||||
io.use((socket, next) => {
|
||||
const auth = socket.handshake.auth;
|
||||
if (!auth) {
|
||||
next(Error("认证失败"));
|
||||
return;
|
||||
return next(Error("认证失败"));
|
||||
}
|
||||
const appName = auth.appName;
|
||||
const appCode = config.appCode[appName];
|
||||
if (!appCode || appCode !== auth.appCode) {
|
||||
next(Error("认证失败"));
|
||||
return;
|
||||
return next(Error("认证失败"));
|
||||
}
|
||||
if (textSHA1(auth.passphrase) !== config.appAuthHash[appName]) {
|
||||
next(Error("口令错误"));
|
||||
return;
|
||||
return next(Error("口令错误"));
|
||||
}
|
||||
if (activeSessions.has(socket.handshake.auth.sessionID)) {
|
||||
return next(Error("会话已存在"));
|
||||
}
|
||||
if (bot?.state !== bot.CONF.STATE.login) {
|
||||
next(Error("服务不可用"));
|
||||
return;
|
||||
return next(Error("服务不可用"));
|
||||
}
|
||||
next().then(() => {
|
||||
bot.sendText(
|
||||
`【上线通知】
|
||||
DATETIME: ${dayjs().format("YYYY-MM-DDTHH:MM:ss")}
|
||||
APP_NAME: ${appName}
|
||||
PLATFORM: ${auth.platform || ""}`,
|
||||
"filehelper"
|
||||
);
|
||||
next();
|
||||
const text = `【上线通知】${"\n"}⌜${appName}⌟ on ${
|
||||
auth.platform
|
||||
} connected at ${dayjs().format("YYYY-MM-DDTHH:MM:ss")}`;
|
||||
bot.sendText(text, "filehelper");
|
||||
pushMessage({
|
||||
messageID: "notice_" + (Date.now() + Math.random()),
|
||||
createTimestamp: Math.trunc(Date.now() / 1000),
|
||||
type: "text",
|
||||
fromID: "filehelper",
|
||||
fromName: "你自己",
|
||||
fromNickName: "你自己",
|
||||
toID: bot.user.UserName,
|
||||
toName: "你自己",
|
||||
toNickName: bot.user.NickName,
|
||||
isGroupMessage: false,
|
||||
isSentBySelf: false,
|
||||
content: text,
|
||||
});
|
||||
});
|
||||
|
||||
function deactivateSession() {
|
||||
activeSessions.delete(socket.id);
|
||||
function deactivateSession(socket) {
|
||||
activeSessions.delete(socket.handshake.auth.sessionID);
|
||||
socketIDToSessionID.delete(socket.id);
|
||||
}
|
||||
|
||||
async function pushMessage(message) {
|
||||
if (message.filename) {
|
||||
for (const sid of await io.allSockets()) {
|
||||
const msg = {
|
||||
...message,
|
||||
fileUrl: `${config.baseUrl}/session/${sid}/media/${message.composedID}/${message.filename}`,
|
||||
};
|
||||
io.to(sid).emit("message", msg);
|
||||
}
|
||||
} else {
|
||||
io.in(DEFAULT_ROOM).emit("message", message);
|
||||
}
|
||||
}
|
||||
|
||||
function login(sessionID) {
|
||||
function login(socket) {
|
||||
const u = bot.user;
|
||||
return {
|
||||
id: u.UserName,
|
||||
nickname: u.NickName,
|
||||
displayName: u.NickName,
|
||||
signature: u.Signature,
|
||||
avatar: `${config.baseUrl}/session/${sessionID}/avatar/${u.UserName}`,
|
||||
avatar: `${config.baseUrl}/session/${socketIDToSessionID.get(
|
||||
socket.id
|
||||
)}/avatar/${u.UserName}`,
|
||||
};
|
||||
}
|
||||
function getContacts(sessionID) {
|
||||
function getContacts(socket) {
|
||||
function wrapContact(contact) {
|
||||
return {
|
||||
id: contact.UserName,
|
||||
@@ -95,54 +95,73 @@ function getContacts(sessionID) {
|
||||
signature: contact.Signature,
|
||||
nameInitial: contact.PYInitial || contact.RemarkPYInitial,
|
||||
nameQuanPin: contact.PYQuanPin || contact.RemarkPYQuanPin,
|
||||
avatar: `${config.baseUrl}/session/${sessionID}/avatar/${contact.UserName}`,
|
||||
isFriend: contact.ContactFlag === 1,
|
||||
avatar: `${config.baseUrl}/session/${socketIDToSessionID.get(
|
||||
socket.id
|
||||
)}/avatar/${contact.UserName}`,
|
||||
type: null,
|
||||
};
|
||||
}
|
||||
const data = { contacts: [], specialContacts: [], groups: [] };
|
||||
for (const uid of Object.keys(bot.contacts)) {
|
||||
const contact = bot.contacts[uid];
|
||||
const wrapped = wrapContact(contact);
|
||||
if (bot.Contact.isPublicContact(contact)) {
|
||||
continue;
|
||||
} else if (bot.Contact.isSpContact(contact)) {
|
||||
if (["filehelper"].includes(contact.UserName)) {
|
||||
data.specialContacts.push(wrapContact(contact));
|
||||
wrapped.type = "special";
|
||||
data.specialContacts.push(wrapped);
|
||||
}
|
||||
continue;
|
||||
} else if (bot.Contact.isRoomContact(contact)) {
|
||||
wrapped.type = "group";
|
||||
data.groups.push(wrapContact(contact));
|
||||
} else if (!contact.isSelf) {
|
||||
wrapped.type = "friend";
|
||||
data.contacts.push(wrapContact(contact));
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
async function sendMessage({ type, target, filename, fileUri, data }) {
|
||||
async function sendMessage(_, { type, target, data, filename }) {
|
||||
const targetContact = bot.contacts[target];
|
||||
if (!targetContact) {
|
||||
return "联系人不存在";
|
||||
}
|
||||
const to = targetContact.UserName;
|
||||
let resp;
|
||||
switch (type) {
|
||||
case "text":
|
||||
await bot.sendText(data, target);
|
||||
resp = await bot.sendText(data, target);
|
||||
break;
|
||||
case "emotion":
|
||||
await bot.sendEmoticon(data, to);
|
||||
resp = await bot.sendEmoticon(data, target);
|
||||
break;
|
||||
case "img":
|
||||
case "video":
|
||||
case "file": {
|
||||
const filepath = path.join(ensureUploadDir(), fileUri);
|
||||
if (!fs.existsSync(filepath)) {
|
||||
const defaultFilenameMap = {
|
||||
img: "pic.jpg",
|
||||
video: "video.mp4",
|
||||
emotion: "pic.gif",
|
||||
file: "file",
|
||||
};
|
||||
const fileUri = data;
|
||||
const oldFilename = path.join(ensureUploadDir(), fileUri);
|
||||
const newFilename = path.join(
|
||||
ensureUploadDir(),
|
||||
filename || Date.now() + Math.random() + defaultFilenameMap[type]
|
||||
);
|
||||
if (!fs.existsSync(oldFilename)) {
|
||||
return "图片不存在";
|
||||
}
|
||||
let mediaId;
|
||||
try {
|
||||
fs.renameSync(oldFilename, newFilename);
|
||||
// NOTICE: uploadMedia函数会优先从file.path中取文件名
|
||||
const { mediaId: id } = await bot.uploadMedia(
|
||||
fs.createReadStream(filepath),
|
||||
filename,
|
||||
targetContact.UserName
|
||||
fs.createReadStream(newFilename),
|
||||
undefined,
|
||||
target
|
||||
);
|
||||
mediaId = id;
|
||||
} catch {
|
||||
@@ -150,56 +169,56 @@ async function sendMessage({ type, target, filename, fileUri, data }) {
|
||||
}
|
||||
try {
|
||||
if (type === "img") {
|
||||
await bot.sendPic(mediaId, to);
|
||||
resp = await bot.sendPic(mediaId, target);
|
||||
} else if (type === "video") {
|
||||
await bot.sendVideo(mediaId, to);
|
||||
resp = await bot.sendVideo(mediaId, target);
|
||||
} else {
|
||||
await bot.sendDoc(
|
||||
resp = await bot.sendDoc(
|
||||
mediaId,
|
||||
filename,
|
||||
fs.statSync(filepath).size,
|
||||
path.extname(filepath),
|
||||
to
|
||||
fs.statSync(newFilename).size,
|
||||
path.extname(newFilename),
|
||||
target
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
return e?.tips || e?.message || "消息发送失败";
|
||||
}
|
||||
// 上传后删除
|
||||
fs.rm(newFilename, () => {});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return "不支持的文件类型";
|
||||
}
|
||||
return {
|
||||
messageID: resp.MsgID,
|
||||
};
|
||||
}
|
||||
async function revokeMessage({ msgID, target }) {
|
||||
await bot.revokeMsg(msgID, target);
|
||||
}
|
||||
async function uploadFile({ data }) {
|
||||
const uploadDir = ensureUploadDir();
|
||||
const filename = dayjs().format("YYYYMMDD_") + String(Math.random()).slice(1);
|
||||
const filepath = path.join(uploadDir, filename);
|
||||
await promisify(fs.writeFile)(filepath, data);
|
||||
// 10分钟后删除上传的临时文件
|
||||
setTimeout(() => {
|
||||
if (fs.existsSync(filepath)) {
|
||||
fs.rmSync(filepath);
|
||||
}
|
||||
}, 600000);
|
||||
return { localFileName: filename };
|
||||
}
|
||||
function handleNotImplemented(_, cb) {
|
||||
cb?.call(null, "Not Implemented");
|
||||
async function revokeMessage(_, { composedID, target }) {
|
||||
await bot.revokeMsg(getOriginalMsgID(composedID), target);
|
||||
}
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
console.log("connected");
|
||||
activeSessions.add(socket.id);
|
||||
const sid = socket.handshake.auth.sessionID;
|
||||
activeSessions.add(sid);
|
||||
socketIDToSessionID.set(socket.id, sid);
|
||||
socket.join(DEFAULT_ROOM);
|
||||
socket.on("disconnect", deactivateSession);
|
||||
socket.on("login", login.bind(null, socket.id));
|
||||
socket.on("get_contacts", getContacts.bind(null, socket.id));
|
||||
socket.on("send_message", sendMessage);
|
||||
socket.on("revoke_message", revokeMessage);
|
||||
socket.on("upload_file", uploadFile);
|
||||
socket.onAny(handleNotImplemented);
|
||||
function wrapWithCallback(func) {
|
||||
return async function (data, callback) {
|
||||
const res = func.call(this, socket, data);
|
||||
if (res instanceof Promise) {
|
||||
callback && callback(await res);
|
||||
} else {
|
||||
callback && callback(res);
|
||||
}
|
||||
};
|
||||
}
|
||||
socket.on("disconnect", wrapWithCallback(deactivateSession));
|
||||
socket.on("login", wrapWithCallback(login));
|
||||
socket.on("get_contacts", wrapWithCallback(getContacts));
|
||||
socket.on("send_message", wrapWithCallback(sendMessage));
|
||||
socket.on("revoke_message", wrapWithCallback(revokeMessage));
|
||||
socket.use((_, next) => {
|
||||
if (bot?.state !== bot.CONF.STATE.login) {
|
||||
return next(Error("服务不可用"));
|
||||
@@ -220,20 +239,26 @@ router.get("/session/:sessionID/avatar/:userID", (ctx, next) => {
|
||||
const filename = composeAvatarFileName(contact);
|
||||
const filepath = filename && path.join(ensureAvatarDir(), filename);
|
||||
if (filepath && fs.existsSync(filepath)) {
|
||||
ctx.response.headers["Cache-Control"] = "max-age=3600";
|
||||
ctx.response.headers["Content-Type"] = "image/jpeg";
|
||||
ctx.response.body = fs.createReadStream(filepath);
|
||||
} else {
|
||||
cacheAvatars();
|
||||
ctx.throw(404);
|
||||
}
|
||||
next();
|
||||
});
|
||||
/** 获取消息文件,包含图片 */
|
||||
router.get("/session/:sessionID/media/:composedID/:filename", (ctx, next) => {
|
||||
if (!activeSessions.has(ctx.params.sessionID)) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
const filename = composeMediaFilePathFromID(
|
||||
ctx.params.composedID,
|
||||
ctx.params.filename
|
||||
);
|
||||
if (fs.existsSync(filename)) {
|
||||
ctx.response.headers["Cache-Control"] = "max-age=3600";
|
||||
ctx.response.body = fs.createReadStream(filename);
|
||||
} else {
|
||||
ctx.throw(404);
|
||||
@@ -241,10 +266,57 @@ router.get("/session/:sessionID/media/:composedID/:filename", (ctx, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
/** 上传文件 */
|
||||
router.post("/upload_file", async (ctx, next) => {
|
||||
if (!activeSessions.has(ctx.request.headers.session_id)) {
|
||||
ctx.throw(401);
|
||||
}
|
||||
const filename = dayjs().format("YYYYMMDD_") + String(Math.random()).slice(2);
|
||||
const filepath = path.join(ensureUploadDir(), filename);
|
||||
const form = formidable({ multiples: false });
|
||||
form.on("fileBegin", (_, file) => {
|
||||
file.filepath = filepath;
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
form.parse(ctx.req, (err, _, files) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.type = "application/json";
|
||||
ctx.response.body = JSON.stringify({ tempFileName: filename });
|
||||
// 10分钟后删除上传的临时文件
|
||||
setTimeout(() => {
|
||||
if (fs.existsSync(filepath)) {
|
||||
fs.rmSync(filepath);
|
||||
}
|
||||
}, 600000);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(router.routes());
|
||||
httpServer.listen(config.httpPort, config.httpHost);
|
||||
|
||||
async function pushMessage(message) {
|
||||
if (message.filename) {
|
||||
for (const sid of await io.allSockets()) {
|
||||
const sessionID = socketIDToSessionID.get(sid);
|
||||
const msg = {
|
||||
...message,
|
||||
fileUrl: `${config.baseUrl}/session/${sessionID}/media/${message.messageID}/${message.filename}`,
|
||||
};
|
||||
io.to(DEFAULT_ROOM).emit("message", msg);
|
||||
}
|
||||
} else {
|
||||
io.in(DEFAULT_ROOM).emit("message", message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pushMessage,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,9 @@ const { config } = require("./config");
|
||||
function composeMessageID(msg) {
|
||||
return dayjs.unix(msg["CreateTime"]).format("YYYYMMDD") + msg["MsgId"];
|
||||
}
|
||||
function getOriginalMsgID(composedID) {
|
||||
return composedID.slice(8);
|
||||
}
|
||||
function composeFilename(msg, suffix = "") {
|
||||
const dt = dayjs.unix(msg["CreateTime"]);
|
||||
const datetime = dt.format("YYYYMMDD");
|
||||
@@ -68,6 +71,7 @@ function textSHA1(text) {
|
||||
|
||||
module.exports = {
|
||||
composeMessageID,
|
||||
getOriginalMsgID,
|
||||
composeFilename,
|
||||
composeMediaFilePath,
|
||||
composeMediaFilePathFromID,
|
||||
|
||||
Reference in New Issue
Block a user