channel-worker 2.5.47 → 2.5.48
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 +39 -16
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');
|
|
@@ -151,7 +173,8 @@ async function scanFeed(page, { skipKeys = [], wantLike = false } = {}) {
|
|
|
151
173
|
return {
|
|
152
174
|
posts, likeKey,
|
|
153
175
|
scrollY: Math.round(window.scrollY || document.documentElement.scrollTop || 0),
|
|
154
|
-
articlesInDom:
|
|
176
|
+
articlesInDom: units.length,
|
|
177
|
+
selector: sel || '(none)',
|
|
155
178
|
};
|
|
156
179
|
}, { skip: skipKeys, wantLike }).catch(() => ({ posts: [], likeKey: null, scrollY: 0, articlesInDom: 0 }));
|
|
157
180
|
}
|
|
@@ -184,12 +207,12 @@ async function clickMarkedLike(page, log) {
|
|
|
184
207
|
// Watch a video that's playing in the feed, counting only seconds where
|
|
185
208
|
// playback actually ADVANCES (a frozen/buffering player must not bank time).
|
|
186
209
|
async function watchFeedVideo(page, minSec, maxSec, log) {
|
|
187
|
-
const has = await page.evaluate(() => {
|
|
210
|
+
const has = await page.evaluate((UNIT) => {
|
|
188
211
|
const vids = [...document.querySelectorAll('video')];
|
|
189
212
|
for (const v of vids) {
|
|
190
213
|
const r = v.getBoundingClientRect();
|
|
191
214
|
if (r.height < 100 || r.bottom < 0 || r.top > window.innerHeight) continue;
|
|
192
|
-
const art = v.closest(
|
|
215
|
+
const art = v.closest(UNIT);
|
|
193
216
|
// Ads are re-detected from text — a node flag would be stale the moment
|
|
194
217
|
// FB recycles the article for a different post.
|
|
195
218
|
if (art && /Được tài trợ|Sponsored/i.test((art.innerText || '').slice(0, 400))) continue;
|
|
@@ -199,7 +222,7 @@ async function watchFeedVideo(page, minSec, maxSec, log) {
|
|
|
199
222
|
return true;
|
|
200
223
|
}
|
|
201
224
|
return false;
|
|
202
|
-
}).catch(() => false);
|
|
225
|
+
}, UNIT_SEL).catch(() => false);
|
|
203
226
|
if (!has) return false;
|
|
204
227
|
|
|
205
228
|
const budgetMs = randInt(minSec, maxSec) * 1000;
|
|
@@ -228,11 +251,11 @@ async function watchFeedVideo(page, minSec, maxSec, log) {
|
|
|
228
251
|
// Phase 2+. Best-effort: if the click doesn't navigate, nothing is lost.
|
|
229
252
|
async function openPostAndRead(page, log) {
|
|
230
253
|
const urlBefore = page.url();
|
|
231
|
-
const marked = await page.evaluate(() => {
|
|
254
|
+
const marked = await page.evaluate((UNIT) => {
|
|
232
255
|
const mid = window.innerHeight / 2;
|
|
233
256
|
let best = null, bestDist = Infinity;
|
|
234
|
-
for (const art of document.querySelectorAll(
|
|
235
|
-
if (art.parentElement && art.parentElement.closest(
|
|
257
|
+
for (const art of document.querySelectorAll(UNIT)) {
|
|
258
|
+
if (art.parentElement && art.parentElement.closest(UNIT)) continue;
|
|
236
259
|
// Node-level flags can't be trusted (FB recycles articles), so ads are
|
|
237
260
|
// re-checked from the text right here.
|
|
238
261
|
if (/Được tài trợ|Sponsored/i.test((art.innerText || '').slice(0, 400))) continue;
|
|
@@ -249,7 +272,7 @@ async function openPostAndRead(page, log) {
|
|
|
249
272
|
return true;
|
|
250
273
|
}
|
|
251
274
|
return false;
|
|
252
|
-
}).catch(() => false);
|
|
275
|
+
}, UNIT_SEL).catch(() => false);
|
|
253
276
|
if (!marked) return false;
|
|
254
277
|
|
|
255
278
|
try {
|
|
@@ -283,7 +306,7 @@ async function openPostAndRead(page, log) {
|
|
|
283
306
|
// Follow a page/creator surfaced by the feed itself (phase 3, ≤ config max).
|
|
284
307
|
// Only clicks a Follow/Like-Page button that's already on screen — never hunts.
|
|
285
308
|
async function followVisiblePage(page, log) {
|
|
286
|
-
const marked = await page.evaluate(() => {
|
|
309
|
+
const marked = await page.evaluate((UNIT) => {
|
|
287
310
|
for (const b of document.querySelectorAll("[role='button'], a[role='button']")) {
|
|
288
311
|
const t = (b.innerText || '').trim();
|
|
289
312
|
const aria = (b.getAttribute('aria-label') || '').trim();
|
|
@@ -292,13 +315,13 @@ async function followVisiblePage(page, log) {
|
|
|
292
315
|
const r = b.getBoundingClientRect();
|
|
293
316
|
if (r.width < 8 || r.height < 8 || b.offsetParent === null) continue;
|
|
294
317
|
if (r.bottom < 0 || r.top > window.innerHeight) continue;
|
|
295
|
-
const art = b.closest(
|
|
318
|
+
const art = b.closest(UNIT);
|
|
296
319
|
if (art && /Được tài trợ|Sponsored/i.test((art.innerText || '').slice(0, 400))) continue; // not an ad's CTA
|
|
297
320
|
b.setAttribute('__nur_follow__', '1');
|
|
298
321
|
return label;
|
|
299
322
|
}
|
|
300
323
|
return null;
|
|
301
|
-
}).catch(() => null);
|
|
324
|
+
}, UNIT_SEL).catch(() => null);
|
|
302
325
|
if (!marked) return false;
|
|
303
326
|
let ok = false;
|
|
304
327
|
try {
|
|
@@ -419,7 +442,7 @@ async function run({ page, payload, log }) {
|
|
|
419
442
|
// moment activity trips a threshold — better to stop than to keep poking.
|
|
420
443
|
if (ticks % 12 === 0) {
|
|
421
444
|
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}
|
|
445
|
+
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
446
|
}
|
|
424
447
|
|
|
425
448
|
// Page refuses to move at all → this is not a nurture session, it's ten
|