nextclaw 0.1.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/bridge/package.json +26 -0
- package/bridge/src/index.ts +68 -0
- package/bridge/src/server.ts +104 -0
- package/bridge/src/types.d.ts +3 -0
- package/bridge/src/whatsapp.ts +187 -0
- package/bridge/tsconfig.json +16 -0
- package/dist/chunk-4I7WMYPU.js +2289 -0
- package/dist/chunk-PIG2O4DT.js +2095 -0
- package/dist/chunk-RTVGGPPW.js +2307 -0
- package/dist/chunk-XAY6UDOR.js +2282 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +3125 -0
- package/dist/index.d.ts +2869 -0
- package/dist/index.js +86 -0
- package/package.json +82 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextclaw-whatsapp-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WhatsApp bridge for nextclaw using Baileys",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"dev": "tsc && node dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
|
14
|
+
"ws": "^8.17.1",
|
|
15
|
+
"qrcode-terminal": "^0.12.0",
|
|
16
|
+
"pino": "^9.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.14.0",
|
|
20
|
+
"@types/ws": "^8.5.10",
|
|
21
|
+
"typescript": "^5.4.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* nextclaw WhatsApp Bridge
|
|
4
|
+
*
|
|
5
|
+
* This bridge connects WhatsApp Web to nextclaw's backend
|
|
6
|
+
* via WebSocket. It handles authentication, message forwarding,
|
|
7
|
+
* and reconnection logic.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npm run build && npm start
|
|
11
|
+
*
|
|
12
|
+
* Or with custom settings:
|
|
13
|
+
* BRIDGE_PORT=3001 AUTH_DIR=~/.nextclaw/whatsapp npm start
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Polyfill crypto for Baileys in ESM
|
|
17
|
+
import { webcrypto } from 'crypto';
|
|
18
|
+
type CryptoLike = typeof webcrypto;
|
|
19
|
+
const globalCrypto = globalThis as typeof globalThis & { crypto?: CryptoLike };
|
|
20
|
+
if (!globalCrypto.crypto) {
|
|
21
|
+
globalCrypto.crypto = webcrypto;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
import { BridgeServer } from './server.js';
|
|
25
|
+
import { existsSync } from 'fs';
|
|
26
|
+
import { homedir } from 'os';
|
|
27
|
+
import { join } from 'path';
|
|
28
|
+
|
|
29
|
+
const PORT = parseInt(process.env.BRIDGE_PORT || '3001', 10);
|
|
30
|
+
const envHome = process.env.NEXTCLAW_HOME?.trim() || process.env.NEXTBOT_HOME?.trim();
|
|
31
|
+
const defaultHome = (() => {
|
|
32
|
+
if (envHome) {
|
|
33
|
+
return envHome;
|
|
34
|
+
}
|
|
35
|
+
const primary = join(homedir(), '.nextclaw');
|
|
36
|
+
if (existsSync(primary)) {
|
|
37
|
+
return primary;
|
|
38
|
+
}
|
|
39
|
+
const legacy = join(homedir(), '.nextbot');
|
|
40
|
+
if (existsSync(legacy)) {
|
|
41
|
+
return legacy;
|
|
42
|
+
}
|
|
43
|
+
return primary;
|
|
44
|
+
})();
|
|
45
|
+
const AUTH_DIR = process.env.AUTH_DIR || join(defaultHome, 'whatsapp-auth');
|
|
46
|
+
|
|
47
|
+
console.log('š¤ nextclaw WhatsApp Bridge');
|
|
48
|
+
console.log('========================\n');
|
|
49
|
+
|
|
50
|
+
const server = new BridgeServer(PORT, AUTH_DIR);
|
|
51
|
+
|
|
52
|
+
// Handle graceful shutdown
|
|
53
|
+
process.on('SIGINT', async () => {
|
|
54
|
+
console.log('\n\nShutting down...');
|
|
55
|
+
await server.stop();
|
|
56
|
+
process.exit(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
process.on('SIGTERM', async () => {
|
|
60
|
+
await server.stop();
|
|
61
|
+
process.exit(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Start the server
|
|
65
|
+
server.start().catch((error) => {
|
|
66
|
+
console.error('Failed to start bridge:', error);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket server for Python-Node.js bridge communication.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
6
|
+
import { WhatsAppClient } from './whatsapp.js';
|
|
7
|
+
|
|
8
|
+
interface SendCommand {
|
|
9
|
+
type: 'send';
|
|
10
|
+
to: string;
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface BridgeMessage {
|
|
15
|
+
type: 'message' | 'status' | 'qr' | 'error';
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class BridgeServer {
|
|
20
|
+
private wss: WebSocketServer | null = null;
|
|
21
|
+
private wa: WhatsAppClient | null = null;
|
|
22
|
+
private clients: Set<WebSocket> = new Set();
|
|
23
|
+
|
|
24
|
+
constructor(private port: number, private authDir: string) {}
|
|
25
|
+
|
|
26
|
+
async start(): Promise<void> {
|
|
27
|
+
// Create WebSocket server
|
|
28
|
+
this.wss = new WebSocketServer({ port: this.port });
|
|
29
|
+
console.log(`š Bridge server listening on ws://localhost:${this.port}`);
|
|
30
|
+
|
|
31
|
+
// Initialize WhatsApp client
|
|
32
|
+
this.wa = new WhatsAppClient({
|
|
33
|
+
authDir: this.authDir,
|
|
34
|
+
onMessage: (msg) => this.broadcast({ type: 'message', ...msg }),
|
|
35
|
+
onQR: (qr) => this.broadcast({ type: 'qr', qr }),
|
|
36
|
+
onStatus: (status) => this.broadcast({ type: 'status', status }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Handle WebSocket connections
|
|
40
|
+
this.wss.on('connection', (ws) => {
|
|
41
|
+
console.log('š Python client connected');
|
|
42
|
+
this.clients.add(ws);
|
|
43
|
+
|
|
44
|
+
ws.on('message', async (data) => {
|
|
45
|
+
try {
|
|
46
|
+
const cmd = JSON.parse(data.toString()) as SendCommand;
|
|
47
|
+
await this.handleCommand(cmd);
|
|
48
|
+
ws.send(JSON.stringify({ type: 'sent', to: cmd.to }));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Error handling command:', error);
|
|
51
|
+
ws.send(JSON.stringify({ type: 'error', error: String(error) }));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
ws.on('close', () => {
|
|
56
|
+
console.log('š Python client disconnected');
|
|
57
|
+
this.clients.delete(ws);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
ws.on('error', (error) => {
|
|
61
|
+
console.error('WebSocket error:', error);
|
|
62
|
+
this.clients.delete(ws);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Connect to WhatsApp
|
|
67
|
+
await this.wa.connect();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async handleCommand(cmd: SendCommand): Promise<void> {
|
|
71
|
+
if (cmd.type === 'send' && this.wa) {
|
|
72
|
+
await this.wa.sendMessage(cmd.to, cmd.text);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private broadcast(msg: BridgeMessage): void {
|
|
77
|
+
const data = JSON.stringify(msg);
|
|
78
|
+
for (const client of this.clients) {
|
|
79
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
80
|
+
client.send(data);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async stop(): Promise<void> {
|
|
86
|
+
// Close all client connections
|
|
87
|
+
for (const client of this.clients) {
|
|
88
|
+
client.close();
|
|
89
|
+
}
|
|
90
|
+
this.clients.clear();
|
|
91
|
+
|
|
92
|
+
// Close WebSocket server
|
|
93
|
+
if (this.wss) {
|
|
94
|
+
this.wss.close();
|
|
95
|
+
this.wss = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Disconnect WhatsApp
|
|
99
|
+
if (this.wa) {
|
|
100
|
+
await this.wa.disconnect();
|
|
101
|
+
this.wa = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp client wrapper using Baileys.
|
|
3
|
+
* Based on OpenClaw's working implementation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
7
|
+
import makeWASocket, {
|
|
8
|
+
DisconnectReason,
|
|
9
|
+
useMultiFileAuthState,
|
|
10
|
+
fetchLatestBaileysVersion,
|
|
11
|
+
makeCacheableSignalKeyStore,
|
|
12
|
+
} from '@whiskeysockets/baileys';
|
|
13
|
+
|
|
14
|
+
import type { Boom } from '@hapi/boom';
|
|
15
|
+
import qrcode from 'qrcode-terminal';
|
|
16
|
+
import pino from 'pino';
|
|
17
|
+
|
|
18
|
+
const VERSION = '0.1.0';
|
|
19
|
+
|
|
20
|
+
export interface InboundMessage {
|
|
21
|
+
id: string;
|
|
22
|
+
sender: string;
|
|
23
|
+
pn: string;
|
|
24
|
+
content: string;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
isGroup: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WhatsAppClientOptions {
|
|
30
|
+
authDir: string;
|
|
31
|
+
onMessage: (msg: InboundMessage) => void;
|
|
32
|
+
onQR: (qr: string) => void;
|
|
33
|
+
onStatus: (status: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class WhatsAppClient {
|
|
37
|
+
private sock: any = null;
|
|
38
|
+
private options: WhatsAppClientOptions;
|
|
39
|
+
private reconnecting = false;
|
|
40
|
+
|
|
41
|
+
constructor(options: WhatsAppClientOptions) {
|
|
42
|
+
this.options = options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async connect(): Promise<void> {
|
|
46
|
+
const logger = pino({ level: 'silent' });
|
|
47
|
+
const { state, saveCreds } = await useMultiFileAuthState(this.options.authDir);
|
|
48
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
49
|
+
|
|
50
|
+
console.log(`Using Baileys version: ${version.join('.')}`);
|
|
51
|
+
|
|
52
|
+
// Create socket following OpenClaw's pattern
|
|
53
|
+
this.sock = makeWASocket({
|
|
54
|
+
auth: {
|
|
55
|
+
creds: state.creds,
|
|
56
|
+
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
|
57
|
+
},
|
|
58
|
+
version,
|
|
59
|
+
logger,
|
|
60
|
+
printQRInTerminal: false,
|
|
61
|
+
browser: ['nextclaw', 'cli', VERSION],
|
|
62
|
+
syncFullHistory: false,
|
|
63
|
+
markOnlineOnConnect: false,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Handle WebSocket errors
|
|
67
|
+
if (this.sock.ws && typeof this.sock.ws.on === 'function') {
|
|
68
|
+
this.sock.ws.on('error', (err: Error) => {
|
|
69
|
+
console.error('WebSocket error:', err.message);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle connection updates
|
|
74
|
+
this.sock.ev.on('connection.update', async (update: any) => {
|
|
75
|
+
const { connection, lastDisconnect, qr } = update;
|
|
76
|
+
|
|
77
|
+
if (qr) {
|
|
78
|
+
// Display QR code in terminal
|
|
79
|
+
console.log('\nš± Scan this QR code with WhatsApp (Linked Devices):\n');
|
|
80
|
+
qrcode.generate(qr, { small: true });
|
|
81
|
+
this.options.onQR(qr);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (connection === 'close') {
|
|
85
|
+
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode;
|
|
86
|
+
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
87
|
+
|
|
88
|
+
console.log(`Connection closed. Status: ${statusCode}, Will reconnect: ${shouldReconnect}`);
|
|
89
|
+
this.options.onStatus('disconnected');
|
|
90
|
+
|
|
91
|
+
if (shouldReconnect && !this.reconnecting) {
|
|
92
|
+
this.reconnecting = true;
|
|
93
|
+
console.log('Reconnecting in 5 seconds...');
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
this.reconnecting = false;
|
|
96
|
+
this.connect();
|
|
97
|
+
}, 5000);
|
|
98
|
+
}
|
|
99
|
+
} else if (connection === 'open') {
|
|
100
|
+
console.log('ā
Connected to WhatsApp');
|
|
101
|
+
this.options.onStatus('connected');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Save credentials on update
|
|
106
|
+
this.sock.ev.on('creds.update', saveCreds);
|
|
107
|
+
|
|
108
|
+
// Handle incoming messages
|
|
109
|
+
this.sock.ev.on('messages.upsert', async ({ messages, type }: { messages: any[]; type: string }) => {
|
|
110
|
+
if (type !== 'notify') return;
|
|
111
|
+
|
|
112
|
+
for (const msg of messages) {
|
|
113
|
+
// Skip own messages
|
|
114
|
+
if (msg.key.fromMe) continue;
|
|
115
|
+
|
|
116
|
+
// Skip status updates
|
|
117
|
+
if (msg.key.remoteJid === 'status@broadcast') continue;
|
|
118
|
+
|
|
119
|
+
const content = this.extractMessageContent(msg);
|
|
120
|
+
if (!content) continue;
|
|
121
|
+
|
|
122
|
+
const isGroup = msg.key.remoteJid?.endsWith('@g.us') || false;
|
|
123
|
+
|
|
124
|
+
this.options.onMessage({
|
|
125
|
+
id: msg.key.id || '',
|
|
126
|
+
sender: msg.key.remoteJid || '',
|
|
127
|
+
pn: msg.key.remoteJidAlt || '',
|
|
128
|
+
content,
|
|
129
|
+
timestamp: msg.messageTimestamp as number,
|
|
130
|
+
isGroup,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private extractMessageContent(msg: any): string | null {
|
|
137
|
+
const message = msg.message;
|
|
138
|
+
if (!message) return null;
|
|
139
|
+
|
|
140
|
+
// Text message
|
|
141
|
+
if (message.conversation) {
|
|
142
|
+
return message.conversation;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Extended text (reply, link preview)
|
|
146
|
+
if (message.extendedTextMessage?.text) {
|
|
147
|
+
return message.extendedTextMessage.text;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Image with caption
|
|
151
|
+
if (message.imageMessage?.caption) {
|
|
152
|
+
return `[Image] ${message.imageMessage.caption}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Video with caption
|
|
156
|
+
if (message.videoMessage?.caption) {
|
|
157
|
+
return `[Video] ${message.videoMessage.caption}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Document with caption
|
|
161
|
+
if (message.documentMessage?.caption) {
|
|
162
|
+
return `[Document] ${message.documentMessage.caption}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Voice/Audio message
|
|
166
|
+
if (message.audioMessage) {
|
|
167
|
+
return `[Voice Message]`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async sendMessage(to: string, text: string): Promise<void> {
|
|
174
|
+
if (!this.sock) {
|
|
175
|
+
throw new Error('Not connected');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await this.sock.sendMessage(to, { text });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async disconnect(): Promise<void> {
|
|
182
|
+
if (this.sock) {
|
|
183
|
+
this.sock.end(undefined);
|
|
184
|
+
this.sock = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": "./src",
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"resolveJsonModule": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|