@deus-ai/whatsapp-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +87 -0
- package/dist/index.js.map +1 -0
- package/dist/whatsapp.d.ts +41 -0
- package/dist/whatsapp.js +351 -0
- package/dist/whatsapp.js.map +1 -0
- package/package.json +36 -0
- package/src/index.ts +111 -0
- package/src/whatsapp.ts +439 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @deus-ai/whatsapp-mcp
|
|
2
|
+
|
|
3
|
+
Standalone MCP server for WhatsApp messaging. Uses [Baileys](https://github.com/WhiskeySockets/Baileys) for the WhatsApp Web API.
|
|
4
|
+
|
|
5
|
+
Works with any MCP client — Claude Code, Claude Desktop, or your own application.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"whatsapp": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["@deus-ai/whatsapp-mcp"],
|
|
15
|
+
"env": {
|
|
16
|
+
"WHATSAPP_AUTH_DIR": "/path/to/auth/dir"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
| Tool | Description |
|
|
26
|
+
|------|-------------|
|
|
27
|
+
| `send_message` | Send a text message to a chat |
|
|
28
|
+
| `send_typing` | Show/hide typing indicator |
|
|
29
|
+
| `get_status` | Connection status and identity info |
|
|
30
|
+
| `list_chats` | List known chats and groups |
|
|
31
|
+
| `sync_groups` | Refresh group metadata from WhatsApp |
|
|
32
|
+
| `get_new_messages` | Poll for incoming messages (cursor-based) |
|
|
33
|
+
| `connect` / `disconnect` | Connection lifecycle |
|
|
34
|
+
| `get_auth_status` | Check if WhatsApp credentials exist |
|
|
35
|
+
| `start_auth` | Begin QR or pairing code authentication |
|
|
36
|
+
|
|
37
|
+
## Incoming Messages
|
|
38
|
+
|
|
39
|
+
Messages are pushed in real-time via MCP logging notifications with `logger: "incoming_message"`. For clients that don't support notifications, use the `get_new_messages` polling tool.
|
|
40
|
+
|
|
41
|
+
## Environment Variables
|
|
42
|
+
|
|
43
|
+
| Variable | Default | Description |
|
|
44
|
+
|----------|---------|-------------|
|
|
45
|
+
| `WHATSAPP_AUTH_DIR` | `./store/auth` | Path to WhatsApp credential storage |
|
|
46
|
+
| `ASSISTANT_NAME` | `Deus` | Name prefix for outgoing messages |
|
|
47
|
+
| `ASSISTANT_HAS_OWN_NUMBER` | `false` | Skip name prefix when bot has its own phone |
|
|
48
|
+
| `LOG_LEVEL` | `info` | Pino log level (debug, info, warn, error) |
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WhatsApp MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Standalone MCP server that provides WhatsApp messaging tools.
|
|
6
|
+
* Communicates via stdio (JSON-RPC). Can be used by any MCP client.
|
|
7
|
+
*
|
|
8
|
+
* Config (env vars):
|
|
9
|
+
* WHATSAPP_AUTH_DIR — path to auth credentials (default: ./store/auth)
|
|
10
|
+
* ASSISTANT_NAME — bot display name (default: Deus)
|
|
11
|
+
* ASSISTANT_HAS_OWN_NUMBER — "true" if bot has a dedicated phone number
|
|
12
|
+
* LOG_LEVEL — pino log level (default: info)
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* WhatsApp MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Standalone MCP server that provides WhatsApp messaging tools.
|
|
6
|
+
* Communicates via stdio (JSON-RPC). Can be used by any MCP client.
|
|
7
|
+
*
|
|
8
|
+
* Config (env vars):
|
|
9
|
+
* WHATSAPP_AUTH_DIR — path to auth credentials (default: ./store/auth)
|
|
10
|
+
* ASSISTANT_NAME — bot display name (default: Deus)
|
|
11
|
+
* ASSISTANT_HAS_OWN_NUMBER — "true" if bot has a dedicated phone number
|
|
12
|
+
* LOG_LEVEL — pino log level (default: info)
|
|
13
|
+
*/
|
|
14
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { registerCommonTools } from '@deus-ai/channel-core';
|
|
18
|
+
import { WhatsAppProvider } from './whatsapp.js';
|
|
19
|
+
const server = new McpServer({
|
|
20
|
+
name: '@deus-ai/whatsapp-mcp',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
});
|
|
23
|
+
const provider = new WhatsAppProvider();
|
|
24
|
+
// Register common tools (send_message, get_status, etc.)
|
|
25
|
+
registerCommonTools(server, provider);
|
|
26
|
+
// ── WhatsApp-specific tools ───────────────────────────────────────────
|
|
27
|
+
server.tool('get_auth_status', 'Check whether WhatsApp credentials exist and the connection is authenticated', {}, async () => {
|
|
28
|
+
const hasAuth = provider.hasAuth();
|
|
29
|
+
const connected = provider.isConnected();
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: 'text',
|
|
34
|
+
text: JSON.stringify({ has_credentials: hasAuth, connected }),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
server.tool('start_auth', 'Begin WhatsApp authentication. Returns QR code data or pairing code.', {
|
|
40
|
+
method: z.enum(['qr', 'pairing-code']).describe('Authentication method'),
|
|
41
|
+
phone: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Phone number (required for pairing-code method, e.g. 14155551234)'),
|
|
45
|
+
}, async (args) => {
|
|
46
|
+
if (args.method === 'pairing-code' && !args.phone) {
|
|
47
|
+
return {
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: 'text',
|
|
51
|
+
text: JSON.stringify({
|
|
52
|
+
error: 'Phone number required for pairing-code method',
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Auth is handled by the connect flow — this tool triggers it.
|
|
60
|
+
// The provider writes QR data to disk; the client reads it.
|
|
61
|
+
if (!provider.isConnected()) {
|
|
62
|
+
await provider.connect();
|
|
63
|
+
}
|
|
64
|
+
const status = provider.getStatus();
|
|
65
|
+
return {
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: 'text',
|
|
69
|
+
text: JSON.stringify({
|
|
70
|
+
status: status.connected ? 'connected' : 'authenticating',
|
|
71
|
+
identity: status.identity,
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
// ── Auto-connect if credentials exist ─────────────────────────────────
|
|
78
|
+
if (provider.hasAuth()) {
|
|
79
|
+
provider.connect().catch((err) => {
|
|
80
|
+
// Log to stderr — don't crash, stay available for auth tools
|
|
81
|
+
console.error('[@deus-ai/whatsapp-mcp] Auto-connect failed:', err.message);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// ── Start MCP transport ───────────────────────────────────────────────
|
|
85
|
+
const transport = new StdioServerTransport();
|
|
86
|
+
await server.connect(transport);
|
|
87
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,uBAAuB;IAC7B,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,MAAM,QAAQ,GAAG,IAAI,gBAAgB,EAAE,CAAC;AAExC,yDAAyD;AACzD,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAEtC,yEAAyE;AAEzE,MAAM,CAAC,IAAI,CACT,iBAAiB,EACjB,8EAA8E,EAC9E,EAAE,EACF,KAAK,IAAI,EAAE;IACT,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC;IACnC,MAAM,SAAS,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACzC,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;aAC9D;SACF;KACF,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,IAAI,CACT,YAAY,EACZ,sEAAsE,EACtE;IACE,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC;IACxE,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,mEAAmE,CACpE;CACJ,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,IAAI,IAAI,CAAC,MAAM,KAAK,cAAc,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAClD,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;wBACnB,KAAK,EAAE,+CAA+C;qBACvD,CAAC;iBACH;aACF;YACD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IACD,+DAA+D;IAC/D,4DAA4D;IAC5D,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,CAAC;QAC5B,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,EAAE,CAAC;IACpC,OAAO;QACL,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,gBAAgB;oBACzD,QAAQ,EAAE,MAAM,CAAC,QAAQ;iBAC1B,CAAC;aACH;SACF;KACF,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,yEAAyE;AAEzE,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QAC/B,6DAA6D;QAC7D,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,yEAAyE;AAEzE,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone WhatsApp connection provider.
|
|
3
|
+
* Extracted from Deus WhatsAppChannel — no Deus-specific dependencies.
|
|
4
|
+
* All config comes from env vars; all messages are forwarded to onMessage.
|
|
5
|
+
*/
|
|
6
|
+
import type { ChannelProvider, ChannelStatus, ChatInfo, IncomingMessage } from '@deus-ai/channel-core';
|
|
7
|
+
export declare class WhatsAppProvider implements ChannelProvider {
|
|
8
|
+
readonly name = "whatsapp";
|
|
9
|
+
private sock;
|
|
10
|
+
private connected;
|
|
11
|
+
private connectTime;
|
|
12
|
+
private lidToPhoneMap;
|
|
13
|
+
private outgoingQueue;
|
|
14
|
+
private flushing;
|
|
15
|
+
private groupSyncTimerStarted;
|
|
16
|
+
private sentMessageCache;
|
|
17
|
+
private groupMetadataCache;
|
|
18
|
+
private botLidUser?;
|
|
19
|
+
private pendingFirstOpen?;
|
|
20
|
+
private lastGroupSync;
|
|
21
|
+
private knownChats;
|
|
22
|
+
private readyPromise;
|
|
23
|
+
private readyResolve;
|
|
24
|
+
onMessage: (msg: IncomingMessage) => void;
|
|
25
|
+
connect(): Promise<void>;
|
|
26
|
+
waitForReady(): Promise<void>;
|
|
27
|
+
private connectInternal;
|
|
28
|
+
sendMessage(chatId: string, text: string): Promise<void>;
|
|
29
|
+
isConnected(): boolean;
|
|
30
|
+
getStatus(): ChannelStatus;
|
|
31
|
+
disconnect(): Promise<void>;
|
|
32
|
+
setTyping(chatId: string, isTyping: boolean): Promise<void>;
|
|
33
|
+
listChats(): Promise<ChatInfo[]>;
|
|
34
|
+
syncGroups(): Promise<ChatInfo[]>;
|
|
35
|
+
/** Check if WhatsApp credentials exist on disk. */
|
|
36
|
+
hasAuth(): boolean;
|
|
37
|
+
private syncGroupMetadata;
|
|
38
|
+
private translateJid;
|
|
39
|
+
private getNormalizedGroupMetadata;
|
|
40
|
+
private flushOutgoingQueue;
|
|
41
|
+
}
|
package/dist/whatsapp.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone WhatsApp connection provider.
|
|
3
|
+
* Extracted from Deus WhatsAppChannel — no Deus-specific dependencies.
|
|
4
|
+
* All config comes from env vars; all messages are forwarded to onMessage.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { makeWASocket, Browsers, DisconnectReason, fetchLatestWaWebVersion, makeCacheableSignalKeyStore, normalizeMessageContent, useMultiFileAuthState, } from '@whiskeysockets/baileys';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
const { proto } = createRequire(import.meta.url)('@whiskeysockets/baileys');
|
|
11
|
+
import pino from 'pino';
|
|
12
|
+
// ── Config from env vars ────────────────────────────────────────────────
|
|
13
|
+
const AUTH_DIR = process.env.WHATSAPP_AUTH_DIR || path.resolve(process.cwd(), 'store', 'auth');
|
|
14
|
+
const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Deus';
|
|
15
|
+
const ASSISTANT_HAS_OWN_NUMBER = process.env.ASSISTANT_HAS_OWN_NUMBER === 'true';
|
|
16
|
+
// Use stderr for logging (stdout is reserved for MCP JSON-RPC)
|
|
17
|
+
const logger = pino({ level: process.env.LOG_LEVEL || 'info' }, pino.destination(2));
|
|
18
|
+
const baileysLogger = pino({ level: 'silent' });
|
|
19
|
+
const GROUP_SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
20
|
+
export class WhatsAppProvider {
|
|
21
|
+
name = 'whatsapp';
|
|
22
|
+
sock;
|
|
23
|
+
connected = false;
|
|
24
|
+
connectTime = 0;
|
|
25
|
+
lidToPhoneMap = {};
|
|
26
|
+
outgoingQueue = [];
|
|
27
|
+
flushing = false;
|
|
28
|
+
groupSyncTimerStarted = false;
|
|
29
|
+
sentMessageCache = new Map();
|
|
30
|
+
groupMetadataCache = new Map();
|
|
31
|
+
botLidUser;
|
|
32
|
+
pendingFirstOpen;
|
|
33
|
+
lastGroupSync = null;
|
|
34
|
+
knownChats = new Map();
|
|
35
|
+
readyPromise = null;
|
|
36
|
+
readyResolve = null;
|
|
37
|
+
// Set by server-base.ts — called for every incoming message
|
|
38
|
+
onMessage = () => { };
|
|
39
|
+
async connect() {
|
|
40
|
+
this.readyPromise = new Promise((resolve) => {
|
|
41
|
+
this.readyResolve = resolve;
|
|
42
|
+
});
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
this.pendingFirstOpen = resolve;
|
|
45
|
+
this.connectInternal().catch(reject);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async waitForReady() {
|
|
49
|
+
if (this.connected)
|
|
50
|
+
return;
|
|
51
|
+
if (this.readyPromise)
|
|
52
|
+
await this.readyPromise;
|
|
53
|
+
}
|
|
54
|
+
async connectInternal() {
|
|
55
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
56
|
+
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
|
|
57
|
+
const { version } = await fetchLatestWaWebVersion({}).catch((err) => {
|
|
58
|
+
logger.warn({ err }, 'Failed to fetch latest WA Web version, using default');
|
|
59
|
+
return { version: undefined };
|
|
60
|
+
});
|
|
61
|
+
this.sock = makeWASocket({
|
|
62
|
+
version,
|
|
63
|
+
auth: {
|
|
64
|
+
creds: state.creds,
|
|
65
|
+
keys: makeCacheableSignalKeyStore(state.keys, baileysLogger),
|
|
66
|
+
},
|
|
67
|
+
printQRInTerminal: false,
|
|
68
|
+
logger: baileysLogger,
|
|
69
|
+
browser: Browsers.macOS('Chrome'),
|
|
70
|
+
cachedGroupMetadata: async (jid) => this.getNormalizedGroupMetadata(jid),
|
|
71
|
+
getMessage: async (key) => {
|
|
72
|
+
const cached = this.sentMessageCache.get(key.id || '');
|
|
73
|
+
if (cached)
|
|
74
|
+
return cached;
|
|
75
|
+
return proto.Message.fromObject({});
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
this.sock.ev.on('connection.update', (update) => {
|
|
79
|
+
const { connection, lastDisconnect, qr } = update;
|
|
80
|
+
if (qr) {
|
|
81
|
+
// Write QR data to a file for auth tools to pick up
|
|
82
|
+
const qrPath = path.join(path.dirname(AUTH_DIR), 'qr-data.txt');
|
|
83
|
+
fs.writeFileSync(qrPath, qr);
|
|
84
|
+
logger.warn('WhatsApp authentication required — use start_auth tool');
|
|
85
|
+
}
|
|
86
|
+
if (connection === 'close') {
|
|
87
|
+
this.connected = false;
|
|
88
|
+
const reason = lastDisconnect?.error?.output?.statusCode;
|
|
89
|
+
const shouldReconnect = reason !== DisconnectReason.loggedOut;
|
|
90
|
+
logger.info({ reason, shouldReconnect }, 'Connection closed');
|
|
91
|
+
if (shouldReconnect) {
|
|
92
|
+
logger.info('Reconnecting...');
|
|
93
|
+
this.connectInternal().catch((err) => {
|
|
94
|
+
logger.error({ err }, 'Failed to reconnect, retrying in 5s');
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
this.connectInternal().catch((err2) => {
|
|
97
|
+
logger.error({ err: err2 }, 'Reconnection retry failed');
|
|
98
|
+
});
|
|
99
|
+
}, 5000);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
logger.info('Logged out. Re-authenticate to continue.');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (connection === 'open') {
|
|
107
|
+
this.connected = true;
|
|
108
|
+
this.connectTime = Date.now();
|
|
109
|
+
logger.info('Connected to WhatsApp');
|
|
110
|
+
this.sock.sendPresenceUpdate('available').catch(() => { });
|
|
111
|
+
// Build LID to phone mapping
|
|
112
|
+
if (this.sock.user) {
|
|
113
|
+
const phoneUser = this.sock.user.id.split(':')[0];
|
|
114
|
+
const lidUser = this.sock.user.lid?.split(':')[0];
|
|
115
|
+
if (lidUser && phoneUser) {
|
|
116
|
+
this.lidToPhoneMap[lidUser] = `${phoneUser}@s.whatsapp.net`;
|
|
117
|
+
this.botLidUser = lidUser;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.flushOutgoingQueue().catch(() => { });
|
|
121
|
+
this.syncGroupMetadata().catch(() => { });
|
|
122
|
+
if (!this.groupSyncTimerStarted) {
|
|
123
|
+
this.groupSyncTimerStarted = true;
|
|
124
|
+
setInterval(() => {
|
|
125
|
+
this.syncGroupMetadata().catch(() => { });
|
|
126
|
+
}, GROUP_SYNC_INTERVAL_MS);
|
|
127
|
+
}
|
|
128
|
+
if (this.pendingFirstOpen) {
|
|
129
|
+
this.pendingFirstOpen();
|
|
130
|
+
this.pendingFirstOpen = undefined;
|
|
131
|
+
}
|
|
132
|
+
if (this.readyResolve) {
|
|
133
|
+
this.readyResolve();
|
|
134
|
+
this.readyResolve = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
this.sock.ev.on('creds.update', saveCreds);
|
|
139
|
+
// Phone number share event — not typed in all baileys versions
|
|
140
|
+
this.sock.ev.on('chats.phoneNumberShare', (data) => {
|
|
141
|
+
const lidUser = data.lid?.split('@')[0].split(':')[0];
|
|
142
|
+
if (lidUser && data.jid) {
|
|
143
|
+
this.lidToPhoneMap[lidUser] = data.jid;
|
|
144
|
+
this.groupMetadataCache.clear();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
this.sock.ev.on('messages.upsert', async ({ messages }) => {
|
|
148
|
+
for (const msg of messages) {
|
|
149
|
+
try {
|
|
150
|
+
if (!msg.message)
|
|
151
|
+
continue;
|
|
152
|
+
const normalized = normalizeMessageContent(msg.message);
|
|
153
|
+
if (!normalized)
|
|
154
|
+
continue;
|
|
155
|
+
const rawJid = msg.key.remoteJid;
|
|
156
|
+
if (!rawJid || rawJid === 'status@broadcast')
|
|
157
|
+
continue;
|
|
158
|
+
let chatJid = await this.translateJid(rawJid);
|
|
159
|
+
if (chatJid.endsWith('@lid') && msg.key.senderPn) {
|
|
160
|
+
const pn = msg.key.senderPn;
|
|
161
|
+
const phoneJid = pn.includes('@') ? pn : `${pn}@s.whatsapp.net`;
|
|
162
|
+
this.lidToPhoneMap[rawJid.split('@')[0].split(':')[0]] = phoneJid;
|
|
163
|
+
chatJid = phoneJid;
|
|
164
|
+
}
|
|
165
|
+
const timestamp = new Date(Number(msg.messageTimestamp) * 1000).toISOString();
|
|
166
|
+
const isGroup = chatJid.endsWith('@g.us');
|
|
167
|
+
// Track chat metadata
|
|
168
|
+
this.knownChats.set(chatJid, {
|
|
169
|
+
name: msg.pushName || chatJid.split('@')[0],
|
|
170
|
+
isGroup,
|
|
171
|
+
});
|
|
172
|
+
let content = normalized.conversation ||
|
|
173
|
+
normalized.extendedTextMessage?.text ||
|
|
174
|
+
normalized.imageMessage?.caption ||
|
|
175
|
+
normalized.videoMessage?.caption ||
|
|
176
|
+
'';
|
|
177
|
+
// Normalize bot LID mentions to assistant name
|
|
178
|
+
if (this.botLidUser && content.includes(`@${this.botLidUser}`)) {
|
|
179
|
+
content = content.replace(`@${this.botLidUser}`, `@${ASSISTANT_NAME}`);
|
|
180
|
+
}
|
|
181
|
+
if (!content)
|
|
182
|
+
continue;
|
|
183
|
+
const sender = msg.key.participant || msg.key.remoteJid || '';
|
|
184
|
+
const senderName = msg.pushName || sender.split('@')[0];
|
|
185
|
+
const fromMe = msg.key.fromMe || false;
|
|
186
|
+
const isBotMessage = ASSISTANT_HAS_OWN_NUMBER
|
|
187
|
+
? fromMe
|
|
188
|
+
: content.startsWith(`${ASSISTANT_NAME}:`);
|
|
189
|
+
// Forward ALL messages — the host decides which to act on
|
|
190
|
+
this.onMessage({
|
|
191
|
+
id: msg.key.id || '',
|
|
192
|
+
chat_id: chatJid,
|
|
193
|
+
sender,
|
|
194
|
+
sender_name: senderName,
|
|
195
|
+
content,
|
|
196
|
+
timestamp,
|
|
197
|
+
is_from_me: fromMe,
|
|
198
|
+
is_group: isGroup,
|
|
199
|
+
metadata: { is_bot_message: isBotMessage },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
logger.error({ err, remoteJid: msg.key?.remoteJid }, 'Error processing message');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
async sendMessage(chatId, text) {
|
|
209
|
+
const prefixed = ASSISTANT_HAS_OWN_NUMBER
|
|
210
|
+
? text
|
|
211
|
+
: `${ASSISTANT_NAME}: ${text}`;
|
|
212
|
+
if (!this.connected) {
|
|
213
|
+
this.outgoingQueue.push({ jid: chatId, text: prefixed });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const sent = await this.sock.sendMessage(chatId, { text: prefixed });
|
|
218
|
+
if (sent?.key?.id && sent.message) {
|
|
219
|
+
this.sentMessageCache.set(sent.key.id, sent.message);
|
|
220
|
+
if (this.sentMessageCache.size > 256) {
|
|
221
|
+
const oldest = this.sentMessageCache.keys().next().value;
|
|
222
|
+
this.sentMessageCache.delete(oldest);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
this.outgoingQueue.push({ jid: chatId, text: prefixed });
|
|
228
|
+
logger.warn({ chatId, err }, 'Failed to send, message queued');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
isConnected() {
|
|
232
|
+
return this.connected;
|
|
233
|
+
}
|
|
234
|
+
getStatus() {
|
|
235
|
+
return {
|
|
236
|
+
connected: this.connected,
|
|
237
|
+
channel: 'whatsapp',
|
|
238
|
+
identity: this.sock?.user?.id?.split(':')[0],
|
|
239
|
+
uptime_seconds: this.connected
|
|
240
|
+
? Math.floor((Date.now() - this.connectTime) / 1000)
|
|
241
|
+
: 0,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async disconnect() {
|
|
245
|
+
this.connected = false;
|
|
246
|
+
this.sock?.end(undefined);
|
|
247
|
+
}
|
|
248
|
+
async setTyping(chatId, isTyping) {
|
|
249
|
+
try {
|
|
250
|
+
await this.sock.sendPresenceUpdate(isTyping ? 'composing' : 'paused', chatId);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Best effort
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async listChats() {
|
|
257
|
+
return Array.from(this.knownChats.entries()).map(([id, info]) => ({
|
|
258
|
+
id,
|
|
259
|
+
name: info.name,
|
|
260
|
+
is_group: info.isGroup,
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
async syncGroups() {
|
|
264
|
+
return this.syncGroupMetadata(true);
|
|
265
|
+
}
|
|
266
|
+
/** Check if WhatsApp credentials exist on disk. */
|
|
267
|
+
hasAuth() {
|
|
268
|
+
return fs.existsSync(path.join(AUTH_DIR, 'creds.json'));
|
|
269
|
+
}
|
|
270
|
+
// ── Internal helpers ──────────────────────────────────────────────────
|
|
271
|
+
async syncGroupMetadata(force = false) {
|
|
272
|
+
if (!force && this.lastGroupSync) {
|
|
273
|
+
const elapsed = Date.now() - new Date(this.lastGroupSync).getTime();
|
|
274
|
+
if (elapsed < GROUP_SYNC_INTERVAL_MS)
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const groups = await this.sock.groupFetchAllParticipating();
|
|
279
|
+
const result = [];
|
|
280
|
+
for (const [jid, metadata] of Object.entries(groups)) {
|
|
281
|
+
if (metadata.subject) {
|
|
282
|
+
this.knownChats.set(jid, { name: metadata.subject, isGroup: true });
|
|
283
|
+
result.push({ id: jid, name: metadata.subject, is_group: true });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
this.lastGroupSync = new Date().toISOString();
|
|
287
|
+
logger.info({ count: result.length }, 'Group metadata synced');
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
logger.error({ err }, 'Failed to sync group metadata');
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async translateJid(jid) {
|
|
296
|
+
if (!jid.endsWith('@lid'))
|
|
297
|
+
return jid;
|
|
298
|
+
const lidUser = jid.split('@')[0].split(':')[0];
|
|
299
|
+
const cached = this.lidToPhoneMap[lidUser];
|
|
300
|
+
if (cached)
|
|
301
|
+
return cached;
|
|
302
|
+
try {
|
|
303
|
+
const pn = await this.sock.signalRepository?.lidMapping?.getPNForLID(jid);
|
|
304
|
+
if (pn) {
|
|
305
|
+
const phoneJid = `${pn.split('@')[0].split(':')[0]}@s.whatsapp.net`;
|
|
306
|
+
this.lidToPhoneMap[lidUser] = phoneJid;
|
|
307
|
+
return phoneJid;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// Best effort
|
|
312
|
+
}
|
|
313
|
+
return jid;
|
|
314
|
+
}
|
|
315
|
+
async getNormalizedGroupMetadata(jid) {
|
|
316
|
+
if (!jid.endsWith('@g.us'))
|
|
317
|
+
return undefined;
|
|
318
|
+
const cached = this.groupMetadataCache.get(jid);
|
|
319
|
+
if (cached && cached.expiresAt > Date.now())
|
|
320
|
+
return cached.metadata;
|
|
321
|
+
const metadata = await this.sock.groupMetadata(jid);
|
|
322
|
+
const participants = await Promise.all(metadata.participants.map(async (p) => ({
|
|
323
|
+
...p,
|
|
324
|
+
id: await this.translateJid(p.id),
|
|
325
|
+
})));
|
|
326
|
+
const normalized = { ...metadata, participants };
|
|
327
|
+
this.groupMetadataCache.set(jid, {
|
|
328
|
+
metadata: normalized,
|
|
329
|
+
expiresAt: Date.now() + 60_000,
|
|
330
|
+
});
|
|
331
|
+
return normalized;
|
|
332
|
+
}
|
|
333
|
+
async flushOutgoingQueue() {
|
|
334
|
+
if (this.flushing || this.outgoingQueue.length === 0)
|
|
335
|
+
return;
|
|
336
|
+
this.flushing = true;
|
|
337
|
+
try {
|
|
338
|
+
while (this.outgoingQueue.length > 0) {
|
|
339
|
+
const item = this.outgoingQueue.shift();
|
|
340
|
+
const sent = await this.sock.sendMessage(item.jid, { text: item.text });
|
|
341
|
+
if (sent?.key?.id && sent.message) {
|
|
342
|
+
this.sentMessageCache.set(sent.key.id, sent.message);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
this.flushing = false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=whatsapp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whatsapp.js","sourceRoot":"","sources":["../src/whatsapp.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deus-ai/whatsapp-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "WhatsApp MCP server — standalone WhatsApp Web integration for any MCP client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"deus-ai-whatsapp-mcp": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "vitest run"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@deus-ai/channel-core": "^1.0.0",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
18
|
+
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
|
19
|
+
"pino": "^10.3.1",
|
|
20
|
+
"qrcode-terminal": "^0.12.0",
|
|
21
|
+
"zod": "^4.3.6"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.10.7",
|
|
25
|
+
"@types/qrcode-terminal": "^0.12.0",
|
|
26
|
+
"typescript": "^6.0.2",
|
|
27
|
+
"vitest": "^4.1.2"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WhatsApp MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Standalone MCP server that provides WhatsApp messaging tools.
|
|
7
|
+
* Communicates via stdio (JSON-RPC). Can be used by any MCP client.
|
|
8
|
+
*
|
|
9
|
+
* Config (env vars):
|
|
10
|
+
* WHATSAPP_AUTH_DIR — path to auth credentials (default: ./store/auth)
|
|
11
|
+
* ASSISTANT_NAME — bot display name (default: Deus)
|
|
12
|
+
* ASSISTANT_HAS_OWN_NUMBER — "true" if bot has a dedicated phone number
|
|
13
|
+
* LOG_LEVEL — pino log level (default: info)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
17
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
import { registerCommonTools } from '@deus-ai/channel-core';
|
|
20
|
+
|
|
21
|
+
import { WhatsAppProvider } from './whatsapp.js';
|
|
22
|
+
|
|
23
|
+
const server = new McpServer({
|
|
24
|
+
name: '@deus-ai/whatsapp-mcp',
|
|
25
|
+
version: '1.0.0',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const provider = new WhatsAppProvider();
|
|
29
|
+
|
|
30
|
+
// Register common tools (send_message, get_status, etc.)
|
|
31
|
+
registerCommonTools(server, provider);
|
|
32
|
+
|
|
33
|
+
// ── WhatsApp-specific tools ───────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
server.tool(
|
|
36
|
+
'get_auth_status',
|
|
37
|
+
'Check whether WhatsApp credentials exist and the connection is authenticated',
|
|
38
|
+
{},
|
|
39
|
+
async () => {
|
|
40
|
+
const hasAuth = provider.hasAuth();
|
|
41
|
+
const connected = provider.isConnected();
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: 'text' as const,
|
|
46
|
+
text: JSON.stringify({ has_credentials: hasAuth, connected }),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
server.tool(
|
|
54
|
+
'start_auth',
|
|
55
|
+
'Begin WhatsApp authentication. Returns QR code data or pairing code.',
|
|
56
|
+
{
|
|
57
|
+
method: z.enum(['qr', 'pairing-code']).describe('Authentication method'),
|
|
58
|
+
phone: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe(
|
|
62
|
+
'Phone number (required for pairing-code method, e.g. 14155551234)',
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
async (args) => {
|
|
66
|
+
if (args.method === 'pairing-code' && !args.phone) {
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: 'text' as const,
|
|
71
|
+
text: JSON.stringify({
|
|
72
|
+
error: 'Phone number required for pairing-code method',
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
isError: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Auth is handled by the connect flow — this tool triggers it.
|
|
80
|
+
// The provider writes QR data to disk; the client reads it.
|
|
81
|
+
if (!provider.isConnected()) {
|
|
82
|
+
await provider.connect();
|
|
83
|
+
}
|
|
84
|
+
const status = provider.getStatus();
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text' as const,
|
|
89
|
+
text: JSON.stringify({
|
|
90
|
+
status: status.connected ? 'connected' : 'authenticating',
|
|
91
|
+
identity: status.identity,
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// ── Auto-connect if credentials exist ─────────────────────────────────
|
|
100
|
+
|
|
101
|
+
if (provider.hasAuth()) {
|
|
102
|
+
provider.connect().catch((err) => {
|
|
103
|
+
// Log to stderr — don't crash, stay available for auth tools
|
|
104
|
+
console.error('[@deus-ai/whatsapp-mcp] Auto-connect failed:', err.message);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Start MCP transport ───────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
const transport = new StdioServerTransport();
|
|
111
|
+
await server.connect(transport);
|
package/src/whatsapp.ts
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone WhatsApp connection provider.
|
|
3
|
+
* Extracted from Deus WhatsAppChannel — no Deus-specific dependencies.
|
|
4
|
+
* All config comes from env vars; all messages are forwarded to onMessage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
makeWASocket,
|
|
12
|
+
Browsers,
|
|
13
|
+
DisconnectReason,
|
|
14
|
+
fetchLatestWaWebVersion,
|
|
15
|
+
makeCacheableSignalKeyStore,
|
|
16
|
+
normalizeMessageContent,
|
|
17
|
+
useMultiFileAuthState,
|
|
18
|
+
} from '@whiskeysockets/baileys';
|
|
19
|
+
import type {
|
|
20
|
+
GroupMetadata,
|
|
21
|
+
WAMessageKey,
|
|
22
|
+
WASocket,
|
|
23
|
+
proto as ProtoTypes,
|
|
24
|
+
} from '@whiskeysockets/baileys';
|
|
25
|
+
import { createRequire } from 'module';
|
|
26
|
+
const { proto } = createRequire(import.meta.url)('@whiskeysockets/baileys') as {
|
|
27
|
+
proto: typeof ProtoTypes;
|
|
28
|
+
};
|
|
29
|
+
import pino from 'pino';
|
|
30
|
+
|
|
31
|
+
import type {
|
|
32
|
+
ChannelProvider,
|
|
33
|
+
ChannelStatus,
|
|
34
|
+
ChatInfo,
|
|
35
|
+
IncomingMessage,
|
|
36
|
+
} from '@deus-ai/channel-core';
|
|
37
|
+
|
|
38
|
+
// ── Config from env vars ────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const AUTH_DIR =
|
|
41
|
+
process.env.WHATSAPP_AUTH_DIR || path.resolve(process.cwd(), 'store', 'auth');
|
|
42
|
+
const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Deus';
|
|
43
|
+
const ASSISTANT_HAS_OWN_NUMBER =
|
|
44
|
+
process.env.ASSISTANT_HAS_OWN_NUMBER === 'true';
|
|
45
|
+
|
|
46
|
+
// Use stderr for logging (stdout is reserved for MCP JSON-RPC)
|
|
47
|
+
const logger = pino(
|
|
48
|
+
{ level: process.env.LOG_LEVEL || 'info' },
|
|
49
|
+
pino.destination(2),
|
|
50
|
+
);
|
|
51
|
+
const baileysLogger = pino({ level: 'silent' });
|
|
52
|
+
|
|
53
|
+
const GROUP_SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
54
|
+
|
|
55
|
+
export class WhatsAppProvider implements ChannelProvider {
|
|
56
|
+
readonly name = 'whatsapp';
|
|
57
|
+
|
|
58
|
+
private sock!: WASocket;
|
|
59
|
+
private connected = false;
|
|
60
|
+
private connectTime = 0;
|
|
61
|
+
private lidToPhoneMap: Record<string, string> = {};
|
|
62
|
+
private outgoingQueue: Array<{ jid: string; text: string }> = [];
|
|
63
|
+
private flushing = false;
|
|
64
|
+
private groupSyncTimerStarted = false;
|
|
65
|
+
private sentMessageCache = new Map<string, ProtoTypes.IMessage>();
|
|
66
|
+
private groupMetadataCache = new Map<
|
|
67
|
+
string,
|
|
68
|
+
{ metadata: GroupMetadata; expiresAt: number }
|
|
69
|
+
>();
|
|
70
|
+
private botLidUser?: string;
|
|
71
|
+
private pendingFirstOpen?: () => void;
|
|
72
|
+
private lastGroupSync: string | null = null;
|
|
73
|
+
private knownChats = new Map<string, { name: string; isGroup: boolean }>();
|
|
74
|
+
private readyPromise: Promise<void> | null = null;
|
|
75
|
+
private readyResolve: (() => void) | null = null;
|
|
76
|
+
|
|
77
|
+
// Set by server-base.ts — called for every incoming message
|
|
78
|
+
onMessage: (msg: IncomingMessage) => void = () => {};
|
|
79
|
+
|
|
80
|
+
async connect(): Promise<void> {
|
|
81
|
+
this.readyPromise = new Promise<void>((resolve) => {
|
|
82
|
+
this.readyResolve = resolve;
|
|
83
|
+
});
|
|
84
|
+
return new Promise<void>((resolve, reject) => {
|
|
85
|
+
this.pendingFirstOpen = resolve;
|
|
86
|
+
this.connectInternal().catch(reject);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async waitForReady(): Promise<void> {
|
|
91
|
+
if (this.connected) return;
|
|
92
|
+
if (this.readyPromise) await this.readyPromise;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async connectInternal(): Promise<void> {
|
|
96
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
97
|
+
|
|
98
|
+
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
|
|
99
|
+
|
|
100
|
+
const { version } = await fetchLatestWaWebVersion({}).catch((err) => {
|
|
101
|
+
logger.warn(
|
|
102
|
+
{ err },
|
|
103
|
+
'Failed to fetch latest WA Web version, using default',
|
|
104
|
+
);
|
|
105
|
+
return { version: undefined };
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.sock = makeWASocket({
|
|
109
|
+
version,
|
|
110
|
+
auth: {
|
|
111
|
+
creds: state.creds,
|
|
112
|
+
keys: makeCacheableSignalKeyStore(state.keys, baileysLogger),
|
|
113
|
+
},
|
|
114
|
+
printQRInTerminal: false,
|
|
115
|
+
logger: baileysLogger,
|
|
116
|
+
browser: Browsers.macOS('Chrome'),
|
|
117
|
+
cachedGroupMetadata: async (jid: string) =>
|
|
118
|
+
this.getNormalizedGroupMetadata(jid),
|
|
119
|
+
getMessage: async (key: WAMessageKey) => {
|
|
120
|
+
const cached = this.sentMessageCache.get(key.id || '');
|
|
121
|
+
if (cached) return cached;
|
|
122
|
+
return proto.Message.fromObject({});
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.sock.ev.on('connection.update', (update) => {
|
|
127
|
+
const { connection, lastDisconnect, qr } = update;
|
|
128
|
+
|
|
129
|
+
if (qr) {
|
|
130
|
+
// Write QR data to a file for auth tools to pick up
|
|
131
|
+
const qrPath = path.join(path.dirname(AUTH_DIR), 'qr-data.txt');
|
|
132
|
+
fs.writeFileSync(qrPath, qr);
|
|
133
|
+
logger.warn('WhatsApp authentication required — use start_auth tool');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (connection === 'close') {
|
|
137
|
+
this.connected = false;
|
|
138
|
+
const reason = (
|
|
139
|
+
lastDisconnect?.error as { output?: { statusCode?: number } }
|
|
140
|
+
)?.output?.statusCode;
|
|
141
|
+
const shouldReconnect = reason !== DisconnectReason.loggedOut;
|
|
142
|
+
logger.info({ reason, shouldReconnect }, 'Connection closed');
|
|
143
|
+
|
|
144
|
+
if (shouldReconnect) {
|
|
145
|
+
logger.info('Reconnecting...');
|
|
146
|
+
this.connectInternal().catch((err) => {
|
|
147
|
+
logger.error({ err }, 'Failed to reconnect, retrying in 5s');
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
this.connectInternal().catch((err2) => {
|
|
150
|
+
logger.error({ err: err2 }, 'Reconnection retry failed');
|
|
151
|
+
});
|
|
152
|
+
}, 5000);
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
logger.info('Logged out. Re-authenticate to continue.');
|
|
156
|
+
}
|
|
157
|
+
} else if (connection === 'open') {
|
|
158
|
+
this.connected = true;
|
|
159
|
+
this.connectTime = Date.now();
|
|
160
|
+
logger.info('Connected to WhatsApp');
|
|
161
|
+
|
|
162
|
+
this.sock.sendPresenceUpdate('available').catch(() => {});
|
|
163
|
+
|
|
164
|
+
// Build LID to phone mapping
|
|
165
|
+
if (this.sock.user) {
|
|
166
|
+
const phoneUser = this.sock.user.id.split(':')[0];
|
|
167
|
+
const lidUser = this.sock.user.lid?.split(':')[0];
|
|
168
|
+
if (lidUser && phoneUser) {
|
|
169
|
+
this.lidToPhoneMap[lidUser] = `${phoneUser}@s.whatsapp.net`;
|
|
170
|
+
this.botLidUser = lidUser;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.flushOutgoingQueue().catch(() => {});
|
|
175
|
+
this.syncGroupMetadata().catch(() => {});
|
|
176
|
+
|
|
177
|
+
if (!this.groupSyncTimerStarted) {
|
|
178
|
+
this.groupSyncTimerStarted = true;
|
|
179
|
+
setInterval(() => {
|
|
180
|
+
this.syncGroupMetadata().catch(() => {});
|
|
181
|
+
}, GROUP_SYNC_INTERVAL_MS);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.pendingFirstOpen) {
|
|
185
|
+
this.pendingFirstOpen();
|
|
186
|
+
this.pendingFirstOpen = undefined;
|
|
187
|
+
}
|
|
188
|
+
if (this.readyResolve) {
|
|
189
|
+
this.readyResolve();
|
|
190
|
+
this.readyResolve = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
this.sock.ev.on('creds.update', saveCreds);
|
|
196
|
+
|
|
197
|
+
// Phone number share event — not typed in all baileys versions
|
|
198
|
+
(this.sock.ev as any).on(
|
|
199
|
+
'chats.phoneNumberShare',
|
|
200
|
+
(data: { lid?: string; jid?: string }) => {
|
|
201
|
+
const lidUser = data.lid?.split('@')[0].split(':')[0];
|
|
202
|
+
if (lidUser && data.jid) {
|
|
203
|
+
this.lidToPhoneMap[lidUser] = data.jid;
|
|
204
|
+
this.groupMetadataCache.clear();
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
this.sock.ev.on('messages.upsert', async ({ messages }) => {
|
|
210
|
+
for (const msg of messages) {
|
|
211
|
+
try {
|
|
212
|
+
if (!msg.message) continue;
|
|
213
|
+
const normalized = normalizeMessageContent(msg.message);
|
|
214
|
+
if (!normalized) continue;
|
|
215
|
+
const rawJid = msg.key.remoteJid;
|
|
216
|
+
if (!rawJid || rawJid === 'status@broadcast') continue;
|
|
217
|
+
|
|
218
|
+
let chatJid = await this.translateJid(rawJid);
|
|
219
|
+
if (chatJid.endsWith('@lid') && (msg.key as any).senderPn) {
|
|
220
|
+
const pn = (msg.key as any).senderPn as string;
|
|
221
|
+
const phoneJid = pn.includes('@') ? pn : `${pn}@s.whatsapp.net`;
|
|
222
|
+
this.lidToPhoneMap[rawJid.split('@')[0].split(':')[0]] = phoneJid;
|
|
223
|
+
chatJid = phoneJid;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const timestamp = new Date(
|
|
227
|
+
Number(msg.messageTimestamp) * 1000,
|
|
228
|
+
).toISOString();
|
|
229
|
+
const isGroup = chatJid.endsWith('@g.us');
|
|
230
|
+
|
|
231
|
+
// Track chat metadata
|
|
232
|
+
this.knownChats.set(chatJid, {
|
|
233
|
+
name: msg.pushName || chatJid.split('@')[0],
|
|
234
|
+
isGroup,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
let content =
|
|
238
|
+
normalized.conversation ||
|
|
239
|
+
normalized.extendedTextMessage?.text ||
|
|
240
|
+
normalized.imageMessage?.caption ||
|
|
241
|
+
normalized.videoMessage?.caption ||
|
|
242
|
+
'';
|
|
243
|
+
|
|
244
|
+
// Normalize bot LID mentions to assistant name
|
|
245
|
+
if (this.botLidUser && content.includes(`@${this.botLidUser}`)) {
|
|
246
|
+
content = content.replace(
|
|
247
|
+
`@${this.botLidUser}`,
|
|
248
|
+
`@${ASSISTANT_NAME}`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!content) continue;
|
|
253
|
+
|
|
254
|
+
const sender = msg.key.participant || msg.key.remoteJid || '';
|
|
255
|
+
const senderName = msg.pushName || sender.split('@')[0];
|
|
256
|
+
const fromMe = msg.key.fromMe || false;
|
|
257
|
+
const isBotMessage = ASSISTANT_HAS_OWN_NUMBER
|
|
258
|
+
? fromMe
|
|
259
|
+
: content.startsWith(`${ASSISTANT_NAME}:`);
|
|
260
|
+
|
|
261
|
+
// Forward ALL messages — the host decides which to act on
|
|
262
|
+
this.onMessage({
|
|
263
|
+
id: msg.key.id || '',
|
|
264
|
+
chat_id: chatJid,
|
|
265
|
+
sender,
|
|
266
|
+
sender_name: senderName,
|
|
267
|
+
content,
|
|
268
|
+
timestamp,
|
|
269
|
+
is_from_me: fromMe,
|
|
270
|
+
is_group: isGroup,
|
|
271
|
+
metadata: { is_bot_message: isBotMessage },
|
|
272
|
+
});
|
|
273
|
+
} catch (err) {
|
|
274
|
+
logger.error(
|
|
275
|
+
{ err, remoteJid: msg.key?.remoteJid },
|
|
276
|
+
'Error processing message',
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async sendMessage(chatId: string, text: string): Promise<void> {
|
|
284
|
+
const prefixed = ASSISTANT_HAS_OWN_NUMBER
|
|
285
|
+
? text
|
|
286
|
+
: `${ASSISTANT_NAME}: ${text}`;
|
|
287
|
+
|
|
288
|
+
if (!this.connected) {
|
|
289
|
+
this.outgoingQueue.push({ jid: chatId, text: prefixed });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const sent = await this.sock.sendMessage(chatId, { text: prefixed });
|
|
294
|
+
if (sent?.key?.id && sent.message) {
|
|
295
|
+
this.sentMessageCache.set(sent.key.id, sent.message);
|
|
296
|
+
if (this.sentMessageCache.size > 256) {
|
|
297
|
+
const oldest = this.sentMessageCache.keys().next().value!;
|
|
298
|
+
this.sentMessageCache.delete(oldest);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch (err) {
|
|
302
|
+
this.outgoingQueue.push({ jid: chatId, text: prefixed });
|
|
303
|
+
logger.warn({ chatId, err }, 'Failed to send, message queued');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
isConnected(): boolean {
|
|
308
|
+
return this.connected;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
getStatus(): ChannelStatus {
|
|
312
|
+
return {
|
|
313
|
+
connected: this.connected,
|
|
314
|
+
channel: 'whatsapp',
|
|
315
|
+
identity: this.sock?.user?.id?.split(':')[0],
|
|
316
|
+
uptime_seconds: this.connected
|
|
317
|
+
? Math.floor((Date.now() - this.connectTime) / 1000)
|
|
318
|
+
: 0,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async disconnect(): Promise<void> {
|
|
323
|
+
this.connected = false;
|
|
324
|
+
this.sock?.end(undefined);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async setTyping(chatId: string, isTyping: boolean): Promise<void> {
|
|
328
|
+
try {
|
|
329
|
+
await this.sock.sendPresenceUpdate(
|
|
330
|
+
isTyping ? 'composing' : 'paused',
|
|
331
|
+
chatId,
|
|
332
|
+
);
|
|
333
|
+
} catch {
|
|
334
|
+
// Best effort
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async listChats(): Promise<ChatInfo[]> {
|
|
339
|
+
return Array.from(this.knownChats.entries()).map(([id, info]) => ({
|
|
340
|
+
id,
|
|
341
|
+
name: info.name,
|
|
342
|
+
is_group: info.isGroup,
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async syncGroups(): Promise<ChatInfo[]> {
|
|
347
|
+
return this.syncGroupMetadata(true);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Check if WhatsApp credentials exist on disk. */
|
|
351
|
+
hasAuth(): boolean {
|
|
352
|
+
return fs.existsSync(path.join(AUTH_DIR, 'creds.json'));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ── Internal helpers ──────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
private async syncGroupMetadata(force = false): Promise<ChatInfo[]> {
|
|
358
|
+
if (!force && this.lastGroupSync) {
|
|
359
|
+
const elapsed = Date.now() - new Date(this.lastGroupSync).getTime();
|
|
360
|
+
if (elapsed < GROUP_SYNC_INTERVAL_MS) return [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const groups = await this.sock.groupFetchAllParticipating();
|
|
365
|
+
const result: ChatInfo[] = [];
|
|
366
|
+
for (const [jid, metadata] of Object.entries(groups)) {
|
|
367
|
+
if (metadata.subject) {
|
|
368
|
+
this.knownChats.set(jid, { name: metadata.subject, isGroup: true });
|
|
369
|
+
result.push({ id: jid, name: metadata.subject, is_group: true });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
this.lastGroupSync = new Date().toISOString();
|
|
373
|
+
logger.info({ count: result.length }, 'Group metadata synced');
|
|
374
|
+
return result;
|
|
375
|
+
} catch (err) {
|
|
376
|
+
logger.error({ err }, 'Failed to sync group metadata');
|
|
377
|
+
return [];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private async translateJid(jid: string): Promise<string> {
|
|
382
|
+
if (!jid.endsWith('@lid')) return jid;
|
|
383
|
+
const lidUser = jid.split('@')[0].split(':')[0];
|
|
384
|
+
const cached = this.lidToPhoneMap[lidUser];
|
|
385
|
+
if (cached) return cached;
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const pn = await (
|
|
389
|
+
this.sock.signalRepository as any
|
|
390
|
+
)?.lidMapping?.getPNForLID(jid);
|
|
391
|
+
if (pn) {
|
|
392
|
+
const phoneJid = `${pn.split('@')[0].split(':')[0]}@s.whatsapp.net`;
|
|
393
|
+
this.lidToPhoneMap[lidUser] = phoneJid;
|
|
394
|
+
return phoneJid;
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
// Best effort
|
|
398
|
+
}
|
|
399
|
+
return jid;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private async getNormalizedGroupMetadata(
|
|
403
|
+
jid: string,
|
|
404
|
+
): Promise<GroupMetadata | undefined> {
|
|
405
|
+
if (!jid.endsWith('@g.us')) return undefined;
|
|
406
|
+
const cached = this.groupMetadataCache.get(jid);
|
|
407
|
+
if (cached && cached.expiresAt > Date.now()) return cached.metadata;
|
|
408
|
+
|
|
409
|
+
const metadata = await this.sock.groupMetadata(jid);
|
|
410
|
+
const participants = await Promise.all(
|
|
411
|
+
metadata.participants.map(async (p) => ({
|
|
412
|
+
...p,
|
|
413
|
+
id: await this.translateJid(p.id),
|
|
414
|
+
})),
|
|
415
|
+
);
|
|
416
|
+
const normalized = { ...metadata, participants };
|
|
417
|
+
this.groupMetadataCache.set(jid, {
|
|
418
|
+
metadata: normalized,
|
|
419
|
+
expiresAt: Date.now() + 60_000,
|
|
420
|
+
});
|
|
421
|
+
return normalized;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private async flushOutgoingQueue(): Promise<void> {
|
|
425
|
+
if (this.flushing || this.outgoingQueue.length === 0) return;
|
|
426
|
+
this.flushing = true;
|
|
427
|
+
try {
|
|
428
|
+
while (this.outgoingQueue.length > 0) {
|
|
429
|
+
const item = this.outgoingQueue.shift()!;
|
|
430
|
+
const sent = await this.sock.sendMessage(item.jid, { text: item.text });
|
|
431
|
+
if (sent?.key?.id && sent.message) {
|
|
432
|
+
this.sentMessageCache.set(sent.key.id, sent.message);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} finally {
|
|
436
|
+
this.flushing = false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"sourceMap": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|