@leg3ndy/otto-bridge 0.7.5 → 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.
- package/dist/executors/native_macos.js +34 -8
- package/dist/macos_whatsapp_helper.js +81 -48
- package/dist/types.js +1 -1
- package/package.json +1 -1
|
@@ -2366,6 +2366,9 @@ function isVisible(element) {
|
|
|
2366
2366
|
|
|
2367
2367
|
function focusAndReplaceContent(element, value) {
|
|
2368
2368
|
element.focus();
|
|
2369
|
+
if (typeof element.click === "function") {
|
|
2370
|
+
element.click();
|
|
2371
|
+
}
|
|
2369
2372
|
const range = document.createRange();
|
|
2370
2373
|
range.selectNodeContents(element);
|
|
2371
2374
|
const selection = window.getSelection();
|
|
@@ -2373,7 +2376,14 @@ function focusAndReplaceContent(element, value) {
|
|
|
2373
2376
|
selection?.addRange(range);
|
|
2374
2377
|
document.execCommand("selectAll", false);
|
|
2375
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
|
+
}
|
|
2376
2383
|
document.execCommand("insertText", false, value);
|
|
2384
|
+
if ((element.innerText || "").trim() !== value.trim()) {
|
|
2385
|
+
element.textContent = value;
|
|
2386
|
+
}
|
|
2377
2387
|
element.dispatchEvent(new InputEvent("input", { bubbles: true, data: value, inputType: "insertText" }));
|
|
2378
2388
|
}
|
|
2379
2389
|
|
|
@@ -2418,18 +2428,36 @@ function isVisible(element) {
|
|
|
2418
2428
|
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
2419
2429
|
}
|
|
2420
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
|
+
|
|
2421
2447
|
const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
|
|
2422
2448
|
.filter((node) => node instanceof HTMLElement)
|
|
2423
2449
|
.filter((node) => isVisible(node))
|
|
2424
2450
|
.map((node) => {
|
|
2425
2451
|
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
2452
|
+
const target = pickClickTarget(node);
|
|
2426
2453
|
let score = 0;
|
|
2427
2454
|
if (text === normalizedQuery) score += 160;
|
|
2428
2455
|
if (text.includes(normalizedQuery)) score += 100;
|
|
2429
2456
|
if (normalizedQuery.includes(text) && text.length >= 3) score += 50;
|
|
2430
|
-
|
|
2431
|
-
if (
|
|
2432
|
-
|
|
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 };
|
|
2433
2461
|
})
|
|
2434
2462
|
.filter((item) => item.score > 0)
|
|
2435
2463
|
.sort((left, right) => right.score - left.score);
|
|
@@ -2439,12 +2467,10 @@ if (!titleNodes.length) {
|
|
|
2439
2467
|
}
|
|
2440
2468
|
|
|
2441
2469
|
const winner = titleNodes[0];
|
|
2442
|
-
const target = winner.
|
|
2470
|
+
const target = winner.target instanceof HTMLElement ? winner.target : winner.node;
|
|
2443
2471
|
target.scrollIntoView({ block: "center", inline: "center", behavior: "auto" });
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
return { clicked: true };
|
|
2447
|
-
}
|
|
2472
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
|
|
2473
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
|
|
2448
2474
|
target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
|
|
2449
2475
|
return { clicked: true };
|
|
2450
2476
|
`, { contact }, this.getWhatsAppWebScriptOptions(false));
|
|
@@ -275,35 +275,37 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
275
275
|
|
|
276
276
|
const searchBox = candidates[0].node;
|
|
277
277
|
searchBox.focus();
|
|
278
|
+
if (typeof searchBox.click === "function") {
|
|
279
|
+
searchBox.click();
|
|
280
|
+
}
|
|
278
281
|
|
|
279
282
|
if (searchBox instanceof HTMLInputElement || searchBox instanceof HTMLTextAreaElement) {
|
|
280
283
|
searchBox.value = "";
|
|
281
284
|
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
282
285
|
searchBox.value = contact;
|
|
283
286
|
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
287
|
+
return { ok: true, nativeInputRequired: false };
|
|
284
288
|
} else {
|
|
285
|
-
|
|
286
|
-
const range = document.createRange();
|
|
287
|
-
range.selectNodeContents(searchBox);
|
|
288
|
-
selection?.removeAllRanges();
|
|
289
|
-
selection?.addRange(range);
|
|
290
|
-
document.execCommand("selectAll", false);
|
|
291
|
-
document.execCommand("delete", false);
|
|
292
|
-
document.execCommand("insertText", false, contact);
|
|
293
|
-
if ((searchBox.innerText || "").trim() !== contact.trim()) {
|
|
294
|
-
searchBox.textContent = contact;
|
|
295
|
-
}
|
|
289
|
+
searchBox.textContent = "";
|
|
296
290
|
searchBox.dispatchEvent(new Event("input", { bubbles: true }));
|
|
291
|
+
return { ok: true, nativeInputRequired: true };
|
|
297
292
|
}
|
|
298
|
-
|
|
299
|
-
return { ok: true };
|
|
300
293
|
})()
|
|
301
294
|
`);
|
|
302
295
|
if (!(prepared.ok === true)) {
|
|
303
296
|
return false;
|
|
304
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);
|
|
305
306
|
let enterTriggered = false;
|
|
306
|
-
let
|
|
307
|
+
let clickAttempts = 0;
|
|
308
|
+
const searchStartedAt = Date.now();
|
|
307
309
|
const deadline = Date.now() + 6_000;
|
|
308
310
|
while (Date.now() < deadline) {
|
|
309
311
|
await delay(500);
|
|
@@ -311,7 +313,8 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
311
313
|
(() => {
|
|
312
314
|
const query = ${JSON.stringify(contact)};
|
|
313
315
|
const enterTriggered = ${JSON.stringify(enterTriggered)};
|
|
314
|
-
const
|
|
316
|
+
const clickAttempts = ${JSON.stringify(clickAttempts)};
|
|
317
|
+
const allowEnterFallback = ${JSON.stringify(Date.now() - searchStartedAt >= 2_500)};
|
|
315
318
|
const normalize = (value) => String(value || "").normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase().trim();
|
|
316
319
|
const normalizedQuery = normalize(query);
|
|
317
320
|
|
|
@@ -324,77 +327,107 @@ export class MacOSWhatsAppHelperRuntime {
|
|
|
324
327
|
return rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth;
|
|
325
328
|
}
|
|
326
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
|
+
|
|
327
346
|
function isConversationOpen() {
|
|
328
347
|
const footerVisible = Array.from(document.querySelectorAll('footer, main footer'))
|
|
329
348
|
.some((node) => isVisible(node));
|
|
330
349
|
const composerVisible = Array.from(document.querySelectorAll('[data-testid="conversation-compose-box-input"], footer div[contenteditable="true"], main footer [contenteditable="true"], footer textarea'))
|
|
331
350
|
.some((node) => isVisible(node));
|
|
332
|
-
const headerMatch = Array.from(document.querySelectorAll('header span[title], header div[title], main span[title], main div[title]'))
|
|
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]'))
|
|
333
352
|
.filter((node) => node instanceof HTMLElement)
|
|
334
353
|
.filter((node) => isVisible(node))
|
|
335
354
|
.some((node) => {
|
|
336
355
|
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
337
|
-
return text === normalizedQuery
|
|
356
|
+
return text === normalizedQuery
|
|
357
|
+
|| text.includes(normalizedQuery)
|
|
358
|
+
|| (normalizedQuery.includes(text) && text.length >= 3);
|
|
338
359
|
});
|
|
339
|
-
|
|
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);
|
|
340
373
|
}
|
|
341
374
|
|
|
342
375
|
if (isConversationOpen()) {
|
|
343
376
|
return { clicked: true, activatedBy: "already-open" };
|
|
344
377
|
}
|
|
345
378
|
|
|
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
|
-
|
|
358
379
|
const titleNodes = Array.from(document.querySelectorAll('span[title], div[title]'))
|
|
359
380
|
.filter((node) => node instanceof HTMLElement)
|
|
360
381
|
.filter((node) => isVisible(node))
|
|
361
382
|
.map((node) => {
|
|
362
383
|
const text = normalize(node.getAttribute("title") || node.textContent || "");
|
|
384
|
+
const target = pickClickTarget(node);
|
|
363
385
|
let score = 0;
|
|
364
386
|
if (text === normalizedQuery) score += 160;
|
|
365
387
|
if (text.includes(normalizedQuery)) score += 100;
|
|
366
388
|
if (normalizedQuery.includes(text) && text.length >= 3) score += 50;
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
|
|
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 };
|
|
370
393
|
})
|
|
371
394
|
.filter((item) => item.score > 0)
|
|
372
395
|
.sort((left, right) => right.score - left.score);
|
|
373
396
|
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
|
|
397
|
+
if (titleNodes.length) {
|
|
398
|
+
if (clickAttempts >= 4) {
|
|
399
|
+
return { clicked: false };
|
|
400
|
+
}
|
|
377
401
|
|
|
378
|
-
|
|
379
|
-
|
|
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 };
|
|
380
409
|
}
|
|
381
410
|
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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 };
|
|
388
421
|
}
|
|
389
|
-
|
|
390
|
-
return { clicked: false
|
|
422
|
+
|
|
423
|
+
return { clicked: false };
|
|
391
424
|
})()
|
|
392
425
|
`);
|
|
393
426
|
if (result.enterTriggered === true) {
|
|
394
427
|
enterTriggered = true;
|
|
395
428
|
}
|
|
396
|
-
if (result.
|
|
397
|
-
|
|
429
|
+
if (typeof result.clickAttempts === "number") {
|
|
430
|
+
clickAttempts = result.clickAttempts;
|
|
398
431
|
}
|
|
399
432
|
if (result.clicked === true) {
|
|
400
433
|
return true;
|
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;
|