Skip to content

Commit e897bda

Browse files
committed
Use WhatsApp WebSocket URL override
1 parent 36072df commit e897bda

10 files changed

Lines changed: 58 additions & 102 deletions

File tree

extensions/whatsapp/api.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
// Whatsapp API module exposes the plugin public contract.
2-
export {
3-
setWhatsAppMonitorRuntimeOptions,
4-
whatsappPlugin,
5-
type WhatsAppMonitorRuntimeOptions,
6-
} from "./src/channel.js";
7-
export type { WhatsAppCreateSocket } from "./src/connection-controller.js";
2+
export { whatsappPlugin } from "./src/channel.js";
83
export { whatsappSetupPlugin } from "./src/channel.setup.js";
94
export {
105
DEFAULT_WHATSAPP_MEDIA_MAX_MB,

extensions/whatsapp/src/auto-reply/monitor.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ export async function monitorWebChannel(
238238
socketTiming,
239239
abortSignal,
240240
sleep,
241-
createSocket: tuning.createSocket,
242241
isNonRetryableStatus: isNonRetryableWebCloseStatus,
243242
});
244243
const statusController = createWebChannelStatusController(tuning.statusSink);

extensions/whatsapp/src/auto-reply/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// Whatsapp type declarations define plugin contracts.
22
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
3-
import type { WhatsAppCreateSocket } from "../connection-controller.js";
43
import type { WebInboundMessage } from "../inbound/types.js";
54
import type { ReconnectPolicy } from "../reconnect.js";
65
import type { WhatsAppSocketTimingOptions } from "../socket-timing.js";
@@ -38,7 +37,6 @@ export type WebChannelStatus = {
3837

3938
export type WebMonitorTuning = {
4039
reconnect?: Partial<ReconnectPolicy>;
41-
createSocket?: WhatsAppCreateSocket;
4240
socketTiming?: WhatsAppSocketTimingOptions;
4341
heartbeatSeconds?: number;
4442
transportTimeoutMs?: number;

extensions/whatsapp/src/channel.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
import { whatsappChannelOutbound, whatsappMessageAdapter } from "./channel-outbound.js";
1919
import { whatsappCommandPolicy } from "./command-policy.js";
2020
import { formatWhatsAppConfigAllowFromEntries } from "./config-accessors.js";
21-
import type { WhatsAppCreateSocket } from "./connection-controller.js";
2221
import {
2322
resolveWhatsAppGroupIntroHint,
2423
resolveWhatsAppMentionStripRegexes,
@@ -48,20 +47,6 @@ import {
4847
import { detectWhatsAppLegacyStateMigrations } from "./state-migrations.js";
4948
import { collectWhatsAppStatusIssues } from "./status-issues.js";
5049

51-
export type WhatsAppMonitorRuntimeOptions = {
52-
createSocket?: WhatsAppCreateSocket;
53-
};
54-
55-
let whatsappMonitorRuntimeOptions: WhatsAppMonitorRuntimeOptions = {};
56-
57-
export function setWhatsAppMonitorRuntimeOptions(options?: WhatsAppMonitorRuntimeOptions): void {
58-
whatsappMonitorRuntimeOptions = { createSocket: options?.createSocket };
59-
}
60-
61-
function getWhatsAppMonitorRuntimeOptions(): WhatsAppMonitorRuntimeOptions {
62-
return { ...whatsappMonitorRuntimeOptions };
63-
}
64-
6550
const loadWhatsAppDirectoryConfig = createLazyRuntimeModule(() => import("./directory-config.js"));
6651
const loadWhatsAppChannelReactAction = createLazyRuntimeModule(
6752
() => import("./channel-react-action.js"),
@@ -339,7 +324,6 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> =
339324
gateway: {
340325
startAccount: async (ctx) => {
341326
const account = ctx.account;
342-
const monitorOptions = getWhatsAppMonitorRuntimeOptions();
343327
const { e164, jid } = (await loadWhatsAppChannelRuntime()).readWebSelfId(account.authDir);
344328
const identity = e164 ? e164 : jid ? `jid ${jid}` : "unknown";
345329
ctx.log?.info(`[${account.accountId}] starting provider (${identity})`);
@@ -355,7 +339,6 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> =
355339
ctx.setStatus({ accountId: ctx.accountId, ...next }),
356340
accountId: account.accountId,
357341
channelRuntime: ctx.channelRuntime,
358-
createSocket: monitorOptions.createSocket,
359342
},
360343
);
361344
},

extensions/whatsapp/src/connection-controller-registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { WASocket } from "baileys";
33
import type { WhatsAppSelfIdentity } from "./identity.js";
44
import type { ActiveWebListener } from "./inbound/types.js";
55

6-
export type WhatsAppConnectionControllerHandle = {
6+
type WhatsAppConnectionControllerHandle = {
77
getActiveListener(): ActiveWebListener | null;
88
getCurrentSock(): WASocket | null;
99
/**

extensions/whatsapp/src/connection-controller.test.ts

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -211,52 +211,6 @@ describe("WhatsAppConnectionController", () => {
211211
});
212212
});
213213

214-
it("uses an injected socket factory when opening a connection", async () => {
215-
const customSock = { ws: { close: vi.fn() } };
216-
const createSocket = vi.fn(async () => customSock);
217-
const waitForConnection = vi.fn(async () => {});
218-
const customController = new WhatsAppConnectionController({
219-
accountId: "work",
220-
authDir: "/tmp/wa-auth",
221-
verbose: false,
222-
keepAlive: false,
223-
heartbeatSeconds: 30,
224-
transportTimeoutMs: 60_000,
225-
messageTimeoutMs: 60_000,
226-
watchdogCheckMs: 5_000,
227-
reconnectPolicy: {
228-
initialMs: 250,
229-
maxMs: 1_000,
230-
factor: 2,
231-
jitter: 0,
232-
maxAttempts: 5,
233-
},
234-
createSocket: createSocket as never,
235-
waitForConnection: waitForConnection as never,
236-
});
237-
238-
try {
239-
await customController.openConnection({
240-
connectionId: "conn-custom",
241-
createListener: async () => createListenerStub() as never,
242-
});
243-
} finally {
244-
await customController.shutdown();
245-
}
246-
247-
expect(createWaSocketMock).not.toHaveBeenCalled();
248-
expect(waitForWaConnectionMock).not.toHaveBeenCalled();
249-
expect(createSocket).toHaveBeenCalledWith(false, false, {
250-
authDir: "/tmp/wa-auth",
251-
connectTimeoutMs: DEFAULT_WHATSAPP_SOCKET_TIMING.connectTimeoutMs,
252-
defaultQueryTimeoutMs: DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs,
253-
keepAliveIntervalMs: DEFAULT_WHATSAPP_SOCKET_TIMING.keepAliveIntervalMs,
254-
});
255-
expect(waitForConnection).toHaveBeenCalledWith(customSock, {
256-
timeoutMs: DEFAULT_WHATSAPP_SOCKET_TIMING.connectTimeoutMs,
257-
});
258-
});
259-
260214
it("restarts login once on status 408 and preserves replacement socket options", async () => {
261215
const harness = createLoginResultHarness();
262216
const waitForConnection = vi

extensions/whatsapp/src/connection-controller.ts

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
logoutWeb,
1717
readWebAuthExistsForDecision,
1818
waitForWaConnection,
19-
type WhatsAppConnectionWaitOptions,
2019
WhatsAppAuthUnstableError,
2120
} from "./session.js";
2221
import {
@@ -46,24 +45,6 @@ export const WHATSAPP_WATCHDOG_TIMEOUT_ERROR = "watchdog-timeout";
4645
type TimerHandle = ReturnType<typeof setInterval>;
4746
type WaSocket = Awaited<ReturnType<typeof createWaSocket>>;
4847

49-
export type WhatsAppCreateSocketOptions = {
50-
authDir?: string;
51-
onQr?: (qr: string) => void;
52-
getMessage?: (key: WAMessageKey) => Promise<proto.IMessage | undefined>;
53-
cachedGroupMetadata?: (jid: string) => Promise<GroupMetadata | undefined>;
54-
} & WhatsAppSocketTimingOptions;
55-
56-
export type WhatsAppCreateSocket = (
57-
printQr: boolean,
58-
verbose: boolean,
59-
opts?: WhatsAppCreateSocketOptions,
60-
) => Promise<WaSocket>;
61-
62-
export type WhatsAppWaitForConnection = (
63-
sock: WaSocket,
64-
options?: WhatsAppConnectionWaitOptions,
65-
) => Promise<void>;
66-
6748
export type ManagedWhatsAppListener = ActiveWebListener & {
6849
close?: () => Promise<void>;
6950
onClose?: Promise<WebListenerCloseReason>;
@@ -246,8 +227,8 @@ export async function waitForWhatsAppLoginResult(params: {
246227
isLegacyAuthDir: boolean;
247228
verbose: boolean;
248229
runtime: RuntimeEnv;
249-
waitForConnection?: WhatsAppWaitForConnection;
250-
createSocket?: WhatsAppCreateSocket;
230+
waitForConnection?: typeof waitForWaConnection;
231+
createSocket?: typeof createWaSocket;
251232
socketTiming?: WhatsAppSocketTimingOptions;
252233
onQr?: (qr: string) => void;
253234
onSocketReplaced?: (sock: WaSocket) => void;
@@ -394,8 +375,6 @@ export class WhatsAppConnectionController {
394375
private readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
395376
private readonly isNonRetryableStatus: (statusCode: unknown) => boolean;
396377
private readonly socketTiming: Required<WhatsAppSocketTimingOptions>;
397-
private readonly createSocket: WhatsAppCreateSocket;
398-
private readonly waitForConnection: WhatsAppWaitForConnection;
399378
private readonly abortPromise?: Promise<"aborted">;
400379
private readonly disconnectRetryController = new AbortController();
401380

@@ -417,8 +396,6 @@ export class WhatsAppConnectionController {
417396
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
418397
isNonRetryableStatus?: (statusCode: unknown) => boolean;
419398
socketTiming?: WhatsAppSocketTimingOptions;
420-
createSocket?: WhatsAppCreateSocket;
421-
waitForConnection?: WhatsAppWaitForConnection;
422399
}) {
423400
this.accountId = params.accountId;
424401
this.authDir = params.authDir;
@@ -437,8 +414,6 @@ export class WhatsAppConnectionController {
437414
...DEFAULT_WHATSAPP_SOCKET_TIMING,
438415
...params.socketTiming,
439416
};
440-
this.createSocket = params.createSocket ?? createWaSocket;
441-
this.waitForConnection = params.waitForConnection ?? waitForWaConnection;
442417
this.socketRef = { current: null };
443418
this.abortPromise =
444419
params.abortSignal &&
@@ -563,13 +538,13 @@ export class WhatsAppConnectionController {
563538
let sock: WaSocket | null = null;
564539
let connection: WhatsAppLiveConnection | null = null;
565540
try {
566-
sock = await this.createSocket(false, this.verbose, {
541+
sock = await createWaSocket(false, this.verbose, {
567542
authDir: this.authDir,
568543
...this.socketTiming,
569544
...(params.getMessage ? { getMessage: params.getMessage } : {}),
570545
...(params.cachedGroupMetadata ? { cachedGroupMetadata: params.cachedGroupMetadata } : {}),
571546
});
572-
await this.waitForConnection(sock, { timeoutMs: this.socketTiming.connectTimeoutMs });
547+
await waitForWaConnection(sock, { timeoutMs: this.socketTiming.connectTimeoutMs });
573548

574549
this.socketRef.current = sock;
575550
const placeholderListener = {} as ManagedWhatsAppListener;

extensions/whatsapp/src/session.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const useMultiFileAuthStateMock = vi.mocked(baileys.useMultiFileAuthState);
4141
let createWaSocket: typeof import("./session.js").createWaSocket;
4242
let formatError: typeof import("./session.js").formatError;
4343
let logWebSelfId: typeof import("./session.js").logWebSelfId;
44+
let OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV: typeof import("./session.js").OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV;
4445
let renderQrTerminalMock: ReturnType<typeof vi.fn>;
4546
let waitForWaConnection: typeof import("./session.js").waitForWaConnection;
4647
let waitForCredsSaveQueue: typeof import("./session.js").waitForCredsSaveQueue;
@@ -153,6 +154,7 @@ function readLastSocketOptions(): {
153154
fetchAgent?: unknown;
154155
keepAliveIntervalMs?: number;
155156
printQRInTerminal?: boolean;
157+
waWebSocketUrl?: string | URL;
156158
logger?: { level?: string; trace?: unknown };
157159
} {
158160
const [options] = firstMockCall(
@@ -169,6 +171,7 @@ function readLastSocketOptions(): {
169171
fetchAgent?: unknown;
170172
keepAliveIntervalMs?: number;
171173
printQRInTerminal?: boolean;
174+
waWebSocketUrl?: string | URL;
172175
logger?: { level?: string; trace?: unknown };
173176
};
174177
}
@@ -224,6 +227,7 @@ describe("web session", () => {
224227
createWaSocket,
225228
formatError,
226229
logWebSelfId,
230+
OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV,
227231
waitForWaConnection,
228232
waitForCredsSaveQueue,
229233
writeCredsJsonAtomically,
@@ -411,6 +415,40 @@ describe("web session", () => {
411415
expect(passed.defaultQueryTimeoutMs).toBe(120_000);
412416
});
413417

418+
it("passes explicit Baileys WebSocket URL overrides", async () => {
419+
await createWaSocket(false, false, {
420+
waWebSocketUrl: " ws://127.0.0.1:49152/ws/chat ",
421+
});
422+
423+
expect(readLastSocketOptions().waWebSocketUrl).toBe("ws://127.0.0.1:49152/ws/chat");
424+
});
425+
426+
it("uses OPENCLAW_WHATSAPP_WEB_SOCKET_URL as the default Baileys WebSocket URL", async () => {
427+
vi.stubEnv(OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV, " ws://127.0.0.1:49153/ws/chat ");
428+
429+
await createWaSocket(false, false);
430+
431+
expect(readLastSocketOptions().waWebSocketUrl).toBe("ws://127.0.0.1:49153/ws/chat");
432+
});
433+
434+
it("preserves explicit Baileys WebSocket URL options over environment", async () => {
435+
vi.stubEnv(OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV, "ws://127.0.0.1:49153/ws/chat");
436+
437+
await createWaSocket(false, false, {
438+
waWebSocketUrl: "ws://127.0.0.1:49154/ws/chat",
439+
});
440+
441+
expect(readLastSocketOptions().waWebSocketUrl).toBe("ws://127.0.0.1:49154/ws/chat");
442+
});
443+
444+
it("ignores blank Baileys WebSocket URL environment overrides", async () => {
445+
vi.stubEnv(OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV, " ");
446+
447+
await createWaSocket(false, false);
448+
449+
expect(readLastSocketOptions().waWebSocketUrl).toBeUndefined();
450+
});
451+
414452
it("uses ambient env proxy agent when HTTPS_PROXY is configured", async () => {
415453
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8080");
416454

extensions/whatsapp/src/session.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const LOGGED_OUT_STATUS = 401;
6868
const WHATSAPP_WEBSOCKET_PROXY_TARGET = "https://mmg.whatsapp.net/";
6969
const CREDS_FLUSH_TIMEOUT_MESSAGE =
7070
"Queued WhatsApp creds save did not finish before auth bootstrap; skipping repair and continuing with primary creds.";
71+
export const OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV = "OPENCLAW_WHATSAPP_WEB_SOCKET_URL";
7172

7273
async function rejectUnsafeWebCredsPath(authDir: string): Promise<void> {
7374
await assertWebCredsPathRegularFileOrMissing(resolveWebCredsPath(authDir));
@@ -125,6 +126,13 @@ async function printTerminalQr(qr: string): Promise<void> {
125126
process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
126127
}
127128

129+
function resolveWaWebSocketUrl(value: string | URL | undefined): string | URL | undefined {
130+
if (typeof value !== "string") {
131+
return value;
132+
}
133+
return value.trim() || undefined;
134+
}
135+
128136
/**
129137
* Create a Baileys socket backed by the multi-file auth store we keep on disk.
130138
* Consumers can opt into QR printing for interactive login flows.
@@ -137,6 +145,7 @@ export async function createWaSocket(
137145
onQr?: (qr: string) => void;
138146
getMessage?: (key: WAMessageKey) => Promise<proto.IMessage | undefined>;
139147
cachedGroupMetadata?: (jid: string) => Promise<GroupMetadata | undefined>;
148+
waWebSocketUrl?: string | URL;
140149
} & WhatsAppSocketTimingOptions = {},
141150
): Promise<ReturnType<typeof makeWASocket>> {
142151
const baseLogger = getChildLogger(
@@ -172,6 +181,9 @@ export async function createWaSocket(
172181
defaultQueryTimeoutMs:
173182
opts.defaultQueryTimeoutMs ?? DEFAULT_WHATSAPP_SOCKET_TIMING.defaultQueryTimeoutMs,
174183
};
184+
const waWebSocketUrl =
185+
resolveWaWebSocketUrl(opts.waWebSocketUrl) ??
186+
resolveWaWebSocketUrl(process.env[OPENCLAW_WHATSAPP_WEB_SOCKET_URL_ENV]);
175187
const sock = makeWASocket({
176188
auth: {
177189
creds: state.creds,
@@ -188,6 +200,7 @@ export async function createWaSocket(
188200
// Baileys types still model `fetchAgent` as a Node agent even though the
189201
// runtime path accepts an undici dispatcher for upload fetches.
190202
fetchAgent: fetchAgent as Agent | undefined,
203+
...(waWebSocketUrl ? { waWebSocketUrl } : {}),
191204
...(opts.getMessage ? { getMessage: opts.getMessage } : {}),
192205
...(opts.cachedGroupMetadata ? { cachedGroupMetadata: opts.cachedGroupMetadata } : {}),
193206
});

src/infra/dotenv.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,7 @@ describe("workspace .env blocklist completeness", () => {
921921
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
922922
"OPENCLAW_ALLOW_INSECURE_PRIVATE_WS",
923923
"OPENCLAW_BROWSER_EXECUTABLE_PATH",
924+
"OPENCLAW_WHATSAPP_WEB_SOCKET_URL",
924925
"EXAMPLE_API_HOST",
925926
"HOMEBREW_BREW_FILE",
926927
"HOMEBREW_PREFIX",

0 commit comments

Comments
 (0)