|
|
|
|
@ -2,15 +2,14 @@ const {
|
|
|
|
|
makeWASocket,
|
|
|
|
|
Browsers,
|
|
|
|
|
DisconnectReason,
|
|
|
|
|
fetchLatestBaileysVersion,
|
|
|
|
|
makeCacheableSignalKeyStore,
|
|
|
|
|
makeInMemoryStore,
|
|
|
|
|
fetchLatestWaWebVersion,
|
|
|
|
|
getMessageFromStore,
|
|
|
|
|
useMultiFileAuthState,
|
|
|
|
|
downloadMediaMessage,
|
|
|
|
|
isJidNewsletter, isJidGroup, isJidBroadcast, isJidStatusBroadcast
|
|
|
|
|
} = require('@whiskeysockets/baileys');
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const { writeFile } = require('fs/promises');
|
|
|
|
|
const waEmitter = require('../emitter');
|
|
|
|
|
const serverConfig = require('../../config').server;
|
|
|
|
|
@ -20,208 +19,297 @@ const generateId = require('../../utils/generateId.util');
|
|
|
|
|
const NodeCache = require('node-cache');
|
|
|
|
|
const P = require('pino');
|
|
|
|
|
|
|
|
|
|
// https://baileys.whiskeysockets.io/
|
|
|
|
|
|
|
|
|
|
// Ref:
|
|
|
|
|
// https://github.com/WhiskeySockets/Baileys/blob/v6.7.19/README.md
|
|
|
|
|
// https://github.com/WhiskeySockets/Baileys/blob/v6.7.19/Example/example.ts
|
|
|
|
|
const createWhatsApp = async phone => {
|
|
|
|
|
let qrCode = null;
|
|
|
|
|
const channelId = generateId();
|
|
|
|
|
const whatsAppNo = phone;
|
|
|
|
|
// 缓存 msgId-externalId,过期时间为 5 分钟
|
|
|
|
|
const externalIdCache = new NodeCache({ stdTTL: 60*5 });
|
|
|
|
|
// 缓存群信息,过期时间为 24 小时
|
|
|
|
|
const groupSubjectCache = new NodeCache({ stdTTL: 60*60*24 });
|
|
|
|
|
// 缓存群信息,过期时间为 5 分钟
|
|
|
|
|
const groupCache = new NodeCache({stdTTL: 5 * 60, useClones: false})
|
|
|
|
|
const logger = P({ timestamp: () => `,"time":"${new Date().toJSON()}"` }, P.destination('./logs/wa-logs-' + phone + '.txt'));
|
|
|
|
|
logger.level = 'trace';
|
|
|
|
|
logger.level = 'warn';
|
|
|
|
|
const msgRetryCounterCache = new NodeCache();
|
|
|
|
|
const storeFilename = './baileys_auth_info/baileys_store_' + phone + '.json'
|
|
|
|
|
const store = makeInMemoryStore({ logger });
|
|
|
|
|
store?.readFromFile(storeFilename);
|
|
|
|
|
// save every 10s
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
store?.writeToFile(storeFilename);
|
|
|
|
|
}, 10_000);
|
|
|
|
|
|
|
|
|
|
const authStateFolder = './baileys_auth_info/' + phone;
|
|
|
|
|
const { state, saveCreds } = await useMultiFileAuthState(authStateFolder);
|
|
|
|
|
// fetch latest version of WA Web
|
|
|
|
|
// const { version, isLatest } = await fetchLatestBaileysVersion();
|
|
|
|
|
const { version, isLatest, } = { version: [2, 3000, 1025091846], isLatest: false };
|
|
|
|
|
const waVersion = version.join('.') + ', ' + (isLatest ? 'latest' : 'out');
|
|
|
|
|
// https://git.../WhiskeySockets/.../src/Utils/generics.ts
|
|
|
|
|
// const fetchLatestWaWebVersion, fetchLatestWaWebVersion
|
|
|
|
|
// https://web.whatsapp.com/sw.js, client_revision: 1031072708
|
|
|
|
|
const { version, isLatest } = await fetchLatestWaWebVersion();
|
|
|
|
|
const whatsAppVersion = version;//[2, 3000, 1031072708];
|
|
|
|
|
|
|
|
|
|
const stop = () => {
|
|
|
|
|
|
|
|
|
|
fs.rm(authStateFolder, { recursive: true, force: true }, (err) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
console.error(`Error deleting authStateFolder directory: ${err.message}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log('Successfully deleted authStateFolder directory: ', authStateFolder);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
waEmitter.emit('request.' + whatsAppNo + '.stop', {});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const start = () => {
|
|
|
|
|
|
|
|
|
|
const waSocket = makeWASocket({
|
|
|
|
|
version,
|
|
|
|
|
version: whatsAppVersion,
|
|
|
|
|
logger,
|
|
|
|
|
auth: {
|
|
|
|
|
creds: state.creds,
|
|
|
|
|
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
|
|
|
|
},
|
|
|
|
|
// https://github.com/WhiskeySockets/Baileys/blob/31bc8ab/src/Utils/generics.ts#L21
|
|
|
|
|
// https://github.com/WhiskeySockets/Baileys/blob/31bc8ab4e2c825c0d774875701ed07e20d05bdb6/WAProto/WAProto.proto
|
|
|
|
|
browser: Browsers.macOS('SAFARI'),//Browsers.macOS('SAFARI'),//Browsers.ubuntu('IOS_PHONE'),//Browsers.baileys('WEAR_OS'),//
|
|
|
|
|
cachedGroupMetadata: async (jid) => groupCache.get(jid),
|
|
|
|
|
getMessage: async (key) => await getMessageFromStore(key),
|
|
|
|
|
// https://github.com/WhiskeySockets/Baileys/blob/master/src/Utils/generics.ts
|
|
|
|
|
// https://github.com/WhiskeySockets/Baileys/blob/master/WAProto/WAProto.proto
|
|
|
|
|
// Browsers.macOS('SAFARI'), Browsers.ubuntu('IOS_PHONE'), Browsers.baileys('WEAR_OS'),
|
|
|
|
|
browser: Browsers.macOS('SAFARI'),
|
|
|
|
|
msgRetryCounterCache,
|
|
|
|
|
generateHighQualityLinkPreview: false,
|
|
|
|
|
syncFullHistory: false,
|
|
|
|
|
markOnlineOnConnect: false // Receive Notifications in Whatsapp App
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
store?.bind(waSocket.ev);
|
|
|
|
|
const getGroupSubject = async jid => {
|
|
|
|
|
const cachedMatadata = groupCache.get(jid);
|
|
|
|
|
|
|
|
|
|
if (cachedMatadata === undefined) {
|
|
|
|
|
const groupMetadata = await waSocket.groupMetadata(jid);
|
|
|
|
|
groupCache.set(jid, groupMetadata);
|
|
|
|
|
return groupMetadata.subject;
|
|
|
|
|
} else {
|
|
|
|
|
return cachedMatadata.subject;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const buildStandardMessage = async msg => {
|
|
|
|
|
// 如果是群发(xxx@broadcast),participant 是发送人,不然则是 remoteJid
|
|
|
|
|
const remoteNo = isJidBroadcast(msg.key.remoteJid) ? decodeJid(msg.key.participant) : decodeJid(msg.key.remoteJid);
|
|
|
|
|
const externalId = externalIdCache.get(msg.key.id);
|
|
|
|
|
const isPersonal = isJidPersonal(msg.key.remoteJid);
|
|
|
|
|
const conversationType = isPersonal ? 'individual' : 'group';
|
|
|
|
|
const isGroup = isJidGroup(msg.key.remoteJid);
|
|
|
|
|
const groupSubject = isGroup ? await getGroupSubject(msg.key.remoteJid) : '';
|
|
|
|
|
|
|
|
|
|
const msgDirection = msg.key.fromMe ? 'outbound' : 'inbound';
|
|
|
|
|
const msgFrom = msg.key.fromMe ? whatsAppNo : remoteNo;
|
|
|
|
|
const msgTo = msg.key.fromMe ? remoteNo : whatsAppNo;
|
|
|
|
|
const msgTimestamp = msg.messageTimestamp === undefined ? new Date().getTime() / 1000 : msg.messageTimestamp;
|
|
|
|
|
const originalStatus = msg.status || msg.update?.status;
|
|
|
|
|
const msgStatus = originalStatus === undefined ? '' : formatStatus(originalStatus);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: msg.key.id,
|
|
|
|
|
externalId,
|
|
|
|
|
status: msgStatus,
|
|
|
|
|
direction: msgDirection,
|
|
|
|
|
from: msgFrom,
|
|
|
|
|
to: msgTo,
|
|
|
|
|
conversation: {
|
|
|
|
|
type: conversationType,
|
|
|
|
|
name: groupSubject,
|
|
|
|
|
},
|
|
|
|
|
customerProfile: {
|
|
|
|
|
id: isGroup ? decodeJid(msg.key.participant) : decodeJid(msg.key.remoteJid),
|
|
|
|
|
// 商业号使用 verifiedBizName,个人使用 pushName
|
|
|
|
|
name: msg.verifiedBizName || msg.pushName,
|
|
|
|
|
},
|
|
|
|
|
whatsAppNo,
|
|
|
|
|
fromMe: msg.key.fromMe,
|
|
|
|
|
updateTime: formatTimestamp(msgTimestamp),
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const saveMediaFile = async (original, fileName) => {
|
|
|
|
|
const mediaBuffer = await downloadMediaMessage(
|
|
|
|
|
original, 'buffer', {}, { logger, reuploadRequest: waSocket.updateMediaMessage, },
|
|
|
|
|
);
|
|
|
|
|
await writeFile(fileName, mediaBuffer);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const parseTextMessage = original => {
|
|
|
|
|
const text = original.message?.conversation;
|
|
|
|
|
return {
|
|
|
|
|
type: 'text',
|
|
|
|
|
text: {
|
|
|
|
|
body: text,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const parseExtendedTextMessage = original => {
|
|
|
|
|
const text = original.message?.extendedTextMessage?.text;
|
|
|
|
|
return {
|
|
|
|
|
type: 'text',
|
|
|
|
|
text: {
|
|
|
|
|
body: text,
|
|
|
|
|
},
|
|
|
|
|
context: {
|
|
|
|
|
from: decodeJid(original.message?.extendedTextMessage?.contextInfo?.participant),
|
|
|
|
|
id: original.message?.extendedTextMessage?.contextInfo?.stanzaId,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const parseReactionMessage = original => {
|
|
|
|
|
const text = original.message?.reactionMessage?.text;
|
|
|
|
|
const context = original.message?.reactionMessage?.key?.id;
|
|
|
|
|
return {
|
|
|
|
|
type: 'reaction',
|
|
|
|
|
reaction: {
|
|
|
|
|
message_id: context,
|
|
|
|
|
emoji: text,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const parseEditedMessage = original => {
|
|
|
|
|
const text = original.message?.protocolMessage?.editedMessage?.conversation;
|
|
|
|
|
return {
|
|
|
|
|
type: 'text',
|
|
|
|
|
text: {
|
|
|
|
|
body: text,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const parseImageMessage = async original => {
|
|
|
|
|
const imageMessage = original.message.imageMessage;
|
|
|
|
|
const fileExtension = getFileExtension(imageMessage.mimetype);
|
|
|
|
|
const imageFilename = './temp/image_' + whatsAppNo + '_' + original.key.id + fileExtension;
|
|
|
|
|
await saveMediaFile(original, imageFilename);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: 'image',
|
|
|
|
|
image: {
|
|
|
|
|
mimetype: imageMessage.mimetype,
|
|
|
|
|
sha256: uint8ArrayToBase64(imageMessage.fileSha256),
|
|
|
|
|
caption: imageMessage.caption,
|
|
|
|
|
filePath: imageFilename,
|
|
|
|
|
link_original: imageMessage.url,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const parseAudioMessage = async original => {
|
|
|
|
|
const audioMessage = original.message.audioMessage;
|
|
|
|
|
const fileExtension = getFileExtension(audioMessage.mimetype);
|
|
|
|
|
const audioFilename = './temp/audio_' + whatsAppNo + '_' + original.key.id + fileExtension;
|
|
|
|
|
await saveMediaFile(original, audioFilename);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: 'audio',
|
|
|
|
|
audio: {
|
|
|
|
|
mimetype: audioMessage.mimetype,
|
|
|
|
|
sha256: uint8ArrayToBase64(audioMessage.fileSha256),
|
|
|
|
|
filePath: audioFilename,
|
|
|
|
|
link_original: audioMessage.url,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const parseDocumentMessage = async original => {
|
|
|
|
|
const documentMessage = original.message.documentMessage || original.message.documentWithCaptionMessage.message.documentMessage;
|
|
|
|
|
const documentFilename = './temp/file_' + whatsAppNo + '_' + original.key.id + '_' + documentMessage.fileName;
|
|
|
|
|
await saveMediaFile(original, documentFilename);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: 'document',
|
|
|
|
|
document: {
|
|
|
|
|
filename: documentMessage.fileName,
|
|
|
|
|
mimetype: documentMessage.mimetype,
|
|
|
|
|
sha256: uint8ArrayToBase64(documentMessage.fileSha256),
|
|
|
|
|
caption: documentMessage.caption,
|
|
|
|
|
filePath: documentFilename,
|
|
|
|
|
link_original: documentMessage.url,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const parseTemplateMessage = original => {
|
|
|
|
|
if (original.message.templateMessage.hydratedTemplate) {
|
|
|
|
|
const text = original.message.templateMessage?.hydratedTemplate?.hydratedContentText;
|
|
|
|
|
return {
|
|
|
|
|
type: 'text',
|
|
|
|
|
text: {
|
|
|
|
|
body: text,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
} else if (original.message.templateMessage.interactiveMessageTemplate) {
|
|
|
|
|
const text = original.message.templateMessage.interactiveMessageTemplate?.body?.text;
|
|
|
|
|
return {
|
|
|
|
|
type: 'text',
|
|
|
|
|
text: {
|
|
|
|
|
body: text,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getMessageParser = (original) => {
|
|
|
|
|
|
|
|
|
|
if (original.message?.conversation) return parseTextMessage;
|
|
|
|
|
|
|
|
|
|
if (original.message?.extendedTextMessage) return parseExtendedTextMessage;
|
|
|
|
|
|
|
|
|
|
if (original.message?.reactionMessage) return parseReactionMessage;
|
|
|
|
|
|
|
|
|
|
if (original.message?.protocolMessage?.editedMessage) return parseEditedMessage;
|
|
|
|
|
|
|
|
|
|
if (original.message?.imageMessage) return parseImageMessage;
|
|
|
|
|
|
|
|
|
|
if (original.message?.documentMessage || original.message?.documentWithCaptionMessage) return parseDocumentMessage;
|
|
|
|
|
|
|
|
|
|
if (original.message?.audioMessage) return parseAudioMessage;
|
|
|
|
|
|
|
|
|
|
if (original.message?.templateMessage) return parseTemplateMessage;
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleMessagesUpsert = async upsert => {
|
|
|
|
|
console.info('messages.upsert: ', JSON.stringify(upsert, undefined, 2));
|
|
|
|
|
const msgEventSource = serverConfig.name + '.messages.upsert.' + upsert.type;
|
|
|
|
|
|
|
|
|
|
if (upsert.type === 'notify') {
|
|
|
|
|
for (const msg of upsert.messages) {
|
|
|
|
|
|
|
|
|
|
// 没有类型的消息,先忽略
|
|
|
|
|
if (!msg.message) {
|
|
|
|
|
console.info('!msg.message, ignored.');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isJidStatusBroadcast(msg.key.remoteJid)) {
|
|
|
|
|
console.info('isJidStatusBroadcast, ignored.');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (isJidNewsletter(msg.key.remoteJid)) {
|
|
|
|
|
console.info('isJidNewsletter, ignored.');
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const messageType = Object.keys(msg.message)[0];
|
|
|
|
|
// 如果是群发(xxx@broadcast),participant 是发送人,不然则是 remoteJid
|
|
|
|
|
const remoteNo = isJidBroadcast(msg.key.remoteJid) ? decodeJid(msg.key.participant) : decodeJid(msg.key.remoteJid);
|
|
|
|
|
const externalId = externalIdCache.get(msg.key.id);
|
|
|
|
|
const isPersonal = isJidPersonal(msg.key.remoteJid);
|
|
|
|
|
const conversationType = isPersonal ? 'individual' : 'group';
|
|
|
|
|
const isGroup = isJidGroup(msg.key.remoteJid);
|
|
|
|
|
let groupSubject = groupSubjectCache.get(msg.key.remoteJid);
|
|
|
|
|
if (isGroup && groupSubject === undefined) {
|
|
|
|
|
const groupMetadata = await waSocket.groupMetadata(msg.key.remoteJid);
|
|
|
|
|
groupSubject = groupMetadata.subject;
|
|
|
|
|
groupSubjectCache.set(msg.key.remoteJid, groupMetadata.subject)
|
|
|
|
|
}
|
|
|
|
|
for (const msg of upsert.messages) {
|
|
|
|
|
|
|
|
|
|
const emitEventName = msg.key.fromMe ? 'message:updated' : 'message:received';
|
|
|
|
|
const msgDirection = msg.key.fromMe ? 'outbound' : 'inbound';
|
|
|
|
|
const msgFrom = msg.key.fromMe ? whatsAppNo : remoteNo;
|
|
|
|
|
const msgTo = msg.key.fromMe ? remoteNo : whatsAppNo;
|
|
|
|
|
const msgStatus = msg.status === undefined ? '' : formatStatus(msg.status);
|
|
|
|
|
|
|
|
|
|
if (msg.message?.conversation || msg.message?.extendedTextMessage?.text) {
|
|
|
|
|
|
|
|
|
|
const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text;
|
|
|
|
|
|
|
|
|
|
waEmitter.emit(emitEventName, {
|
|
|
|
|
id: msg.key.id,
|
|
|
|
|
externalId,
|
|
|
|
|
status: msgStatus,
|
|
|
|
|
direction: msgDirection,
|
|
|
|
|
from: msgFrom,
|
|
|
|
|
to: msgTo,
|
|
|
|
|
type: 'text',
|
|
|
|
|
text: {
|
|
|
|
|
body: text,
|
|
|
|
|
},
|
|
|
|
|
conversation: {
|
|
|
|
|
type: conversationType,
|
|
|
|
|
name: groupSubject,
|
|
|
|
|
},
|
|
|
|
|
customerProfile: {
|
|
|
|
|
id: decodeJid(msg.key.participant),
|
|
|
|
|
name: msg.pushName,
|
|
|
|
|
},
|
|
|
|
|
whatsAppNo,
|
|
|
|
|
fromMe: msg.key.fromMe,
|
|
|
|
|
eventSource: serverConfig.name + '.messages.upsert.notify',
|
|
|
|
|
updateTime: formatTimestamp(msg.messageTimestamp),
|
|
|
|
|
});
|
|
|
|
|
} else if (messageType === 'imageMessage') {
|
|
|
|
|
const imageMessage = msg.message.imageMessage;
|
|
|
|
|
const fileExtension = getFileExtension(imageMessage.mimetype);
|
|
|
|
|
const imageBuffer = await downloadMediaMessage(
|
|
|
|
|
msg, 'buffer', {}, { logger, reuploadRequest: waSocket.updateMediaMessage, },
|
|
|
|
|
);
|
|
|
|
|
const imageFilename = './temp/image_' + whatsAppNo + '_' + msg.key.id + fileExtension;
|
|
|
|
|
await writeFile(imageFilename, imageBuffer);
|
|
|
|
|
|
|
|
|
|
waEmitter.emit(emitEventName, {
|
|
|
|
|
id: msg.key.id,
|
|
|
|
|
externalId,
|
|
|
|
|
status: msgStatus,
|
|
|
|
|
direction: msgDirection,
|
|
|
|
|
from: msgFrom,
|
|
|
|
|
to: msgTo,
|
|
|
|
|
type: 'image',
|
|
|
|
|
image: {
|
|
|
|
|
mimetype: imageMessage.mimetype,
|
|
|
|
|
sha256: uint8ArrayToBase64(imageMessage.fileSha256),
|
|
|
|
|
caption: imageMessage.caption,
|
|
|
|
|
filePath: imageFilename,
|
|
|
|
|
link_original: imageMessage.url,
|
|
|
|
|
},
|
|
|
|
|
conversation: {
|
|
|
|
|
type: conversationType,
|
|
|
|
|
name: groupSubject,
|
|
|
|
|
},
|
|
|
|
|
customerProfile: {
|
|
|
|
|
id: decodeJid(msg.key.participant),
|
|
|
|
|
name: msg.pushName,
|
|
|
|
|
},
|
|
|
|
|
whatsAppNo,
|
|
|
|
|
fromMe: msg.key.fromMe,
|
|
|
|
|
eventSource: serverConfig.name + '.messages.upsert.notify',
|
|
|
|
|
updateTime: formatTimestamp(msg.messageTimestamp),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
// 没有类型的消息,先忽略
|
|
|
|
|
if (!msg.message) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isJidStatusBroadcast(msg.key.remoteJid)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
} else if (upsert.type === 'append') {
|
|
|
|
|
for (const msg of upsert.messages) {
|
|
|
|
|
if (msg.message?.conversation || msg.message?.extendedTextMessage?.text) {
|
|
|
|
|
const text = msg.message?.conversation || msg.message?.extendedTextMessage?.text;
|
|
|
|
|
// 如果是群发(xxx@broadcast),participant 是发送人,不然则是 remoteJid
|
|
|
|
|
const remoteNo = isJidBroadcast(msg.key.remoteJid) ? decodeJid(msg.key.participant) : decodeJid(msg.key.remoteJid);
|
|
|
|
|
const externalId = externalIdCache.get(msg.key.id);
|
|
|
|
|
const isPersonal = isJidPersonal(msg.key.remoteJid);
|
|
|
|
|
const conversationType = isPersonal ? 'individual' : 'group';
|
|
|
|
|
|
|
|
|
|
const emitEventName = msg.key.fromMe ? 'message:updated' : 'message:received';
|
|
|
|
|
const msgDirection = msg.key.fromMe ? 'outbound' : 'inbound';
|
|
|
|
|
const msgFrom = msg.key.fromMe ? whatsAppNo : remoteNo;
|
|
|
|
|
const msgTo = msg.key.fromMe ? remoteNo : whatsAppNo;
|
|
|
|
|
const msgStatus = msg.status === undefined ? '' : formatStatus(msg.status);
|
|
|
|
|
|
|
|
|
|
waEmitter.emit(emitEventName, {
|
|
|
|
|
id: msg.key.id,
|
|
|
|
|
externalId,
|
|
|
|
|
status: msgStatus,
|
|
|
|
|
direction: msgDirection,
|
|
|
|
|
from: msgFrom,
|
|
|
|
|
to: msgTo,
|
|
|
|
|
type: 'text',
|
|
|
|
|
text: {
|
|
|
|
|
body: text,
|
|
|
|
|
},
|
|
|
|
|
conversation: {
|
|
|
|
|
type: conversationType,
|
|
|
|
|
},
|
|
|
|
|
customerProfile: {
|
|
|
|
|
id: decodeJid(msg.participant),
|
|
|
|
|
name: msg.pushName,
|
|
|
|
|
},
|
|
|
|
|
whatsAppNo,
|
|
|
|
|
fromMe: msg.key.fromMe,
|
|
|
|
|
eventSource: serverConfig.name + '.messages.upsert.append',
|
|
|
|
|
updateTime: formatTimestamp(msg.messageTimestamp),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (isJidNewsletter(msg.key.remoteJid)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const standardMessage = await buildStandardMessage(msg)
|
|
|
|
|
const messageParser = getMessageParser(msg);
|
|
|
|
|
|
|
|
|
|
if (messageParser) {
|
|
|
|
|
const parsedMessage = await messageParser(msg);
|
|
|
|
|
const mergedMessage = Object.assign({}, standardMessage, parsedMessage, {
|
|
|
|
|
eventSource: msgEventSource
|
|
|
|
|
});
|
|
|
|
|
console.info('upsert.mergedMessage: ', mergedMessage);
|
|
|
|
|
|
|
|
|
|
const emitEventName = msg.key.fromMe ? 'message:updated' : 'message:received';
|
|
|
|
|
waEmitter.emit(emitEventName, mergedMessage);
|
|
|
|
|
} else {
|
|
|
|
|
console.info('不支持该消息类型:', msg);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleMessagesUpdate = async messageUpdate => {
|
|
|
|
|
console.info('messages.update: ', JSON.stringify(messageUpdate, undefined, 2));
|
|
|
|
|
const msgEventSource = serverConfig.name + '.messages.updated'
|
|
|
|
|
|
|
|
|
|
for (const msg of messageUpdate) {
|
|
|
|
|
|
|
|
|
|
@ -230,36 +318,14 @@ const createWhatsApp = async phone => {
|
|
|
|
|
|
|
|
|
|
if (ignore) continue;
|
|
|
|
|
|
|
|
|
|
// 如果是群发(xxx@broadcast),participant 是发送人,不然则是 remoteJid
|
|
|
|
|
const remoteNo = isJidBroadcast(msg.key.remoteJid) ? decodeJid(msg.key.participant) : decodeJid(msg.key.remoteJid);
|
|
|
|
|
const externalId = externalIdCache.get(msg.key.id);
|
|
|
|
|
const isPersonal = isJidPersonal(msg.key.remoteJid);
|
|
|
|
|
const conversationType = isPersonal ? 'individual' : 'group';
|
|
|
|
|
|
|
|
|
|
const msgDirection = msg.key.fromMe ? 'outbound' : 'inbound';
|
|
|
|
|
const msgFrom = msg.key.fromMe ? whatsAppNo : remoteNo;
|
|
|
|
|
const msgTo = msg.key.fromMe ? remoteNo : whatsAppNo;
|
|
|
|
|
const msgStatus = formatStatus(msg.update.status);
|
|
|
|
|
|
|
|
|
|
waEmitter.emit('message:updated', {
|
|
|
|
|
id: msg.key.id,
|
|
|
|
|
externalId,
|
|
|
|
|
status: msgStatus,
|
|
|
|
|
direction: msgDirection,
|
|
|
|
|
from: msgFrom,
|
|
|
|
|
to: msgTo,
|
|
|
|
|
conversation: {
|
|
|
|
|
type: conversationType,
|
|
|
|
|
},
|
|
|
|
|
customerProfile: {
|
|
|
|
|
id: decodeJid(msg.key.participant),
|
|
|
|
|
name: msg.pushName,
|
|
|
|
|
},
|
|
|
|
|
whatsAppNo,
|
|
|
|
|
fromMe: msg.key.fromMe,
|
|
|
|
|
eventSource: serverConfig.name + '.messages.updated',
|
|
|
|
|
updateTime: formatTimestamp(new Date().getTime() / 1000),
|
|
|
|
|
const standardMessage = await buildStandardMessage(msg)
|
|
|
|
|
const mergedMessage = Object.assign({}, standardMessage, {
|
|
|
|
|
eventSource: msgEventSource
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.info('updated.mergedMessage: ', mergedMessage);
|
|
|
|
|
//
|
|
|
|
|
waEmitter.emit('message:updated', mergedMessage);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -290,13 +356,22 @@ const createWhatsApp = async phone => {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const stopHandler = () => {
|
|
|
|
|
waSocket.ev.off('messages.upsert', handleMessagesUpsert);
|
|
|
|
|
waSocket.ev.off('messages.update', handleMessagesUpdate);
|
|
|
|
|
waSocket.ev.off('creds.update', handleCredsUpdate);
|
|
|
|
|
waSocket.logout(() => '实例已停止');
|
|
|
|
|
waEmitter.off('request.' + whatsAppNo + '.send.message', sendMessageHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
waSocket.ev.on('connection.update', async update => {
|
|
|
|
|
|
|
|
|
|
const { connection, lastDisconnect, qr } = update;
|
|
|
|
|
|
|
|
|
|
if (connection === 'close') {
|
|
|
|
|
waEmitter.off('request.' + whatsAppNo + '.send.message', sendMessageHandler);
|
|
|
|
|
if((lastDisconnect?.error)?.output?.statusCode !== DisconnectReason.loggedOut) {
|
|
|
|
|
waEmitter.off('request.' + whatsAppNo + '.stop', stopHandler);
|
|
|
|
|
if ((lastDisconnect?.error)?.output?.statusCode !== DisconnectReason.loggedOut) {
|
|
|
|
|
start();
|
|
|
|
|
} else {
|
|
|
|
|
// logout 异步删除验证目录
|
|
|
|
|
@ -313,12 +388,12 @@ const createWhatsApp = async phone => {
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else if (connection === 'open') {
|
|
|
|
|
waEmitter.on('request.' + whatsAppNo + '.send.message', sendMessageHandler);
|
|
|
|
|
waEmitter.on('request.' + whatsAppNo + '.stop', stopHandler);
|
|
|
|
|
waEmitter.emit('connection:open', {
|
|
|
|
|
status: 'open', whatsAppNo, channelId,
|
|
|
|
|
eventSource: serverConfig.name + '.connection.update.open',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
waEmitter.on('request.' + whatsAppNo + '.send.message', sendMessageHandler);
|
|
|
|
|
} else if (qr !== undefined) {
|
|
|
|
|
// WebSocket 创建成功等待扫码,如果没有扫码会更新 qr
|
|
|
|
|
// 第一次一分钟,后面是 20 秒更新一次
|
|
|
|
|
@ -341,15 +416,24 @@ const createWhatsApp = async phone => {
|
|
|
|
|
waSocket.ev.on('creds.update', handleCredsUpdate);
|
|
|
|
|
waSocket.ev.on('messages.upsert', handleMessagesUpsert);
|
|
|
|
|
waSocket.ev.on('messages.update', handleMessagesUpdate);
|
|
|
|
|
waSocket.ev.on('groups.update', async ([event]) => {
|
|
|
|
|
const metadata = await waSocket.groupMetadata(event.id);
|
|
|
|
|
groupCache.set(event.id, metadata);
|
|
|
|
|
})
|
|
|
|
|
waSocket.ev.on('group-participants.update', async (event) => {
|
|
|
|
|
const metadata = await waSocket.groupMetadata(event.id)
|
|
|
|
|
groupCache.set(event.id, metadata);
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
createTimestamp: Date.now(),
|
|
|
|
|
status: 'offline',
|
|
|
|
|
version: waVersion,
|
|
|
|
|
version: whatsAppVersion.join('.') + ', maybe',
|
|
|
|
|
channelId: channelId,
|
|
|
|
|
phone: phone,
|
|
|
|
|
start,
|
|
|
|
|
stop,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|