@deckasoft/waify 0.3.10 → 0.4.1
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/cli/index.js +281 -77
- package/package.json +6 -1
package/dist/cli/index.js
CHANGED
|
@@ -12,7 +12,7 @@ var __export = (target, all) => {
|
|
|
12
12
|
// src/core/paths.ts
|
|
13
13
|
import { join } from "path";
|
|
14
14
|
import { homedir } from "os";
|
|
15
|
-
var dataDir, configPath, promptPath, scheduleJsonPath, schedulePath, logPath, envPath, composePath;
|
|
15
|
+
var dataDir, configPath, promptPath, scheduleJsonPath, schedulePath, logPath, envPath, composePath, qrImagePath;
|
|
16
16
|
var init_paths = __esm({
|
|
17
17
|
"src/core/paths.ts"() {
|
|
18
18
|
"use strict";
|
|
@@ -24,6 +24,7 @@ var init_paths = __esm({
|
|
|
24
24
|
logPath = () => join(dataDir(), "messages.log");
|
|
25
25
|
envPath = () => process.env["WAIFY_ENV_PATH"] ?? join(dataDir(), ".env");
|
|
26
26
|
composePath = () => join(dataDir(), "docker-compose.yml");
|
|
27
|
+
qrImagePath = () => join(dataDir(), "qr.png");
|
|
27
28
|
}
|
|
28
29
|
});
|
|
29
30
|
|
|
@@ -203,7 +204,7 @@ var init_schedule = __esm({
|
|
|
203
204
|
});
|
|
204
205
|
|
|
205
206
|
// src/core/secrets.ts
|
|
206
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as
|
|
207
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
207
208
|
import { z as z4 } from "zod";
|
|
208
209
|
var SecretsSchema, parseEnvFile, loadSecrets, tryLoadSecrets, saveSecrets;
|
|
209
210
|
var init_secrets = __esm({
|
|
@@ -231,7 +232,7 @@ var init_secrets = __esm({
|
|
|
231
232
|
const existing = existsSync5(path) ? parseEnvFile(readFileSync4(path, "utf-8")) : {};
|
|
232
233
|
const merged = { ...existing, ...next };
|
|
233
234
|
const body = Object.entries(merged).filter(([, v]) => typeof v === "string" && v.length > 0).map(([k, v]) => `${k}=${v}`).join("\n");
|
|
234
|
-
|
|
235
|
+
writeFileSync5(path, body + "\n", "utf-8");
|
|
235
236
|
};
|
|
236
237
|
}
|
|
237
238
|
});
|
|
@@ -307,8 +308,8 @@ var init_sender = __esm({
|
|
|
307
308
|
});
|
|
308
309
|
|
|
309
310
|
// src/core/logger.ts
|
|
310
|
-
import { appendFileSync, existsSync as existsSync7, mkdirSync as
|
|
311
|
-
import { dirname as
|
|
311
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5 } from "fs";
|
|
312
|
+
import { dirname as dirname5 } from "path";
|
|
312
313
|
var log, LINE_RE, parseLine, readHistory;
|
|
313
314
|
var init_logger = __esm({
|
|
314
315
|
"src/core/logger.ts"() {
|
|
@@ -316,7 +317,7 @@ var init_logger = __esm({
|
|
|
316
317
|
init_paths();
|
|
317
318
|
log = (status, detail) => {
|
|
318
319
|
const path = logPath();
|
|
319
|
-
|
|
320
|
+
mkdirSync6(dirname5(path), { recursive: true });
|
|
320
321
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
321
322
|
const line = `[${timestamp}] ${status.toUpperCase()} | ${detail}
|
|
322
323
|
`;
|
|
@@ -953,17 +954,50 @@ var registerInit = (program2) => {
|
|
|
953
954
|
};
|
|
954
955
|
|
|
955
956
|
// src/cli/commands/setup.ts
|
|
956
|
-
init_paths();
|
|
957
|
-
init_config();
|
|
958
|
-
init_secrets();
|
|
959
|
-
init_schedule();
|
|
960
|
-
init_prompt();
|
|
961
957
|
import { spawnSync } from "child_process";
|
|
962
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
958
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
963
959
|
import { homedir as homedir2 } from "os";
|
|
964
960
|
import { join as join2 } from "path";
|
|
965
961
|
import { createInterface } from "readline";
|
|
966
962
|
import qrcode from "qrcode-terminal";
|
|
963
|
+
|
|
964
|
+
// src/core/qr.ts
|
|
965
|
+
init_paths();
|
|
966
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
967
|
+
import { dirname as dirname4 } from "path";
|
|
968
|
+
import { PNG } from "pngjs";
|
|
969
|
+
import jsQR from "jsqr";
|
|
970
|
+
var stripDataUrlPrefix = (dataUrl) => dataUrl.replace(/^data:image\/\w+;base64,/, "");
|
|
971
|
+
var decodeQrDataUrl = (dataUrl) => {
|
|
972
|
+
try {
|
|
973
|
+
const buffer = Buffer.from(stripDataUrlPrefix(dataUrl), "base64");
|
|
974
|
+
const png = PNG.sync.read(buffer);
|
|
975
|
+
const result = jsQR(new Uint8ClampedArray(png.data), png.width, png.height);
|
|
976
|
+
return result?.data ?? null;
|
|
977
|
+
} catch {
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
var saveQrImage = (dataUrl) => {
|
|
982
|
+
try {
|
|
983
|
+
const buffer = Buffer.from(stripDataUrlPrefix(dataUrl), "base64");
|
|
984
|
+
const path = qrImagePath();
|
|
985
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
986
|
+
writeFileSync4(path, buffer);
|
|
987
|
+
return path;
|
|
988
|
+
} catch (err) {
|
|
989
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
990
|
+
console.warn(`Could not save QR image: ${msg}`);
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
// src/cli/commands/setup.ts
|
|
996
|
+
init_paths();
|
|
997
|
+
init_config();
|
|
998
|
+
init_secrets();
|
|
999
|
+
init_schedule();
|
|
1000
|
+
init_prompt();
|
|
967
1001
|
import { z as z5 } from "zod";
|
|
968
1002
|
var SessionResponseSchema = z5.object({
|
|
969
1003
|
id: z5.string().optional(),
|
|
@@ -976,8 +1010,76 @@ var QrResponseSchema = z5.object({
|
|
|
976
1010
|
var StatusResponseSchema = z5.object({
|
|
977
1011
|
status: z5.string().optional()
|
|
978
1012
|
});
|
|
1013
|
+
var SESSION_NAME = "waify";
|
|
1014
|
+
var SPINNER_FRAMES = [
|
|
1015
|
+
"\u280B",
|
|
1016
|
+
"\u2819",
|
|
1017
|
+
"\u2839",
|
|
1018
|
+
"\u2838",
|
|
1019
|
+
"\u283C",
|
|
1020
|
+
"\u2834",
|
|
1021
|
+
"\u2826",
|
|
1022
|
+
"\u2827",
|
|
1023
|
+
"\u2807",
|
|
1024
|
+
"\u280F"
|
|
1025
|
+
];
|
|
1026
|
+
var createSpinner = (message) => {
|
|
1027
|
+
let current = message;
|
|
1028
|
+
let frame = 0;
|
|
1029
|
+
const interval = setInterval(() => {
|
|
1030
|
+
process.stderr.write(`\r${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${current}`);
|
|
1031
|
+
frame++;
|
|
1032
|
+
}, 80).unref();
|
|
1033
|
+
return {
|
|
1034
|
+
update: (msg) => {
|
|
1035
|
+
current = msg;
|
|
1036
|
+
},
|
|
1037
|
+
succeed: (msg) => {
|
|
1038
|
+
clearInterval(interval);
|
|
1039
|
+
process.stderr.write(`\r\u2713 ${msg}
|
|
1040
|
+
`);
|
|
1041
|
+
},
|
|
1042
|
+
fail: (msg) => {
|
|
1043
|
+
clearInterval(interval);
|
|
1044
|
+
process.stderr.write(`\r\u2717 ${msg}
|
|
1045
|
+
`);
|
|
1046
|
+
},
|
|
1047
|
+
stop: () => {
|
|
1048
|
+
clearInterval(interval);
|
|
1049
|
+
process.stderr.write("\r\x1B[K");
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
};
|
|
979
1053
|
var wait = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
980
1054
|
var fetchWithTimeout = (url, opts = {}, timeoutMs = 5e3) => fetch(url, { ...opts, signal: AbortSignal.timeout(timeoutMs) });
|
|
1055
|
+
var renderQrInTerminal = (dataUrl) => {
|
|
1056
|
+
const raw = decodeQrDataUrl(dataUrl);
|
|
1057
|
+
if (!raw) return false;
|
|
1058
|
+
qrcode.generate(raw, { small: true });
|
|
1059
|
+
return true;
|
|
1060
|
+
};
|
|
1061
|
+
var presentQr = (dataUrl, sessionId, baseUrl, apiKey) => {
|
|
1062
|
+
const rendered = renderQrInTerminal(dataUrl);
|
|
1063
|
+
if (!rendered) {
|
|
1064
|
+
console.warn(
|
|
1065
|
+
" (Could not decode QR image \u2014 use the saved PNG or curl command below)"
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
const savedPath = saveQrImage(dataUrl);
|
|
1069
|
+
if (savedPath) {
|
|
1070
|
+
console.warn(`
|
|
1071
|
+
QR also saved to: ${savedPath}`);
|
|
1072
|
+
console.warn(` Open it with: open ${savedPath}`);
|
|
1073
|
+
}
|
|
1074
|
+
console.warn("\n To re-fetch the QR if it expires:");
|
|
1075
|
+
console.warn(
|
|
1076
|
+
` curl -s -H "X-API-Key: ${apiKey}" ${baseUrl}/api/sessions/${sessionId}/qr \\`
|
|
1077
|
+
);
|
|
1078
|
+
console.warn(
|
|
1079
|
+
` | sed 's/.*"qrCode":"data:image\\/png;base64,//;s/".*//' \\`
|
|
1080
|
+
);
|
|
1081
|
+
console.warn(` | base64 -d > waify-qr.png`);
|
|
1082
|
+
};
|
|
981
1083
|
var composeTemplate = () => `services:
|
|
982
1084
|
openwa-api:
|
|
983
1085
|
image: ghcr.io/deckasoft/openwa:latest
|
|
@@ -1004,21 +1106,37 @@ var composeTemplate = () => `services:
|
|
|
1004
1106
|
volumes:
|
|
1005
1107
|
openwa-data:
|
|
1006
1108
|
`;
|
|
1109
|
+
var finalizeSetup = (sessionId) => {
|
|
1110
|
+
saveConfig({ ...loadConfig(), openwaSessionId: sessionId });
|
|
1111
|
+
if (!existsSync6(promptPath())) {
|
|
1112
|
+
savePrompt(defaultPrompt);
|
|
1113
|
+
}
|
|
1114
|
+
if (!existsSync6(scheduleJsonPath())) {
|
|
1115
|
+
saveSchedule(defaultSchedule);
|
|
1116
|
+
}
|
|
1117
|
+
console.warn("\n\u2713 All done! Run `waify send` to send your first message.");
|
|
1118
|
+
};
|
|
1007
1119
|
var promptLine = (rl, question) => new Promise((resolve2) => rl.question(question, resolve2));
|
|
1008
|
-
var renderQr = (qrString) => new Promise((resolve2) => qrcode.generate(qrString, { small: true }, () => resolve2()));
|
|
1009
1120
|
var registerSetup = (program2) => {
|
|
1010
|
-
program2.command("setup").description(
|
|
1011
|
-
|
|
1121
|
+
program2.command("setup").description(
|
|
1122
|
+
"Guided first-run wizard: installs OpenWA, authenticates WhatsApp, and configures waify"
|
|
1123
|
+
).action(async () => {
|
|
1124
|
+
const rl = createInterface({
|
|
1125
|
+
input: process.stdin,
|
|
1126
|
+
output: process.stdout
|
|
1127
|
+
});
|
|
1012
1128
|
try {
|
|
1013
1129
|
console.warn("Checking Docker...");
|
|
1014
1130
|
const dockerCheck = spawnSync("docker", ["info"], { stdio: "pipe" });
|
|
1015
1131
|
if (dockerCheck.status !== 0) {
|
|
1016
|
-
console.error(
|
|
1132
|
+
console.error(
|
|
1133
|
+
"Docker is not running or not installed. Please install Docker and start it before running setup."
|
|
1134
|
+
);
|
|
1017
1135
|
process.exitCode = 1;
|
|
1018
1136
|
return;
|
|
1019
1137
|
}
|
|
1020
1138
|
console.warn("Creating config directory...");
|
|
1021
|
-
|
|
1139
|
+
mkdirSync5(join2(homedir2(), ".config", "waify"), { recursive: true });
|
|
1022
1140
|
const baseUrl = loadConfig().openwaBaseUrl;
|
|
1023
1141
|
let geminiKey = "";
|
|
1024
1142
|
while (!geminiKey.trim()) {
|
|
@@ -1038,24 +1156,42 @@ var registerSetup = (program2) => {
|
|
|
1038
1156
|
"Enter your recipient's WhatsApp number (e.g. 5511999998888 \u2014 digits only, no + or spaces):\n> "
|
|
1039
1157
|
);
|
|
1040
1158
|
if (!phoneRegex.test(recipientNumber.trim())) {
|
|
1041
|
-
console.warn(
|
|
1159
|
+
console.warn(
|
|
1160
|
+
"Invalid number format. Use digits only, 8\u201315 characters. Please try again."
|
|
1161
|
+
);
|
|
1042
1162
|
}
|
|
1043
1163
|
}
|
|
1044
1164
|
const chatId = `${recipientNumber.trim()}@c.us`;
|
|
1045
1165
|
saveSecrets({ GEMINI_API_KEY: geminiKey.trim(), OPENWA_API_KEY: "" });
|
|
1046
1166
|
saveConfig({ ...loadConfig(), recipients: [{ chatId }] });
|
|
1047
1167
|
console.warn("Writing docker-compose.yml...");
|
|
1048
|
-
|
|
1049
|
-
console.warn(
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1168
|
+
writeFileSync6(composePath(), composeTemplate(), "utf-8");
|
|
1169
|
+
console.warn(
|
|
1170
|
+
"Starting OpenWA containers (this may take a minute on first run)..."
|
|
1171
|
+
);
|
|
1172
|
+
const upResult = spawnSync(
|
|
1173
|
+
"docker",
|
|
1174
|
+
[
|
|
1175
|
+
"compose",
|
|
1176
|
+
"-f",
|
|
1177
|
+
composePath(),
|
|
1178
|
+
"up",
|
|
1179
|
+
"-d",
|
|
1180
|
+
"--no-deps",
|
|
1181
|
+
"openwa-api"
|
|
1182
|
+
],
|
|
1183
|
+
{
|
|
1184
|
+
stdio: "inherit"
|
|
1185
|
+
}
|
|
1186
|
+
);
|
|
1053
1187
|
if (upResult.status !== 0) {
|
|
1054
|
-
console.error(
|
|
1188
|
+
console.error(
|
|
1189
|
+
"Failed to start OpenWA containers. Check docker compose logs for details."
|
|
1190
|
+
);
|
|
1055
1191
|
process.exitCode = 1;
|
|
1056
1192
|
return;
|
|
1057
1193
|
}
|
|
1058
|
-
|
|
1194
|
+
const apiSpinner = createSpinner("Waiting for OpenWA API to start...");
|
|
1059
1195
|
let apiReady = false;
|
|
1060
1196
|
for (let attempt = 0; attempt < 30; attempt++) {
|
|
1061
1197
|
try {
|
|
@@ -1069,53 +1205,84 @@ var registerSetup = (program2) => {
|
|
|
1069
1205
|
await wait(2e3);
|
|
1070
1206
|
}
|
|
1071
1207
|
if (!apiReady) {
|
|
1072
|
-
|
|
1073
|
-
|
|
1208
|
+
apiSpinner.fail(
|
|
1209
|
+
`OpenWA API did not become ready in time. Check logs with: docker compose -f ${composePath()} logs openwa-api`
|
|
1074
1210
|
);
|
|
1075
1211
|
process.exitCode = 1;
|
|
1076
1212
|
return;
|
|
1077
1213
|
}
|
|
1214
|
+
apiSpinner.succeed("OpenWA API is ready");
|
|
1078
1215
|
console.warn("Reading API key from container...");
|
|
1079
1216
|
const keyResult = spawnSync(
|
|
1080
1217
|
"docker",
|
|
1081
|
-
[
|
|
1218
|
+
[
|
|
1219
|
+
"compose",
|
|
1220
|
+
"-f",
|
|
1221
|
+
composePath(),
|
|
1222
|
+
"exec",
|
|
1223
|
+
"-T",
|
|
1224
|
+
"openwa-api",
|
|
1225
|
+
"cat",
|
|
1226
|
+
"/app/data/.api-key"
|
|
1227
|
+
],
|
|
1082
1228
|
{ encoding: "utf-8" }
|
|
1083
1229
|
);
|
|
1084
1230
|
const openwaApiKey = keyResult.stdout?.trim();
|
|
1085
1231
|
if (keyResult.status !== 0 || !openwaApiKey) {
|
|
1086
1232
|
const errorMsg = keyResult.stderr?.trim() || "Could not read API key from container.";
|
|
1087
|
-
throw new Error(
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`${errorMsg} Check logs with: docker compose -f ${composePath()} logs openwa-api`
|
|
1235
|
+
);
|
|
1088
1236
|
}
|
|
1089
|
-
saveSecrets({
|
|
1237
|
+
saveSecrets({
|
|
1238
|
+
GEMINI_API_KEY: geminiKey.trim(),
|
|
1239
|
+
OPENWA_API_KEY: openwaApiKey
|
|
1240
|
+
});
|
|
1090
1241
|
saveConfig({ ...loadConfig(), openwaApiKey, recipients: [{ chatId }] });
|
|
1091
1242
|
console.warn("Creating WhatsApp session...");
|
|
1092
|
-
const sessionRes = await fetchWithTimeout(
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
"
|
|
1096
|
-
|
|
1243
|
+
const sessionRes = await fetchWithTimeout(
|
|
1244
|
+
`${baseUrl}/api/sessions`,
|
|
1245
|
+
{
|
|
1246
|
+
method: "POST",
|
|
1247
|
+
headers: {
|
|
1248
|
+
"X-API-Key": openwaApiKey,
|
|
1249
|
+
"Content-Type": "application/json"
|
|
1250
|
+
},
|
|
1251
|
+
body: JSON.stringify({ name: SESSION_NAME })
|
|
1097
1252
|
},
|
|
1098
|
-
|
|
1099
|
-
|
|
1253
|
+
1e4
|
|
1254
|
+
);
|
|
1100
1255
|
let sessionId;
|
|
1101
1256
|
if (sessionRes.status === 409) {
|
|
1102
|
-
const listRes = await fetchWithTimeout(
|
|
1103
|
-
|
|
1104
|
-
|
|
1257
|
+
const listRes = await fetchWithTimeout(
|
|
1258
|
+
`${baseUrl}/api/sessions`,
|
|
1259
|
+
{
|
|
1260
|
+
headers: { "X-API-Key": openwaApiKey }
|
|
1261
|
+
},
|
|
1262
|
+
1e4
|
|
1263
|
+
);
|
|
1105
1264
|
if (!listRes.ok) {
|
|
1106
|
-
throw new Error(
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
`Failed to list sessions: ${listRes.status} ${listRes.statusText}`
|
|
1267
|
+
);
|
|
1107
1268
|
}
|
|
1108
1269
|
const sessions = SessionListSchema.parse(await listRes.json());
|
|
1109
|
-
const existing = sessions.find((s) => s.name ===
|
|
1270
|
+
const existing = sessions.find((s) => s.name === SESSION_NAME);
|
|
1110
1271
|
if (!existing?.id) {
|
|
1111
|
-
throw new Error(
|
|
1272
|
+
throw new Error(
|
|
1273
|
+
'Session "waify" already exists but could not be retrieved'
|
|
1274
|
+
);
|
|
1112
1275
|
}
|
|
1113
1276
|
sessionId = existing.id;
|
|
1114
1277
|
} else if (!sessionRes.ok) {
|
|
1115
|
-
throw new Error(
|
|
1278
|
+
throw new Error(
|
|
1279
|
+
`Failed to create session: ${sessionRes.status} ${sessionRes.statusText}`
|
|
1280
|
+
);
|
|
1116
1281
|
} else {
|
|
1117
|
-
const sessionData = SessionResponseSchema.parse(
|
|
1118
|
-
|
|
1282
|
+
const sessionData = SessionResponseSchema.parse(
|
|
1283
|
+
await sessionRes.json()
|
|
1284
|
+
);
|
|
1285
|
+
sessionId = sessionData.id ?? sessionData.name ?? SESSION_NAME;
|
|
1119
1286
|
}
|
|
1120
1287
|
spawnSync(
|
|
1121
1288
|
"docker",
|
|
@@ -1128,25 +1295,54 @@ var registerSetup = (program2) => {
|
|
|
1128
1295
|
"openwa-api",
|
|
1129
1296
|
"sh",
|
|
1130
1297
|
"-c",
|
|
1131
|
-
|
|
1298
|
+
`rm -f /app/data/sessions/session-${SESSION_NAME}/Singleton*`
|
|
1132
1299
|
],
|
|
1133
1300
|
{ encoding: "utf-8" }
|
|
1134
1301
|
);
|
|
1135
1302
|
console.warn("Starting WhatsApp engine...");
|
|
1136
|
-
const startRes = await fetchWithTimeout(
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1303
|
+
const startRes = await fetchWithTimeout(
|
|
1304
|
+
`${baseUrl}/api/sessions/${sessionId}/start`,
|
|
1305
|
+
{
|
|
1306
|
+
method: "POST",
|
|
1307
|
+
headers: { "X-API-Key": openwaApiKey }
|
|
1308
|
+
},
|
|
1309
|
+
1e4
|
|
1310
|
+
);
|
|
1140
1311
|
if (!startRes.ok && startRes.status !== 400) {
|
|
1141
|
-
throw new Error(
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`Failed to start session: ${startRes.status} ${startRes.statusText}`
|
|
1314
|
+
);
|
|
1142
1315
|
}
|
|
1143
|
-
|
|
1316
|
+
try {
|
|
1317
|
+
const preStatusRes = await fetchWithTimeout(
|
|
1318
|
+
`${baseUrl}/api/sessions/${sessionId}`,
|
|
1319
|
+
{ headers: { "X-API-Key": openwaApiKey } }
|
|
1320
|
+
);
|
|
1321
|
+
if (preStatusRes.ok) {
|
|
1322
|
+
const preStatus = StatusResponseSchema.safeParse(
|
|
1323
|
+
await preStatusRes.json()
|
|
1324
|
+
);
|
|
1325
|
+
if (preStatus.success && preStatus.data.status === "ready") {
|
|
1326
|
+
console.warn("\u2713 WhatsApp already linked \u2014 skipping QR scan");
|
|
1327
|
+
finalizeSetup(sessionId);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
1333
|
+
const qrSpinner = createSpinner("Waiting for QR code (Chromium is starting)...");
|
|
1144
1334
|
let qrCode;
|
|
1145
|
-
|
|
1335
|
+
const qrStart = Date.now();
|
|
1336
|
+
for (let attempt = 0; attempt < 150; attempt++) {
|
|
1337
|
+
const elapsed = Math.round((Date.now() - qrStart) / 1e3);
|
|
1338
|
+
qrSpinner.update(`Waiting for QR code... (${elapsed}s / 5 min)`);
|
|
1146
1339
|
try {
|
|
1147
|
-
const qrRes = await fetchWithTimeout(
|
|
1148
|
-
|
|
1149
|
-
|
|
1340
|
+
const qrRes = await fetchWithTimeout(
|
|
1341
|
+
`${baseUrl}/api/sessions/${sessionId}/qr`,
|
|
1342
|
+
{
|
|
1343
|
+
headers: { "X-API-Key": openwaApiKey }
|
|
1344
|
+
}
|
|
1345
|
+
);
|
|
1150
1346
|
if (qrRes.ok) {
|
|
1151
1347
|
const qrData = QrResponseSchema.parse(await qrRes.json());
|
|
1152
1348
|
if (qrData.qrCode) {
|
|
@@ -1158,23 +1354,36 @@ var registerSetup = (program2) => {
|
|
|
1158
1354
|
}
|
|
1159
1355
|
await wait(2e3);
|
|
1160
1356
|
}
|
|
1161
|
-
|
|
1357
|
+
qrSpinner.stop();
|
|
1358
|
+
console.warn(
|
|
1359
|
+
"\n\u{1F4F1} Scan the QR code below with WhatsApp to link your device:"
|
|
1360
|
+
);
|
|
1162
1361
|
console.warn(" Settings \u2192 Linked Devices \u2192 Link a Device\n");
|
|
1163
1362
|
if (qrCode) {
|
|
1164
|
-
|
|
1165
|
-
console.warn(
|
|
1363
|
+
presentQr(qrCode, sessionId, baseUrl, openwaApiKey);
|
|
1364
|
+
console.warn(
|
|
1365
|
+
"\n (QR expires in ~20s \u2014 re-run setup if it expires before you scan)"
|
|
1366
|
+
);
|
|
1166
1367
|
} else {
|
|
1167
|
-
console.warn(
|
|
1168
|
-
|
|
1368
|
+
console.warn(
|
|
1369
|
+
" QR code was not ready in time. Re-run `waify setup` to try again."
|
|
1370
|
+
);
|
|
1169
1371
|
}
|
|
1170
|
-
|
|
1372
|
+
const connectSpinner = createSpinner(
|
|
1373
|
+
"Waiting for you to scan the QR code..."
|
|
1374
|
+
);
|
|
1171
1375
|
let connected = false;
|
|
1172
1376
|
for (let attempt = 0; attempt < 60; attempt++) {
|
|
1173
1377
|
try {
|
|
1174
|
-
const statusRes = await fetchWithTimeout(
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1378
|
+
const statusRes = await fetchWithTimeout(
|
|
1379
|
+
`${baseUrl}/api/sessions/${sessionId}`,
|
|
1380
|
+
{
|
|
1381
|
+
headers: { "X-API-Key": openwaApiKey }
|
|
1382
|
+
}
|
|
1383
|
+
);
|
|
1384
|
+
const parsed = StatusResponseSchema.safeParse(
|
|
1385
|
+
await statusRes.json()
|
|
1386
|
+
);
|
|
1178
1387
|
if (!parsed.success) continue;
|
|
1179
1388
|
if (parsed.data.status === "ready") {
|
|
1180
1389
|
connected = true;
|
|
@@ -1185,19 +1394,14 @@ var registerSetup = (program2) => {
|
|
|
1185
1394
|
await wait(2e3);
|
|
1186
1395
|
}
|
|
1187
1396
|
if (!connected) {
|
|
1188
|
-
|
|
1397
|
+
connectSpinner.fail(
|
|
1398
|
+
"WhatsApp did not connect within 2 minutes. Please re-run `waify setup` to try again."
|
|
1399
|
+
);
|
|
1189
1400
|
process.exitCode = 1;
|
|
1190
1401
|
return;
|
|
1191
1402
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
if (!existsSync6(promptPath())) {
|
|
1195
|
-
savePrompt(defaultPrompt);
|
|
1196
|
-
}
|
|
1197
|
-
if (!existsSync6(scheduleJsonPath())) {
|
|
1198
|
-
saveSchedule(defaultSchedule);
|
|
1199
|
-
}
|
|
1200
|
-
console.warn("\n\u2713 All done! Run `waify send` to send your first message.");
|
|
1403
|
+
connectSpinner.succeed("WhatsApp connected!");
|
|
1404
|
+
finalizeSetup(sessionId);
|
|
1201
1405
|
} catch (err) {
|
|
1202
1406
|
console.error(err instanceof Error ? err.message : String(err));
|
|
1203
1407
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deckasoft/waify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "AI-powered daily message sender for WhatsApp, powered by OpenWA",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"whatsapp",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"ink-select-input": "^6.2.0",
|
|
42
42
|
"ink-spinner": "^5.0.0",
|
|
43
43
|
"ink-text-input": "^6.0.0",
|
|
44
|
+
"jsqr": "^1.4.0",
|
|
45
|
+
"pngjs": "^7.0.0",
|
|
44
46
|
"qrcode-terminal": "^0.12.0",
|
|
45
47
|
"react": "^19.2.6",
|
|
46
48
|
"zod": "^3.24.0"
|
|
@@ -49,8 +51,11 @@
|
|
|
49
51
|
"@semantic-release/changelog": "^6.0.3",
|
|
50
52
|
"@semantic-release/git": "^10.0.1",
|
|
51
53
|
"@types/node": "^22.0.0",
|
|
54
|
+
"@types/pngjs": "^6.0.5",
|
|
55
|
+
"@types/qrcode": "^1.5.6",
|
|
52
56
|
"@types/qrcode-terminal": "^0.12.2",
|
|
53
57
|
"@types/react": "^19.2.15",
|
|
58
|
+
"qrcode": "^1.5.4",
|
|
54
59
|
"semantic-release": "^25.0.3",
|
|
55
60
|
"tsup": "^8.5.1",
|
|
56
61
|
"tsx": "^4.19.0",
|