fluxy-bot 0.13.2 → 0.13.5
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/package.json
CHANGED
|
@@ -7,7 +7,6 @@ import { log } from '../../shared/logger.js';
|
|
|
7
7
|
import { initRoleResolver } from './role-resolver.js';
|
|
8
8
|
import { ChannelRouter, type ChannelRouterOpts } from './router.js';
|
|
9
9
|
import { ChatChannel } from './chat-channel.js';
|
|
10
|
-
import { WhatsAppChannel } from './whatsapp-channel.js';
|
|
11
10
|
|
|
12
11
|
export { ChannelRouter } from './router.js';
|
|
13
12
|
export type { Channel, ChannelType, SenderRole, SenderIdentity, IncomingMessage, OutgoingMessage } from './types.js';
|
|
@@ -30,17 +29,20 @@ export async function initializeChannels(
|
|
|
30
29
|
router.registerChannel(chatChannel);
|
|
31
30
|
await chatChannel.initialize(router);
|
|
32
31
|
|
|
33
|
-
// WhatsApp channel —
|
|
32
|
+
// WhatsApp channel — dynamic import so supervisor starts even without baileys installed
|
|
34
33
|
if (config.channels?.whatsapp?.enabled) {
|
|
35
34
|
try {
|
|
35
|
+
const { WhatsAppChannel } = await import('./whatsapp-channel.js');
|
|
36
36
|
const waChannel = new WhatsAppChannel((qr) => {
|
|
37
|
-
// Broadcast QR to chat UI so owner can scan from dashboard
|
|
38
37
|
opts.broadcastFluxy('whatsapp:qr', { qr });
|
|
39
38
|
});
|
|
40
39
|
router.registerChannel(waChannel);
|
|
41
40
|
await waChannel.initialize(router);
|
|
42
41
|
} catch (err: any) {
|
|
43
42
|
log.warn(`[channels] WhatsApp initialization failed: ${err.message}`);
|
|
43
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND') {
|
|
44
|
+
log.warn('[channels] Install baileys: npm install @whiskeysockets/baileys');
|
|
45
|
+
}
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -2,95 +2,116 @@
|
|
|
2
2
|
* WhatsApp Channel — Baileys adapter for WhatsApp Web.
|
|
3
3
|
* Receives messages, routes through ChannelRouter, sends responses back.
|
|
4
4
|
* Session persisted in ~/.fluxy/whatsapp-session/.
|
|
5
|
+
*
|
|
6
|
+
* Baileys is imported dynamically so the supervisor starts even if
|
|
7
|
+
* @whiskeysockets/baileys is not installed.
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
|
-
import makeWASocket, {
|
|
8
|
-
useMultiFileAuthState,
|
|
9
|
-
DisconnectReason,
|
|
10
|
-
type WASocket,
|
|
11
|
-
} from '@whiskeysockets/baileys';
|
|
12
|
-
import { Boom } from '@hapi/boom';
|
|
13
10
|
import path from 'path';
|
|
14
11
|
import { DATA_DIR } from '../../shared/paths.js';
|
|
15
12
|
import { log } from '../../shared/logger.js';
|
|
16
13
|
import type { Channel, ChannelRouter, OutgoingMessage } from './types.js';
|
|
17
14
|
|
|
18
15
|
const SESSION_DIR = path.join(DATA_DIR, 'whatsapp-session');
|
|
16
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
17
|
+
const RECONNECT_BASE_DELAY = 5_000; // 5s, doubles each retry
|
|
19
18
|
|
|
20
19
|
export class WhatsAppChannel implements Channel {
|
|
21
20
|
readonly type = 'whatsapp' as const;
|
|
22
|
-
private sock:
|
|
21
|
+
private sock: any = null;
|
|
23
22
|
private router: ChannelRouter | null = null;
|
|
24
23
|
private onQR?: (qr: string) => void;
|
|
25
|
-
private
|
|
24
|
+
private reconnectAttempts = 0;
|
|
25
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
26
|
+
private shuttingDown = false;
|
|
26
27
|
|
|
27
|
-
/**
|
|
28
|
-
* @param onQR - Optional callback to surface QR code to the chat UI
|
|
29
|
-
*/
|
|
30
28
|
constructor(onQR?: (qr: string) => void) {
|
|
31
29
|
this.onQR = onQR;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
async initialize(router: ChannelRouter): Promise<void> {
|
|
35
33
|
this.router = router;
|
|
36
|
-
|
|
34
|
+
try {
|
|
35
|
+
await this.connect();
|
|
36
|
+
} catch (err: any) {
|
|
37
|
+
// Don't let baileys crash the supervisor — log and move on
|
|
38
|
+
log.warn(`[whatsapp] Initial connection failed: ${err.message}`);
|
|
39
|
+
log.warn('[whatsapp] WhatsApp will remain inactive. Restart Fluxy after scanning QR to retry.');
|
|
40
|
+
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
private async connect(): Promise<void> {
|
|
44
|
+
if (this.shuttingDown) return;
|
|
45
|
+
|
|
46
|
+
const baileys = await import('@whiskeysockets/baileys');
|
|
47
|
+
const makeWASocket = baileys.default;
|
|
48
|
+
const { useMultiFileAuthState, DisconnectReason } = baileys;
|
|
49
|
+
|
|
40
50
|
const { state, saveCreds } = await useMultiFileAuthState(SESSION_DIR);
|
|
41
51
|
|
|
42
52
|
this.sock = makeWASocket({
|
|
43
53
|
auth: state,
|
|
44
|
-
printQRInTerminal
|
|
54
|
+
// printQRInTerminal is deprecated in baileys v7 — we handle QR via connection.update
|
|
45
55
|
});
|
|
46
56
|
|
|
47
57
|
this.sock.ev.on('creds.update', saveCreds);
|
|
48
58
|
|
|
49
|
-
this.sock.ev.on('connection.update', (update) => {
|
|
59
|
+
this.sock.ev.on('connection.update', (update: any) => {
|
|
50
60
|
const { connection, lastDisconnect, qr } = update;
|
|
51
61
|
|
|
52
|
-
//
|
|
53
|
-
if (qr
|
|
54
|
-
|
|
62
|
+
// Handle QR code — surface it to chat UI
|
|
63
|
+
if (qr) {
|
|
64
|
+
log.info('[whatsapp] QR code received — scan with your phone to link');
|
|
65
|
+
if (this.onQR) this.onQR(qr);
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
if (connection === 'close') {
|
|
58
|
-
const statusCode = (lastDisconnect?.error as
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const statusCode = (lastDisconnect?.error as any)?.output?.statusCode;
|
|
70
|
+
const isLoggedOut = statusCode === DisconnectReason.loggedOut;
|
|
71
|
+
|
|
72
|
+
if (isLoggedOut) {
|
|
73
|
+
log.warn('[whatsapp] Logged out. Scan QR again to reconnect.');
|
|
74
|
+
this.sock = null;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.reconnectAttempts++;
|
|
79
|
+
|
|
80
|
+
if (this.reconnectAttempts > MAX_RECONNECT_ATTEMPTS) {
|
|
81
|
+
log.warn(`[whatsapp] Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached. Giving up.`);
|
|
82
|
+
log.warn('[whatsapp] WhatsApp will remain inactive. Restart Fluxy to retry.');
|
|
83
|
+
this.sock = null;
|
|
84
|
+
return;
|
|
73
85
|
}
|
|
86
|
+
|
|
87
|
+
// Exponential backoff: 5s, 10s, 20s, 40s, 80s
|
|
88
|
+
const delay = RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1);
|
|
89
|
+
log.warn(`[whatsapp] Connection closed (${statusCode}). Reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
|
|
90
|
+
|
|
91
|
+
this.reconnectTimer = setTimeout(() => {
|
|
92
|
+
this.reconnectTimer = null;
|
|
93
|
+
this.connect().catch((err) => {
|
|
94
|
+
log.warn(`[whatsapp] Reconnect failed: ${err.message}`);
|
|
95
|
+
});
|
|
96
|
+
}, delay);
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
if (connection === 'open') {
|
|
77
100
|
log.ok('[whatsapp] Connected to WhatsApp');
|
|
101
|
+
this.reconnectAttempts = 0; // Reset on successful connection
|
|
78
102
|
}
|
|
79
103
|
});
|
|
80
104
|
|
|
81
|
-
this.sock.ev.on('messages.upsert', ({ messages, type }) => {
|
|
105
|
+
this.sock.ev.on('messages.upsert', ({ messages, type }: any) => {
|
|
82
106
|
if (type !== 'notify') return;
|
|
83
107
|
|
|
84
108
|
for (const msg of messages) {
|
|
85
|
-
// Skip messages sent by us
|
|
86
109
|
if (msg.key.fromMe) continue;
|
|
87
|
-
// Skip status broadcasts
|
|
88
110
|
if (msg.key.remoteJid === 'status@broadcast') continue;
|
|
89
111
|
|
|
90
112
|
const remoteJid = msg.key.remoteJid!;
|
|
91
113
|
const senderId = remoteJid.replace('@s.whatsapp.net', '').replace('@g.us', '');
|
|
92
114
|
|
|
93
|
-
// Extract text content
|
|
94
115
|
const content =
|
|
95
116
|
msg.message?.conversation ||
|
|
96
117
|
msg.message?.extendedTextMessage?.text ||
|
|
@@ -104,13 +125,12 @@ export class WhatsAppChannel implements Channel {
|
|
|
104
125
|
|
|
105
126
|
log.info(`[whatsapp] Message from ${displayName} (${senderId}): ${content.slice(0, 80)}`);
|
|
106
127
|
|
|
107
|
-
// Route through the channel router
|
|
108
128
|
this.router!.handleIncoming({
|
|
109
129
|
sender: {
|
|
110
130
|
channelType: 'whatsapp',
|
|
111
131
|
senderId,
|
|
112
132
|
displayName,
|
|
113
|
-
role: 'customer',
|
|
133
|
+
role: 'customer',
|
|
114
134
|
conversationKey,
|
|
115
135
|
},
|
|
116
136
|
content,
|
|
@@ -138,6 +158,11 @@ export class WhatsAppChannel implements Channel {
|
|
|
138
158
|
}
|
|
139
159
|
|
|
140
160
|
async shutdown(): Promise<void> {
|
|
161
|
+
this.shuttingDown = true;
|
|
162
|
+
if (this.reconnectTimer) {
|
|
163
|
+
clearTimeout(this.reconnectTimer);
|
|
164
|
+
this.reconnectTimer = null;
|
|
165
|
+
}
|
|
141
166
|
if (this.sock) {
|
|
142
167
|
this.sock.end(undefined);
|
|
143
168
|
this.sock = null;
|