@leg3ndy/otto-bridge 0.7.4 → 0.7.6
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.
|
@@ -1190,10 +1190,14 @@ export class NativeMacOSJobExecutor {
|
|
|
1190
1190
|
if (!selected) {
|
|
1191
1191
|
throw new Error(`Nao consegui localizar a conversa do WhatsApp com ${action.contact}.`);
|
|
1192
1192
|
}
|
|
1193
|
+
const beforeSend = await this.readWhatsAppVisibleConversation(action.contact, 8).catch(() => ({
|
|
1194
|
+
messages: [],
|
|
1195
|
+
summary: "",
|
|
1196
|
+
}));
|
|
1193
1197
|
await reporter.progress(progressPercent, `Enviando a mensagem para ${action.contact} no WhatsApp`);
|
|
1194
1198
|
await this.sendWhatsAppMessage(action.text);
|
|
1195
1199
|
await delay(900);
|
|
1196
|
-
const verification = await this.
|
|
1200
|
+
const verification = await this.verifyWhatsAppLastMessageAgainstBaseline(action.text, beforeSend.messages);
|
|
1197
1201
|
if (!verification.ok) {
|
|
1198
1202
|
throw new Error(verification.reason || `Nao consegui confirmar o envio da mensagem para ${action.contact} no WhatsApp.`);
|
|
1199
1203
|
}
|
|
@@ -2362,6 +2366,9 @@ function isVisible(element) {
|
|
|
2362
2366
|
|
|
2363
2367
|
function focusAndReplaceContent(element, value) {
|
|
2364
2368
|
element.focus();
|
|
2369
|
+
if (typeof element.click === "function") {
|
|
2370
|
+
element.click();
|
|
2371
|
+
}
|
|
2365
2372
|
const range = document.createRange();
|
|
2366
2373
|
range.selectNodeContents(element);
|
|
2367
2374
|
const selection = window.getSelection();
|
|
@@ -2369,7 +2376,14 @@ function focusAndReplaceContent(element, value) {
|
|
|
2369
2376
|
selection?.addRange(range);
|
|
2370
2377
|
document.execCommand("selectAll", false);
|
|
2371
2378
|
document.execCommand("delete", false);
|
|
2379
|
+
if ((element.innerText || element.textContent || "").trim().length > 0) {
|
|
2380
|
+
element.textContent = "";
|
|
2381
|
+
element.dispatchEvent(new InputEvent("input", { bubbles: true, data: "", inputType: "deleteContentBackward" }));
|
|
2382
|
+
}
|
|
2372
2383
|
document.execCommand("insertText", false, value);
|
|
2384
|
+
if ((element.innerText || "").trim() !== value.trim()) {
|
|
2385
|
+
element.textContent = value;
|
|
2386
|
+
}
|
|
2373
2387
|
element.dispatchEvent(new InputEvent("input", { bubbles: true, data: value, inputType: "insertText" }));
|
|
2374
2388
|
}
|
|
2375
2389
|
|
|
@@ -2414,18 +2428,36 @@ function isVisible(element) {
|
|
|
2414
2428
|
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
2415
2429
|
}
|
|
2416
2430
|
|
|
2431
|
+
function pickClickTarget(node) {
|
|
2432
|
+
const selectors = [
|
|
2433
|
+
'[role="gridcell"]',
|
|
2434
|
+
'[role="listitem"]',
|
|
2435
|
+
'[data-testid="cell-frame-container"]',
|
|
2436
|
+
'div[tabindex]',
|
|
2437
|
+
];
|
|
2438
|
+
for (const selector of selectors) {
|
|
2439
|
+
const candidate = node.closest(selector);
|
|
2440
|
+
if (candidate instanceof HTMLElement && isVisible(candidate)) {
|
|
2441
|
+
return candidate;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
return node instanceof HTMLElement && isVisible(node) ? node : null;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2417
2447
|
const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
|
|
2418
2448
|
.filter((node) => node instanceof HTMLElement)
|
|
2419
2449
|
.filter((node) => isVisible(node))
|
|
2420
2450
|
.map((node) => {
|
|
2421
2451
|
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
2452
|
+
const target = pickClickTarget(node);
|
|
2422
2453
|
let score = 0;
|
|
2423
2454
|
if (text === normalizedQuery) score += 160;
|
|
2424
2455
|
if (text.includes(normalizedQuery)) score += 100;
|
|
2425
2456
|
if (normalizedQuery.includes(text) && text.length >= 3) score += 50;
|
|
2426
|
-
|
|
2427
|
-
if (
|
|
2428
|
-
|
|
2457
|
+
if (target instanceof HTMLElement && target !== node) score += 20;
|
|
2458
|
+
if (target instanceof HTMLElement && target.getAttribute("role") === "gridcell") score += 30;
|
|
2459
|
+
if (target instanceof HTMLElement && target.getAttribute("role") === "listitem") score += 20;
|
|
2460
|
+
return { node, target, text, score };
|
|
2429
2461
|
})
|
|
2430
2462
|
.filter((item) => item.score > 0)
|
|
2431
2463
|
.sort((left, right) => right.score - left.score);
|
|
@@ -2435,12 +2467,10 @@ if (!titleNodes.length) {
|
|
|
2435
2467
|
}
|
|
2436
2468
|
|
|
2437
2469
|
const winner = titleNodes[0];
|
|
2438
|
-
const target = winner.
|
|
2470
|
+
const target = winner.target instanceof HTMLElement ? winner.target : winner.node;
|
|
2439
2471
|
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
return { clicked: true };
|
|
2443
|
-
}
|
|
2472
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
|
|
2473
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
|
|
2444
2474
|
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
2445
2475
|
return { clicked: true };
|
|
2446
2476
|
`, { contact }, this.getWhatsAppWebScriptOptions(false));
|
|
@@ -2619,26 +2649,51 @@ return { messages: messages.slice(-maxMessages) };
|
|
|
2619
2649
|
};
|
|
2620
2650
|
}
|
|
2621
2651
|
async verifyWhatsAppLastMessage(expectedText) {
|
|
2652
|
+
return this.verifyWhatsAppLastMessageAgainstBaseline(expectedText);
|
|
2653
|
+
}
|
|
2654
|
+
async verifyWhatsAppLastMessageAgainstBaseline(expectedText, previousMessages) {
|
|
2622
2655
|
const backgroundBrowser = await this.getWhatsAppBackgroundBrowser().catch(() => null);
|
|
2623
2656
|
if (backgroundBrowser) {
|
|
2624
|
-
return backgroundBrowser.verifyLastMessage(expectedText);
|
|
2657
|
+
return backgroundBrowser.verifyLastMessage(expectedText, previousMessages);
|
|
2625
2658
|
}
|
|
2626
|
-
const
|
|
2659
|
+
const baseline = Array.isArray(previousMessages) ? previousMessages : [];
|
|
2660
|
+
const chat = await this.readWhatsAppVisibleConversation("Contato", Math.max(8, baseline.length + 2));
|
|
2627
2661
|
if (!chat.messages.length) {
|
|
2628
2662
|
return {
|
|
2629
2663
|
ok: false,
|
|
2630
2664
|
reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
|
|
2631
2665
|
};
|
|
2632
2666
|
}
|
|
2633
|
-
const normalizedExpected = normalizeText(expectedText).slice(0,
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2667
|
+
const normalizedExpected = normalizeText(expectedText).slice(0, 120);
|
|
2668
|
+
const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
|
|
2669
|
+
const beforeSignature = baseline.map(normalizeMessage).join("\n");
|
|
2670
|
+
const afterSignature = chat.messages.map(normalizeMessage).join("\n");
|
|
2671
|
+
const changed = beforeSignature !== afterSignature;
|
|
2672
|
+
const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
2673
|
+
const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
2674
|
+
const latest = chat.messages[chat.messages.length - 1] || null;
|
|
2675
|
+
const latestAuthor = normalizeText(latest?.author || "");
|
|
2676
|
+
const latestText = normalizeText(latest?.text || "");
|
|
2677
|
+
const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
|
|
2678
|
+
if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
|
|
2679
|
+
return { ok: true, reason: "" };
|
|
2680
|
+
}
|
|
2681
|
+
if (!changed) {
|
|
2636
2682
|
return {
|
|
2637
2683
|
ok: false,
|
|
2638
|
-
reason: "
|
|
2684
|
+
reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
|
|
2639
2685
|
};
|
|
2640
2686
|
}
|
|
2641
|
-
|
|
2687
|
+
if (afterMatches <= beforeMatches) {
|
|
2688
|
+
return {
|
|
2689
|
+
ok: false,
|
|
2690
|
+
reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
return {
|
|
2694
|
+
ok: false,
|
|
2695
|
+
reason: "Nao consegui confirmar visualmente a nova mensagem enviada no WhatsApp.",
|
|
2696
|
+
};
|
|
2642
2697
|
}
|
|
2643
2698
|
async takeScreenshot(targetPath) {
|
|
2644
2699
|
const artifactsDir = path.join(os.homedir(), ".otto-bridge", "artifacts");
|
|
@@ -125,6 +125,21 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
125
125
|
await this.start();
|
|
126
126
|
await this.call("hide_background");
|
|
127
127
|
}
|
|
128
|
+
async nativeInsertText(text) {
|
|
129
|
+
await this.start();
|
|
130
|
+
const result = await this.call("native_insert_text", { text });
|
|
131
|
+
return result?.ok === true;
|
|
132
|
+
}
|
|
133
|
+
async nativeClearText() {
|
|
134
|
+
await this.start();
|
|
135
|
+
const result = await this.call("native_clear_text");
|
|
136
|
+
return result?.ok === true;
|
|
137
|
+
}
|
|
138
|
+
async nativePressEnter() {
|
|
139
|
+
await this.start();
|
|
140
|
+
const result = await this.call("native_press_enter");
|
|
141
|
+
return result?.ok === true;
|
|
142
|
+
}
|
|
128
143
|
async evaluate(script) {
|
|
129
144
|
await this.start();
|
|
130
145
|
return await this.call("evaluate_js", { script });
|
|
@@ -260,39 +275,46 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
260
275
|
|
|
261
276
|
const searchBox = candidates[0].node;
|
|
262
277
|
searchBox.focus();
|
|
278
|
+
if (typeof searchBox.click === "function") {
|
|
279
|
+
searchBox.click();
|
|
280
|
+
}
|
|
263
281
|
|
|
264
282
|
if (searchBox instanceof HTMLInputElement || searchBox instanceof HTMLTextAreaElement) {
|
|
265
283
|
searchBox.value = "";
|
|
266
284
|
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
267
285
|
searchBox.value = contact;
|
|
268
286
|
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
287
|
+
return { ok: true, nativeInputRequired: false };
|
|
269
288
|
} else {
|
|
270
|
-
|
|
271
|
-
const range = document.createRange();
|
|
272
|
-
range.selectNodeContents(searchBox);
|
|
273
|
-
selection?.removeAllRanges();
|
|
274
|
-
selection?.addRange(range);
|
|
275
|
-
document.execCommand("selectAll", false);
|
|
276
|
-
document.execCommand("delete", false);
|
|
277
|
-
document.execCommand("insertText", false, contact);
|
|
278
|
-
if ((searchBox.innerText || "").trim() !== contact.trim()) {
|
|
279
|
-
searchBox.textContent = contact;
|
|
280
|
-
}
|
|
289
|
+
searchBox.textContent = "";
|
|
281
290
|
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
291
|
+
return { ok: true, nativeInputRequired: true };
|
|
282
292
|
}
|
|
283
|
-
|
|
284
|
-
return { ok: true };
|
|
285
293
|
})()
|
|
286
294
|
`);
|
|
287
295
|
if (!(prepared.ok === true)) {
|
|
288
296
|
return false;
|
|
289
297
|
}
|
|
298
|
+
if (prepared.nativeInputRequired === true) {
|
|
299
|
+
await this.nativeClearText().catch(() => false);
|
|
300
|
+
const inserted = await this.nativeInsertText(contact).catch(() => false);
|
|
301
|
+
if (!inserted) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
await delay(1000);
|
|
306
|
+
let enterTriggered = false;
|
|
307
|
+
let clickAttempts = 0;
|
|
308
|
+
const searchStartedAt = Date.now();
|
|
290
309
|
const deadline = Date.now() + 6_000;
|
|
291
310
|
while (Date.now() < deadline) {
|
|
292
311
|
await delay(500);
|
|
293
312
|
const result = await this.evaluate(`
|
|
294
313
|
(() => {
|
|
295
314
|
const query = ${JSON.stringify(contact)};
|
|
315
|
+
const enterTriggered = ${JSON.stringify(enterTriggered)};
|
|
316
|
+
const clickAttempts = ${JSON.stringify(clickAttempts)};
|
|
317
|
+
const allowEnterFallback = ${JSON.stringify(Date.now() - searchStartedAt >= 2_500)};
|
|
296
318
|
const normalize = (value) => String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
|
|
297
319
|
const normalizedQuery = normalize(query);
|
|
298
320
|
|
|
@@ -305,37 +327,108 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
305
327
|
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
306
328
|
}
|
|
307
329
|
|
|
330
|
+
function pickClickTarget(node) {
|
|
331
|
+
const selectors = [
|
|
332
|
+
'[role="gridcell"]',
|
|
333
|
+
'[role="listitem"]',
|
|
334
|
+
'[data-testid="cell-frame-container"]',
|
|
335
|
+
'div[tabindex]',
|
|
336
|
+
];
|
|
337
|
+
for (const selector of selectors) {
|
|
338
|
+
const candidate = node.closest(selector);
|
|
339
|
+
if (candidate instanceof HTMLElement && isVisible(candidate)) {
|
|
340
|
+
return candidate;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return node instanceof HTMLElement && isVisible(node) ? node : null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isConversationOpen() {
|
|
347
|
+
const footerVisible = Array.from(document.querySelectorAll('footer, main footer'))
|
|
348
|
+
.some((node) => isVisible(node));
|
|
349
|
+
const composerVisible = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea'))
|
|
350
|
+
.some((node) => isVisible(node));
|
|
351
|
+
const headerMatch = Array.from(document.querySelectorAll('main header span[title], main header div[title], [data-testid="conversation-info-header-chat-title"], header span[title], header div[title], main span[title], main div[title]'))
|
|
352
|
+
.filter((node) => node instanceof HTMLElement)
|
|
353
|
+
.filter((node) => isVisible(node))
|
|
354
|
+
.some((node) => {
|
|
355
|
+
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
356
|
+
return text === normalizedQuery
|
|
357
|
+
|| text.includes(normalizedQuery)
|
|
358
|
+
|| (normalizedQuery.includes(text) && text.length >= 3);
|
|
359
|
+
});
|
|
360
|
+
const composerTargetMatch = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"] [contenteditable="true"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea'))
|
|
361
|
+
.filter((node) => node instanceof HTMLElement)
|
|
362
|
+
.filter((node) => isVisible(node))
|
|
363
|
+
.some((node) => {
|
|
364
|
+
const text = normalize(
|
|
365
|
+
node.getAttribute("aria-label")
|
|
366
|
+
|| node.getAttribute("aria-placeholder")
|
|
367
|
+
|| node.getAttribute("placeholder")
|
|
368
|
+
|| ""
|
|
369
|
+
);
|
|
370
|
+
return text.includes(normalizedQuery);
|
|
371
|
+
});
|
|
372
|
+
return (headerMatch || composerTargetMatch) && (footerVisible || composerVisible);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (isConversationOpen()) {
|
|
376
|
+
return { clicked: true, activatedBy: "already-open" };
|
|
377
|
+
}
|
|
378
|
+
|
|
308
379
|
const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
|
|
309
380
|
.filter((node) => node instanceof HTMLElement)
|
|
310
381
|
.filter((node) => isVisible(node))
|
|
311
382
|
.map((node) => {
|
|
312
383
|
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
384
|
+
const target = pickClickTarget(node);
|
|
313
385
|
let score = 0;
|
|
314
386
|
if (text === normalizedQuery) score += 160;
|
|
315
387
|
if (text.includes(normalizedQuery)) score += 100;
|
|
316
388
|
if (normalizedQuery.includes(text) && text.length >= 3) score += 50;
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
|
|
389
|
+
if (target instanceof HTMLElement && target !== node) score += 20;
|
|
390
|
+
if (target instanceof HTMLElement && target.getAttribute("role") === "gridcell") score += 30;
|
|
391
|
+
if (target instanceof HTMLElement && target.getAttribute("role") === "listitem") score += 20;
|
|
392
|
+
return { node, target, score };
|
|
320
393
|
})
|
|
321
394
|
.filter((item) => item.score > 0)
|
|
322
395
|
.sort((left, right) => right.score - left.score);
|
|
323
396
|
|
|
324
|
-
if (
|
|
325
|
-
|
|
397
|
+
if (titleNodes.length) {
|
|
398
|
+
if (clickAttempts >= 4) {
|
|
399
|
+
return { clicked: false };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const winner = titleNodes[0];
|
|
403
|
+
const target = winner.target instanceof HTMLElement ? winner.target : winner.node;
|
|
404
|
+
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
405
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
|
|
406
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
|
|
407
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
408
|
+
return { clicked: false, clickAttempts: clickAttempts + 1 };
|
|
326
409
|
}
|
|
327
410
|
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
411
|
+
const searchCandidates = Array.from(document.querySelectorAll('div[contenteditable="true"][role="textbox"], input[role="textbox"], textarea, div[contenteditable="true"][data-tab], [data-testid="chat-list-search"] [contenteditable="true"]'))
|
|
412
|
+
.filter((node) => node instanceof HTMLElement)
|
|
413
|
+
.filter((node) => isVisible(node));
|
|
414
|
+
const searchBox = searchCandidates[0];
|
|
415
|
+
if (allowEnterFallback && !enterTriggered && searchBox instanceof HTMLElement) {
|
|
416
|
+
searchBox.focus();
|
|
417
|
+
searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
418
|
+
searchBox.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
419
|
+
searchBox.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
420
|
+
return { clicked: false, enterTriggered: true };
|
|
334
421
|
}
|
|
335
|
-
|
|
336
|
-
return { clicked:
|
|
422
|
+
|
|
423
|
+
return { clicked: false };
|
|
337
424
|
})()
|
|
338
425
|
`);
|
|
426
|
+
if (result.enterTriggered === true) {
|
|
427
|
+
enterTriggered = true;
|
|
428
|
+
}
|
|
429
|
+
if (typeof result.clickAttempts === "number") {
|
|
430
|
+
clickAttempts = result.clickAttempts;
|
|
431
|
+
}
|
|
339
432
|
if (result.clicked === true) {
|
|
340
433
|
return true;
|
|
341
434
|
}
|
|
@@ -398,50 +491,91 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
398
491
|
}
|
|
399
492
|
|
|
400
493
|
const composer = candidates[0].node;
|
|
401
|
-
composer.focus();
|
|
402
494
|
if (composer instanceof HTMLInputElement || composer instanceof HTMLTextAreaElement) {
|
|
495
|
+
composer.focus();
|
|
496
|
+
composer.click();
|
|
403
497
|
composer.value = "";
|
|
404
498
|
composer.dispatchEvent(new Event("input", { bubbles: true }));
|
|
405
499
|
composer.value = value;
|
|
406
500
|
composer.dispatchEvent(new Event("input", { bubbles: true }));
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
501
|
+
composer.click();
|
|
502
|
+
|
|
503
|
+
const sendCandidates = Array.from(document.querySelectorAll('[data-testid="compose-btn-send"], button[aria-label*="Send"], button[aria-label*="Enviar"], span[data-icon="send"], div[role="button"][aria-label*="Send"], div[role="button"][aria-label*="Enviar"]'))
|
|
504
|
+
.map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
|
|
505
|
+
.filter((node) => node instanceof HTMLElement)
|
|
506
|
+
.filter((node) => isVisible(node));
|
|
507
|
+
|
|
508
|
+
const sendButton = sendCandidates[0];
|
|
509
|
+
if (sendButton instanceof HTMLElement) {
|
|
510
|
+
sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
511
|
+
if (typeof sendButton.click === "function") {
|
|
512
|
+
sendButton.click();
|
|
513
|
+
return { sent: true };
|
|
514
|
+
}
|
|
515
|
+
sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
516
|
+
return { sent: true };
|
|
418
517
|
}
|
|
419
|
-
|
|
518
|
+
|
|
519
|
+
composer.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
520
|
+
composer.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
521
|
+
composer.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
522
|
+
return { sent: true };
|
|
523
|
+
} else {
|
|
524
|
+
composer.focus();
|
|
525
|
+
composer.click();
|
|
526
|
+
return { nativeInputRequired: true };
|
|
527
|
+
}
|
|
528
|
+
})()
|
|
529
|
+
`);
|
|
530
|
+
if (result.nativeInputRequired === true) {
|
|
531
|
+
const cleared = await this.nativeClearText();
|
|
532
|
+
if (!cleared) {
|
|
533
|
+
throw new Error("O helper nativo do WhatsApp nao conseguiu limpar o rascunho da mensagem.");
|
|
534
|
+
}
|
|
535
|
+
const inserted = await this.nativeInsertText(text);
|
|
536
|
+
if (!inserted) {
|
|
537
|
+
throw new Error("O helper nativo do WhatsApp nao conseguiu digitar a mensagem.");
|
|
538
|
+
}
|
|
539
|
+
await delay(250);
|
|
540
|
+
const clickResult = await this.evaluate(`
|
|
541
|
+
(() => {
|
|
542
|
+
function isVisible(element) {
|
|
543
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
544
|
+
const rect = element.getBoundingClientRect();
|
|
545
|
+
if (rect.width < 6 || rect.height < 6) return false;
|
|
546
|
+
const style = window.getComputedStyle(element);
|
|
547
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
|
|
548
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
420
549
|
}
|
|
421
|
-
composer.click();
|
|
422
550
|
|
|
423
|
-
const sendCandidates = Array.from(document.querySelectorAll('[data-testid="compose-btn-send"], button[aria-label*="Send"], button[aria-label*="Enviar"], span[data-icon="send"], div[role="button"][aria-label*="Send"], div[role="button"][aria-label*="Enviar"]'))
|
|
551
|
+
const sendCandidates = Array.from(document.querySelectorAll('[data-testid="compose-btn-send"], button[aria-label*="Send"], button[aria-label*="Enviar"], span[data-icon="send"], div[role="button"][aria-label*="Send"], div[role="button"][aria-label*="Enviar"], footer [data-icon="send-filled"], footer [data-icon="wds-ic-send-filled"]'))
|
|
424
552
|
.map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
|
|
425
553
|
.filter((node) => node instanceof HTMLElement)
|
|
426
554
|
.filter((node) => isVisible(node));
|
|
427
555
|
|
|
428
556
|
const sendButton = sendCandidates[0];
|
|
429
|
-
if (sendButton instanceof HTMLElement) {
|
|
430
|
-
|
|
431
|
-
if (typeof sendButton.click === "function") {
|
|
432
|
-
sendButton.click();
|
|
433
|
-
return { sent: true };
|
|
434
|
-
}
|
|
435
|
-
sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
436
|
-
return { sent: true };
|
|
557
|
+
if (!(sendButton instanceof HTMLElement)) {
|
|
558
|
+
return { clicked: false };
|
|
437
559
|
}
|
|
438
560
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
561
|
+
sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
562
|
+
if (typeof sendButton.click === "function") {
|
|
563
|
+
sendButton.click();
|
|
564
|
+
return { clicked: true };
|
|
565
|
+
}
|
|
566
|
+
sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
567
|
+
return { clicked: true };
|
|
443
568
|
})()
|
|
444
|
-
|
|
569
|
+
`);
|
|
570
|
+
if (clickResult.clicked === true) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const submitted = await this.nativePressEnter();
|
|
574
|
+
if (!submitted) {
|
|
575
|
+
throw new Error("O helper nativo do WhatsApp nao conseguiu enviar a mensagem.");
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
445
579
|
if (result.sent === true) {
|
|
446
580
|
return;
|
|
447
581
|
}
|
|
@@ -495,23 +629,45 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
495
629
|
: "(sem mensagens visiveis na conversa)",
|
|
496
630
|
};
|
|
497
631
|
}
|
|
498
|
-
async verifyLastMessage(expectedText) {
|
|
499
|
-
const
|
|
632
|
+
async verifyLastMessage(expectedText, previousMessages) {
|
|
633
|
+
const baseline = Array.isArray(previousMessages) ? previousMessages : [];
|
|
634
|
+
const chat = await this.readVisibleConversation(Math.max(8, baseline.length + 2));
|
|
500
635
|
if (!chat.messages.length) {
|
|
501
636
|
return {
|
|
502
637
|
ok: false,
|
|
503
638
|
reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
|
|
504
639
|
};
|
|
505
640
|
}
|
|
506
|
-
const normalizedExpected = normalizeText(expectedText).slice(0,
|
|
507
|
-
const
|
|
508
|
-
|
|
641
|
+
const normalizedExpected = normalizeText(expectedText).slice(0, 120);
|
|
642
|
+
const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
|
|
643
|
+
const beforeSignature = baseline.map(normalizeMessage).join("\n");
|
|
644
|
+
const afterSignature = chat.messages.map(normalizeMessage).join("\n");
|
|
645
|
+
const changed = beforeSignature !== afterSignature;
|
|
646
|
+
const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
647
|
+
const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
648
|
+
const latest = chat.messages[chat.messages.length - 1] || null;
|
|
649
|
+
const latestAuthor = normalizeText(latest?.author || "");
|
|
650
|
+
const latestText = normalizeText(latest?.text || "");
|
|
651
|
+
const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
|
|
652
|
+
if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
|
|
653
|
+
return { ok: true, reason: "" };
|
|
654
|
+
}
|
|
655
|
+
if (!changed) {
|
|
656
|
+
return {
|
|
657
|
+
ok: false,
|
|
658
|
+
reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (afterMatches <= beforeMatches) {
|
|
509
662
|
return {
|
|
510
663
|
ok: false,
|
|
511
|
-
reason: "
|
|
664
|
+
reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
|
|
512
665
|
};
|
|
513
666
|
}
|
|
514
|
-
return {
|
|
667
|
+
return {
|
|
668
|
+
ok: false,
|
|
669
|
+
reason: "Nao consegui confirmar na conversa do WhatsApp se a nova mensagem foi enviada.",
|
|
670
|
+
};
|
|
515
671
|
}
|
|
516
672
|
handleStdout(chunk) {
|
|
517
673
|
this.stdoutBuffer += chunk;
|
|
@@ -90,6 +90,13 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
|
|
|
90
90
|
case "hide_background":
|
|
91
91
|
hideBackground()
|
|
92
92
|
sendResponse(id: id, result: ["background": true])
|
|
93
|
+
case "native_insert_text":
|
|
94
|
+
let text = String(describing: params["text"] ?? "")
|
|
95
|
+
sendResponse(id: id, result: ["ok": nativeInsertText(text)])
|
|
96
|
+
case "native_clear_text":
|
|
97
|
+
sendResponse(id: id, result: ["ok": nativeClearText()])
|
|
98
|
+
case "native_press_enter":
|
|
99
|
+
sendResponse(id: id, result: ["ok": nativePressEnter()])
|
|
93
100
|
case "load_whatsapp":
|
|
94
101
|
ensureWhatsAppLoaded()
|
|
95
102
|
sendResponse(id: id, result: ["url": webView.url?.absoluteString ?? ""])
|
|
@@ -134,6 +141,14 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
|
|
|
134
141
|
app.activate(ignoringOtherApps: true)
|
|
135
142
|
}
|
|
136
143
|
|
|
144
|
+
private func prepareInteractionWindow() {
|
|
145
|
+
window.ignoresMouseEvents = true
|
|
146
|
+
window.alphaValue = 0
|
|
147
|
+
window.setFrame(NSRect(x: -2200, y: 80, width: 1320, height: 920), display: false)
|
|
148
|
+
window.makeKeyAndOrderFront(nil)
|
|
149
|
+
app.activate(ignoringOtherApps: true)
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
private func hideBackground() {
|
|
138
153
|
window.ignoresMouseEvents = true
|
|
139
154
|
window.alphaValue = 0
|
|
@@ -141,6 +156,44 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
|
|
|
141
156
|
window.orderFrontRegardless()
|
|
142
157
|
}
|
|
143
158
|
|
|
159
|
+
private func currentTextInputClient() -> NSTextInputClient? {
|
|
160
|
+
if let client = window.firstResponder as? NSTextInputClient {
|
|
161
|
+
return client
|
|
162
|
+
}
|
|
163
|
+
if let client = webView as? NSTextInputClient {
|
|
164
|
+
return client
|
|
165
|
+
}
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private func nativeInsertText(_ text: String) -> Bool {
|
|
170
|
+
prepareInteractionWindow()
|
|
171
|
+
guard let client = currentTextInputClient() else {
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
client.insertText(text, replacementRange: NSRange(location: NSNotFound, length: 0))
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private func nativeClearText() -> Bool {
|
|
179
|
+
prepareInteractionWindow()
|
|
180
|
+
guard let responder = window.firstResponder else {
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
responder.selectAll(nil)
|
|
184
|
+
responder.doCommand(by: #selector(NSResponder.deleteBackward(_:)))
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private func nativePressEnter() -> Bool {
|
|
189
|
+
prepareInteractionWindow()
|
|
190
|
+
guard let responder = window.firstResponder else {
|
|
191
|
+
return false
|
|
192
|
+
}
|
|
193
|
+
responder.doCommand(by: #selector(NSResponder.insertNewline(_:)))
|
|
194
|
+
return true
|
|
195
|
+
}
|
|
196
|
+
|
|
144
197
|
private func evaluateJavaScript(_ script: String, completion: @escaping (Any?, Error?) -> Void) {
|
|
145
198
|
webView.evaluateJavaScript(script) { result, error in
|
|
146
199
|
completion(result, error)
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const BRIDGE_CONFIG_VERSION = 1;
|
|
2
|
-
export const BRIDGE_VERSION = "0.7.
|
|
2
|
+
export const BRIDGE_VERSION = "0.7.6";
|
|
3
3
|
export const BRIDGE_PACKAGE_NAME = "@leg3ndy/otto-bridge";
|
|
4
4
|
export const DEFAULT_API_BASE_URL = "http://localhost:8000";
|
|
5
5
|
export const DEFAULT_POLL_INTERVAL_MS = 3000;
|
|
@@ -693,26 +693,48 @@ export class WhatsAppBackgroundBrowser {
|
|
|
693
693
|
: "(sem mensagens visiveis na conversa)",
|
|
694
694
|
};
|
|
695
695
|
}
|
|
696
|
-
async verifyLastMessage(expectedText) {
|
|
696
|
+
async verifyLastMessage(expectedText, previousMessages) {
|
|
697
697
|
if (this.helperRuntime) {
|
|
698
|
-
return await this.helperRuntime.verifyLastMessage(expectedText);
|
|
698
|
+
return await this.helperRuntime.verifyLastMessage(expectedText, previousMessages);
|
|
699
699
|
}
|
|
700
|
-
const
|
|
700
|
+
const baseline = Array.isArray(previousMessages) ? previousMessages : [];
|
|
701
|
+
const chat = await this.readVisibleConversation(Math.max(8, baseline.length + 2));
|
|
701
702
|
if (!chat.messages.length) {
|
|
702
703
|
return {
|
|
703
704
|
ok: false,
|
|
704
705
|
reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
|
|
705
706
|
};
|
|
706
707
|
}
|
|
707
|
-
const normalizedExpected = normalizeText(expectedText).slice(0,
|
|
708
|
-
const
|
|
709
|
-
|
|
708
|
+
const normalizedExpected = normalizeText(expectedText).slice(0, 120);
|
|
709
|
+
const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
|
|
710
|
+
const beforeSignature = baseline.map(normalizeMessage).join("\n");
|
|
711
|
+
const afterSignature = chat.messages.map(normalizeMessage).join("\n");
|
|
712
|
+
const changed = beforeSignature !== afterSignature;
|
|
713
|
+
const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
714
|
+
const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
715
|
+
const latest = chat.messages[chat.messages.length - 1] || null;
|
|
716
|
+
const latestAuthor = normalizeText(latest?.author || "");
|
|
717
|
+
const latestText = normalizeText(latest?.text || "");
|
|
718
|
+
const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
|
|
719
|
+
if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
|
|
720
|
+
return { ok: true, reason: "" };
|
|
721
|
+
}
|
|
722
|
+
if (!changed) {
|
|
710
723
|
return {
|
|
711
724
|
ok: false,
|
|
712
|
-
reason: "
|
|
725
|
+
reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
|
|
713
726
|
};
|
|
714
727
|
}
|
|
715
|
-
|
|
728
|
+
if (afterMatches <= beforeMatches) {
|
|
729
|
+
return {
|
|
730
|
+
ok: false,
|
|
731
|
+
reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
ok: false,
|
|
736
|
+
reason: "Nao consegui confirmar na conversa do WhatsApp se a nova mensagem foi enviada.",
|
|
737
|
+
};
|
|
716
738
|
}
|
|
717
739
|
async ensureWhatsAppPage() {
|
|
718
740
|
const page = this.page;
|