owpenwork 0.1.12 → 0.1.14
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 +116 -0
- package/dist/cli.js +1 -1
- package/dist/telegram.js +11 -6
- package/dist/whatsapp-session.js +166 -0
- package/dist/whatsapp.js +187 -151
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -19,6 +19,11 @@ const TOOL_LABELS = {
|
|
|
19
19
|
task: "agent",
|
|
20
20
|
webfetch: "webfetch",
|
|
21
21
|
};
|
|
22
|
+
const CHANNEL_LABELS = {
|
|
23
|
+
whatsapp: "WhatsApp",
|
|
24
|
+
telegram: "Telegram",
|
|
25
|
+
};
|
|
26
|
+
const TYPING_INTERVAL_MS = 6000;
|
|
22
27
|
export async function startBridge(config, logger, reporter) {
|
|
23
28
|
const reportStatus = reporter?.onStatus;
|
|
24
29
|
const client = createClient(config);
|
|
@@ -43,6 +48,67 @@ export async function startBridge(config, logger, reporter) {
|
|
|
43
48
|
}
|
|
44
49
|
const sessionQueue = new Map();
|
|
45
50
|
const activeRuns = new Map();
|
|
51
|
+
const sessionModels = new Map();
|
|
52
|
+
const typingLoops = new Map();
|
|
53
|
+
const formatPeer = (channel, peerId) => channel === "whatsapp" ? normalizeWhatsAppId(peerId) : peerId;
|
|
54
|
+
const formatModelLabel = (model) => model ? `${model.providerID}/${model.modelID}` : null;
|
|
55
|
+
const extractModelRef = (info) => {
|
|
56
|
+
if (!info || typeof info !== "object")
|
|
57
|
+
return null;
|
|
58
|
+
const record = info;
|
|
59
|
+
if (record.role !== "user")
|
|
60
|
+
return null;
|
|
61
|
+
if (!record.model || typeof record.model !== "object")
|
|
62
|
+
return null;
|
|
63
|
+
const model = record.model;
|
|
64
|
+
if (typeof model.providerID !== "string" || typeof model.modelID !== "string")
|
|
65
|
+
return null;
|
|
66
|
+
return { providerID: model.providerID, modelID: model.modelID };
|
|
67
|
+
};
|
|
68
|
+
const reportThinking = (run) => {
|
|
69
|
+
if (!reportStatus)
|
|
70
|
+
return;
|
|
71
|
+
const modelLabel = formatModelLabel(sessionModels.get(run.sessionID));
|
|
72
|
+
const nextLabel = modelLabel ? `Thinking (${modelLabel})` : "Thinking...";
|
|
73
|
+
if (run.thinkingLabel === nextLabel && run.thinkingActive)
|
|
74
|
+
return;
|
|
75
|
+
run.thinkingLabel = nextLabel;
|
|
76
|
+
run.thinkingActive = true;
|
|
77
|
+
reportStatus(`[${CHANNEL_LABELS[run.channel]}] ${formatPeer(run.channel, run.peerId)} ${nextLabel}`);
|
|
78
|
+
};
|
|
79
|
+
const reportDone = (run) => {
|
|
80
|
+
if (!reportStatus || !run.thinkingActive)
|
|
81
|
+
return;
|
|
82
|
+
const modelLabel = formatModelLabel(sessionModels.get(run.sessionID));
|
|
83
|
+
const suffix = modelLabel ? ` (${modelLabel})` : "";
|
|
84
|
+
reportStatus(`[${CHANNEL_LABELS[run.channel]}] ${formatPeer(run.channel, run.peerId)} Done${suffix}`);
|
|
85
|
+
run.thinkingActive = false;
|
|
86
|
+
};
|
|
87
|
+
const startTyping = (run) => {
|
|
88
|
+
const adapter = adapters.get(run.channel);
|
|
89
|
+
if (!adapter?.sendTyping)
|
|
90
|
+
return;
|
|
91
|
+
if (typingLoops.has(run.sessionID))
|
|
92
|
+
return;
|
|
93
|
+
const sendTyping = async () => {
|
|
94
|
+
try {
|
|
95
|
+
await adapter.sendTyping?.(run.peerId);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
logger.warn({ error, channel: run.channel }, "typing update failed");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
void sendTyping();
|
|
102
|
+
const timer = setInterval(sendTyping, TYPING_INTERVAL_MS);
|
|
103
|
+
typingLoops.set(run.sessionID, timer);
|
|
104
|
+
};
|
|
105
|
+
const stopTyping = (sessionID) => {
|
|
106
|
+
const timer = typingLoops.get(sessionID);
|
|
107
|
+
if (!timer)
|
|
108
|
+
return;
|
|
109
|
+
clearInterval(timer);
|
|
110
|
+
typingLoops.delete(sessionID);
|
|
111
|
+
};
|
|
46
112
|
let opencodeHealthy = false;
|
|
47
113
|
let opencodeVersion;
|
|
48
114
|
async function refreshHealth() {
|
|
@@ -80,6 +146,46 @@ export async function startBridge(config, logger, reporter) {
|
|
|
80
146
|
const event = normalizeEvent(raw);
|
|
81
147
|
if (!event)
|
|
82
148
|
continue;
|
|
149
|
+
if (event.type === "message.updated") {
|
|
150
|
+
if (event.properties && typeof event.properties === "object") {
|
|
151
|
+
const record = event.properties;
|
|
152
|
+
const info = record.info;
|
|
153
|
+
const sessionID = typeof info?.sessionID === "string" ? info.sessionID : null;
|
|
154
|
+
const model = extractModelRef(info);
|
|
155
|
+
if (sessionID && model) {
|
|
156
|
+
sessionModels.set(sessionID, model);
|
|
157
|
+
const run = activeRuns.get(sessionID);
|
|
158
|
+
if (run)
|
|
159
|
+
reportThinking(run);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (event.type === "session.status") {
|
|
164
|
+
if (event.properties && typeof event.properties === "object") {
|
|
165
|
+
const record = event.properties;
|
|
166
|
+
const sessionID = typeof record.sessionID === "string" ? record.sessionID : null;
|
|
167
|
+
const status = record.status;
|
|
168
|
+
if (sessionID && (status?.type === "busy" || status?.type === "retry")) {
|
|
169
|
+
const run = activeRuns.get(sessionID);
|
|
170
|
+
if (run) {
|
|
171
|
+
reportThinking(run);
|
|
172
|
+
startTyping(run);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (event.type === "session.idle") {
|
|
178
|
+
if (event.properties && typeof event.properties === "object") {
|
|
179
|
+
const record = event.properties;
|
|
180
|
+
const sessionID = typeof record.sessionID === "string" ? record.sessionID : null;
|
|
181
|
+
if (sessionID) {
|
|
182
|
+
stopTyping(sessionID);
|
|
183
|
+
const run = activeRuns.get(sessionID);
|
|
184
|
+
if (run)
|
|
185
|
+
reportDone(run);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
83
189
|
if (event.type === "message.part.updated") {
|
|
84
190
|
const part = event.properties?.part;
|
|
85
191
|
if (!part?.sessionID)
|
|
@@ -201,6 +307,8 @@ export async function startBridge(config, logger, reporter) {
|
|
|
201
307
|
seenToolStates: new Map(),
|
|
202
308
|
};
|
|
203
309
|
activeRuns.set(sessionID, runState);
|
|
310
|
+
reportThinking(runState);
|
|
311
|
+
startTyping(runState);
|
|
204
312
|
try {
|
|
205
313
|
const response = await client.session.prompt({
|
|
206
314
|
sessionID,
|
|
@@ -228,6 +336,8 @@ export async function startBridge(config, logger, reporter) {
|
|
|
228
336
|
});
|
|
229
337
|
}
|
|
230
338
|
finally {
|
|
339
|
+
stopTyping(sessionID);
|
|
340
|
+
reportDone(runState);
|
|
231
341
|
activeRuns.delete(sessionID);
|
|
232
342
|
}
|
|
233
343
|
});
|
|
@@ -243,6 +353,8 @@ export async function startBridge(config, logger, reporter) {
|
|
|
243
353
|
throw new Error("Failed to create session");
|
|
244
354
|
store.upsertSession(message.channel, message.peerId, sessionID);
|
|
245
355
|
logger.info({ sessionID, channel: message.channel, peerId: message.peerId }, "session created");
|
|
356
|
+
reportStatus?.(`${CHANNEL_LABELS[message.channel]} session created for ${formatPeer(message.channel, message.peerId)} (ID: ${sessionID}).`);
|
|
357
|
+
await sendText(message.channel, message.peerId, "🧭 Session started.", { kind: "system" });
|
|
246
358
|
return sessionID;
|
|
247
359
|
}
|
|
248
360
|
function enqueue(sessionID, task) {
|
|
@@ -271,6 +383,10 @@ export async function startBridge(config, logger, reporter) {
|
|
|
271
383
|
clearInterval(healthTimer);
|
|
272
384
|
if (stopHealthServer)
|
|
273
385
|
stopHealthServer();
|
|
386
|
+
for (const timer of typingLoops.values()) {
|
|
387
|
+
clearInterval(timer);
|
|
388
|
+
}
|
|
389
|
+
typingLoops.clear();
|
|
274
390
|
for (const adapter of adapters.values()) {
|
|
275
391
|
await adapter.stop();
|
|
276
392
|
}
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { createLogger } from "./logger.js";
|
|
|
10
10
|
import { createClient } from "./opencode.js";
|
|
11
11
|
import { truncateText } from "./text.js";
|
|
12
12
|
import { loginWhatsApp, unpairWhatsApp } from "./whatsapp.js";
|
|
13
|
-
const VERSION = "0.1.
|
|
13
|
+
const VERSION = "0.1.14";
|
|
14
14
|
const STEP_OPTIONS = [
|
|
15
15
|
{
|
|
16
16
|
value: "config",
|
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,90 +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
|
-
|
|
46
|
-
|
|
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);
|
|
47
74
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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;
|
|
51
85
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
|
|
64
|
-
if (shouldReconnect && !stopped) {
|
|
65
|
-
log.warn("whatsapp connection closed, reconnecting");
|
|
66
|
-
opts.onStatus?.("WhatsApp connection closed; reconnecting.");
|
|
67
|
-
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();
|
|
68
95
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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);
|
|
72
107
|
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
}
|
|
87
143
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
text,
|
|
95
|
-
raw: msg,
|
|
96
|
-
fromMe,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
socket = sock;
|
|
144
|
+
});
|
|
145
|
+
socket = sock;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
connecting = false;
|
|
149
|
+
}
|
|
101
150
|
}
|
|
102
151
|
return {
|
|
103
152
|
name: "whatsapp",
|
|
@@ -107,98 +156,85 @@ export function createWhatsAppAdapter(config, logger, onMessage, opts = {}) {
|
|
|
107
156
|
},
|
|
108
157
|
async stop() {
|
|
109
158
|
stopped = true;
|
|
159
|
+
if (reconnectTimer) {
|
|
160
|
+
clearTimeout(reconnectTimer);
|
|
161
|
+
reconnectTimer = null;
|
|
162
|
+
}
|
|
110
163
|
if (socket) {
|
|
111
|
-
socket
|
|
164
|
+
closeWhatsAppSocket(socket);
|
|
112
165
|
socket = null;
|
|
113
166
|
}
|
|
114
167
|
},
|
|
115
168
|
async sendText(peerId, text) {
|
|
116
169
|
if (!socket)
|
|
117
170
|
throw new Error("WhatsApp socket not initialized");
|
|
118
|
-
await socket.sendMessage(peerId, { text });
|
|
171
|
+
const sent = await socket.sendMessage(peerId, { text });
|
|
172
|
+
recordSentMessage(sent?.key?.id);
|
|
173
|
+
},
|
|
174
|
+
async sendTyping(peerId) {
|
|
175
|
+
if (!socket)
|
|
176
|
+
return;
|
|
177
|
+
try {
|
|
178
|
+
await socket.sendPresenceUpdate("composing", peerId);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
log.warn({ error, peerId }, "whatsapp typing update failed");
|
|
182
|
+
}
|
|
119
183
|
},
|
|
120
184
|
};
|
|
121
185
|
}
|
|
122
186
|
export async function loginWhatsApp(config, logger, options = {}) {
|
|
123
187
|
const authDir = path.resolve(config.whatsappAuthDir);
|
|
124
|
-
ensureDir(authDir);
|
|
125
188
|
const log = logger.child({ channel: "whatsapp" });
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const sock = makeWASocket({
|
|
134
|
-
auth: {
|
|
135
|
-
creds: state.creds,
|
|
136
|
-
keys: makeCacheableSignalKeyStore(state.keys, log),
|
|
137
|
-
},
|
|
138
|
-
version,
|
|
139
|
-
logger: log,
|
|
140
|
-
printQRInTerminal: false,
|
|
141
|
-
syncFullHistory: false,
|
|
142
|
-
markOnlineOnConnect: false,
|
|
143
|
-
browser: ["owpenbot", "cli", "0.1.0"],
|
|
144
|
-
});
|
|
145
|
-
const finish = (reason, status) => {
|
|
146
|
-
if (finished)
|
|
147
|
-
return;
|
|
148
|
-
finished = true;
|
|
149
|
-
log.info({ reason }, "whatsapp login finished");
|
|
150
|
-
sock.end(undefined);
|
|
151
|
-
resolve(status);
|
|
152
|
-
};
|
|
153
|
-
sock.ev.on("creds.update", async () => {
|
|
154
|
-
await saveCreds();
|
|
155
|
-
if (state.creds?.registered) {
|
|
156
|
-
finish("creds.registered", "linked");
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
sock.ev.on("connection.update", (update) => {
|
|
160
|
-
if (update.qr) {
|
|
161
|
-
qrcode.generate(update.qr, { small: true });
|
|
162
|
-
log.info("scan the QR code to connect WhatsApp");
|
|
163
|
-
options.onStatus?.("Scan the QR code to connect WhatsApp.");
|
|
164
|
-
}
|
|
165
|
-
if (update.connection === "open") {
|
|
166
|
-
options.onStatus?.("WhatsApp linked.");
|
|
167
|
-
finish("connection.open", "linked");
|
|
168
|
-
}
|
|
169
|
-
if (update.connection === "close") {
|
|
170
|
-
const lastDisconnect = update.lastDisconnect;
|
|
171
|
-
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
172
|
-
if (statusCode === 515) {
|
|
173
|
-
if (state.creds?.registered || fs.existsSync(credsPath)) {
|
|
174
|
-
log.info("whatsapp login requires reconnect; completing login");
|
|
175
|
-
options.onStatus?.("WhatsApp login requires reconnect; completing login.");
|
|
176
|
-
finish("connection.restart.required", "linked");
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
log.warn("whatsapp restart requested before creds registered");
|
|
180
|
-
options.onStatus?.("WhatsApp login needs another scan.");
|
|
181
|
-
finish("connection.restart.required", "restart");
|
|
182
|
-
}
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
if (state.creds?.registered) {
|
|
186
|
-
options.onStatus?.("WhatsApp linked.");
|
|
187
|
-
finish("connection.close.registered", "linked");
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
});
|
|
189
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
190
|
+
const attemptLogin = async (phase, printQr) => {
|
|
191
|
+
const sock = await createWhatsAppSocket({
|
|
192
|
+
authDir,
|
|
193
|
+
logger: log,
|
|
194
|
+
printQr,
|
|
195
|
+
onStatus: options.onStatus,
|
|
191
196
|
});
|
|
192
|
-
|
|
193
|
-
|
|
197
|
+
try {
|
|
198
|
+
if (phase === "initial") {
|
|
199
|
+
options.onStatus?.("Waiting for WhatsApp scan...");
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
options.onStatus?.("Reconnecting WhatsApp session...");
|
|
203
|
+
}
|
|
204
|
+
await waitForWhatsAppConnection(sock, { timeoutMs });
|
|
205
|
+
options.onStatus?.("WhatsApp linked.");
|
|
206
|
+
return { ok: true };
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
const statusCode = getStatusCode(error);
|
|
210
|
+
return { ok: false, error, statusCode };
|
|
194
211
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
212
|
+
finally {
|
|
213
|
+
setTimeout(() => closeWhatsAppSocket(sock), 500);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const initial = await attemptLogin("initial", true);
|
|
217
|
+
if (initial.ok)
|
|
218
|
+
return;
|
|
219
|
+
if (initial.statusCode === DisconnectReason.loggedOut) {
|
|
220
|
+
options.onStatus?.("WhatsApp logged out. Run: owpenwork whatsapp login.");
|
|
221
|
+
throw new Error("WhatsApp logged out");
|
|
222
|
+
}
|
|
223
|
+
const shouldRetry = initial.statusCode === 515 || (initial.statusCode === undefined && hasWhatsAppCreds(authDir));
|
|
224
|
+
if (shouldRetry) {
|
|
225
|
+
options.onStatus?.("WhatsApp asked for a restart; retrying connection...");
|
|
226
|
+
const retry = await attemptLogin("restart", false);
|
|
227
|
+
if (retry.ok)
|
|
228
|
+
return;
|
|
229
|
+
if (retry.statusCode === DisconnectReason.loggedOut) {
|
|
230
|
+
options.onStatus?.("WhatsApp logged out. Run: owpenwork whatsapp login.");
|
|
199
231
|
}
|
|
232
|
+
throw new Error(`WhatsApp login failed after restart: ${String(retry.error)}`);
|
|
233
|
+
}
|
|
234
|
+
if (!initial.statusCode && !hasWhatsAppCreds(authDir)) {
|
|
235
|
+
options.onStatus?.("Timed out waiting for QR scan. Run login again for a fresh QR.");
|
|
200
236
|
}
|
|
201
|
-
throw new Error(
|
|
237
|
+
throw new Error(`WhatsApp login failed: ${String(initial.error)}`);
|
|
202
238
|
}
|
|
203
239
|
export function unpairWhatsApp(config, logger) {
|
|
204
240
|
const authDir = path.resolve(config.whatsappAuthDir);
|