@deckasoft/waify 0.3.10 → 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 +200 -56
- 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,53 +1153,84 @@ 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
|
}
|
|
1120
1235
|
spawnSync(
|
|
1121
1236
|
"docker",
|
|
@@ -1128,25 +1243,37 @@ var registerSetup = (program2) => {
|
|
|
1128
1243
|
"openwa-api",
|
|
1129
1244
|
"sh",
|
|
1130
1245
|
"-c",
|
|
1131
|
-
|
|
1246
|
+
`rm -f /app/data/sessions/session-${SESSION_NAME}/Singleton*`
|
|
1132
1247
|
],
|
|
1133
1248
|
{ encoding: "utf-8" }
|
|
1134
1249
|
);
|
|
1135
1250
|
console.warn("Starting WhatsApp engine...");
|
|
1136
|
-
const startRes = await fetchWithTimeout(
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
+
);
|
|
1140
1259
|
if (!startRes.ok && startRes.status !== 400) {
|
|
1141
|
-
throw new Error(
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
`Failed to start session: ${startRes.status} ${startRes.statusText}`
|
|
1262
|
+
);
|
|
1142
1263
|
}
|
|
1143
|
-
|
|
1264
|
+
const qrSpinner = createSpinner("Waiting for QR code (Chromium is starting)...");
|
|
1144
1265
|
let qrCode;
|
|
1145
|
-
|
|
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)`);
|
|
1146
1270
|
try {
|
|
1147
|
-
const qrRes = await fetchWithTimeout(
|
|
1148
|
-
|
|
1149
|
-
|
|
1271
|
+
const qrRes = await fetchWithTimeout(
|
|
1272
|
+
`${baseUrl}/api/sessions/${sessionId}/qr`,
|
|
1273
|
+
{
|
|
1274
|
+
headers: { "X-API-Key": openwaApiKey }
|
|
1275
|
+
}
|
|
1276
|
+
);
|
|
1150
1277
|
if (qrRes.ok) {
|
|
1151
1278
|
const qrData = QrResponseSchema.parse(await qrRes.json());
|
|
1152
1279
|
if (qrData.qrCode) {
|
|
@@ -1158,23 +1285,36 @@ var registerSetup = (program2) => {
|
|
|
1158
1285
|
}
|
|
1159
1286
|
await wait(2e3);
|
|
1160
1287
|
}
|
|
1161
|
-
|
|
1288
|
+
qrSpinner.stop();
|
|
1289
|
+
console.warn(
|
|
1290
|
+
"\n\u{1F4F1} Scan the QR code below with WhatsApp to link your device:"
|
|
1291
|
+
);
|
|
1162
1292
|
console.warn(" Settings \u2192 Linked Devices \u2192 Link a Device\n");
|
|
1163
1293
|
if (qrCode) {
|
|
1164
|
-
await
|
|
1165
|
-
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
|
+
);
|
|
1166
1298
|
} else {
|
|
1167
|
-
console.warn(
|
|
1168
|
-
|
|
1299
|
+
console.warn(
|
|
1300
|
+
" QR code was not ready in time. Re-run `waify setup` to try again."
|
|
1301
|
+
);
|
|
1169
1302
|
}
|
|
1170
|
-
|
|
1303
|
+
const connectSpinner = createSpinner(
|
|
1304
|
+
"Waiting for you to scan the QR code..."
|
|
1305
|
+
);
|
|
1171
1306
|
let connected = false;
|
|
1172
1307
|
for (let attempt = 0; attempt < 60; attempt++) {
|
|
1173
1308
|
try {
|
|
1174
|
-
const statusRes = await fetchWithTimeout(
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
+
);
|
|
1178
1318
|
if (!parsed.success) continue;
|
|
1179
1319
|
if (parsed.data.status === "ready") {
|
|
1180
1320
|
connected = true;
|
|
@@ -1185,11 +1325,13 @@ var registerSetup = (program2) => {
|
|
|
1185
1325
|
await wait(2e3);
|
|
1186
1326
|
}
|
|
1187
1327
|
if (!connected) {
|
|
1188
|
-
|
|
1328
|
+
connectSpinner.fail(
|
|
1329
|
+
"WhatsApp did not connect within 2 minutes. Please re-run `waify setup` to try again."
|
|
1330
|
+
);
|
|
1189
1331
|
process.exitCode = 1;
|
|
1190
1332
|
return;
|
|
1191
1333
|
}
|
|
1192
|
-
|
|
1334
|
+
connectSpinner.succeed("WhatsApp connected!");
|
|
1193
1335
|
saveConfig({ ...loadConfig(), openwaSessionId: sessionId });
|
|
1194
1336
|
if (!existsSync6(promptPath())) {
|
|
1195
1337
|
savePrompt(defaultPrompt);
|
|
@@ -1197,7 +1339,9 @@ var registerSetup = (program2) => {
|
|
|
1197
1339
|
if (!existsSync6(scheduleJsonPath())) {
|
|
1198
1340
|
saveSchedule(defaultSchedule);
|
|
1199
1341
|
}
|
|
1200
|
-
console.warn(
|
|
1342
|
+
console.warn(
|
|
1343
|
+
"\n\u2713 All done! Run `waify send` to send your first message."
|
|
1344
|
+
);
|
|
1201
1345
|
} catch (err) {
|
|
1202
1346
|
console.error(err instanceof Error ? err.message : String(err));
|
|
1203
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",
|