新增大量WhatsApp适配器相关代码,包括账号管理、会话处理、消息收发、验证授权、媒体处理、互动命令、审批流程、健康检测等完整功能模块,搭建基础的Baileys协议WhatsApp接入能力
493 lines
16 KiB
JavaScript
493 lines
16 KiB
JavaScript
const { downloadMediaMessage } = require('@whiskeysockets/baileys');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
function buildSendRoutes(app, getSock, notifySSE) {
|
|
|
|
// Send text message
|
|
app.post('/api/send', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, content, reply_to } = req.body;
|
|
const options = {};
|
|
if (reply_to) {
|
|
options.quoted = {
|
|
key: { remoteJid: jid, id: reply_to },
|
|
message: { conversation: '' },
|
|
};
|
|
}
|
|
|
|
const result = await sock.sendMessage(jid, { text: content }, options);
|
|
res.json({
|
|
success: true,
|
|
message_id: result?.key?.id || null,
|
|
});
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Send read receipt
|
|
app.post('/api/read-receipt', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, message_ids } = req.body;
|
|
const keys = (message_ids || []).map(id => ({
|
|
remoteJid: jid,
|
|
id: id,
|
|
}));
|
|
if (keys.length > 0) {
|
|
await sock.readMessages(keys);
|
|
}
|
|
res.json({ success: true });
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Send media message
|
|
app.post('/api/send-media', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, media_type, media_path, caption, reply_to } = req.body;
|
|
if (!fs.existsSync(media_path)) {
|
|
return res.json({ success: false, error: `File not found: ${media_path}` });
|
|
}
|
|
|
|
const mediaBuffer = fs.readFileSync(media_path);
|
|
const options = {};
|
|
if (reply_to) {
|
|
options.quoted = {
|
|
key: { remoteJid: jid, id: reply_to },
|
|
message: { conversation: '' },
|
|
};
|
|
}
|
|
if (caption) {
|
|
options.caption = caption;
|
|
}
|
|
|
|
let content;
|
|
const mimeTypes = {
|
|
image: 'image/jpeg',
|
|
video: 'video/mp4',
|
|
audio: 'audio/ogg',
|
|
document: 'application/octet-stream',
|
|
sticker: 'image/webp',
|
|
};
|
|
|
|
switch (media_type) {
|
|
case 'image':
|
|
content = { image: mediaBuffer, mimetype: 'image/jpeg' };
|
|
break;
|
|
case 'video':
|
|
content = { video: mediaBuffer, mimetype: 'video/mp4' };
|
|
break;
|
|
case 'audio':
|
|
content = { audio: mediaBuffer, mimetype: 'audio/ogg', ptt: false };
|
|
break;
|
|
case 'document':
|
|
const filename = path.basename(media_path);
|
|
const ext = path.extname(filename).toLowerCase();
|
|
content = { document: mediaBuffer, mimetype: mimeTypes.document, fileName: filename };
|
|
break;
|
|
case 'sticker':
|
|
content = { sticker: mediaBuffer };
|
|
break;
|
|
default:
|
|
return res.json({ success: false, error: `Unsupported media type: ${media_type}` });
|
|
}
|
|
|
|
const result = await sock.sendMessage(jid, content, options);
|
|
res.json({
|
|
success: true,
|
|
message_id: result?.key?.id || null,
|
|
});
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Send reaction
|
|
app.post('/api/react', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, message_id, emoji } = req.body;
|
|
await sock.sendMessage(jid, {
|
|
react: {
|
|
text: emoji,
|
|
key: { remoteJid: jid, id: message_id },
|
|
},
|
|
});
|
|
res.json({ success: true });
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Send presence update
|
|
app.post('/api/presence', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, presence } = req.body;
|
|
await sock.sendPresenceUpdate(presence || 'available', jid);
|
|
res.json({ success: true });
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Group list
|
|
app.post('/api/group/list', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const groups = await sock.groupFetchAllParticipating();
|
|
const groupList = Object.entries(groups).map(([id, info]) => ({
|
|
id,
|
|
subject: info.subject,
|
|
size: info.participants?.length || 0,
|
|
}));
|
|
res.json({ success: true, groups: groupList });
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Group info
|
|
app.post('/api/group/info', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid } = req.body;
|
|
const metadata = await sock.groupMetadata(jid);
|
|
res.json({ success: true, metadata });
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Message history (from in-memory store)
|
|
app.post('/api/message/history', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, limit = 50 } = req.body;
|
|
const messages = await sock.loadMessages(jid, limit);
|
|
res.json({ success: true, messages });
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Media download - returns raw buffer
|
|
app.post('/api/media/download', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { remoteJid, id, message } = req.body;
|
|
if (!message) {
|
|
return res.json({ success: false, error: 'Missing message object' });
|
|
}
|
|
|
|
const buffer = await downloadMediaMessage(
|
|
{ key: { remoteJid, id }, message },
|
|
'buffer',
|
|
{}
|
|
);
|
|
res.set('Content-Type', 'application/octet-stream');
|
|
res.send(buffer);
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// User profile picture
|
|
app.post('/api/user/picture', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid } = req.body;
|
|
const url = await sock.profilePictureUrl(jid, 'image').catch(() => null);
|
|
res.json({ success: true, url });
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Delete (revoke) message
|
|
app.post('/api/message/delete', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, message_id } = req.body;
|
|
const result = await sock.sendMessage(jid, {
|
|
delete: {
|
|
remoteJid: jid,
|
|
id: message_id,
|
|
participant: undefined,
|
|
},
|
|
});
|
|
res.json({
|
|
success: true,
|
|
message_id: result?.key?.id || null,
|
|
});
|
|
} catch (e) {
|
|
res.status(400).json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Create poll
|
|
app.post('/api/poll/create', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, name, options, selectable_count } = req.body;
|
|
if (!name || !options || !Array.isArray(options) || options.length < 2) {
|
|
return res.json({ success: false, error: 'Poll requires name and at least 2 options' });
|
|
}
|
|
if (options.length > 12) {
|
|
return res.json({ success: false, error: 'Poll supports max 12 options' });
|
|
}
|
|
|
|
const result = await sock.sendMessage(jid, {
|
|
poll: {
|
|
name,
|
|
values: options,
|
|
selectableCount: selectable_count || 1,
|
|
},
|
|
});
|
|
res.json({
|
|
success: true,
|
|
message_id: result?.key?.id || null,
|
|
});
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Send location
|
|
app.post('/api/location/send', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, latitude, longitude, name, address } = req.body;
|
|
if (latitude === undefined || longitude === undefined) {
|
|
return res.json({ success: false, error: 'Latitude and longitude required' });
|
|
}
|
|
|
|
const locationContent = {
|
|
location: {
|
|
degreesLatitude: latitude,
|
|
degreesLongitude: longitude,
|
|
},
|
|
};
|
|
if (name) locationContent.location.name = name;
|
|
if (address) locationContent.location.address = address;
|
|
|
|
const result = await sock.sendMessage(jid, locationContent);
|
|
res.json({
|
|
success: true,
|
|
message_id: result?.key?.id || null,
|
|
});
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Send contact
|
|
app.post('/api/contact/send', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, contacts } = req.body;
|
|
if (!contacts || !Array.isArray(contacts) || contacts.length === 0) {
|
|
return res.json({ success: false, error: 'Contacts array required' });
|
|
}
|
|
|
|
const vcardEntries = contacts.map(c => {
|
|
const displayName = c.displayName || c.fullName || '';
|
|
const phone = c.phone || '';
|
|
const org = c.organization || '';
|
|
let vcard = 'BEGIN:VCARD\nVERSION:3.0\n';
|
|
vcard += `FN:${displayName}\n`;
|
|
if (phone) vcard += `TEL;TYPE=CELL:${phone}\n`;
|
|
if (org) vcard += `ORG:${org}\n`;
|
|
vcard += 'END:VCARD';
|
|
return vcard;
|
|
});
|
|
|
|
const result = await sock.sendMessage(jid, {
|
|
contacts: {
|
|
displayName: contacts[0]?.displayName || contacts[0]?.fullName || 'Contact',
|
|
contacts: vcardEntries.map(vcard => ({ vcard })),
|
|
},
|
|
});
|
|
res.json({
|
|
success: true,
|
|
message_id: result?.key?.id || null,
|
|
});
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Send sticker (dedicated endpoint)
|
|
app.post('/api/sticker/send', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, sticker_path, reply_to } = req.body;
|
|
if (!sticker_path) {
|
|
return res.json({ success: false, error: 'Sticker path required' });
|
|
}
|
|
if (!fs.existsSync(sticker_path)) {
|
|
return res.json({ success: false, error: `File not found: ${sticker_path}` });
|
|
}
|
|
|
|
const stickerBuffer = fs.readFileSync(sticker_path);
|
|
const options = {};
|
|
if (reply_to) {
|
|
options.quoted = {
|
|
key: { remoteJid: jid, id: reply_to },
|
|
message: { conversation: '' },
|
|
};
|
|
}
|
|
|
|
const result = await sock.sendMessage(jid, { sticker: stickerBuffer }, options);
|
|
res.json({
|
|
success: true,
|
|
message_id: result?.key?.id || null,
|
|
});
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Send buttons message
|
|
app.post('/api/buttons/send', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, title, text, footer, buttons } = req.body;
|
|
if (!text) {
|
|
return res.json({ success: false, error: 'Text required' });
|
|
}
|
|
|
|
const buttonEntries = (buttons || []).map((btn, idx) => ({
|
|
buttonId: btn.id || `btn_${idx}`,
|
|
buttonText: { displayText: btn.text || btn.displayText || `Option ${idx + 1}` },
|
|
type: 1,
|
|
}));
|
|
|
|
const result = await sock.sendMessage(jid, {
|
|
buttonsMessage: {
|
|
title: title || '',
|
|
text,
|
|
footerText: footer || '',
|
|
buttons: buttonEntries,
|
|
headerType: 0,
|
|
},
|
|
});
|
|
res.json({
|
|
success: true,
|
|
message_id: result?.key?.id || null,
|
|
});
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
|
|
// Send list message
|
|
app.post('/api/list/send', async (req, res) => {
|
|
try {
|
|
const sock = getSock();
|
|
if (!sock) {
|
|
return res.json({ success: false, error: 'Not connected' });
|
|
}
|
|
|
|
const { jid, title, text, footer, button_text, sections } = req.body;
|
|
if (!text) {
|
|
return res.json({ success: false, error: 'Text required' });
|
|
}
|
|
|
|
const result = await sock.sendMessage(jid, {
|
|
listMessage: {
|
|
title: title || '',
|
|
description: text,
|
|
footerText: footer || '',
|
|
buttonText: button_text || 'Select',
|
|
listType: 0,
|
|
sections: (sections || []).map(section => ({
|
|
title: section.title || '',
|
|
rows: (section.rows || []).map(row => ({
|
|
title: row.title || '',
|
|
description: row.description || '',
|
|
rowId: row.id || row.rowId || row.title || '',
|
|
})),
|
|
})),
|
|
},
|
|
});
|
|
res.json({
|
|
success: true,
|
|
message_id: result?.key?.id || null,
|
|
});
|
|
} catch (e) {
|
|
res.json({ success: false, error: e.message });
|
|
}
|
|
});
|
|
}
|
|
|
|
module.exports = { buildSendRoutes }; |