Skip to content

Commit 29d0db7

Browse files
authored
fix(whatsapp): wait for creds update and fix config path (#38)
1 parent f950c37 commit 29d0db7

File tree

5 files changed

+147
-22
lines changed

5 files changed

+147
-22
lines changed

electron/main/ipc-handlers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ function registerOpenClawHandlers(): void {
532532
// Save channel configuration
533533
ipcMain.handle('channel:saveConfig', async (_, channelType: string, config: Record<string, unknown>) => {
534534
try {
535+
logger.info('channel:saveConfig', { channelType, keys: Object.keys(config || {}) });
535536
saveChannelConfig(channelType, config);
536537
return { success: true };
537538
} catch (error) {
@@ -625,9 +626,11 @@ function registerWhatsAppHandlers(mainWindow: BrowserWindow): void {
625626
// Request WhatsApp QR code
626627
ipcMain.handle('channel:requestWhatsAppQr', async (_, accountId: string) => {
627628
try {
629+
logger.info('channel:requestWhatsAppQr', { accountId });
628630
await whatsAppLoginManager.start(accountId);
629631
return { success: true };
630632
} catch (error) {
633+
logger.error('channel:requestWhatsAppQr failed', error);
631634
return { success: false, error: String(error) };
632635
}
633636
});
@@ -638,6 +641,7 @@ function registerWhatsAppHandlers(mainWindow: BrowserWindow): void {
638641
await whatsAppLoginManager.stop();
639642
return { success: true };
640643
} catch (error) {
644+
logger.error('channel:cancelWhatsAppQr failed', error);
641645
return { success: false, error: String(error) };
642646
}
643647
});
@@ -654,12 +658,14 @@ function registerWhatsAppHandlers(mainWindow: BrowserWindow): void {
654658

655659
whatsAppLoginManager.on('success', (data) => {
656660
if (!mainWindow.isDestroyed()) {
661+
logger.info('whatsapp:login-success', data);
657662
mainWindow.webContents.send('channel:whatsapp-success', data);
658663
}
659664
});
660665

661666
whatsAppLoginManager.on('error', (error) => {
662667
if (!mainWindow.isDestroyed()) {
668+
logger.error('whatsapp:login-error', error);
663669
mainWindow.webContents.send('channel:whatsapp-error', error);
664670
}
665671
});

electron/utils/channel-config.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,27 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSy
66
import { join } from 'path';
77
import { homedir } from 'os';
88
import { getOpenClawResolvedDir } from './paths';
9+
import * as logger from './logger';
910

1011
const OPENCLAW_DIR = join(homedir(), '.openclaw');
1112
const CONFIG_FILE = join(OPENCLAW_DIR, 'openclaw.json');
1213

14+
// Channels that are managed as plugins (config goes under plugins.entries, not channels)
15+
const PLUGIN_CHANNELS = ['whatsapp'];
16+
1317
export interface ChannelConfigData {
1418
enabled?: boolean;
1519
[key: string]: unknown;
1620
}
1721

22+
export interface PluginsConfig {
23+
entries?: Record<string, ChannelConfigData>;
24+
[key: string]: unknown;
25+
}
26+
1827
export interface OpenClawConfig {
1928
channels?: Record<string, ChannelConfigData>;
29+
plugins?: PluginsConfig;
2030
[key: string]: unknown;
2131
}
2232

@@ -43,6 +53,7 @@ export function readOpenClawConfig(): OpenClawConfig {
4353
const content = readFileSync(CONFIG_FILE, 'utf-8');
4454
return JSON.parse(content) as OpenClawConfig;
4555
} catch (error) {
56+
logger.error('Failed to read OpenClaw config', error);
4657
console.error('Failed to read OpenClaw config:', error);
4758
return {};
4859
}
@@ -57,6 +68,7 @@ export function writeOpenClawConfig(config: OpenClawConfig): void {
5768
try {
5869
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
5970
} catch (error) {
71+
logger.error('Failed to write OpenClaw config', error);
6072
console.error('Failed to write OpenClaw config:', error);
6173
throw error;
6274
}
@@ -73,6 +85,28 @@ export function saveChannelConfig(
7385
): void {
7486
const currentConfig = readOpenClawConfig();
7587

88+
// Plugin-based channels (e.g. WhatsApp) go under plugins.entries, not channels
89+
if (PLUGIN_CHANNELS.includes(channelType)) {
90+
if (!currentConfig.plugins) {
91+
currentConfig.plugins = {};
92+
}
93+
if (!currentConfig.plugins.entries) {
94+
currentConfig.plugins.entries = {};
95+
}
96+
currentConfig.plugins.entries[channelType] = {
97+
...currentConfig.plugins.entries[channelType],
98+
enabled: config.enabled ?? true,
99+
};
100+
writeOpenClawConfig(currentConfig);
101+
logger.info('Plugin channel config saved', {
102+
channelType,
103+
configFile: CONFIG_FILE,
104+
path: `plugins.entries.${channelType}`,
105+
});
106+
console.log(`Saved plugin channel config for ${channelType}`);
107+
return;
108+
}
109+
76110
if (!currentConfig.channels) {
77111
currentConfig.channels = {};
78112
}
@@ -146,6 +180,13 @@ export function saveChannelConfig(
146180
};
147181

148182
writeOpenClawConfig(currentConfig);
183+
logger.info('Channel config saved', {
184+
channelType,
185+
configFile: CONFIG_FILE,
186+
rawKeys: Object.keys(config),
187+
transformedKeys: Object.keys(transformedConfig),
188+
enabled: currentConfig.channels[channelType]?.enabled,
189+
});
149190
console.log(`Saved channel config for ${channelType}`);
150191
}
151192

@@ -289,6 +330,23 @@ export function listConfiguredChannels(): string[] {
289330
export function setChannelEnabled(channelType: string, enabled: boolean): void {
290331
const currentConfig = readOpenClawConfig();
291332

333+
// Plugin-based channels go under plugins.entries
334+
if (PLUGIN_CHANNELS.includes(channelType)) {
335+
if (!currentConfig.plugins) {
336+
currentConfig.plugins = {};
337+
}
338+
if (!currentConfig.plugins.entries) {
339+
currentConfig.plugins.entries = {};
340+
}
341+
if (!currentConfig.plugins.entries[channelType]) {
342+
currentConfig.plugins.entries[channelType] = {};
343+
}
344+
currentConfig.plugins.entries[channelType].enabled = enabled;
345+
writeOpenClawConfig(currentConfig);
346+
console.log(`Set plugin channel ${channelType} enabled: ${enabled}`);
347+
return;
348+
}
349+
292350
if (!currentConfig.channels) {
293351
currentConfig.channels = {};
294352
}
@@ -457,10 +515,16 @@ async function validateTelegramCredentials(
457515
): Promise<CredentialValidationResult> {
458516
const botToken = config.botToken?.trim();
459517

518+
const allowedUsers = config.allowedUsers?.trim();
519+
460520
if (!botToken) {
461521
return { valid: false, errors: ['Bot token is required'], warnings: [] };
462522
}
463523

524+
if (!allowedUsers) {
525+
return { valid: false, errors: ['At least one allowed user ID is required'], warnings: [] };
526+
}
527+
464528
try {
465529
const response = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
466530
const data = (await response.json()) as { ok?: boolean; description?: string; result?: { username?: string } };
@@ -553,6 +617,12 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
553617
result.errors.push('Telegram: Bot token is required');
554618
result.valid = false;
555619
}
620+
// Check allowed users (stored as allowFrom array)
621+
const allowedUsers = telegramConfig?.allowFrom as string[] | undefined;
622+
if (!allowedUsers || allowedUsers.length === 0) {
623+
result.errors.push('Telegram: Allowed User IDs are required');
624+
result.valid = false;
625+
}
556626
}
557627

558628
if (result.errors.length === 0 && result.warnings.length === 0) {

electron/utils/whatsapp-login.ts

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ const QRCodeModule = require(join(qrcodeTerminalPath, 'vendor', 'QRCode', 'index
4343
const QRErrorCorrectLevelModule = require(join(qrcodeTerminalPath, 'vendor', 'QRCode', 'QRErrorCorrectLevel.js'));
4444

4545
// Types from Baileys (approximate since we don't have types for dynamic require)
46-
type BaileysSocket = any;
46+
interface BaileysError extends Error {
47+
output?: { statusCode?: number };
48+
}
49+
type BaileysSocket = ReturnType<typeof makeWASocket>;
4750
type ConnectionState = {
4851
connection: 'close' | 'open' | 'connecting';
4952
lastDisconnect?: {
@@ -186,6 +189,18 @@ export class WhatsAppLoginManager extends EventEmitter {
186189
super();
187190
}
188191

192+
/**
193+
* Finish login: close socket and emit success after credentials are saved
194+
*/
195+
private async finishLogin(accountId: string): Promise<void> {
196+
if (!this.active) return;
197+
console.log('[WhatsAppLogin] Finishing login, closing socket to hand over to Gateway...');
198+
await this.stop();
199+
// Delay to ensure socket is fully released before Gateway connects
200+
await new Promise(resolve => setTimeout(resolve, 2000));
201+
this.emit('success', { accountId });
202+
}
203+
189204
/**
190205
* Start WhatsApp pairing process
191206
*/
@@ -226,7 +241,8 @@ export class WhatsAppLoginManager extends EventEmitter {
226241

227242
console.log(`[WhatsAppLogin] Connecting for ${accountId} at ${authDir} (Attempt ${this.retryCount + 1})`);
228243

229-
let pino: any;
244+
245+
let pino: (...args: unknown[]) => Record<string, unknown>;
230246
try {
231247
// Try to resolve pino from baileys context since it's a dependency of baileys
232248
const baileysRequire = createRequire(join(baileysPath, 'package.json'));
@@ -268,7 +284,21 @@ export class WhatsAppLoginManager extends EventEmitter {
268284
// browser: ['ClawX', 'Chrome', '1.0.0'],
269285
});
270286

271-
this.socket.ev.on('creds.update', saveCreds);
287+
let connectionOpened = false;
288+
let credsReceived = false;
289+
let credsTimeout: ReturnType<typeof setTimeout> | null = null;
290+
291+
this.socket.ev.on('creds.update', async () => {
292+
await saveCreds();
293+
if (connectionOpened && !credsReceived) {
294+
credsReceived = true;
295+
if (credsTimeout) clearTimeout(credsTimeout);
296+
console.log('[WhatsAppLogin] Credentials saved after connection open, finishing login...');
297+
// Small delay to ensure file writes are fully flushed
298+
await new Promise(resolve => setTimeout(resolve, 3000));
299+
await this.finishLogin(accountId);
300+
}
301+
});
272302

273303
this.socket.ev.on('connection.update', async (update: ConnectionState) => {
274304
try {
@@ -282,7 +312,7 @@ export class WhatsAppLoginManager extends EventEmitter {
282312
}
283313

284314
if (connection === 'close') {
285-
const error = (lastDisconnect?.error as any);
315+
const error = lastDisconnect?.error as BaileysError | undefined;
286316
const shouldReconnect = error?.output?.statusCode !== DisconnectReason.loggedOut;
287317
console.log('[WhatsAppLogin] Connection closed.',
288318
'Reconnect:', shouldReconnect,
@@ -317,17 +347,17 @@ export class WhatsAppLoginManager extends EventEmitter {
317347
this.emit('error', 'Logged out');
318348
}
319349
} else if (connection === 'open') {
320-
console.log('[WhatsAppLogin] Connection opened! Closing socket to hand over to Gateway...');
350+
console.log('[WhatsAppLogin] Connection opened! Waiting for credentials to be saved...');
321351
this.retryCount = 0;
352+
connectionOpened = true;
322353

323-
// Close socket gracefully to avoid conflict with Gateway
324-
await this.stop();
325-
326-
// Add a small delay to ensure socket is fully closed and released
327-
// This prevents "401 Conflict" when Gateway tries to connect immediately
328-
await new Promise(resolve => setTimeout(resolve, 2000));
329-
330-
this.emit('success', { accountId });
354+
// Safety timeout: if creds don't update within 15s, proceed anyway
355+
credsTimeout = setTimeout(async () => {
356+
if (!credsReceived && this.active) {
357+
console.warn('[WhatsAppLogin] Timed out waiting for creds.update after connection open, proceeding...');
358+
await this.finishLogin(accountId);
359+
}
360+
}, 15000);
331361
}
332362
} catch (innerErr) {
333363
console.error('[WhatsAppLogin] Error in connection update:', innerErr);

src/pages/Channels/index.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,29 @@ function AddChannelDialog({ selectedType, onSelectType, onClose, onChannelAdded
412412
useEffect(() => {
413413
if (selectedType !== 'whatsapp') return;
414414

415-
const onQr = (data: { qr: string; raw: string }) => {
415+
const onQr = (...args: unknown[]) => {
416+
const data = args[0] as { qr: string; raw: string };
416417
setQrCode(`data:image/png;base64,${data.qr}`);
417418
};
418419

419-
const onSuccess = () => {
420+
const onSuccess = async (...args: unknown[]) => {
421+
const data = args[0] as { accountId?: string } | undefined;
420422
toast.success('WhatsApp connected successfully!');
423+
const accountId = data?.accountId || channelName.trim() || 'default';
424+
try {
425+
const saveResult = await window.electron.ipcRenderer.invoke(
426+
'channel:saveConfig',
427+
'whatsapp',
428+
{ enabled: true }
429+
) as { success?: boolean; error?: string };
430+
if (!saveResult?.success) {
431+
console.error('Failed to save WhatsApp config:', saveResult?.error);
432+
} else {
433+
console.info('Saved WhatsApp config for account:', accountId);
434+
}
435+
} catch (error) {
436+
console.error('Failed to save WhatsApp config:', error);
437+
}
421438
// Register the channel locally so it shows up immediately
422439
addChannel({
423440
type: 'whatsapp',
@@ -429,16 +446,17 @@ function AddChannelDialog({ selectedType, onSelectType, onClose, onChannelAdded
429446
});
430447
};
431448

432-
const onError = (err: string) => {
449+
const onError = (...args: unknown[]) => {
450+
const err = args[0] as string;
433451
console.error('WhatsApp Login Error:', err);
434452
toast.error(`WhatsApp Login Failed: ${err}`);
435453
setQrCode(null);
436454
setConnecting(false);
437455
};
438456

439-
const removeQrListener = (window.electron.ipcRenderer.on as any)('channel:whatsapp-qr', onQr);
440-
const removeSuccessListener = (window.electron.ipcRenderer.on as any)('channel:whatsapp-success', onSuccess);
441-
const removeErrorListener = (window.electron.ipcRenderer.on as any)('channel:whatsapp-error', onError);
457+
const removeQrListener = window.electron.ipcRenderer.on('channel:whatsapp-qr', onQr);
458+
const removeSuccessListener = window.electron.ipcRenderer.on('channel:whatsapp-success', onSuccess);
459+
const removeErrorListener = window.electron.ipcRenderer.on('channel:whatsapp-error', onError);
442460

443461
return () => {
444462
if (typeof removeQrListener === 'function') removeQrListener();

src/types/channel.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,18 +129,19 @@ export const CHANNEL_META: Record<ChannelType, ChannelMeta> = {
129129
},
130130
{
131131
key: 'allowedUsers',
132-
label: 'Allowed User IDs (optional)',
132+
label: 'Allowed User IDs',
133133
type: 'text',
134134
placeholder: 'e.g. 123456789, 987654321',
135-
description: 'Comma separated list of User IDs allowed to use the bot. Leave empty to allow everyone (if public) or require pairing.',
136-
required: false,
135+
description: 'Comma separated list of User IDs allowed to use the bot. Required for security.',
136+
required: true,
137137
},
138138
],
139139
instructions: [
140140
'Open Telegram and search for @BotFather',
141141
'Send /newbot and follow the instructions',
142142
'Copy the bot token provided',
143143
'Paste the token below',
144+
'Get your User ID from @userinfobot and paste it below',
144145
],
145146
},
146147
discord: {

0 commit comments

Comments
 (0)