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

393 lines
14 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,
fetchLatestBaileysVersion,
getContentType,
makeCacheableSignalKeyStore,
makeInMemoryStore,
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;
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');
// Reference: https://github.com/WhiskeySockets/Baileys/blob/master/README.md
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 });
const logger = P({ timestamp: () => `,"time":"${new Date().toJSON()}"` }, P.destination('./logs/wa-logs-' + phone + '.txt'));
logger.level = 'trace';
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');
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);
}
});
fs.unlink(storeFilename, (err) => {
if (err && err.code !== 'ENOENT') {
console.error(`Error deleting storeFilename file: ${err.message}`);
} else if (!err) {
console.log('Successfully deleted storeFilename file: ', storeFilename);
}
});
waEmitter.emit('request.' + whatsAppNo + '.stop', {});
};
const start = () => {
const waSocket = makeWASocket({
version,
logger,
auth: {
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, logger),
},
// 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.ubuntu('Sales'),
msgRetryCounterCache,
generateHighQualityLinkPreview: false,
syncFullHistory: false,
});
store?.bind(waSocket.ev);
const parseTextMessage = original => {
const text = original.message?.conversation || original.message?.extendedTextMessage?.text;
return {
type: 'text',
text: {
body: text,
},
};
};
const parseImageMessage = async original => {
const imageMessage = original.message.imageMessage;
const fileExtension = getFileExtension(imageMessage.mimetype);
const imageBuffer = await downloadMediaMessage(
original, 'buffer', {}, { logger, reuploadRequest: waSocket.updateMediaMessage, },
);
const imageFilename = './temp/image_' + whatsAppNo + '_' + original.key.id + fileExtension;
await writeFile(imageFilename, imageBuffer);
return {
type: 'image',
image: {
mimetype: imageMessage.mimetype,
sha256: uint8ArrayToBase64(imageMessage.fileSha256),
caption: imageMessage.caption,
filePath: imageFilename,
link_original: imageMessage.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) => {
const contentType = getContentType(original);
// ...
if (original.message?.conversation || original.message?.extendedTextMessage?.text) return parseTextMessage;
if (original.message?.imageMessage) return parseImageMessage;
if (original.message?.templateMessage) return parseTemplateMessage;
return null;
};
const getEventSource = (upsert) => {
// source = prefix + infix + suffix
// prefix: server.name
// infix: upsert/update
// suffix: notify/append...
if (upsert.type === 'notify') {
return serverConfig.name + '.messages.upsert.notify';
} else if (upsert.type === 'append') {
return serverConfig.name + '.messages.upsert.append';
} else {
return serverConfig.name + '.messages.upsert.unknown';
}
};
const handleMessagesUpsert = async upsert => {
console.info('messages.upsert: ', JSON.stringify(upsert, undefined, 2));
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;
}
// 如果是群发(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)
}
const emitEventName = msg.key.fromMe ? 'message:updated' : 'message:received';
const msgEventSource = getEventSource(upsert);
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);
const standardMessage = {
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,
eventSource: msgEventSource,
updateTime: formatTimestamp(msg.messageTimestamp),
}
const messageParser = getMessageParser(msg);
if (messageParser) {
const parsedMessage = await messageParser(msg);
const mergedMessage = Object.assign({}, standardMessage, parsedMessage);
waEmitter.emit(emitEventName, mergedMessage);
} else {
console.info('不支持该消息类型:', msg);
}
}
}
const handleMessagesUpdate = async messageUpdate => {
console.info('messages.update: ', JSON.stringify(messageUpdate, undefined, 2));
for (const msg of messageUpdate) {
// 没有明确标识状态的更新,忽略
const ignore = msg.update === undefined || msg.update.status === undefined;
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 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.emit('connection:open', {
status: 'open', whatsAppNo, channelId,
eventSource: serverConfig.name + '.connection.update.open',
});
waEmitter.on('request.' + whatsAppNo + '.send.message', sendMessageHandler);
waEmitter.on('request.' + whatsAppNo + '.stop', stopHandler);
} 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);
};
return {
createTimestamp: Date.now(),
status: 'offline',
version: waVersion,
channelId: channelId,
phone: phone,
start,
stop,
};
};
module.exports = {
createWhatsApp,
};