ForcePilot/backend/package/yuxi/channels/adapters/whatsapp/baileys-bridge/send.js
Kris e9b57546ea feat(whatsapp): 新增WhatsApp适配器完整功能模块
新增大量WhatsApp适配器相关代码,包括账号管理、会话处理、消息收发、验证授权、媒体处理、互动命令、审批流程、健康检测等完整功能模块,搭建基础的Baileys协议WhatsApp接入能力
2026-05-12 00:51:58 +08:00

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 };