@leg3ndy/otto-bridge 0.7.3 → 0.7.5
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/README.md +6 -6
- package/dist/executors/native_macos.js +37 -8
- package/dist/macos_whatsapp_helper.js +165 -42
- package/dist/macos_whatsapp_helper_source.js +62 -1
- package/dist/types.js +1 -1
- package/dist/whatsapp_background.js +30 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,12 +33,12 @@ Enquanto o pacote nao estiver publicado, voce pode gerar um tarball local:
|
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
35
|
npm pack
|
|
36
|
-
npm install -g ./leg3ndy-otto-bridge-0.7.
|
|
36
|
+
npm install -g ./leg3ndy-otto-bridge-0.7.4.tgz
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
No `0.7.
|
|
39
|
+
No `0.7.4`, `playwright` segue como dependencia obrigatoria no `otto-bridge`. O primeiro `npm install -g @leg3ndy/otto-bridge` pode demorar mais porque instala o browser persistente usado pelo WhatsApp Web e pelos fluxos web em background do bridge.
|
|
40
40
|
|
|
41
|
-
No macOS, o `0.7.
|
|
41
|
+
No macOS, o `0.7.4` usa o provider `macos-helper`, um helper `WKWebView` sem Dock para o WhatsApp Web. O helper sobe com user-agent de Chrome moderno para evitar o bloqueio do WhatsApp ao detectar Safari/WebKit. O runtime antigo com Chromium/Playwright fica disponivel apenas como override explicito via `OTTO_BRIDGE_WHATSAPP_RUNTIME_PROVIDER=embedded-playwright`.
|
|
42
42
|
|
|
43
43
|
## Publicacao
|
|
44
44
|
|
|
@@ -108,7 +108,7 @@ otto-bridge run --executor clawd-cursor --clawd-url http://127.0.0.1:3847
|
|
|
108
108
|
|
|
109
109
|
### WhatsApp Web em background
|
|
110
110
|
|
|
111
|
-
Fluxo recomendado no `0.7.
|
|
111
|
+
Fluxo recomendado no `0.7.4`:
|
|
112
112
|
|
|
113
113
|
```bash
|
|
114
114
|
otto-bridge extensions --install whatsappweb
|
|
@@ -118,13 +118,13 @@ otto-bridge extensions --status whatsappweb
|
|
|
118
118
|
|
|
119
119
|
O setup agora abre o login do WhatsApp Web no helper/background browser do proprio bridge. Depois do QR code, o Otto usa a sessao local em background, sem depender de aba visivel no Safari.
|
|
120
120
|
|
|
121
|
-
Contrato do `0.7.
|
|
121
|
+
Contrato do `0.7.4`:
|
|
122
122
|
|
|
123
123
|
- `otto-bridge extensions --setup whatsappweb`: autentica a sessao uma vez
|
|
124
124
|
- `otto-bridge run`: mantem o browser persistente do WhatsApp vivo em background enquanto o runtime estiver ativo, sem depender de uma aba aberta no Safari
|
|
125
125
|
- ao parar o `otto-bridge run`: o browser em background e desligado, mas a sessao local fica lembrada para o proximo boot
|
|
126
126
|
|
|
127
|
-
## Handoff rapido do 0.7.
|
|
127
|
+
## Handoff rapido do 0.7.4
|
|
128
128
|
|
|
129
129
|
Ja fechado no codigo:
|
|
130
130
|
|
|
@@ -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
|
}
|
|
@@ -2619,26 +2623,51 @@ return { messages: messages.slice(-maxMessages) };
|
|
|
2619
2623
|
};
|
|
2620
2624
|
}
|
|
2621
2625
|
async verifyWhatsAppLastMessage(expectedText) {
|
|
2626
|
+
return this.verifyWhatsAppLastMessageAgainstBaseline(expectedText);
|
|
2627
|
+
}
|
|
2628
|
+
async verifyWhatsAppLastMessageAgainstBaseline(expectedText, previousMessages) {
|
|
2622
2629
|
const backgroundBrowser = await this.getWhatsAppBackgroundBrowser().catch(() => null);
|
|
2623
2630
|
if (backgroundBrowser) {
|
|
2624
|
-
return backgroundBrowser.verifyLastMessage(expectedText);
|
|
2631
|
+
return backgroundBrowser.verifyLastMessage(expectedText, previousMessages);
|
|
2625
2632
|
}
|
|
2626
|
-
const
|
|
2633
|
+
const baseline = Array.isArray(previousMessages) ? previousMessages : [];
|
|
2634
|
+
const chat = await this.readWhatsAppVisibleConversation("Contato", Math.max(8, baseline.length + 2));
|
|
2627
2635
|
if (!chat.messages.length) {
|
|
2628
2636
|
return {
|
|
2629
2637
|
ok: false,
|
|
2630
2638
|
reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
|
|
2631
2639
|
};
|
|
2632
2640
|
}
|
|
2633
|
-
const normalizedExpected = normalizeText(expectedText).slice(0,
|
|
2634
|
-
const
|
|
2635
|
-
|
|
2641
|
+
const normalizedExpected = normalizeText(expectedText).slice(0, 120);
|
|
2642
|
+
const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
|
|
2643
|
+
const beforeSignature = baseline.map(normalizeMessage).join("\n");
|
|
2644
|
+
const afterSignature = chat.messages.map(normalizeMessage).join("\n");
|
|
2645
|
+
const changed = beforeSignature !== afterSignature;
|
|
2646
|
+
const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
2647
|
+
const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
2648
|
+
const latest = chat.messages[chat.messages.length - 1] || null;
|
|
2649
|
+
const latestAuthor = normalizeText(latest?.author || "");
|
|
2650
|
+
const latestText = normalizeText(latest?.text || "");
|
|
2651
|
+
const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
|
|
2652
|
+
if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
|
|
2653
|
+
return { ok: true, reason: "" };
|
|
2654
|
+
}
|
|
2655
|
+
if (!changed) {
|
|
2656
|
+
return {
|
|
2657
|
+
ok: false,
|
|
2658
|
+
reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
if (afterMatches <= beforeMatches) {
|
|
2636
2662
|
return {
|
|
2637
2663
|
ok: false,
|
|
2638
|
-
reason: "
|
|
2664
|
+
reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
|
|
2639
2665
|
};
|
|
2640
2666
|
}
|
|
2641
|
-
return {
|
|
2667
|
+
return {
|
|
2668
|
+
ok: false,
|
|
2669
|
+
reason: "Nao consegui confirmar visualmente a nova mensagem enviada no WhatsApp.",
|
|
2670
|
+
};
|
|
2642
2671
|
}
|
|
2643
2672
|
async takeScreenshot(targetPath) {
|
|
2644
2673
|
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 });
|
|
@@ -263,9 +278,9 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
263
278
|
|
|
264
279
|
if (searchBox instanceof HTMLInputElement || searchBox instanceof HTMLTextAreaElement) {
|
|
265
280
|
searchBox.value = "";
|
|
266
|
-
searchBox.dispatchEvent(new
|
|
281
|
+
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
267
282
|
searchBox.value = contact;
|
|
268
|
-
searchBox.dispatchEvent(new
|
|
283
|
+
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
269
284
|
} else {
|
|
270
285
|
const selection = window.getSelection();
|
|
271
286
|
const range = document.createRange();
|
|
@@ -278,7 +293,7 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
278
293
|
if ((searchBox.innerText || "").trim() !== contact.trim()) {
|
|
279
294
|
searchBox.textContent = contact;
|
|
280
295
|
}
|
|
281
|
-
searchBox.dispatchEvent(new
|
|
296
|
+
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
282
297
|
}
|
|
283
298
|
|
|
284
299
|
return { ok: true };
|
|
@@ -287,12 +302,16 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
287
302
|
if (!(prepared.ok === true)) {
|
|
288
303
|
return false;
|
|
289
304
|
}
|
|
305
|
+
let enterTriggered = false;
|
|
306
|
+
let clickFallbackTriggered = false;
|
|
290
307
|
const deadline = Date.now() + 6_000;
|
|
291
308
|
while (Date.now() < deadline) {
|
|
292
309
|
await delay(500);
|
|
293
310
|
const result = await this.evaluate(`
|
|
294
311
|
(() => {
|
|
295
312
|
const query = ${JSON.stringify(contact)};
|
|
313
|
+
const enterTriggered = ${JSON.stringify(enterTriggered)};
|
|
314
|
+
const clickFallbackTriggered = ${JSON.stringify(clickFallbackTriggered)};
|
|
296
315
|
const normalize = (value) => String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
|
|
297
316
|
const normalizedQuery = normalize(query);
|
|
298
317
|
|
|
@@ -305,6 +324,37 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
305
324
|
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
306
325
|
}
|
|
307
326
|
|
|
327
|
+
function isConversationOpen() {
|
|
328
|
+
const footerVisible = Array.from(document.querySelectorAll('footer, main footer'))
|
|
329
|
+
.some((node) => isVisible(node));
|
|
330
|
+
const composerVisible = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea'))
|
|
331
|
+
.some((node) => isVisible(node));
|
|
332
|
+
const headerMatch = Array.from(document.querySelectorAll('header span[title], header div[title], main span[title], main div[title]'))
|
|
333
|
+
.filter((node) => node instanceof HTMLElement)
|
|
334
|
+
.filter((node) => isVisible(node))
|
|
335
|
+
.some((node) => {
|
|
336
|
+
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
337
|
+
return text === normalizedQuery || text.includes(normalizedQuery);
|
|
338
|
+
});
|
|
339
|
+
return footerVisible || composerVisible || headerMatch;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (isConversationOpen()) {
|
|
343
|
+
return { clicked: true, activatedBy: "already-open" };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
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"]'))
|
|
347
|
+
.filter((node) => node instanceof HTMLElement)
|
|
348
|
+
.filter((node) => isVisible(node));
|
|
349
|
+
const searchBox = searchCandidates[0];
|
|
350
|
+
if (!enterTriggered && searchBox instanceof HTMLElement) {
|
|
351
|
+
searchBox.focus();
|
|
352
|
+
searchBox.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
353
|
+
searchBox.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
354
|
+
searchBox.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
355
|
+
return { clicked: false, enterTriggered: true };
|
|
356
|
+
}
|
|
357
|
+
|
|
308
358
|
const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
|
|
309
359
|
.filter((node) => node instanceof HTMLElement)
|
|
310
360
|
.filter((node) => isVisible(node))
|
|
@@ -325,17 +375,27 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
325
375
|
return { clicked: false };
|
|
326
376
|
}
|
|
327
377
|
|
|
378
|
+
if (clickFallbackTriggered) {
|
|
379
|
+
return { clicked: false };
|
|
380
|
+
}
|
|
381
|
+
|
|
328
382
|
const winner = titleNodes[0];
|
|
329
383
|
const target = winner.container instanceof HTMLElement ? winner.container : winner.node;
|
|
330
384
|
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
331
385
|
if (typeof target.click === "function") {
|
|
332
386
|
target.click();
|
|
333
|
-
return { clicked: true };
|
|
387
|
+
return { clicked: false, clickFallbackTriggered: true };
|
|
334
388
|
}
|
|
335
389
|
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
336
|
-
return { clicked: true };
|
|
390
|
+
return { clicked: false, clickFallbackTriggered: true };
|
|
337
391
|
})()
|
|
338
392
|
`);
|
|
393
|
+
if (result.enterTriggered === true) {
|
|
394
|
+
enterTriggered = true;
|
|
395
|
+
}
|
|
396
|
+
if (result.clickFallbackTriggered === true) {
|
|
397
|
+
clickFallbackTriggered = true;
|
|
398
|
+
}
|
|
339
399
|
if (result.clicked === true) {
|
|
340
400
|
return true;
|
|
341
401
|
}
|
|
@@ -398,50 +458,91 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
398
458
|
}
|
|
399
459
|
|
|
400
460
|
const composer = candidates[0].node;
|
|
401
|
-
composer.focus();
|
|
402
461
|
if (composer instanceof HTMLInputElement || composer instanceof HTMLTextAreaElement) {
|
|
462
|
+
composer.focus();
|
|
463
|
+
composer.click();
|
|
403
464
|
composer.value = "";
|
|
404
|
-
composer.dispatchEvent(new
|
|
465
|
+
composer.dispatchEvent(new Event("input", { bubbles: true }));
|
|
405
466
|
composer.value = value;
|
|
406
|
-
composer.dispatchEvent(new
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
467
|
+
composer.dispatchEvent(new Event("input", { bubbles: true }));
|
|
468
|
+
composer.click();
|
|
469
|
+
|
|
470
|
+
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"]'))
|
|
471
|
+
.map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
|
|
472
|
+
.filter((node) => node instanceof HTMLElement)
|
|
473
|
+
.filter((node) => isVisible(node));
|
|
474
|
+
|
|
475
|
+
const sendButton = sendCandidates[0];
|
|
476
|
+
if (sendButton instanceof HTMLElement) {
|
|
477
|
+
sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
478
|
+
if (typeof sendButton.click === "function") {
|
|
479
|
+
sendButton.click();
|
|
480
|
+
return { sent: true };
|
|
481
|
+
}
|
|
482
|
+
sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
483
|
+
return { sent: true };
|
|
418
484
|
}
|
|
419
|
-
|
|
485
|
+
|
|
486
|
+
composer.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
487
|
+
composer.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
488
|
+
composer.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter", code: "Enter", keyCode: 13, which: 13, bubbles: true }));
|
|
489
|
+
return { sent: true };
|
|
490
|
+
} else {
|
|
491
|
+
composer.focus();
|
|
492
|
+
composer.click();
|
|
493
|
+
return { nativeInputRequired: true };
|
|
494
|
+
}
|
|
495
|
+
})()
|
|
496
|
+
`);
|
|
497
|
+
if (result.nativeInputRequired === true) {
|
|
498
|
+
const cleared = await this.nativeClearText();
|
|
499
|
+
if (!cleared) {
|
|
500
|
+
throw new Error("O helper nativo do WhatsApp nao conseguiu limpar o rascunho da mensagem.");
|
|
501
|
+
}
|
|
502
|
+
const inserted = await this.nativeInsertText(text);
|
|
503
|
+
if (!inserted) {
|
|
504
|
+
throw new Error("O helper nativo do WhatsApp nao conseguiu digitar a mensagem.");
|
|
505
|
+
}
|
|
506
|
+
await delay(250);
|
|
507
|
+
const clickResult = await this.evaluate(`
|
|
508
|
+
(() => {
|
|
509
|
+
function isVisible(element) {
|
|
510
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
511
|
+
const rect = element.getBoundingClientRect();
|
|
512
|
+
if (rect.width < 6 || rect.height < 6) return false;
|
|
513
|
+
const style = window.getComputedStyle(element);
|
|
514
|
+
if (style.visibility === "hidden" || style.display === "none" || Number(style.opacity || "1") === 0) return false;
|
|
515
|
+
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
420
516
|
}
|
|
421
|
-
composer.click();
|
|
422
517
|
|
|
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"]'))
|
|
518
|
+
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
519
|
.map((node) => node instanceof HTMLElement ? (node.closest('button, div[role="button"]') || node) : null)
|
|
425
520
|
.filter((node) => node instanceof HTMLElement)
|
|
426
521
|
.filter((node) => isVisible(node));
|
|
427
522
|
|
|
428
523
|
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 };
|
|
524
|
+
if (!(sendButton instanceof HTMLElement)) {
|
|
525
|
+
return { clicked: false };
|
|
437
526
|
}
|
|
438
527
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
528
|
+
sendButton.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
529
|
+
if (typeof sendButton.click === "function") {
|
|
530
|
+
sendButton.click();
|
|
531
|
+
return { clicked: true };
|
|
532
|
+
}
|
|
533
|
+
sendButton.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
534
|
+
return { clicked: true };
|
|
443
535
|
})()
|
|
444
|
-
|
|
536
|
+
`);
|
|
537
|
+
if (clickResult.clicked === true) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const submitted = await this.nativePressEnter();
|
|
541
|
+
if (!submitted) {
|
|
542
|
+
throw new Error("O helper nativo do WhatsApp nao conseguiu enviar a mensagem.");
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
445
546
|
if (result.sent === true) {
|
|
446
547
|
return;
|
|
447
548
|
}
|
|
@@ -495,23 +596,45 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
495
596
|
: "(sem mensagens visiveis na conversa)",
|
|
496
597
|
};
|
|
497
598
|
}
|
|
498
|
-
async verifyLastMessage(expectedText) {
|
|
499
|
-
const
|
|
599
|
+
async verifyLastMessage(expectedText, previousMessages) {
|
|
600
|
+
const baseline = Array.isArray(previousMessages) ? previousMessages : [];
|
|
601
|
+
const chat = await this.readVisibleConversation(Math.max(8, baseline.length + 2));
|
|
500
602
|
if (!chat.messages.length) {
|
|
501
603
|
return {
|
|
502
604
|
ok: false,
|
|
503
605
|
reason: "Nao consegui ler as mensagens visiveis apos o envio no WhatsApp.",
|
|
504
606
|
};
|
|
505
607
|
}
|
|
506
|
-
const normalizedExpected = normalizeText(expectedText).slice(0,
|
|
507
|
-
const
|
|
508
|
-
|
|
608
|
+
const normalizedExpected = normalizeText(expectedText).slice(0, 120);
|
|
609
|
+
const normalizeMessage = (item) => `${normalizeText(item.author)}|${normalizeText(item.text)}`;
|
|
610
|
+
const beforeSignature = baseline.map(normalizeMessage).join("\n");
|
|
611
|
+
const afterSignature = chat.messages.map(normalizeMessage).join("\n");
|
|
612
|
+
const changed = beforeSignature !== afterSignature;
|
|
613
|
+
const beforeMatches = baseline.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
614
|
+
const afterMatches = chat.messages.filter((item) => normalizeText(item.text).includes(normalizedExpected)).length;
|
|
615
|
+
const latest = chat.messages[chat.messages.length - 1] || null;
|
|
616
|
+
const latestAuthor = normalizeText(latest?.author || "");
|
|
617
|
+
const latestText = normalizeText(latest?.text || "");
|
|
618
|
+
const latestMatches = latestText.includes(normalizedExpected) && (latestAuthor === "voce" || latestAuthor === "você");
|
|
619
|
+
if ((changed && latestMatches) || (changed && afterMatches > beforeMatches)) {
|
|
620
|
+
return { ok: true, reason: "" };
|
|
621
|
+
}
|
|
622
|
+
if (!changed) {
|
|
623
|
+
return {
|
|
624
|
+
ok: false,
|
|
625
|
+
reason: "O WhatsApp nao mostrou mudanca visivel na conversa depois da tentativa de envio.",
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
if (afterMatches <= beforeMatches) {
|
|
509
629
|
return {
|
|
510
630
|
ok: false,
|
|
511
|
-
reason: "
|
|
631
|
+
reason: "A conversa mudou, mas nao apareceu uma nova mensagem com o texto esperado no WhatsApp.",
|
|
512
632
|
};
|
|
513
633
|
}
|
|
514
|
-
return {
|
|
634
|
+
return {
|
|
635
|
+
ok: false,
|
|
636
|
+
reason: "Nao consegui confirmar na conversa do WhatsApp se a nova mensagem foi enviada.",
|
|
637
|
+
};
|
|
515
638
|
}
|
|
516
639
|
handleStdout(chunk) {
|
|
517
640
|
this.stdoutBuffer += chunk;
|
|
@@ -24,6 +24,10 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
|
|
|
24
24
|
app.setActivationPolicy(.accessory)
|
|
25
25
|
window.title = "Otto WhatsApp Helper"
|
|
26
26
|
window.isReleasedWhenClosed = false
|
|
27
|
+
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]
|
|
28
|
+
window.level = .normal
|
|
29
|
+
window.hasShadow = false
|
|
30
|
+
window.hidesOnDeactivate = false
|
|
27
31
|
window.contentView = webView
|
|
28
32
|
webView.navigationDelegate = self
|
|
29
33
|
webView.customUserAgent = Self.chromeUserAgent
|
|
@@ -86,6 +90,13 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
|
|
|
86
90
|
case "hide_background":
|
|
87
91
|
hideBackground()
|
|
88
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()])
|
|
89
100
|
case "load_whatsapp":
|
|
90
101
|
ensureWhatsAppLoaded()
|
|
91
102
|
sendResponse(id: id, result: ["url": webView.url?.absoluteString ?? ""])
|
|
@@ -116,6 +127,8 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
|
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
private func showSetup() {
|
|
130
|
+
window.alphaValue = 1
|
|
131
|
+
window.ignoresMouseEvents = false
|
|
119
132
|
if let screen = NSScreen.main {
|
|
120
133
|
let frame = screen.visibleFrame
|
|
121
134
|
let originX = frame.origin.x + max(0, (frame.width - 1320) / 2)
|
|
@@ -128,11 +141,59 @@ final class OttoWhatsAppHelper: NSObject, WKNavigationDelegate {
|
|
|
128
141
|
app.activate(ignoringOtherApps: true)
|
|
129
142
|
}
|
|
130
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
|
+
|
|
131
152
|
private func hideBackground() {
|
|
132
|
-
window.
|
|
153
|
+
window.ignoresMouseEvents = true
|
|
154
|
+
window.alphaValue = 0
|
|
155
|
+
window.setFrame(NSRect(x: -2200, y: 80, width: 1320, height: 920), display: false)
|
|
133
156
|
window.orderFrontRegardless()
|
|
134
157
|
}
|
|
135
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
|
+
|
|
136
197
|
private func evaluateJavaScript(_ script: String, completion: @escaping (Any?, Error?) -> Void) {
|
|
137
198
|
webView.evaluateJavaScript(script) { result, error in
|
|
138
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.5";
|
|
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;
|