owpenwork 0.1.11 → 0.1.13
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/dist/bridge.js +33 -12
- package/dist/cli.js +44 -10
- package/dist/config.js +2 -0
- package/dist/logger.js +8 -1
- package/dist/telegram.js +11 -6
- package/dist/whatsapp-session.js +166 -0
- package/dist/whatsapp.js +178 -141
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -19,7 +19,8 @@ const TOOL_LABELS = {
|
|
|
19
19
|
task: "agent",
|
|
20
20
|
webfetch: "webfetch",
|
|
21
21
|
};
|
|
22
|
-
export async function startBridge(config, logger) {
|
|
22
|
+
export async function startBridge(config, logger, reporter) {
|
|
23
|
+
const reportStatus = reporter?.onStatus;
|
|
23
24
|
const client = createClient(config);
|
|
24
25
|
const store = new BridgeStore(config.dbPath);
|
|
25
26
|
store.seedAllowlist("telegram", config.allowlist.telegram);
|
|
@@ -31,12 +32,14 @@ export async function startBridge(config, logger) {
|
|
|
31
32
|
}
|
|
32
33
|
else {
|
|
33
34
|
logger.info("telegram adapter disabled");
|
|
35
|
+
reportStatus?.("Telegram adapter disabled.");
|
|
34
36
|
}
|
|
35
37
|
if (config.whatsappEnabled) {
|
|
36
|
-
adapters.set("whatsapp", createWhatsAppAdapter(config, logger, handleInbound, { printQr: true }));
|
|
38
|
+
adapters.set("whatsapp", createWhatsAppAdapter(config, logger, handleInbound, { printQr: true, onStatus: reportStatus }));
|
|
37
39
|
}
|
|
38
40
|
else {
|
|
39
41
|
logger.info("whatsapp adapter disabled");
|
|
42
|
+
reportStatus?.("WhatsApp adapter disabled.");
|
|
40
43
|
}
|
|
41
44
|
const sessionQueue = new Map();
|
|
42
45
|
const activeRuns = new Map();
|
|
@@ -102,7 +105,7 @@ export async function startBridge(config, logger) {
|
|
|
102
105
|
if (output)
|
|
103
106
|
message += `\n${output}`;
|
|
104
107
|
}
|
|
105
|
-
await sendText(run.channel, run.peerId, message);
|
|
108
|
+
await sendText(run.channel, run.peerId, message, { kind: "tool" });
|
|
106
109
|
}
|
|
107
110
|
if (event.type === "permission.asked") {
|
|
108
111
|
const permission = event.properties;
|
|
@@ -117,7 +120,9 @@ export async function startBridge(config, logger) {
|
|
|
117
120
|
if (response === "reject") {
|
|
118
121
|
const run = activeRuns.get(permission.sessionID);
|
|
119
122
|
if (run) {
|
|
120
|
-
await sendText(run.channel, run.peerId, "Permission denied. Update configuration to allow tools."
|
|
123
|
+
await sendText(run.channel, run.peerId, "Permission denied. Update configuration to allow tools.", {
|
|
124
|
+
kind: "system",
|
|
125
|
+
});
|
|
121
126
|
}
|
|
122
127
|
}
|
|
123
128
|
}
|
|
@@ -125,10 +130,14 @@ export async function startBridge(config, logger) {
|
|
|
125
130
|
})().catch((error) => {
|
|
126
131
|
logger.error({ error }, "event stream closed");
|
|
127
132
|
});
|
|
128
|
-
async function sendText(channel, peerId, text) {
|
|
133
|
+
async function sendText(channel, peerId, text, options = {}) {
|
|
129
134
|
const adapter = adapters.get(channel);
|
|
130
135
|
if (!adapter)
|
|
131
136
|
return;
|
|
137
|
+
const kind = options.kind ?? "system";
|
|
138
|
+
if (options.display !== false) {
|
|
139
|
+
reporter?.onOutbound?.({ channel, peerId, text, kind });
|
|
140
|
+
}
|
|
132
141
|
const chunks = chunkText(text, adapter.maxTextLength);
|
|
133
142
|
for (const chunk of chunks) {
|
|
134
143
|
logger.info({ channel, peerId, length: chunk.length }, "sending message");
|
|
@@ -151,30 +160,36 @@ export async function startBridge(config, logger) {
|
|
|
151
160
|
const allowed = allowAll || isSelf || store.isAllowed("whatsapp", peerKey);
|
|
152
161
|
if (!allowed) {
|
|
153
162
|
if (config.whatsappDmPolicy === "allowlist") {
|
|
154
|
-
await sendText(inbound.channel, inbound.peerId, "Access denied. Ask the owner to allowlist your number.");
|
|
163
|
+
await sendText(inbound.channel, inbound.peerId, "Access denied. Ask the owner to allowlist your number.", { kind: "system" });
|
|
155
164
|
return;
|
|
156
165
|
}
|
|
157
166
|
store.prunePairingRequests();
|
|
158
167
|
const active = store.getPairingRequest("whatsapp", peerKey);
|
|
159
168
|
const pending = store.listPairingRequests("whatsapp");
|
|
160
169
|
if (!active && pending.length >= 3) {
|
|
161
|
-
await sendText(inbound.channel, inbound.peerId, "Pairing queue full. Ask the owner to approve pending requests.");
|
|
170
|
+
await sendText(inbound.channel, inbound.peerId, "Pairing queue full. Ask the owner to approve pending requests.", { kind: "system" });
|
|
162
171
|
return;
|
|
163
172
|
}
|
|
164
173
|
const code = active?.code ?? String(Math.floor(100000 + Math.random() * 900000));
|
|
165
174
|
if (!active) {
|
|
166
175
|
store.createPairingRequest("whatsapp", peerKey, code, 60 * 60_000);
|
|
167
176
|
}
|
|
168
|
-
await sendText(inbound.channel, inbound.peerId, `Pairing required. Ask the owner to approve code: ${code}
|
|
177
|
+
await sendText(inbound.channel, inbound.peerId, `Pairing required. Ask the owner to approve code: ${code}`, { kind: "system" });
|
|
169
178
|
return;
|
|
170
179
|
}
|
|
171
180
|
}
|
|
172
181
|
else if (config.allowlist[inbound.channel].size > 0) {
|
|
173
182
|
if (!store.isAllowed(inbound.channel, peerKey)) {
|
|
174
|
-
await sendText(inbound.channel, inbound.peerId, "Access denied.");
|
|
183
|
+
await sendText(inbound.channel, inbound.peerId, "Access denied.", { kind: "system" });
|
|
175
184
|
return;
|
|
176
185
|
}
|
|
177
186
|
}
|
|
187
|
+
reporter?.onInbound?.({
|
|
188
|
+
channel: inbound.channel,
|
|
189
|
+
peerId: inbound.peerId,
|
|
190
|
+
text: inbound.text,
|
|
191
|
+
fromMe: inbound.fromMe,
|
|
192
|
+
});
|
|
178
193
|
const session = store.getSession(inbound.channel, peerKey);
|
|
179
194
|
const sessionID = session?.session_id ?? (await createSession({ ...inbound, peerId: peerKey }));
|
|
180
195
|
enqueue(sessionID, async () => {
|
|
@@ -198,15 +213,19 @@ export async function startBridge(config, logger) {
|
|
|
198
213
|
.join("\n")
|
|
199
214
|
.trim();
|
|
200
215
|
if (reply) {
|
|
201
|
-
await sendText(inbound.channel, inbound.peerId, reply);
|
|
216
|
+
await sendText(inbound.channel, inbound.peerId, reply, { kind: "reply" });
|
|
202
217
|
}
|
|
203
218
|
else {
|
|
204
|
-
await sendText(inbound.channel, inbound.peerId, "No response generated. Try again."
|
|
219
|
+
await sendText(inbound.channel, inbound.peerId, "No response generated. Try again.", {
|
|
220
|
+
kind: "system",
|
|
221
|
+
});
|
|
205
222
|
}
|
|
206
223
|
}
|
|
207
224
|
catch (error) {
|
|
208
225
|
logger.error({ error }, "prompt failed");
|
|
209
|
-
await sendText(inbound.channel, inbound.peerId, "Error: failed to reach OpenCode."
|
|
226
|
+
await sendText(inbound.channel, inbound.peerId, "Error: failed to reach OpenCode.", {
|
|
227
|
+
kind: "system",
|
|
228
|
+
});
|
|
210
229
|
}
|
|
211
230
|
finally {
|
|
212
231
|
activeRuns.delete(sessionID);
|
|
@@ -242,8 +261,10 @@ export async function startBridge(config, logger) {
|
|
|
242
261
|
}
|
|
243
262
|
for (const adapter of adapters.values()) {
|
|
244
263
|
await adapter.start();
|
|
264
|
+
reportStatus?.(`${adapter.name === "whatsapp" ? "WhatsApp" : "Telegram"} adapter started.`);
|
|
245
265
|
}
|
|
246
266
|
logger.info({ channels: Array.from(adapters.keys()) }, "bridge started");
|
|
267
|
+
reportStatus?.(`Bridge running. Logs: ${config.logFile}`);
|
|
247
268
|
return {
|
|
248
269
|
async stop() {
|
|
249
270
|
eventAbort.abort();
|
package/dist/cli.js
CHANGED
|
@@ -8,8 +8,9 @@ import { loadConfig, normalizeWhatsAppId, readConfigFile, writeConfigFile, } fro
|
|
|
8
8
|
import { BridgeStore } from "./db.js";
|
|
9
9
|
import { createLogger } from "./logger.js";
|
|
10
10
|
import { createClient } from "./opencode.js";
|
|
11
|
+
import { truncateText } from "./text.js";
|
|
11
12
|
import { loginWhatsApp, unpairWhatsApp } from "./whatsapp.js";
|
|
12
|
-
const VERSION = "0.1.
|
|
13
|
+
const VERSION = "0.1.13";
|
|
13
14
|
const STEP_OPTIONS = [
|
|
14
15
|
{
|
|
15
16
|
value: "config",
|
|
@@ -62,6 +63,38 @@ function defaultSelections(configExists, whatsappLinked) {
|
|
|
62
63
|
selections.push("start");
|
|
63
64
|
return selections;
|
|
64
65
|
}
|
|
66
|
+
function createAppLogger(config) {
|
|
67
|
+
return createLogger(config.logLevel, { logFile: config.logFile });
|
|
68
|
+
}
|
|
69
|
+
function createConsoleReporter() {
|
|
70
|
+
const formatChannel = (channel) => (channel === "whatsapp" ? "WhatsApp" : "Telegram");
|
|
71
|
+
const formatPeer = (channel, peerId, fromMe) => {
|
|
72
|
+
const base = channel === "whatsapp" ? normalizeWhatsAppId(peerId) : peerId;
|
|
73
|
+
return fromMe ? `${base} (me)` : base;
|
|
74
|
+
};
|
|
75
|
+
const printBlock = (prefix, text) => {
|
|
76
|
+
const lines = text.split(/\r?\n/).map((line) => truncateText(line.trim(), 240));
|
|
77
|
+
const [first, ...rest] = lines.length ? lines : ["(empty)"];
|
|
78
|
+
console.log(`${prefix} ${first}`);
|
|
79
|
+
for (const line of rest) {
|
|
80
|
+
console.log(`${" ".repeat(prefix.length)} ${line}`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
onStatus(message) {
|
|
85
|
+
console.log(message);
|
|
86
|
+
},
|
|
87
|
+
onInbound({ channel, peerId, text, fromMe }) {
|
|
88
|
+
const prefix = `[${formatChannel(channel)}] ${formatPeer(channel, peerId, fromMe)} >`;
|
|
89
|
+
printBlock(prefix, text);
|
|
90
|
+
},
|
|
91
|
+
onOutbound({ channel, peerId, text, kind }) {
|
|
92
|
+
const marker = kind === "reply" ? "<" : kind === "tool" ? "*" : "!";
|
|
93
|
+
const prefix = `[${formatChannel(channel)}] ${formatPeer(channel, peerId)} ${marker}`;
|
|
94
|
+
printBlock(prefix, text);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
65
98
|
function updateConfig(configPath, updater) {
|
|
66
99
|
const { config } = readConfigFile(configPath);
|
|
67
100
|
const base = config ?? { version: 1 };
|
|
@@ -195,12 +228,13 @@ async function runStart(pathOverride) {
|
|
|
195
228
|
process.env.OPENCODE_DIRECTORY = pathOverride.trim();
|
|
196
229
|
}
|
|
197
230
|
const config = loadConfig();
|
|
198
|
-
const logger =
|
|
231
|
+
const logger = createAppLogger(config);
|
|
232
|
+
const reporter = createConsoleReporter();
|
|
199
233
|
if (!process.env.OPENCODE_DIRECTORY) {
|
|
200
234
|
process.env.OPENCODE_DIRECTORY = config.opencodeDirectory;
|
|
201
235
|
}
|
|
202
|
-
const bridge = await startBridge(config, logger);
|
|
203
|
-
|
|
236
|
+
const bridge = await startBridge(config, logger, reporter);
|
|
237
|
+
reporter.onStatus?.("Commands: owpenwork whatsapp login, owpenwork pairing list, owpenwork status");
|
|
204
238
|
const shutdown = async () => {
|
|
205
239
|
logger.info("shutting down");
|
|
206
240
|
await bridge.stop();
|
|
@@ -277,7 +311,7 @@ async function runGuidedFlow(pathArg, opts) {
|
|
|
277
311
|
if (!relink)
|
|
278
312
|
return;
|
|
279
313
|
}
|
|
280
|
-
await loginWhatsApp(config,
|
|
314
|
+
await loginWhatsApp(config, createAppLogger(config), { onStatus: console.log });
|
|
281
315
|
});
|
|
282
316
|
continue;
|
|
283
317
|
}
|
|
@@ -338,7 +372,7 @@ login
|
|
|
338
372
|
.description("Login to WhatsApp via QR code")
|
|
339
373
|
.action(async () => {
|
|
340
374
|
const config = loadConfig(process.env, { requireOpencode: false });
|
|
341
|
-
await loginWhatsApp(config,
|
|
375
|
+
await loginWhatsApp(config, createAppLogger(config), { onStatus: console.log });
|
|
342
376
|
});
|
|
343
377
|
login
|
|
344
378
|
.command("telegram")
|
|
@@ -372,28 +406,28 @@ whatsapp
|
|
|
372
406
|
.description("Login to WhatsApp via QR code")
|
|
373
407
|
.action(async () => {
|
|
374
408
|
const config = loadConfig(process.env, { requireOpencode: false });
|
|
375
|
-
await loginWhatsApp(config,
|
|
409
|
+
await loginWhatsApp(config, createAppLogger(config), { onStatus: console.log });
|
|
376
410
|
});
|
|
377
411
|
whatsapp
|
|
378
412
|
.command("logout")
|
|
379
413
|
.description("Logout of WhatsApp and clear auth state")
|
|
380
414
|
.action(() => {
|
|
381
415
|
const config = loadConfig(process.env, { requireOpencode: false });
|
|
382
|
-
unpairWhatsApp(config,
|
|
416
|
+
unpairWhatsApp(config, createAppLogger(config));
|
|
383
417
|
});
|
|
384
418
|
program
|
|
385
419
|
.command("qr")
|
|
386
420
|
.description("Print a WhatsApp QR code to pair")
|
|
387
421
|
.action(async () => {
|
|
388
422
|
const config = loadConfig(process.env, { requireOpencode: false });
|
|
389
|
-
await loginWhatsApp(config,
|
|
423
|
+
await loginWhatsApp(config, createAppLogger(config), { onStatus: console.log });
|
|
390
424
|
});
|
|
391
425
|
program
|
|
392
426
|
.command("unpair")
|
|
393
427
|
.description("Clear WhatsApp pairing data")
|
|
394
428
|
.action(() => {
|
|
395
429
|
const config = loadConfig(process.env, { requireOpencode: false });
|
|
396
|
-
unpairWhatsApp(config,
|
|
430
|
+
unpairWhatsApp(config, createAppLogger(config));
|
|
397
431
|
});
|
|
398
432
|
const pairing = program.command("pairing").description("Pairing requests");
|
|
399
433
|
pairing
|
package/dist/config.js
CHANGED
|
@@ -125,6 +125,7 @@ export function loadConfig(env = process.env, options = {}) {
|
|
|
125
125
|
const resolvedDirectory = opencodeDirectory || process.cwd();
|
|
126
126
|
const dataDir = expandHome(env.OWPENBOT_DATA_DIR ?? "~/.owpenbot");
|
|
127
127
|
const dbPath = expandHome(env.OWPENBOT_DB_PATH ?? path.join(dataDir, "owpenbot.db"));
|
|
128
|
+
const logFile = expandHome(env.OWPENBOT_LOG_FILE ?? path.join(dataDir, "logs", "owpenbot.log"));
|
|
128
129
|
const configPath = resolveConfigPath(dataDir, env);
|
|
129
130
|
const { config: configFile } = readConfigFile(configPath);
|
|
130
131
|
const whatsappFile = configFile.channels?.whatsapp ?? {};
|
|
@@ -159,6 +160,7 @@ export function loadConfig(env = process.env, options = {}) {
|
|
|
159
160
|
whatsappEnabled: parseBoolean(env.WHATSAPP_ENABLED, true),
|
|
160
161
|
dataDir,
|
|
161
162
|
dbPath,
|
|
163
|
+
logFile,
|
|
162
164
|
allowlist: envAllowlist,
|
|
163
165
|
toolUpdatesEnabled: parseBoolean(env.TOOL_UPDATES_ENABLED, false),
|
|
164
166
|
groupsEnabled: parseBoolean(env.GROUPS_ENABLED, false),
|
package/dist/logger.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import pino from "pino";
|
|
2
|
-
export function createLogger(level) {
|
|
4
|
+
export function createLogger(level, options) {
|
|
5
|
+
if (options?.logFile) {
|
|
6
|
+
fs.mkdirSync(path.dirname(options.logFile), { recursive: true });
|
|
7
|
+
const destination = pino.destination({ dest: options.logFile, sync: false });
|
|
8
|
+
return pino({ level, base: undefined }, destination);
|
|
9
|
+
}
|
|
3
10
|
return pino({
|
|
4
11
|
level,
|
|
5
12
|
base: undefined,
|
package/dist/telegram.js
CHANGED
|
@@ -20,12 +20,17 @@ export function createTelegramAdapter(config, logger, onMessage) {
|
|
|
20
20
|
const text = msg.text ?? msg.caption ?? "";
|
|
21
21
|
if (!text.trim())
|
|
22
22
|
return;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
try {
|
|
24
|
+
await onMessage({
|
|
25
|
+
channel: "telegram",
|
|
26
|
+
peerId: String(msg.chat.id),
|
|
27
|
+
text,
|
|
28
|
+
raw: msg,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger.error({ error, peerId: msg.chat.id }, "telegram inbound handler failed");
|
|
33
|
+
}
|
|
29
34
|
});
|
|
30
35
|
return {
|
|
31
36
|
name: "telegram",
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fetchLatestBaileysVersion, makeCacheableSignalKeyStore, makeWASocket, useMultiFileAuthState, } from "@whiskeysockets/baileys";
|
|
4
|
+
import qrcode from "qrcode-terminal";
|
|
5
|
+
const CREDS_FILE = "creds.json";
|
|
6
|
+
const CREDS_BACKUP = "creds.json.bak";
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
8
|
+
let credsSaveQueue = Promise.resolve();
|
|
9
|
+
function readCredsRaw(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
if (!fs.existsSync(filePath))
|
|
12
|
+
return null;
|
|
13
|
+
const stats = fs.statSync(filePath);
|
|
14
|
+
if (!stats.isFile() || stats.size <= 1)
|
|
15
|
+
return null;
|
|
16
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function isValidJson(raw) {
|
|
23
|
+
try {
|
|
24
|
+
JSON.parse(raw);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function ensureDir(dir) {
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
function backupCreds(authDir, logger) {
|
|
35
|
+
try {
|
|
36
|
+
const credsPath = path.join(authDir, CREDS_FILE);
|
|
37
|
+
const backupPath = path.join(authDir, CREDS_BACKUP);
|
|
38
|
+
const raw = readCredsRaw(credsPath);
|
|
39
|
+
if (!raw || !isValidJson(raw))
|
|
40
|
+
return;
|
|
41
|
+
fs.copyFileSync(credsPath, backupPath);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.warn({ error }, "whatsapp creds backup failed");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function maybeRestoreCreds(authDir, logger) {
|
|
48
|
+
try {
|
|
49
|
+
const credsPath = path.join(authDir, CREDS_FILE);
|
|
50
|
+
const backupPath = path.join(authDir, CREDS_BACKUP);
|
|
51
|
+
const raw = readCredsRaw(credsPath);
|
|
52
|
+
if (raw && isValidJson(raw))
|
|
53
|
+
return;
|
|
54
|
+
const backupRaw = readCredsRaw(backupPath);
|
|
55
|
+
if (!backupRaw || !isValidJson(backupRaw))
|
|
56
|
+
return;
|
|
57
|
+
fs.copyFileSync(backupPath, credsPath);
|
|
58
|
+
logger.warn({ credsPath }, "restored whatsapp creds from backup");
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
logger.warn({ error }, "whatsapp creds restore failed");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function enqueueSaveCreds(authDir, saveCreds, logger) {
|
|
65
|
+
credsSaveQueue = credsSaveQueue
|
|
66
|
+
.then(async () => {
|
|
67
|
+
backupCreds(authDir, logger);
|
|
68
|
+
await Promise.resolve(saveCreds());
|
|
69
|
+
})
|
|
70
|
+
.catch((error) => {
|
|
71
|
+
logger.warn({ error }, "whatsapp creds save failed");
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function hasWhatsAppCreds(authDir) {
|
|
75
|
+
const raw = readCredsRaw(path.join(authDir, CREDS_FILE));
|
|
76
|
+
if (!raw)
|
|
77
|
+
return false;
|
|
78
|
+
return isValidJson(raw);
|
|
79
|
+
}
|
|
80
|
+
export function getStatusCode(error) {
|
|
81
|
+
return (error?.output?.statusCode ??
|
|
82
|
+
error?.error?.output?.statusCode ??
|
|
83
|
+
error?.status);
|
|
84
|
+
}
|
|
85
|
+
export async function createWhatsAppSocket(options) {
|
|
86
|
+
const { authDir, logger, printQr, onStatus, onQr } = options;
|
|
87
|
+
ensureDir(authDir);
|
|
88
|
+
maybeRestoreCreds(authDir, logger);
|
|
89
|
+
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
|
90
|
+
const { version } = await fetchLatestBaileysVersion();
|
|
91
|
+
const sock = makeWASocket({
|
|
92
|
+
auth: {
|
|
93
|
+
creds: state.creds,
|
|
94
|
+
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
|
95
|
+
},
|
|
96
|
+
version,
|
|
97
|
+
logger,
|
|
98
|
+
printQRInTerminal: false,
|
|
99
|
+
syncFullHistory: false,
|
|
100
|
+
markOnlineOnConnect: false,
|
|
101
|
+
browser: ["owpenbot", "cli", "0.1.0"],
|
|
102
|
+
});
|
|
103
|
+
sock.ev.on("creds.update", () => enqueueSaveCreds(authDir, saveCreds, logger));
|
|
104
|
+
sock.ev.on("connection.update", (update) => {
|
|
105
|
+
if (update.qr) {
|
|
106
|
+
onQr?.(update.qr);
|
|
107
|
+
if (printQr) {
|
|
108
|
+
qrcode.generate(update.qr, { small: true });
|
|
109
|
+
onStatus?.("Scan the QR code to connect WhatsApp.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (update.connection === "open") {
|
|
113
|
+
onStatus?.("WhatsApp connected.");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
sock.ws?.on?.("error", (error) => {
|
|
117
|
+
logger.error({ error }, "whatsapp websocket error");
|
|
118
|
+
});
|
|
119
|
+
return sock;
|
|
120
|
+
}
|
|
121
|
+
export function closeWhatsAppSocket(sock) {
|
|
122
|
+
try {
|
|
123
|
+
sock.ws?.close();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// ignore
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
sock.end?.(undefined);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// ignore
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export function waitForWhatsAppConnection(sock, options = {}) {
|
|
136
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const ev = sock.ev;
|
|
139
|
+
let timer = null;
|
|
140
|
+
const cleanup = () => {
|
|
141
|
+
ev.off?.("connection.update", handler);
|
|
142
|
+
ev.removeListener?.("connection.update", handler);
|
|
143
|
+
if (timer)
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
};
|
|
146
|
+
const handler = (...args) => {
|
|
147
|
+
const update = (args[0] ?? {});
|
|
148
|
+
if (update.connection === "open") {
|
|
149
|
+
cleanup();
|
|
150
|
+
resolve();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (update.connection === "close") {
|
|
154
|
+
cleanup();
|
|
155
|
+
reject(update.lastDisconnect ?? new Error("Connection closed"));
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
if (timeoutMs > 0) {
|
|
159
|
+
timer = setTimeout(() => {
|
|
160
|
+
cleanup();
|
|
161
|
+
reject(new Error("Timed out waiting for WhatsApp connection"));
|
|
162
|
+
}, timeoutMs);
|
|
163
|
+
}
|
|
164
|
+
sock.ev.on("connection.update", handler);
|
|
165
|
+
});
|
|
166
|
+
}
|
package/dist/whatsapp.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { DisconnectReason,
|
|
4
|
-
import
|
|
3
|
+
import { DisconnectReason, isJidGroup } from "@whiskeysockets/baileys";
|
|
4
|
+
import { closeWhatsAppSocket, createWhatsAppSocket, getStatusCode, hasWhatsAppCreds, waitForWhatsAppConnection, } from "./whatsapp-session.js";
|
|
5
5
|
const MAX_TEXT_LENGTH = 3800;
|
|
6
|
+
const SENT_MESSAGE_TTL_MS = 10 * 60_000;
|
|
6
7
|
function extractText(message) {
|
|
7
8
|
const content = message.message;
|
|
8
9
|
if (!content)
|
|
@@ -14,85 +15,138 @@ function extractText(message) {
|
|
|
14
15
|
content.documentMessage?.caption ||
|
|
15
16
|
"");
|
|
16
17
|
}
|
|
17
|
-
function ensureDir(dir) {
|
|
18
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
19
|
-
}
|
|
20
18
|
export function createWhatsAppAdapter(config, logger, onMessage, opts = {}) {
|
|
21
19
|
let socket = null;
|
|
22
20
|
let stopped = false;
|
|
21
|
+
let connecting = false;
|
|
22
|
+
let reconnectAttempts = 0;
|
|
23
|
+
let reconnectTimer = null;
|
|
24
|
+
const sentMessageIds = new Map();
|
|
23
25
|
const log = logger.child({ channel: "whatsapp" });
|
|
24
26
|
const authDir = path.resolve(config.whatsappAuthDir);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
const reconnectPolicy = {
|
|
28
|
+
initialMs: 1500,
|
|
29
|
+
maxMs: 30_000,
|
|
30
|
+
factor: 1.6,
|
|
31
|
+
jitter: 0.25,
|
|
32
|
+
maxAttempts: 10,
|
|
33
|
+
};
|
|
34
|
+
const computeDelay = (attempt) => {
|
|
35
|
+
const base = reconnectPolicy.initialMs * Math.pow(reconnectPolicy.factor, Math.max(0, attempt - 1));
|
|
36
|
+
const capped = Math.min(base, reconnectPolicy.maxMs);
|
|
37
|
+
const jitter = capped * reconnectPolicy.jitter * (Math.random() * 2 - 1);
|
|
38
|
+
return Math.max(250, Math.round(capped + jitter));
|
|
39
|
+
};
|
|
40
|
+
const resetReconnect = () => {
|
|
41
|
+
reconnectAttempts = 0;
|
|
42
|
+
if (reconnectTimer) {
|
|
43
|
+
clearTimeout(reconnectTimer);
|
|
44
|
+
reconnectTimer = null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const scheduleReconnect = (statusCode) => {
|
|
48
|
+
if (stopped || reconnectTimer)
|
|
49
|
+
return;
|
|
50
|
+
reconnectAttempts += 1;
|
|
51
|
+
if (reconnectAttempts > reconnectPolicy.maxAttempts) {
|
|
52
|
+
log.warn({ attempts: reconnectAttempts }, "whatsapp reconnect attempts exhausted");
|
|
53
|
+
opts.onStatus?.("WhatsApp reconnect attempts exhausted. Run: owpenwork whatsapp login.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const delayMs = statusCode === 515 ? 1000 : computeDelay(reconnectAttempts);
|
|
57
|
+
log.warn({ delayMs, statusCode }, "whatsapp reconnect scheduled");
|
|
58
|
+
opts.onStatus?.(`WhatsApp reconnecting in ${Math.round(delayMs / 1000)}s...`);
|
|
59
|
+
reconnectTimer = setTimeout(() => {
|
|
60
|
+
reconnectTimer = null;
|
|
61
|
+
void connect({ printQr: false });
|
|
62
|
+
}, delayMs);
|
|
63
|
+
};
|
|
64
|
+
const recordSentMessage = (messageId) => {
|
|
65
|
+
if (!messageId)
|
|
66
|
+
return;
|
|
67
|
+
sentMessageIds.set(messageId, Date.now());
|
|
68
|
+
};
|
|
69
|
+
const pruneSentMessages = () => {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
for (const [id, timestamp] of sentMessageIds) {
|
|
72
|
+
if (now - timestamp > SENT_MESSAGE_TTL_MS) {
|
|
73
|
+
sentMessageIds.delete(id);
|
|
46
74
|
}
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
async function connect(options = {}) {
|
|
78
|
+
if (stopped || connecting)
|
|
79
|
+
return;
|
|
80
|
+
connecting = true;
|
|
81
|
+
try {
|
|
82
|
+
if (socket) {
|
|
83
|
+
closeWhatsAppSocket(socket);
|
|
84
|
+
socket = null;
|
|
49
85
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
61
|
-
if (shouldReconnect && !stopped) {
|
|
62
|
-
log.warn("whatsapp connection closed, reconnecting");
|
|
63
|
-
void connect();
|
|
86
|
+
const sock = await createWhatsAppSocket({
|
|
87
|
+
authDir,
|
|
88
|
+
logger: log,
|
|
89
|
+
printQr: options.printQr ?? opts.printQr,
|
|
90
|
+
onStatus: opts.onStatus,
|
|
91
|
+
});
|
|
92
|
+
sock.ev.on("connection.update", (update) => {
|
|
93
|
+
if (update.connection === "open") {
|
|
94
|
+
resetReconnect();
|
|
64
95
|
}
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
if (update.connection === "close") {
|
|
97
|
+
const statusCode = getStatusCode(update.lastDisconnect?.error ?? update.lastDisconnect);
|
|
98
|
+
if (statusCode === DisconnectReason.loggedOut) {
|
|
99
|
+
log.warn("whatsapp logged out, run 'owpenbot whatsapp login'");
|
|
100
|
+
opts.onStatus?.("WhatsApp logged out. Run: owpenwork whatsapp login.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (statusCode === 515) {
|
|
104
|
+
opts.onStatus?.("WhatsApp asked for a restart; reconnecting.");
|
|
105
|
+
}
|
|
106
|
+
scheduleReconnect(statusCode);
|
|
67
107
|
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
108
|
+
});
|
|
109
|
+
sock.ev.on("messages.upsert", async ({ messages }) => {
|
|
110
|
+
pruneSentMessages();
|
|
111
|
+
for (const msg of messages) {
|
|
112
|
+
if (!msg.message)
|
|
113
|
+
continue;
|
|
114
|
+
const fromMe = Boolean(msg.key.fromMe);
|
|
115
|
+
const messageId = msg.key.id;
|
|
116
|
+
if (fromMe && messageId && sentMessageIds.has(messageId)) {
|
|
117
|
+
sentMessageIds.delete(messageId);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (fromMe && !config.whatsappSelfChatMode)
|
|
121
|
+
continue;
|
|
122
|
+
const peerId = msg.key.remoteJid;
|
|
123
|
+
if (!peerId)
|
|
124
|
+
continue;
|
|
125
|
+
if (isJidGroup(peerId) && !config.groupsEnabled) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const text = extractText(msg);
|
|
129
|
+
if (!text.trim())
|
|
130
|
+
continue;
|
|
131
|
+
try {
|
|
132
|
+
await onMessage({
|
|
133
|
+
channel: "whatsapp",
|
|
134
|
+
peerId,
|
|
135
|
+
text,
|
|
136
|
+
raw: msg,
|
|
137
|
+
fromMe,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
log.error({ error, peerId }, "whatsapp inbound handler failed");
|
|
142
|
+
}
|
|
82
143
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
text,
|
|
90
|
-
raw: msg,
|
|
91
|
-
fromMe,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
socket = sock;
|
|
144
|
+
});
|
|
145
|
+
socket = sock;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
connecting = false;
|
|
149
|
+
}
|
|
96
150
|
}
|
|
97
151
|
return {
|
|
98
152
|
name: "whatsapp",
|
|
@@ -102,92 +156,75 @@ export function createWhatsAppAdapter(config, logger, onMessage, opts = {}) {
|
|
|
102
156
|
},
|
|
103
157
|
async stop() {
|
|
104
158
|
stopped = true;
|
|
159
|
+
if (reconnectTimer) {
|
|
160
|
+
clearTimeout(reconnectTimer);
|
|
161
|
+
reconnectTimer = null;
|
|
162
|
+
}
|
|
105
163
|
if (socket) {
|
|
106
|
-
socket
|
|
164
|
+
closeWhatsAppSocket(socket);
|
|
107
165
|
socket = null;
|
|
108
166
|
}
|
|
109
167
|
},
|
|
110
168
|
async sendText(peerId, text) {
|
|
111
169
|
if (!socket)
|
|
112
170
|
throw new Error("WhatsApp socket not initialized");
|
|
113
|
-
await socket.sendMessage(peerId, { text });
|
|
171
|
+
const sent = await socket.sendMessage(peerId, { text });
|
|
172
|
+
recordSentMessage(sent?.key?.id);
|
|
114
173
|
},
|
|
115
174
|
};
|
|
116
175
|
}
|
|
117
|
-
export async function loginWhatsApp(config, logger) {
|
|
176
|
+
export async function loginWhatsApp(config, logger, options = {}) {
|
|
118
177
|
const authDir = path.resolve(config.whatsappAuthDir);
|
|
119
|
-
ensureDir(authDir);
|
|
120
178
|
const log = logger.child({ channel: "whatsapp" });
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const sock = makeWASocket({
|
|
129
|
-
auth: {
|
|
130
|
-
creds: state.creds,
|
|
131
|
-
keys: makeCacheableSignalKeyStore(state.keys, log),
|
|
132
|
-
},
|
|
133
|
-
version,
|
|
134
|
-
logger: log,
|
|
135
|
-
printQRInTerminal: false,
|
|
136
|
-
syncFullHistory: false,
|
|
137
|
-
markOnlineOnConnect: false,
|
|
138
|
-
browser: ["owpenbot", "cli", "0.1.0"],
|
|
139
|
-
});
|
|
140
|
-
const finish = (reason, status) => {
|
|
141
|
-
if (finished)
|
|
142
|
-
return;
|
|
143
|
-
finished = true;
|
|
144
|
-
log.info({ reason }, "whatsapp login finished");
|
|
145
|
-
sock.end(undefined);
|
|
146
|
-
resolve(status);
|
|
147
|
-
};
|
|
148
|
-
sock.ev.on("creds.update", async () => {
|
|
149
|
-
await saveCreds();
|
|
150
|
-
if (state.creds?.registered) {
|
|
151
|
-
finish("creds.registered", "linked");
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
sock.ev.on("connection.update", (update) => {
|
|
155
|
-
if (update.qr) {
|
|
156
|
-
qrcode.generate(update.qr, { small: true });
|
|
157
|
-
log.info("scan the QR code to connect WhatsApp");
|
|
158
|
-
}
|
|
159
|
-
if (update.connection === "open") {
|
|
160
|
-
finish("connection.open", "linked");
|
|
161
|
-
}
|
|
162
|
-
if (update.connection === "close") {
|
|
163
|
-
const lastDisconnect = update.lastDisconnect;
|
|
164
|
-
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
165
|
-
if (statusCode === 515) {
|
|
166
|
-
if (state.creds?.registered || fs.existsSync(credsPath)) {
|
|
167
|
-
log.info("whatsapp login requires reconnect; completing login");
|
|
168
|
-
finish("connection.restart.required", "linked");
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
log.warn("whatsapp restart requested before creds registered");
|
|
172
|
-
finish("connection.restart.required", "restart");
|
|
173
|
-
}
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (state.creds?.registered) {
|
|
177
|
-
finish("connection.close.registered", "linked");
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
});
|
|
179
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
180
|
+
const attemptLogin = async (phase, printQr) => {
|
|
181
|
+
const sock = await createWhatsAppSocket({
|
|
182
|
+
authDir,
|
|
183
|
+
logger: log,
|
|
184
|
+
printQr,
|
|
185
|
+
onStatus: options.onStatus,
|
|
181
186
|
});
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
try {
|
|
188
|
+
if (phase === "initial") {
|
|
189
|
+
options.onStatus?.("Waiting for WhatsApp scan...");
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
options.onStatus?.("Reconnecting WhatsApp session...");
|
|
193
|
+
}
|
|
194
|
+
await waitForWhatsAppConnection(sock, { timeoutMs });
|
|
195
|
+
options.onStatus?.("WhatsApp linked.");
|
|
196
|
+
return { ok: true };
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const statusCode = getStatusCode(error);
|
|
200
|
+
return { ok: false, error, statusCode };
|
|
184
201
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
202
|
+
finally {
|
|
203
|
+
setTimeout(() => closeWhatsAppSocket(sock), 500);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const initial = await attemptLogin("initial", true);
|
|
207
|
+
if (initial.ok)
|
|
208
|
+
return;
|
|
209
|
+
if (initial.statusCode === DisconnectReason.loggedOut) {
|
|
210
|
+
options.onStatus?.("WhatsApp logged out. Run: owpenwork whatsapp login.");
|
|
211
|
+
throw new Error("WhatsApp logged out");
|
|
212
|
+
}
|
|
213
|
+
const shouldRetry = initial.statusCode === 515 || (initial.statusCode === undefined && hasWhatsAppCreds(authDir));
|
|
214
|
+
if (shouldRetry) {
|
|
215
|
+
options.onStatus?.("WhatsApp asked for a restart; retrying connection...");
|
|
216
|
+
const retry = await attemptLogin("restart", false);
|
|
217
|
+
if (retry.ok)
|
|
218
|
+
return;
|
|
219
|
+
if (retry.statusCode === DisconnectReason.loggedOut) {
|
|
220
|
+
options.onStatus?.("WhatsApp logged out. Run: owpenwork whatsapp login.");
|
|
188
221
|
}
|
|
222
|
+
throw new Error(`WhatsApp login failed after restart: ${String(retry.error)}`);
|
|
223
|
+
}
|
|
224
|
+
if (!initial.statusCode && !hasWhatsAppCreds(authDir)) {
|
|
225
|
+
options.onStatus?.("Timed out waiting for QR scan. Run login again for a fresh QR.");
|
|
189
226
|
}
|
|
190
|
-
throw new Error(
|
|
227
|
+
throw new Error(`WhatsApp login failed: ${String(initial.error)}`);
|
|
191
228
|
}
|
|
192
229
|
export function unpairWhatsApp(config, logger) {
|
|
193
230
|
const authDir = path.resolve(config.whatsappAuthDir);
|