channel-worker 2.5.51 → 2.5.53
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/package.json +1 -1
- package/scripts/nurture_facebook.js +52 -1
- package/scripts/upload_facebook.js +59 -11
package/package.json
CHANGED
|
@@ -282,6 +282,11 @@ async function openPostAndRead(page, log) {
|
|
|
282
282
|
for (const a of best.querySelectorAll("a[href*='/posts/'], a[href*='/permalink/'], a[href*='story_fbid'], a[href*='/photo']")) {
|
|
283
283
|
const r = a.getBoundingClientRect();
|
|
284
284
|
if (r.width < 8 || r.height < 8 || a.offsetParent === null) continue;
|
|
285
|
+
// Facebook ships most permalinks with target="_blank": clicked as-is the
|
|
286
|
+
// post lands in a NEW tab while the script keeps driving the old one, so
|
|
287
|
+
// every opened post leaves a dead tab behind. Force same-tab — goBack()
|
|
288
|
+
// below is what returns us to the feed.
|
|
289
|
+
a.setAttribute('target', '_self');
|
|
285
290
|
a.setAttribute('__nur_open__', '1');
|
|
286
291
|
return true;
|
|
287
292
|
}
|
|
@@ -377,6 +382,43 @@ async function detectProfileMode(page) {
|
|
|
377
382
|
}).catch(() => ({ isPage: false, why: 'detect failed', navSample: '' }));
|
|
378
383
|
}
|
|
379
384
|
|
|
385
|
+
// ONE tab, always.
|
|
386
|
+
//
|
|
387
|
+
// Two things pile tabs up behind a session, and neither is ours: NSTBrowser
|
|
388
|
+
// restores whatever the profile had open the last time it was used (publishing
|
|
389
|
+
// leaves Studio/Business tabs), and Facebook opens permalinks, photos and
|
|
390
|
+
// notification links with target="_blank". The script drives pages()[0] and
|
|
391
|
+
// never looks at the rest, so they just sit there — by the third session the
|
|
392
|
+
// window is a wall of tabs.
|
|
393
|
+
//
|
|
394
|
+
// Closing them is safe: nurturing needs exactly one tab (the feed), and the
|
|
395
|
+
// permalink click is forced to same-tab navigation in openPostAndRead.
|
|
396
|
+
async function closeOtherTabs(context, keep, log) {
|
|
397
|
+
const before = context.pages().length;
|
|
398
|
+
let closed = 0;
|
|
399
|
+
for (const p of context.pages()) {
|
|
400
|
+
if (p === keep || p.isClosed()) continue;
|
|
401
|
+
try { await p.close({ runBeforeUnload: false }); closed++; } catch { /* already gone */ }
|
|
402
|
+
}
|
|
403
|
+
// Log even when nothing was closed: "no line" would otherwise read the same
|
|
404
|
+
// as "the fix never loaded", and that is exactly what we need to tell apart.
|
|
405
|
+
if (log) log('info', `[nurture-fb] tab: ${before} lúc vào → đóng ${closed} → còn ${context.pages().length}`);
|
|
406
|
+
return closed;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Anything Facebook manages to pop open mid-session dies on arrival. Registered
|
|
410
|
+
// once per session; the listener is scoped to this context, which the runner
|
|
411
|
+
// detaches from when the script returns.
|
|
412
|
+
function guardSingleTab(context, keep, log) {
|
|
413
|
+
context.on('page', async (p) => {
|
|
414
|
+
if (p === keep || p.isClosed()) return;
|
|
415
|
+
try {
|
|
416
|
+
await p.close({ runBeforeUnload: false });
|
|
417
|
+
log('info', '[nurture-fb] tab mới bị Facebook bung ra — đã đóng ngay');
|
|
418
|
+
} catch { /* raced with its own close */ }
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
380
422
|
// debug:true only — screenshot the feed and hand back a viewable URL. Cheaper
|
|
381
423
|
// than another round of guessing at selectors from log lines.
|
|
382
424
|
async function dumpFeedShot(page, log, tag) {
|
|
@@ -398,7 +440,7 @@ async function dumpFeedShot(page, log, tag) {
|
|
|
398
440
|
}
|
|
399
441
|
|
|
400
442
|
// ─── main ───────────────────────────────────────────────────────────────────
|
|
401
|
-
async function run({ page, payload, log }) {
|
|
443
|
+
async function run({ page, context, payload, log }) {
|
|
402
444
|
const t0 = Date.now();
|
|
403
445
|
const cfg = payload.config || {};
|
|
404
446
|
const phase = Math.max(1, Math.min(3, parseInt(payload.phase, 10) || 1));
|
|
@@ -417,6 +459,12 @@ async function run({ page, payload, log }) {
|
|
|
417
459
|
|
|
418
460
|
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
419
461
|
|
|
462
|
+
// Clear the profile's leftovers BEFORE loading the feed, then keep it clear.
|
|
463
|
+
if (context) {
|
|
464
|
+
await closeOtherTabs(context, page, log);
|
|
465
|
+
guardSingleTab(context, page, log);
|
|
466
|
+
}
|
|
467
|
+
|
|
420
468
|
await page.goto('https://www.facebook.com/', { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
421
469
|
await page.waitForTimeout(randInt(3000, 5000));
|
|
422
470
|
// Guard FIRST — everything below assumes a live logged-in session.
|
|
@@ -522,6 +570,9 @@ async function run({ page, payload, log }) {
|
|
|
522
570
|
// moment activity trips a threshold — better to stop than to keep poking.
|
|
523
571
|
if (ticks % 12 === 0) {
|
|
524
572
|
await assertAccountUsable(page, log);
|
|
573
|
+
// Belt to the context listener: a tab opened as a separate window, or one
|
|
574
|
+
// that slipped in while we were navigating, still gets swept.
|
|
575
|
+
if (context && context.pages().length > 1) await closeOtherTabs(context, page, log);
|
|
525
576
|
log('info', `[nurture-fb] …${Math.round((Date.now() - t0) / 1000)}s: posts=${postsSeen} likes=${likes}/${likesTarget} videos=${videos} scrollY=${lastScan.scrollY} units=${lastScan.articlesInDom} sel=${lastScan.selector}`);
|
|
526
577
|
}
|
|
527
578
|
|
|
@@ -1255,24 +1255,58 @@ async function runOnce({ page, payload, log }) {
|
|
|
1255
1255
|
await page.locator(editBtn.selector).click({ timeout: 5000 });
|
|
1256
1256
|
await page.waitForTimeout(2500);
|
|
1257
1257
|
|
|
1258
|
-
// Wait for the thumb
|
|
1259
|
-
//
|
|
1258
|
+
// Wait for the thumb editor to mount. FB serves (A/B, seen
|
|
1259
|
+
// 2026-08-19) TWO variants and any given account can get either:
|
|
1260
|
+
// modal — a separate [role='dialog'] titled "Chỉnh sửa hình
|
|
1261
|
+
// thu nhỏ" (the original form).
|
|
1262
|
+
// nested — the editor renders INSIDE the reel-composer dialog
|
|
1263
|
+
// ("Cài đặt thước phim"): no own dialog, and the
|
|
1264
|
+
// header sits too deep in innerText for a 300-char
|
|
1265
|
+
// signature check. Recognize it by its upload button
|
|
1266
|
+
// (aria "Tải hình thu nhỏ tùy chỉnh lên…"), which only
|
|
1267
|
+
// exists while the editor is open, and tag the nearest
|
|
1268
|
+
// container that also holds Lưu + Hủy so the scoped
|
|
1269
|
+
// selectors below keep working for both variants.
|
|
1270
|
+
// Check modal FIRST so old-UI accounts behave exactly as before.
|
|
1260
1271
|
await page.evaluate(() => document.querySelectorAll("[__fbpw_thumb_edit__]").forEach((el) => el.removeAttribute('__fbpw_thumb_edit__'))).catch(() => {});
|
|
1261
|
-
const
|
|
1272
|
+
const detectThumbEditor = () => page.evaluate(() => {
|
|
1262
1273
|
document.querySelectorAll('[__fbpw_thumb_dialog__]').forEach((el) => el.removeAttribute('__fbpw_thumb_dialog__'));
|
|
1274
|
+
const vis = (el) => {
|
|
1275
|
+
const r = el.getBoundingClientRect();
|
|
1276
|
+
if (r.width < 8 || r.height < 8) return false;
|
|
1277
|
+
const cs = getComputedStyle(el);
|
|
1278
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
1279
|
+
};
|
|
1263
1280
|
for (const dlg of document.querySelectorAll("[role='dialog']")) {
|
|
1264
|
-
|
|
1265
|
-
if (r.width < 8 || r.height < 8) continue;
|
|
1266
|
-
const cs = getComputedStyle(dlg);
|
|
1267
|
-
if (cs.visibility === 'hidden' || cs.display === 'none' || cs.opacity === '0') continue;
|
|
1281
|
+
if (!vis(dlg)) continue;
|
|
1268
1282
|
const sig = `${dlg.getAttribute('aria-label') || ''}\n${(dlg.innerText || '').slice(0, 300)}`;
|
|
1269
1283
|
if (!/Chỉnh sửa hình thu nhỏ|Edit thumbnail/i.test(sig)) continue;
|
|
1270
1284
|
dlg.setAttribute('__fbpw_thumb_dialog__', '1');
|
|
1271
|
-
return
|
|
1285
|
+
return 'modal';
|
|
1272
1286
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1287
|
+
const uploadBtn = [...document.querySelectorAll('[aria-label]')].find((el) => {
|
|
1288
|
+
const al = el.getAttribute('aria-label') || '';
|
|
1289
|
+
return /Tải hình thu nhỏ|thumbnail/i.test(al) && /Tải|upload/i.test(al) && vis(el);
|
|
1290
|
+
});
|
|
1291
|
+
if (uploadBtn) {
|
|
1292
|
+
const hasBtn = (root, re) => [...root.querySelectorAll("[role='button'], button")]
|
|
1293
|
+
.some((b) => re.test((b.innerText || '').trim()) && vis(b));
|
|
1294
|
+
let box = uploadBtn.parentElement;
|
|
1295
|
+
while (box && box !== document.body && !(hasBtn(box, /^(Lưu|Save)$/) && hasBtn(box, /^(Hủy|Cancel)$/))) box = box.parentElement;
|
|
1296
|
+
if (box === document.body) box = null;
|
|
1297
|
+
(box || uploadBtn.closest("[role='dialog']") || document.body).setAttribute('__fbpw_thumb_dialog__', '1');
|
|
1298
|
+
return 'nested';
|
|
1299
|
+
}
|
|
1300
|
+
return null;
|
|
1301
|
+
}).catch(() => null);
|
|
1302
|
+
let thumbEditorVia = await detectThumbEditor();
|
|
1303
|
+
const mountDeadline = Date.now() + 12_000;
|
|
1304
|
+
while (!thumbEditorVia && Date.now() < mountDeadline) {
|
|
1305
|
+
await page.waitForTimeout(2000);
|
|
1306
|
+
thumbEditorVia = await detectThumbEditor();
|
|
1307
|
+
}
|
|
1308
|
+
if (!thumbEditorVia) throw new Error('thumbnail editor dialog did not mount');
|
|
1309
|
+
log('info', `[fb-pw] page-wall thumb — editor mounted (variant=${thumbEditorVia})`);
|
|
1276
1310
|
|
|
1277
1311
|
// Click "Tải lên" button — opens OS file picker. Use filechooser
|
|
1278
1312
|
// race to inject the file path directly. Scope every action to the
|
|
@@ -1355,6 +1389,11 @@ async function runOnce({ page, payload, log }) {
|
|
|
1355
1389
|
const RETRY_AFTER_MS = 12_000;
|
|
1356
1390
|
let lastRetryAt = Date.now();
|
|
1357
1391
|
while (Date.now() < closeDeadline) {
|
|
1392
|
+
// "Editor still open" must cover both variants: the modal
|
|
1393
|
+
// form (dialog whose text leads with the header) and the
|
|
1394
|
+
// nested form (no own dialog — its upload button, aria
|
|
1395
|
+
// "Tải hình thu nhỏ tùy chỉnh lên…", only exists while the
|
|
1396
|
+
// editor is showing).
|
|
1358
1397
|
const still = await page.evaluate(() => {
|
|
1359
1398
|
const dlgs = document.querySelectorAll("[role='dialog']");
|
|
1360
1399
|
for (const dlg of dlgs) {
|
|
@@ -1364,6 +1403,15 @@ async function runOnce({ page, payload, log }) {
|
|
|
1364
1403
|
if (r.width > 8 && r.height > 8) return true;
|
|
1365
1404
|
}
|
|
1366
1405
|
}
|
|
1406
|
+
for (const el of document.querySelectorAll('[aria-label]')) {
|
|
1407
|
+
const al = el.getAttribute('aria-label') || '';
|
|
1408
|
+
if (!(/Tải hình thu nhỏ|thumbnail/i.test(al) && /Tải|upload/i.test(al))) continue;
|
|
1409
|
+
const r = el.getBoundingClientRect();
|
|
1410
|
+
if (r.width < 8 || r.height < 8) continue;
|
|
1411
|
+
const cs = getComputedStyle(el);
|
|
1412
|
+
if (cs.visibility === 'hidden' || cs.display === 'none' || cs.opacity === '0') continue;
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1367
1415
|
return false;
|
|
1368
1416
|
}).catch(() => false);
|
|
1369
1417
|
if (!still) { modalClosed = true; break; }
|