You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Global-sales/wai-server/core/baileys/index.js

443 lines
15 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

const {
makeWASocket,
Browsers,
DisconnectReason,
makeCacheableSignalKeyStore,
fetchLatestWaWebVersion,
getMessageFromStore,
useMultiFileAuthState,
downloadMediaMessage,
isJidNewsletter, isJidGroup, isJidBroadcast, isJidStatusBroadcast
} = require('@whiskeysockets/baileys');
const fs = require('fs');
const { writeFile } = require('fs/promises');
const waEmitter = require('../emitter');
const serverConfig = require('../../config').server;
const { encodeJid, decodeJid, formatStatus, formatTimestamp, getFileExtension, uint8ArrayToBase64, isJidPersonal } = require('./helper');
const generateId = require('../../utils/generateId.util');
const NodeCache = require('node-cache');
const P = require('pino');
// 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 });
// 缓存群信息,过期时间为 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 = 'warn';
const msgRetryCounterCache = new NodeCache();
const authStateFolder = './baileys_auth_info/' + phone;
const { state, saveCreds } = await useMultiFileAuthState(authStateFolder);
// 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: whatsAppVersion,
logger,
auth: {
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, logger),
},
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
});
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;
for (const msg of upsert.messages) {
// 没有类型的消息,先忽略
if (!msg.message) {
continue;
}
if (isJidStatusBroadcast(msg.key.remoteJid)) {
continue;
}
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) {
// 没有明确标识状态的更新,忽略
const ignore = msg.update === undefined || msg.update.status === undefined;
if (ignore) continue;
const standardMessage = await buildStandardMessage(msg)
const mergedMessage = Object.assign({}, standardMessage, {
eventSource: msgEventSource
});
console.info('updated.mergedMessage: ', mergedMessage);
//
waEmitter.emit('message:updated', mergedMessage);
}
}
const handleCredsUpdate = async () => {
await saveCreds();
}
const sendMessageHandler = (event) => {
const { to: number, externalId, ...content } = event;
const jid = encodeJid(number);
waSocket.sendMessage(
jid, content
).then(msg => {
externalIdCache.set(msg.key.id, externalId)
}).catch(ex => {
waEmitter.emit('message:updated', {
id: generateId(),
externalId,
status: 'failed',
direction: 'outbound',
from: whatsAppNo,
to: number,
error: `发送消息出错 ` + ex,
whatsAppNo,
eventSource: serverConfig.name + '.sendMessage.catch',
updateTime: formatTimestamp(new Date().getTime() / 1000),
});
});
}
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);
waEmitter.off('request.' + whatsAppNo + '.stop', stopHandler);
if ((lastDisconnect?.error)?.output?.statusCode !== DisconnectReason.loggedOut) {
start();
} else {
// logout 异步删除验证目录
fs.rm(authStateFolder, { recursive: true, force: true }, (err) => {
if (err) {
return console.error(`Error deleting directory: ${err.message}`);
}
console.log('Directory deleted successfully: ', authStateFolder);
});
waEmitter.emit('connection:close', {
whatsAppNo, channelId,
eventSource: serverConfig.name + '.connection.update.close',
status: 'offline',
});
}
} 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',
});
} else if (qr !== undefined) {
// WebSocket 创建成功等待扫码,如果没有扫码会更新 qr
// 第一次一分钟,后面是 20 秒更新一次
if (qrCode === null) {
qrCode = qr;
waEmitter.emit('creds:update', {
id: generateId(),
qr, whatsAppNo,
eventSource: serverConfig.name + '.connection.update.qr',
createTime: formatTimestamp(new Date().getTime() / 1000),
});
} else {
// 第一次二维码时效后退出,不需要等待更新二维码
waSocket.logout(() => '二维码已过期');
}
}
});
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: whatsAppVersion.join('.') + ', maybe',
channelId: channelId,
phone: phone,
start,
stop,
};
};
module.exports = {
createWhatsApp,
};