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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.13.2",
3
+ "version": "0.13.5",
4
4
  "releaseNotes": [
5
5
  "1. react router implemented",
6
6
  "2. new workspace design",
@@ -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 — registered if enabled in config
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: WASocket | null = null;
21
+ private sock: any = null;
23
22
  private router: ChannelRouter | null = null;
24
23
  private onQR?: (qr: string) => void;
25
- private reconnecting = false;
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
- await this.connect();
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: true,
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
- // Surface QR to chat UI for scanning
53
- if (qr && this.onQR) {
54
- this.onQR(qr);
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 Boom)?.output?.statusCode;
59
- const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
60
-
61
- log.warn(`[whatsapp] Connection closed: ${statusCode}. Reconnect: ${shouldReconnect}`);
62
-
63
- if (shouldReconnect && !this.reconnecting) {
64
- this.reconnecting = true;
65
- setTimeout(() => {
66
- this.reconnecting = false;
67
- this.connect().catch((err) => {
68
- log.warn(`[whatsapp] Reconnect failed: ${err.message}`);
69
- });
70
- }, 5000);
71
- } else if (!shouldReconnect) {
72
- log.warn('[whatsapp] Logged out — scan QR again to reconnect');
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', // Router will resolve the actual role
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;