mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-14 14:12:44 +00:00
Add WhatsApp dashboard pairing flow
This commit is contained in:
parent
6015ee5d2a
commit
4f620a0bbc
6 changed files with 1485 additions and 41 deletions
|
|
@ -100,7 +100,9 @@ try {
|
|||
.slice(0, 16);
|
||||
} catch {}
|
||||
const PAIR_ONLY = args.includes('--pair-only');
|
||||
const PAIR_JSON = args.includes('--pair-json');
|
||||
const WHATSAPP_MODE = getArg('mode', process.env.WHATSAPP_MODE || 'self-chat'); // "bot" or "self-chat"
|
||||
const WHATSAPP_DM_POLICY = String(process.env.WHATSAPP_DM_POLICY || 'open').trim().toLowerCase();
|
||||
const ALLOWED_USERS = parseAllowedUsers(process.env.WHATSAPP_ALLOWED_USERS || '');
|
||||
const DEFAULT_REPLY_PREFIX = '⚕ *Hermes Agent*\n────────────\n';
|
||||
const REPLY_PREFIX = process.env.WHATSAPP_REPLY_PREFIX === undefined
|
||||
|
|
@ -197,6 +199,23 @@ function normalizeWhatsAppId(value) {
|
|||
return String(value).replace(':', '@');
|
||||
}
|
||||
|
||||
function redactWhatsAppId(value) {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return '';
|
||||
const [userPart, domainPart = ''] = raw.split('@', 2);
|
||||
const bare = userPart.split(':', 1)[0];
|
||||
const digits = bare.replace(/\D/g, '');
|
||||
const suffix = digits ? digits.slice(-4) : bare.slice(-4);
|
||||
return `${suffix ? `…${suffix}` : '…'}${domainPart ? `@${domainPart}` : ''}`;
|
||||
}
|
||||
|
||||
function emitDebugEvent(payload) {
|
||||
if (!WHATSAPP_DEBUG) return;
|
||||
try {
|
||||
console.log(JSON.stringify({ event: 'debug', ...payload }));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getMessageContent(msg) {
|
||||
const content = msg?.message || {};
|
||||
if (content.ephemeralMessage?.message) return content.ephemeralMessage.message;
|
||||
|
|
@ -360,6 +379,13 @@ function rememberSentId(id) {
|
|||
let sock = null;
|
||||
let connectionState = 'disconnected';
|
||||
|
||||
function emitPairEvent(event) {
|
||||
if (!PAIR_JSON) return;
|
||||
try {
|
||||
console.log(JSON.stringify({ ts: Date.now(), ...event }));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function startSocket() {
|
||||
const { state, saveCreds } = await useMultiFileAuthState(SESSION_DIR);
|
||||
const { version } = await fetchLatestBaileysVersion();
|
||||
|
|
@ -387,9 +413,13 @@ async function startSocket() {
|
|||
const { connection, lastDisconnect, qr } = update;
|
||||
|
||||
if (qr) {
|
||||
console.log('\n📱 Scan this QR code with WhatsApp on your phone:\n');
|
||||
qrcode.generate(qr, { small: true });
|
||||
console.log('\nWaiting for scan...\n');
|
||||
if (PAIR_JSON) {
|
||||
emitPairEvent({ event: 'qr', qr });
|
||||
} else {
|
||||
console.log('\n📱 Scan this QR code with WhatsApp on your phone:\n');
|
||||
qrcode.generate(qr, { small: true });
|
||||
console.log('\nWaiting for scan...\n');
|
||||
}
|
||||
}
|
||||
|
||||
if (connection === 'close') {
|
||||
|
|
@ -397,22 +427,39 @@ async function startSocket() {
|
|||
connectionState = 'disconnected';
|
||||
|
||||
if (reason === DisconnectReason.loggedOut) {
|
||||
console.log('❌ Logged out. Delete session and restart to re-authenticate.');
|
||||
emitPairEvent({ event: 'error', error: 'logged_out', reason });
|
||||
if (!PAIR_JSON) {
|
||||
console.log('❌ Logged out. Delete session and restart to re-authenticate.');
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
// 515 = restart requested (common after pairing). Always reconnect.
|
||||
if (reason === 515) {
|
||||
console.log('↻ WhatsApp requested restart (code 515). Reconnecting...');
|
||||
} else {
|
||||
console.log(`⚠️ Connection closed (reason: ${reason}). Reconnecting in 3s...`);
|
||||
emitPairEvent({ event: 'disconnected', reason });
|
||||
if (!PAIR_JSON) {
|
||||
if (reason === 515) {
|
||||
console.log('↻ WhatsApp requested restart (code 515). Reconnecting...');
|
||||
} else {
|
||||
console.log(`⚠️ Connection closed (reason: ${reason}). Reconnecting in 3s...`);
|
||||
}
|
||||
}
|
||||
setTimeout(startSocket, reason === 515 ? 1000 : 3000);
|
||||
}
|
||||
} else if (connection === 'open') {
|
||||
connectionState = 'connected';
|
||||
console.log('✅ WhatsApp connected!');
|
||||
const connectedUser = sock?.user
|
||||
? {
|
||||
id: sock.user.id || null,
|
||||
name: sock.user.name || sock.user.verifiedName || null,
|
||||
}
|
||||
: null;
|
||||
emitPairEvent({ event: 'connected', user: connectedUser });
|
||||
if (!PAIR_JSON) {
|
||||
console.log('✅ WhatsApp connected!');
|
||||
}
|
||||
if (PAIR_ONLY) {
|
||||
console.log('✅ Pairing complete. Credentials saved.');
|
||||
if (!PAIR_JSON) {
|
||||
console.log('✅ Pairing complete. Credentials saved.');
|
||||
}
|
||||
// Give Baileys a moment to flush creds, then exit cleanly
|
||||
setTimeout(() => process.exit(0), 2000);
|
||||
}
|
||||
|
|
@ -484,24 +531,29 @@ async function startSocket() {
|
|||
if (!msg.message) continue;
|
||||
|
||||
const chatId = msg.key.remoteJid;
|
||||
if (WHATSAPP_DEBUG) {
|
||||
try {
|
||||
console.log(JSON.stringify({
|
||||
event: 'upsert', type,
|
||||
fromMe: !!msg.key.fromMe, chatId,
|
||||
senderId: msg.key.participant || chatId,
|
||||
messageKeys: Object.keys(msg.message || {}),
|
||||
}));
|
||||
} catch {}
|
||||
}
|
||||
const senderId = msg.key.participant || chatId;
|
||||
const isGroup = chatId.endsWith('@g.us');
|
||||
const senderNumber = senderId.replace(/@.*/, '');
|
||||
emitDebugEvent({
|
||||
stage: 'upsert',
|
||||
type,
|
||||
fromMe: !!msg.key.fromMe,
|
||||
chatId: redactWhatsAppId(chatId),
|
||||
senderId: redactWhatsAppId(senderId),
|
||||
messageKeys: Object.keys(msg.message || {}),
|
||||
});
|
||||
|
||||
// Handle fromMe messages based on mode
|
||||
let fromOwner = false;
|
||||
if (msg.key.fromMe) {
|
||||
if (isGroup || chatId.includes('status')) continue;
|
||||
if (isGroup || chatId.includes('status')) {
|
||||
emitDebugEvent({
|
||||
stage: 'ignored',
|
||||
reason: isGroup ? 'from_me_group' : 'from_me_status',
|
||||
chatId: redactWhatsAppId(chatId),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (WHATSAPP_MODE === 'bot') {
|
||||
// Bot mode: separate bot number. fromMe inbound is either
|
||||
|
|
@ -546,7 +598,22 @@ async function startSocket() {
|
|||
const myLid = (sock.user?.lid || '').replace(/:.*@/, '@').replace(/@.*/, '');
|
||||
const chatNumber = chatId.replace(/@.*/, '');
|
||||
const isSelfChat = (myNumber && chatNumber === myNumber) || (myLid && chatNumber === myLid);
|
||||
if (!isSelfChat) continue;
|
||||
emitDebugEvent({
|
||||
stage: 'self_chat_check',
|
||||
matched: !!isSelfChat,
|
||||
chatId: redactWhatsAppId(chatId),
|
||||
accountId: redactWhatsAppId(sock.user?.id),
|
||||
accountLid: redactWhatsAppId(sock.user?.lid),
|
||||
});
|
||||
if (!isSelfChat) {
|
||||
emitDebugEvent({
|
||||
stage: 'ignored',
|
||||
reason: 'self_chat_mismatch',
|
||||
chatId: redactWhatsAppId(chatId),
|
||||
senderId: redactWhatsAppId(senderId),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -567,7 +634,7 @@ async function startSocket() {
|
|||
} catch {}
|
||||
continue;
|
||||
}
|
||||
if (!matchesAllowedUser(senderId, ALLOWED_USERS, SESSION_DIR)) {
|
||||
if (WHATSAPP_DM_POLICY !== 'pairing' && !matchesAllowedUser(senderId, ALLOWED_USERS, SESSION_DIR)) {
|
||||
try {
|
||||
console.log(JSON.stringify({
|
||||
event: 'ignored',
|
||||
|
|
@ -659,25 +726,39 @@ async function startSocket() {
|
|||
// Ignore Hermes' own reply messages in self-chat mode to avoid loops.
|
||||
if (msg.key.fromMe && ((REPLY_PREFIX && event.body.startsWith(REPLY_PREFIX)) || recentlySentIds.has(msg.key.id))) {
|
||||
if (WHATSAPP_DEBUG) {
|
||||
try { console.log(JSON.stringify({ event: 'ignored', reason: 'agent_echo', chatId, messageId: msg.key.id })); } catch {}
|
||||
emitDebugEvent({
|
||||
stage: 'ignored',
|
||||
reason: 'agent_echo',
|
||||
chatId: redactWhatsAppId(chatId),
|
||||
messageId: msg.key.id,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty messages
|
||||
if (!event.body && !event.hasMedia) {
|
||||
if (WHATSAPP_DEBUG) {
|
||||
try {
|
||||
console.log(JSON.stringify({ event: 'ignored', reason: 'empty', chatId, messageKeys: Object.keys(msg.message || {}) }));
|
||||
} catch (err) {
|
||||
console.error('Failed to log empty message event:', err);
|
||||
}
|
||||
}
|
||||
emitDebugEvent({
|
||||
stage: 'ignored',
|
||||
reason: 'empty',
|
||||
chatId: redactWhatsAppId(chatId),
|
||||
messageKeys: Object.keys(msg.message || {}),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
messageStore.remember(msg);
|
||||
messageQueue.push(event);
|
||||
emitDebugEvent({
|
||||
stage: 'queued',
|
||||
chatId: redactWhatsAppId(chatId),
|
||||
senderId: redactWhatsAppId(senderId),
|
||||
fromOwner: !!fromOwner,
|
||||
bodyLength: event.body.length,
|
||||
hasMedia: event.hasMedia,
|
||||
mediaType: event.mediaType,
|
||||
queueLength: messageQueue.length,
|
||||
});
|
||||
if (messageQueue.length > MAX_QUEUE_SIZE) {
|
||||
messageQueue.shift();
|
||||
}
|
||||
|
|
@ -999,10 +1080,20 @@ app.get('/health', (req, res) => {
|
|||
// Start
|
||||
if (PAIR_ONLY) {
|
||||
// Pair-only mode: just connect, show QR, save creds, exit. No HTTP server.
|
||||
console.log('📱 WhatsApp pairing mode');
|
||||
console.log(`📁 Session: ${SESSION_DIR}`);
|
||||
console.log();
|
||||
startSocket();
|
||||
if (PAIR_JSON) {
|
||||
emitPairEvent({ event: 'started', session: SESSION_DIR });
|
||||
} else {
|
||||
console.log('📱 WhatsApp pairing mode');
|
||||
console.log(`📁 Session: ${SESSION_DIR}`);
|
||||
console.log();
|
||||
}
|
||||
startSocket().catch((err) => {
|
||||
emitPairEvent({ event: 'error', error: err?.message || String(err) });
|
||||
if (!PAIR_JSON) {
|
||||
console.error(err);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
app.listen(PORT, '127.0.0.1', () => {
|
||||
console.log(`🌉 WhatsApp bridge listening on port ${PORT} (mode: ${WHATSAPP_MODE})`);
|
||||
|
|
@ -1011,6 +1102,8 @@ if (PAIR_ONLY) {
|
|||
console.log(`🔒 Allowed users: ${Array.from(ALLOWED_USERS).join(', ')}`);
|
||||
} else if (WHATSAPP_MODE === 'self-chat') {
|
||||
console.log(`🔒 Self-chat mode — only your own messages to yourself are processed.`);
|
||||
} else if (WHATSAPP_MODE === 'bot' && WHATSAPP_DM_POLICY === 'pairing') {
|
||||
console.log(`🤝 WHATSAPP_DM_POLICY=pairing — unknown DMs are forwarded for gateway pairing.`);
|
||||
} else {
|
||||
console.log(`🔒 No WHATSAPP_ALLOWED_USERS set — incoming messages are rejected.`);
|
||||
console.log(` Set WHATSAPP_ALLOWED_USERS=<phone> to authorize specific users,`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue