fluxy-bot 0.13.1 → 0.13.2
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.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"releaseNotes": [
|
|
5
5
|
"1. react router implemented",
|
|
6
6
|
"2. new workspace design",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@clack/prompts": "^1.1.0",
|
|
56
56
|
"@tailwindcss/vite": "^4.2.0",
|
|
57
57
|
"@vitejs/plugin-react": "^6.0.1",
|
|
58
|
+
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
|
58
59
|
"better-sqlite3": "^12.6.2",
|
|
59
60
|
"class-variance-authority": "^0.7.1",
|
|
60
61
|
"clsx": "^2.1.1",
|
|
@@ -7,6 +7,7 @@ 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';
|
|
10
11
|
|
|
11
12
|
export { ChannelRouter } from './router.js';
|
|
12
13
|
export type { Channel, ChannelType, SenderRole, SenderIdentity, IncomingMessage, OutgoingMessage } from './types.js';
|
|
@@ -30,12 +31,17 @@ export async function initializeChannels(
|
|
|
30
31
|
await chatChannel.initialize(router);
|
|
31
32
|
|
|
32
33
|
// WhatsApp channel — registered if enabled in config
|
|
33
|
-
// The actual implementation is done by Fluxy as a skill.
|
|
34
|
-
// See docs/whatsapp-channel-guide.md for implementation details.
|
|
35
34
|
if (config.channels?.whatsapp?.enabled) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
try {
|
|
36
|
+
const waChannel = new WhatsAppChannel((qr) => {
|
|
37
|
+
// Broadcast QR to chat UI so owner can scan from dashboard
|
|
38
|
+
opts.broadcastFluxy('whatsapp:qr', { qr });
|
|
39
|
+
});
|
|
40
|
+
router.registerChannel(waChannel);
|
|
41
|
+
await waChannel.initialize(router);
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
log.warn(`[channels] WhatsApp initialization failed: ${err.message}`);
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
// Telegram channel — placeholder for future implementation
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WhatsApp Channel — Baileys adapter for WhatsApp Web.
|
|
3
|
+
* Receives messages, routes through ChannelRouter, sends responses back.
|
|
4
|
+
* Session persisted in ~/.fluxy/whatsapp-session/.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import makeWASocket, {
|
|
8
|
+
useMultiFileAuthState,
|
|
9
|
+
DisconnectReason,
|
|
10
|
+
type WASocket,
|
|
11
|
+
} from '@whiskeysockets/baileys';
|
|
12
|
+
import { Boom } from '@hapi/boom';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { DATA_DIR } from '../../shared/paths.js';
|
|
15
|
+
import { log } from '../../shared/logger.js';
|
|
16
|
+
import type { Channel, ChannelRouter, OutgoingMessage } from './types.js';
|
|
17
|
+
|
|
18
|
+
const SESSION_DIR = path.join(DATA_DIR, 'whatsapp-session');
|
|
19
|
+
|
|
20
|
+
export class WhatsAppChannel implements Channel {
|
|
21
|
+
readonly type = 'whatsapp' as const;
|
|
22
|
+
private sock: WASocket | null = null;
|
|
23
|
+
private router: ChannelRouter | null = null;
|
|
24
|
+
private onQR?: (qr: string) => void;
|
|
25
|
+
private reconnecting = false;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param onQR - Optional callback to surface QR code to the chat UI
|
|
29
|
+
*/
|
|
30
|
+
constructor(onQR?: (qr: string) => void) {
|
|
31
|
+
this.onQR = onQR;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async initialize(router: ChannelRouter): Promise<void> {
|
|
35
|
+
this.router = router;
|
|
36
|
+
await this.connect();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async connect(): Promise<void> {
|
|
40
|
+
const { state, saveCreds } = await useMultiFileAuthState(SESSION_DIR);
|
|
41
|
+
|
|
42
|
+
this.sock = makeWASocket({
|
|
43
|
+
auth: state,
|
|
44
|
+
printQRInTerminal: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.sock.ev.on('creds.update', saveCreds);
|
|
48
|
+
|
|
49
|
+
this.sock.ev.on('connection.update', (update) => {
|
|
50
|
+
const { connection, lastDisconnect, qr } = update;
|
|
51
|
+
|
|
52
|
+
// Surface QR to chat UI for scanning
|
|
53
|
+
if (qr && this.onQR) {
|
|
54
|
+
this.onQR(qr);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
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');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (connection === 'open') {
|
|
77
|
+
log.ok('[whatsapp] Connected to WhatsApp');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.sock.ev.on('messages.upsert', ({ messages, type }) => {
|
|
82
|
+
if (type !== 'notify') return;
|
|
83
|
+
|
|
84
|
+
for (const msg of messages) {
|
|
85
|
+
// Skip messages sent by us
|
|
86
|
+
if (msg.key.fromMe) continue;
|
|
87
|
+
// Skip status broadcasts
|
|
88
|
+
if (msg.key.remoteJid === 'status@broadcast') continue;
|
|
89
|
+
|
|
90
|
+
const remoteJid = msg.key.remoteJid!;
|
|
91
|
+
const senderId = remoteJid.replace('@s.whatsapp.net', '').replace('@g.us', '');
|
|
92
|
+
|
|
93
|
+
// Extract text content
|
|
94
|
+
const content =
|
|
95
|
+
msg.message?.conversation ||
|
|
96
|
+
msg.message?.extendedTextMessage?.text ||
|
|
97
|
+
msg.message?.imageMessage?.caption ||
|
|
98
|
+
'';
|
|
99
|
+
|
|
100
|
+
if (!content) continue;
|
|
101
|
+
|
|
102
|
+
const displayName = msg.pushName || senderId;
|
|
103
|
+
const conversationKey = `whatsapp:${senderId}`;
|
|
104
|
+
|
|
105
|
+
log.info(`[whatsapp] Message from ${displayName} (${senderId}): ${content.slice(0, 80)}`);
|
|
106
|
+
|
|
107
|
+
// Route through the channel router
|
|
108
|
+
this.router!.handleIncoming({
|
|
109
|
+
sender: {
|
|
110
|
+
channelType: 'whatsapp',
|
|
111
|
+
senderId,
|
|
112
|
+
displayName,
|
|
113
|
+
role: 'customer', // Router will resolve the actual role
|
|
114
|
+
conversationKey,
|
|
115
|
+
},
|
|
116
|
+
content,
|
|
117
|
+
timestamp: (msg.messageTimestamp as number) * 1000 || Date.now(),
|
|
118
|
+
}).catch((err) => {
|
|
119
|
+
log.warn(`[whatsapp] Router error: ${err.message}`);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async sendMessage(to: string, message: OutgoingMessage): Promise<void> {
|
|
126
|
+
if (!this.sock) {
|
|
127
|
+
log.warn('[whatsapp] Cannot send — not connected');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const jid = to.includes('@') ? to : `${to}@s.whatsapp.net`;
|
|
132
|
+
await this.sock.sendMessage(jid, { text: message.content });
|
|
133
|
+
log.info(`[whatsapp] Sent to ${to}: ${message.content.slice(0, 80)}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ownsConversation(conversationKey: string): boolean {
|
|
137
|
+
return conversationKey.startsWith('whatsapp:');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async shutdown(): Promise<void> {
|
|
141
|
+
if (this.sock) {
|
|
142
|
+
this.sock.end(undefined);
|
|
143
|
+
this.sock = null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|