channel-worker 2.5.47 → 2.5.49
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 +59 -17
package/package.json
CHANGED
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
|
|
22
22
|
const { assertAccountUsable } = require('./lib/fb-guard');
|
|
23
23
|
|
|
24
|
+
// Every markup shape Facebook has used for "one post in the feed". Used by the
|
|
25
|
+
// secondary helpers (ad check, open-post) via closest()/querySelectorAll;
|
|
26
|
+
// scanFeed picks the single best-yielding one per render instead.
|
|
27
|
+
const UNIT_SEL = "div[role='feed'] > div, div[aria-posinset], [data-pagelet^='FeedUnit'], [role='article']";
|
|
28
|
+
|
|
24
29
|
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
25
30
|
function randInt(min, max) { return Math.floor(min + Math.random() * (max - min + 1)); }
|
|
26
31
|
function chance(p) { return Math.random() < p; }
|
|
@@ -107,6 +112,26 @@ async function dismissDialogs(page, log) {
|
|
|
107
112
|
async function scanFeed(page, { skipKeys = [], wantLike = false } = {}) {
|
|
108
113
|
return page.evaluate(({ skip, wantLike }) => {
|
|
109
114
|
const skipSet = new Set(skip);
|
|
115
|
+
|
|
116
|
+
// WHICH element is "a post"? Not a fixed selector — Facebook's feed markup
|
|
117
|
+
// moves. Measured on the live account 2026-08-11: the whole document held
|
|
118
|
+
// exactly ONE [role='article'] while the feed scrolled fine, so a hardcoded
|
|
119
|
+
// selector counted 1 post per 10 minutes. Try the known shapes and keep
|
|
120
|
+
// whichever yields the most post-sized blocks on THIS render.
|
|
121
|
+
const CANDIDATES = [
|
|
122
|
+
"div[role='feed'] > div",
|
|
123
|
+
'div[aria-posinset]',
|
|
124
|
+
"[data-pagelet^='FeedUnit']",
|
|
125
|
+
"[role='article']",
|
|
126
|
+
];
|
|
127
|
+
let sel = null, nodes = [];
|
|
128
|
+
for (const s of CANDIDATES) {
|
|
129
|
+
const found = [...document.querySelectorAll(s)].filter((e) => e.getBoundingClientRect().height > 80);
|
|
130
|
+
if (found.length > nodes.length) { nodes = found; sel = s; }
|
|
131
|
+
}
|
|
132
|
+
// Drop blocks nested inside another block of the same kind (comments,
|
|
133
|
+
// embedded shares) — only outermost units are posts.
|
|
134
|
+
const units = nodes.filter((n) => !nodes.some((o) => o !== n && o.contains(n)));
|
|
110
135
|
const keyOf = (art) => {
|
|
111
136
|
for (const a of art.querySelectorAll('a[href]')) {
|
|
112
137
|
const h = a.getAttribute('href') || '';
|
|
@@ -120,9 +145,7 @@ async function scanFeed(page, { skipKeys = [], wantLike = false } = {}) {
|
|
|
120
145
|
const mid = window.innerHeight / 2;
|
|
121
146
|
const posts = [];
|
|
122
147
|
let best = null, bestDist = Infinity, bestKey = '';
|
|
123
|
-
for (const art of
|
|
124
|
-
// Comments render as NESTED articles — only top-level posts count.
|
|
125
|
-
if (art.parentElement && art.parentElement.closest("[role='article']")) continue;
|
|
148
|
+
for (const art of units) {
|
|
126
149
|
const r = art.getBoundingClientRect();
|
|
127
150
|
if (r.bottom < 0 || r.top > window.innerHeight || r.height < 80) continue;
|
|
128
151
|
const text = art.innerText || '';
|
|
@@ -140,7 +163,6 @@ async function scanFeed(page, { skipKeys = [], wantLike = false } = {}) {
|
|
|
140
163
|
const aria = (b.getAttribute('aria-label') || '').trim();
|
|
141
164
|
if (!/^(thích|like)$/i.test(aria)) continue;
|
|
142
165
|
if (b.getAttribute('aria-pressed') === 'true') continue; // already liked
|
|
143
|
-
if (b.closest("[role='article']") !== best) continue; // a comment's button
|
|
144
166
|
const br = b.getBoundingClientRect();
|
|
145
167
|
if (br.width < 8 || br.height < 8 || b.offsetParent === null) continue;
|
|
146
168
|
b.setAttribute('__nur_like__', '1');
|
|
@@ -148,10 +170,25 @@ async function scanFeed(page, { skipKeys = [], wantLike = false } = {}) {
|
|
|
148
170
|
break;
|
|
149
171
|
}
|
|
150
172
|
}
|
|
173
|
+
// Which element is actually scrolling? Facebook has shipped both a
|
|
174
|
+
// window-scrolled feed and one inside its own overflow container; if we
|
|
175
|
+
// measure the wrong one, "the page didn't move" is a false alarm.
|
|
176
|
+
let scroller = 'window', scrollTop = Math.round(window.scrollY || 0);
|
|
177
|
+
if (!scrollTop) {
|
|
178
|
+
for (const el of document.querySelectorAll('div')) {
|
|
179
|
+
if (el.scrollTop > 50 && el.scrollHeight > el.clientHeight + 200) {
|
|
180
|
+
scroller = 'div.' + String(el.className || '').split(' ')[0];
|
|
181
|
+
scrollTop = Math.round(el.scrollTop);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
151
186
|
return {
|
|
152
|
-
posts, likeKey,
|
|
187
|
+
posts, likeKey, scroller, scrollTop,
|
|
153
188
|
scrollY: Math.round(window.scrollY || document.documentElement.scrollTop || 0),
|
|
154
|
-
articlesInDom:
|
|
189
|
+
articlesInDom: units.length,
|
|
190
|
+
selector: sel || '(none)',
|
|
191
|
+
sample: posts.slice(0, 3).map((p) => p.key.slice(0, 46)),
|
|
155
192
|
};
|
|
156
193
|
}, { skip: skipKeys, wantLike }).catch(() => ({ posts: [], likeKey: null, scrollY: 0, articlesInDom: 0 }));
|
|
157
194
|
}
|
|
@@ -184,12 +221,12 @@ async function clickMarkedLike(page, log) {
|
|
|
184
221
|
// Watch a video that's playing in the feed, counting only seconds where
|
|
185
222
|
// playback actually ADVANCES (a frozen/buffering player must not bank time).
|
|
186
223
|
async function watchFeedVideo(page, minSec, maxSec, log) {
|
|
187
|
-
const has = await page.evaluate(() => {
|
|
224
|
+
const has = await page.evaluate((UNIT) => {
|
|
188
225
|
const vids = [...document.querySelectorAll('video')];
|
|
189
226
|
for (const v of vids) {
|
|
190
227
|
const r = v.getBoundingClientRect();
|
|
191
228
|
if (r.height < 100 || r.bottom < 0 || r.top > window.innerHeight) continue;
|
|
192
|
-
const art = v.closest(
|
|
229
|
+
const art = v.closest(UNIT);
|
|
193
230
|
// Ads are re-detected from text — a node flag would be stale the moment
|
|
194
231
|
// FB recycles the article for a different post.
|
|
195
232
|
if (art && /Được tài trợ|Sponsored/i.test((art.innerText || '').slice(0, 400))) continue;
|
|
@@ -199,7 +236,7 @@ async function watchFeedVideo(page, minSec, maxSec, log) {
|
|
|
199
236
|
return true;
|
|
200
237
|
}
|
|
201
238
|
return false;
|
|
202
|
-
}).catch(() => false);
|
|
239
|
+
}, UNIT_SEL).catch(() => false);
|
|
203
240
|
if (!has) return false;
|
|
204
241
|
|
|
205
242
|
const budgetMs = randInt(minSec, maxSec) * 1000;
|
|
@@ -228,11 +265,11 @@ async function watchFeedVideo(page, minSec, maxSec, log) {
|
|
|
228
265
|
// Phase 2+. Best-effort: if the click doesn't navigate, nothing is lost.
|
|
229
266
|
async function openPostAndRead(page, log) {
|
|
230
267
|
const urlBefore = page.url();
|
|
231
|
-
const marked = await page.evaluate(() => {
|
|
268
|
+
const marked = await page.evaluate((UNIT) => {
|
|
232
269
|
const mid = window.innerHeight / 2;
|
|
233
270
|
let best = null, bestDist = Infinity;
|
|
234
|
-
for (const art of document.querySelectorAll(
|
|
235
|
-
if (art.parentElement && art.parentElement.closest(
|
|
271
|
+
for (const art of document.querySelectorAll(UNIT)) {
|
|
272
|
+
if (art.parentElement && art.parentElement.closest(UNIT)) continue;
|
|
236
273
|
// Node-level flags can't be trusted (FB recycles articles), so ads are
|
|
237
274
|
// re-checked from the text right here.
|
|
238
275
|
if (/Được tài trợ|Sponsored/i.test((art.innerText || '').slice(0, 400))) continue;
|
|
@@ -249,7 +286,7 @@ async function openPostAndRead(page, log) {
|
|
|
249
286
|
return true;
|
|
250
287
|
}
|
|
251
288
|
return false;
|
|
252
|
-
}).catch(() => false);
|
|
289
|
+
}, UNIT_SEL).catch(() => false);
|
|
253
290
|
if (!marked) return false;
|
|
254
291
|
|
|
255
292
|
try {
|
|
@@ -283,7 +320,7 @@ async function openPostAndRead(page, log) {
|
|
|
283
320
|
// Follow a page/creator surfaced by the feed itself (phase 3, ≤ config max).
|
|
284
321
|
// Only clicks a Follow/Like-Page button that's already on screen — never hunts.
|
|
285
322
|
async function followVisiblePage(page, log) {
|
|
286
|
-
const marked = await page.evaluate(() => {
|
|
323
|
+
const marked = await page.evaluate((UNIT) => {
|
|
287
324
|
for (const b of document.querySelectorAll("[role='button'], a[role='button']")) {
|
|
288
325
|
const t = (b.innerText || '').trim();
|
|
289
326
|
const aria = (b.getAttribute('aria-label') || '').trim();
|
|
@@ -292,13 +329,13 @@ async function followVisiblePage(page, log) {
|
|
|
292
329
|
const r = b.getBoundingClientRect();
|
|
293
330
|
if (r.width < 8 || r.height < 8 || b.offsetParent === null) continue;
|
|
294
331
|
if (r.bottom < 0 || r.top > window.innerHeight) continue;
|
|
295
|
-
const art = b.closest(
|
|
332
|
+
const art = b.closest(UNIT);
|
|
296
333
|
if (art && /Được tài trợ|Sponsored/i.test((art.innerText || '').slice(0, 400))) continue; // not an ad's CTA
|
|
297
334
|
b.setAttribute('__nur_follow__', '1');
|
|
298
335
|
return label;
|
|
299
336
|
}
|
|
300
337
|
return null;
|
|
301
|
-
}).catch(() => null);
|
|
338
|
+
}, UNIT_SEL).catch(() => null);
|
|
302
339
|
if (!marked) return false;
|
|
303
340
|
let ok = false;
|
|
304
341
|
try {
|
|
@@ -362,6 +399,11 @@ async function run({ page, payload, log }) {
|
|
|
362
399
|
seenKeys.add(p.key);
|
|
363
400
|
if (!p.isAd) fresh++; // scrolling past an ad isn't reading
|
|
364
401
|
}
|
|
402
|
+
// debug:true (probe runs only) — dumps what the scan actually saw so a
|
|
403
|
+
// wrong selector / duplicate key shows up as data instead of a guess.
|
|
404
|
+
if (payload.debug && ticks % 3 === 0) {
|
|
405
|
+
log('info', `[nurture-fb][dbg] t${ticks} scroller=${scan.scroller}@${scan.scrollTop} winY=${scan.scrollY} sel=${scan.selector} units=${scan.articlesInDom} fresh=${fresh} keys=${JSON.stringify(scan.sample)}`);
|
|
406
|
+
}
|
|
365
407
|
postsSeen += fresh;
|
|
366
408
|
postsSinceLike += fresh;
|
|
367
409
|
// Feed that stops yielding posts for ~15 ticks = end of feed, a wall, or a
|
|
@@ -419,7 +461,7 @@ async function run({ page, payload, log }) {
|
|
|
419
461
|
// moment activity trips a threshold — better to stop than to keep poking.
|
|
420
462
|
if (ticks % 12 === 0) {
|
|
421
463
|
await assertAccountUsable(page, log);
|
|
422
|
-
log('info', `[nurture-fb] …${Math.round((Date.now() - t0) / 1000)}s: posts=${postsSeen} likes=${likes}/${likesTarget} videos=${videos} scrollY=${lastScan.scrollY}
|
|
464
|
+
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}`);
|
|
423
465
|
}
|
|
424
466
|
|
|
425
467
|
// Page refuses to move at all → this is not a nurture session, it's ten
|