@deckasoft/waify 0.3.9 → 0.4.0
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 +214 -55
- package/package.json +4 -1
package/dist/cli/index.js
CHANGED
|
@@ -964,6 +964,8 @@ import { homedir as homedir2 } from "os";
|
|
|
964
964
|
import { join as join2 } from "path";
|
|
965
965
|
import { createInterface } from "readline";
|
|
966
966
|
import qrcode from "qrcode-terminal";
|
|
967
|
+
import { PNG } from "pngjs";
|
|
968
|
+
import jsQR from "jsqr";
|
|
967
969
|
import { z as z5 } from "zod";
|
|
968
970
|
var SessionResponseSchema = z5.object({
|
|
969
971
|
id: z5.string().optional(),
|
|
@@ -976,8 +978,66 @@ var QrResponseSchema = z5.object({
|
|
|
976
978
|
var StatusResponseSchema = z5.object({
|
|
977
979
|
status: z5.string().optional()
|
|
978
980
|
});
|
|
981
|
+
var SESSION_NAME = "waify";
|
|
982
|
+
var SPINNER_FRAMES = [
|
|
983
|
+
"\u280B",
|
|
984
|
+
"\u2819",
|
|
985
|
+
"\u2839",
|
|
986
|
+
"\u2838",
|
|
987
|
+
"\u283C",
|
|
988
|
+
"\u2834",
|
|
989
|
+
"\u2826",
|
|
990
|
+
"\u2827",
|
|
991
|
+
"\u2807",
|
|
992
|
+
"\u280F"
|
|
993
|
+
];
|
|
994
|
+
var createSpinner = (message) => {
|
|
995
|
+
let current = message;
|
|
996
|
+
let frame = 0;
|
|
997
|
+
const interval = setInterval(() => {
|
|
998
|
+
process.stderr.write(`\r${SPINNER_FRAMES[frame % SPINNER_FRAMES.length]} ${current}`);
|
|
999
|
+
frame++;
|
|
1000
|
+
}, 80).unref();
|
|
1001
|
+
return {
|
|
1002
|
+
update: (msg) => {
|
|
1003
|
+
current = msg;
|
|
1004
|
+
},
|
|
1005
|
+
succeed: (msg) => {
|
|
1006
|
+
clearInterval(interval);
|
|
1007
|
+
process.stderr.write(`\r\u2713 ${msg}
|
|
1008
|
+
`);
|
|
1009
|
+
},
|
|
1010
|
+
fail: (msg) => {
|
|
1011
|
+
clearInterval(interval);
|
|
1012
|
+
process.stderr.write(`\r\u2717 ${msg}
|
|
1013
|
+
`);
|
|
1014
|
+
},
|
|
1015
|
+
stop: () => {
|
|
1016
|
+
clearInterval(interval);
|
|
1017
|
+
process.stderr.write("\r\x1B[K");
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
};
|
|
979
1021
|
var wait = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
980
1022
|
var fetchWithTimeout = (url, opts = {}, timeoutMs = 5e3) => fetch(url, { ...opts, signal: AbortSignal.timeout(timeoutMs) });
|
|
1023
|
+
var decodeQrDataUrl = (dataUrl) => {
|
|
1024
|
+
const base64 = dataUrl.replace(/^data:image\/\w+;base64,/, "");
|
|
1025
|
+
const buffer = Buffer.from(base64, "base64");
|
|
1026
|
+
const png = PNG.sync.read(buffer);
|
|
1027
|
+
const result = jsQR(new Uint8ClampedArray(png.data), png.width, png.height);
|
|
1028
|
+
return result?.data ?? null;
|
|
1029
|
+
};
|
|
1030
|
+
var renderQrInTerminal = (dataUrl) => new Promise((resolve2) => {
|
|
1031
|
+
const raw = decodeQrDataUrl(dataUrl);
|
|
1032
|
+
if (raw) {
|
|
1033
|
+
qrcode.generate(raw, { small: true }, () => resolve2());
|
|
1034
|
+
} else {
|
|
1035
|
+
console.warn(
|
|
1036
|
+
" (Could not decode QR image \u2014 try scanning from the API URL instead)"
|
|
1037
|
+
);
|
|
1038
|
+
resolve2();
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
981
1041
|
var composeTemplate = () => `services:
|
|
982
1042
|
openwa-api:
|
|
983
1043
|
image: ghcr.io/deckasoft/openwa:latest
|
|
@@ -1005,15 +1065,21 @@ volumes:
|
|
|
1005
1065
|
openwa-data:
|
|
1006
1066
|
`;
|
|
1007
1067
|
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
1068
|
var registerSetup = (program2) => {
|
|
1010
|
-
program2.command("setup").description(
|
|
1011
|
-
|
|
1069
|
+
program2.command("setup").description(
|
|
1070
|
+
"Guided first-run wizard: installs OpenWA, authenticates WhatsApp, and configures waify"
|
|
1071
|
+
).action(async () => {
|
|
1072
|
+
const rl = createInterface({
|
|
1073
|
+
input: process.stdin,
|
|
1074
|
+
output: process.stdout
|
|
1075
|
+
});
|
|
1012
1076
|
try {
|
|
1013
1077
|
console.warn("Checking Docker...");
|
|
1014
1078
|
const dockerCheck = spawnSync("docker", ["info"], { stdio: "pipe" });
|
|
1015
1079
|
if (dockerCheck.status !== 0) {
|
|
1016
|
-
console.error(
|
|
1080
|
+
console.error(
|
|
1081
|
+
"Docker is not running or not installed. Please install Docker and start it before running setup."
|
|
1082
|
+
);
|
|
1017
1083
|
process.exitCode = 1;
|
|
1018
1084
|
return;
|
|
1019
1085
|
}
|
|
@@ -1038,7 +1104,9 @@ var registerSetup = (program2) => {
|
|
|
1038
1104
|
"Enter your recipient's WhatsApp number (e.g. 5511999998888 \u2014 digits only, no + or spaces):\n> "
|
|
1039
1105
|
);
|
|
1040
1106
|
if (!phoneRegex.test(recipientNumber.trim())) {
|
|
1041
|
-
console.warn(
|
|
1107
|
+
console.warn(
|
|
1108
|
+
"Invalid number format. Use digits only, 8\u201315 characters. Please try again."
|
|
1109
|
+
);
|
|
1042
1110
|
}
|
|
1043
1111
|
}
|
|
1044
1112
|
const chatId = `${recipientNumber.trim()}@c.us`;
|
|
@@ -1046,16 +1114,32 @@ var registerSetup = (program2) => {
|
|
|
1046
1114
|
saveConfig({ ...loadConfig(), recipients: [{ chatId }] });
|
|
1047
1115
|
console.warn("Writing docker-compose.yml...");
|
|
1048
1116
|
writeFileSync5(composePath(), composeTemplate(), "utf-8");
|
|
1049
|
-
console.warn(
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1117
|
+
console.warn(
|
|
1118
|
+
"Starting OpenWA containers (this may take a minute on first run)..."
|
|
1119
|
+
);
|
|
1120
|
+
const upResult = spawnSync(
|
|
1121
|
+
"docker",
|
|
1122
|
+
[
|
|
1123
|
+
"compose",
|
|
1124
|
+
"-f",
|
|
1125
|
+
composePath(),
|
|
1126
|
+
"up",
|
|
1127
|
+
"-d",
|
|
1128
|
+
"--no-deps",
|
|
1129
|
+
"openwa-api"
|
|
1130
|
+
],
|
|
1131
|
+
{
|
|
1132
|
+
stdio: "inherit"
|
|
1133
|
+
}
|
|
1134
|
+
);
|
|
1053
1135
|
if (upResult.status !== 0) {
|
|
1054
|
-
console.error(
|
|
1136
|
+
console.error(
|
|
1137
|
+
"Failed to start OpenWA containers. Check docker compose logs for details."
|
|
1138
|
+
);
|
|
1055
1139
|
process.exitCode = 1;
|
|
1056
1140
|
return;
|
|
1057
1141
|
}
|
|
1058
|
-
|
|
1142
|
+
const apiSpinner = createSpinner("Waiting for OpenWA API to start...");
|
|
1059
1143
|
let apiReady = false;
|
|
1060
1144
|
for (let attempt = 0; attempt < 30; attempt++) {
|
|
1061
1145
|
try {
|
|
@@ -1069,69 +1153,127 @@ var registerSetup = (program2) => {
|
|
|
1069
1153
|
await wait(2e3);
|
|
1070
1154
|
}
|
|
1071
1155
|
if (!apiReady) {
|
|
1072
|
-
|
|
1073
|
-
|
|
1156
|
+
apiSpinner.fail(
|
|
1157
|
+
`OpenWA API did not become ready in time. Check logs with: docker compose -f ${composePath()} logs openwa-api`
|
|
1074
1158
|
);
|
|
1075
1159
|
process.exitCode = 1;
|
|
1076
1160
|
return;
|
|
1077
1161
|
}
|
|
1162
|
+
apiSpinner.succeed("OpenWA API is ready");
|
|
1078
1163
|
console.warn("Reading API key from container...");
|
|
1079
1164
|
const keyResult = spawnSync(
|
|
1080
1165
|
"docker",
|
|
1081
|
-
[
|
|
1166
|
+
[
|
|
1167
|
+
"compose",
|
|
1168
|
+
"-f",
|
|
1169
|
+
composePath(),
|
|
1170
|
+
"exec",
|
|
1171
|
+
"-T",
|
|
1172
|
+
"openwa-api",
|
|
1173
|
+
"cat",
|
|
1174
|
+
"/app/data/.api-key"
|
|
1175
|
+
],
|
|
1082
1176
|
{ encoding: "utf-8" }
|
|
1083
1177
|
);
|
|
1084
1178
|
const openwaApiKey = keyResult.stdout?.trim();
|
|
1085
1179
|
if (keyResult.status !== 0 || !openwaApiKey) {
|
|
1086
1180
|
const errorMsg = keyResult.stderr?.trim() || "Could not read API key from container.";
|
|
1087
|
-
throw new Error(
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
`${errorMsg} Check logs with: docker compose -f ${composePath()} logs openwa-api`
|
|
1183
|
+
);
|
|
1088
1184
|
}
|
|
1089
|
-
saveSecrets({
|
|
1185
|
+
saveSecrets({
|
|
1186
|
+
GEMINI_API_KEY: geminiKey.trim(),
|
|
1187
|
+
OPENWA_API_KEY: openwaApiKey
|
|
1188
|
+
});
|
|
1090
1189
|
saveConfig({ ...loadConfig(), openwaApiKey, recipients: [{ chatId }] });
|
|
1091
1190
|
console.warn("Creating WhatsApp session...");
|
|
1092
|
-
const sessionRes = await fetchWithTimeout(
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
"
|
|
1096
|
-
|
|
1191
|
+
const sessionRes = await fetchWithTimeout(
|
|
1192
|
+
`${baseUrl}/api/sessions`,
|
|
1193
|
+
{
|
|
1194
|
+
method: "POST",
|
|
1195
|
+
headers: {
|
|
1196
|
+
"X-API-Key": openwaApiKey,
|
|
1197
|
+
"Content-Type": "application/json"
|
|
1198
|
+
},
|
|
1199
|
+
body: JSON.stringify({ name: SESSION_NAME })
|
|
1097
1200
|
},
|
|
1098
|
-
|
|
1099
|
-
|
|
1201
|
+
1e4
|
|
1202
|
+
);
|
|
1100
1203
|
let sessionId;
|
|
1101
1204
|
if (sessionRes.status === 409) {
|
|
1102
|
-
const listRes = await fetchWithTimeout(
|
|
1103
|
-
|
|
1104
|
-
|
|
1205
|
+
const listRes = await fetchWithTimeout(
|
|
1206
|
+
`${baseUrl}/api/sessions`,
|
|
1207
|
+
{
|
|
1208
|
+
headers: { "X-API-Key": openwaApiKey }
|
|
1209
|
+
},
|
|
1210
|
+
1e4
|
|
1211
|
+
);
|
|
1105
1212
|
if (!listRes.ok) {
|
|
1106
|
-
throw new Error(
|
|
1213
|
+
throw new Error(
|
|
1214
|
+
`Failed to list sessions: ${listRes.status} ${listRes.statusText}`
|
|
1215
|
+
);
|
|
1107
1216
|
}
|
|
1108
1217
|
const sessions = SessionListSchema.parse(await listRes.json());
|
|
1109
|
-
const existing = sessions.find((s) => s.name ===
|
|
1218
|
+
const existing = sessions.find((s) => s.name === SESSION_NAME);
|
|
1110
1219
|
if (!existing?.id) {
|
|
1111
|
-
throw new Error(
|
|
1220
|
+
throw new Error(
|
|
1221
|
+
'Session "waify" already exists but could not be retrieved'
|
|
1222
|
+
);
|
|
1112
1223
|
}
|
|
1113
1224
|
sessionId = existing.id;
|
|
1114
1225
|
} else if (!sessionRes.ok) {
|
|
1115
|
-
throw new Error(
|
|
1226
|
+
throw new Error(
|
|
1227
|
+
`Failed to create session: ${sessionRes.status} ${sessionRes.statusText}`
|
|
1228
|
+
);
|
|
1116
1229
|
} else {
|
|
1117
|
-
const sessionData = SessionResponseSchema.parse(
|
|
1118
|
-
|
|
1230
|
+
const sessionData = SessionResponseSchema.parse(
|
|
1231
|
+
await sessionRes.json()
|
|
1232
|
+
);
|
|
1233
|
+
sessionId = sessionData.id ?? sessionData.name ?? SESSION_NAME;
|
|
1119
1234
|
}
|
|
1235
|
+
spawnSync(
|
|
1236
|
+
"docker",
|
|
1237
|
+
[
|
|
1238
|
+
"compose",
|
|
1239
|
+
"-f",
|
|
1240
|
+
composePath(),
|
|
1241
|
+
"exec",
|
|
1242
|
+
"-T",
|
|
1243
|
+
"openwa-api",
|
|
1244
|
+
"sh",
|
|
1245
|
+
"-c",
|
|
1246
|
+
`rm -f /app/data/sessions/session-${SESSION_NAME}/Singleton*`
|
|
1247
|
+
],
|
|
1248
|
+
{ encoding: "utf-8" }
|
|
1249
|
+
);
|
|
1120
1250
|
console.warn("Starting WhatsApp engine...");
|
|
1121
|
-
const startRes = await fetchWithTimeout(
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1251
|
+
const startRes = await fetchWithTimeout(
|
|
1252
|
+
`${baseUrl}/api/sessions/${sessionId}/start`,
|
|
1253
|
+
{
|
|
1254
|
+
method: "POST",
|
|
1255
|
+
headers: { "X-API-Key": openwaApiKey }
|
|
1256
|
+
},
|
|
1257
|
+
1e4
|
|
1258
|
+
);
|
|
1125
1259
|
if (!startRes.ok && startRes.status !== 400) {
|
|
1126
|
-
throw new Error(
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
`Failed to start session: ${startRes.status} ${startRes.statusText}`
|
|
1262
|
+
);
|
|
1127
1263
|
}
|
|
1128
|
-
|
|
1264
|
+
const qrSpinner = createSpinner("Waiting for QR code (Chromium is starting)...");
|
|
1129
1265
|
let qrCode;
|
|
1130
|
-
|
|
1266
|
+
const qrStart = Date.now();
|
|
1267
|
+
for (let attempt = 0; attempt < 150; attempt++) {
|
|
1268
|
+
const elapsed = Math.round((Date.now() - qrStart) / 1e3);
|
|
1269
|
+
qrSpinner.update(`Waiting for QR code... (${elapsed}s / 5 min)`);
|
|
1131
1270
|
try {
|
|
1132
|
-
const qrRes = await fetchWithTimeout(
|
|
1133
|
-
|
|
1134
|
-
|
|
1271
|
+
const qrRes = await fetchWithTimeout(
|
|
1272
|
+
`${baseUrl}/api/sessions/${sessionId}/qr`,
|
|
1273
|
+
{
|
|
1274
|
+
headers: { "X-API-Key": openwaApiKey }
|
|
1275
|
+
}
|
|
1276
|
+
);
|
|
1135
1277
|
if (qrRes.ok) {
|
|
1136
1278
|
const qrData = QrResponseSchema.parse(await qrRes.json());
|
|
1137
1279
|
if (qrData.qrCode) {
|
|
@@ -1143,23 +1285,36 @@ var registerSetup = (program2) => {
|
|
|
1143
1285
|
}
|
|
1144
1286
|
await wait(2e3);
|
|
1145
1287
|
}
|
|
1146
|
-
|
|
1288
|
+
qrSpinner.stop();
|
|
1289
|
+
console.warn(
|
|
1290
|
+
"\n\u{1F4F1} Scan the QR code below with WhatsApp to link your device:"
|
|
1291
|
+
);
|
|
1147
1292
|
console.warn(" Settings \u2192 Linked Devices \u2192 Link a Device\n");
|
|
1148
1293
|
if (qrCode) {
|
|
1149
|
-
await
|
|
1150
|
-
console.warn(
|
|
1294
|
+
await renderQrInTerminal(qrCode);
|
|
1295
|
+
console.warn(
|
|
1296
|
+
"\n (QR expires in ~20s \u2014 re-run setup if it expires before you scan)"
|
|
1297
|
+
);
|
|
1151
1298
|
} else {
|
|
1152
|
-
console.warn(
|
|
1153
|
-
|
|
1299
|
+
console.warn(
|
|
1300
|
+
" QR code was not ready in time. Re-run `waify setup` to try again."
|
|
1301
|
+
);
|
|
1154
1302
|
}
|
|
1155
|
-
|
|
1303
|
+
const connectSpinner = createSpinner(
|
|
1304
|
+
"Waiting for you to scan the QR code..."
|
|
1305
|
+
);
|
|
1156
1306
|
let connected = false;
|
|
1157
1307
|
for (let attempt = 0; attempt < 60; attempt++) {
|
|
1158
1308
|
try {
|
|
1159
|
-
const statusRes = await fetchWithTimeout(
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1309
|
+
const statusRes = await fetchWithTimeout(
|
|
1310
|
+
`${baseUrl}/api/sessions/${sessionId}`,
|
|
1311
|
+
{
|
|
1312
|
+
headers: { "X-API-Key": openwaApiKey }
|
|
1313
|
+
}
|
|
1314
|
+
);
|
|
1315
|
+
const parsed = StatusResponseSchema.safeParse(
|
|
1316
|
+
await statusRes.json()
|
|
1317
|
+
);
|
|
1163
1318
|
if (!parsed.success) continue;
|
|
1164
1319
|
if (parsed.data.status === "ready") {
|
|
1165
1320
|
connected = true;
|
|
@@ -1170,11 +1325,13 @@ var registerSetup = (program2) => {
|
|
|
1170
1325
|
await wait(2e3);
|
|
1171
1326
|
}
|
|
1172
1327
|
if (!connected) {
|
|
1173
|
-
|
|
1328
|
+
connectSpinner.fail(
|
|
1329
|
+
"WhatsApp did not connect within 2 minutes. Please re-run `waify setup` to try again."
|
|
1330
|
+
);
|
|
1174
1331
|
process.exitCode = 1;
|
|
1175
1332
|
return;
|
|
1176
1333
|
}
|
|
1177
|
-
|
|
1334
|
+
connectSpinner.succeed("WhatsApp connected!");
|
|
1178
1335
|
saveConfig({ ...loadConfig(), openwaSessionId: sessionId });
|
|
1179
1336
|
if (!existsSync6(promptPath())) {
|
|
1180
1337
|
savePrompt(defaultPrompt);
|
|
@@ -1182,7 +1339,9 @@ var registerSetup = (program2) => {
|
|
|
1182
1339
|
if (!existsSync6(scheduleJsonPath())) {
|
|
1183
1340
|
saveSchedule(defaultSchedule);
|
|
1184
1341
|
}
|
|
1185
|
-
console.warn(
|
|
1342
|
+
console.warn(
|
|
1343
|
+
"\n\u2713 All done! Run `waify send` to send your first message."
|
|
1344
|
+
);
|
|
1186
1345
|
} catch (err) {
|
|
1187
1346
|
console.error(err instanceof Error ? err.message : String(err));
|
|
1188
1347
|
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.0",
|
|
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,6 +51,7 @@
|
|
|
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",
|
|
52
55
|
"@types/qrcode-terminal": "^0.12.2",
|
|
53
56
|
"@types/react": "^19.2.15",
|
|
54
57
|
"semantic-release": "^25.0.3",
|