channel-worker 2.5.135 → 2.5.137
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/upload_facebook.js +264 -45
package/package.json
CHANGED
|
@@ -741,9 +741,26 @@ const NEW_COMPOSER_URL_RE = /^https?:\/\/(www\.)?facebook\.com\/(post\/create|re
|
|
|
741
741
|
function isNewComposerUrl(url) {
|
|
742
742
|
return NEW_COMPOSER_URL_RE.test(String(url || ''));
|
|
743
743
|
}
|
|
744
|
+
// /reel/ hoặc /reel/<id>: Facebook có lúc PHỦ hộp thoại composer lên feed
|
|
745
|
+
// Reels (10/09; Nuôi Gà Làm Giàu 23/09 13:16Z — đúng composer KIỂU CŨ "Cài đặt
|
|
746
|
+
// thước phim" + nút "Chỉnh sửa" trên ảnh video, ảnh dump lệnh 6ab3d12d). Trước
|
|
747
|
+
// đây cả hai khối ảnh bìa đều bỏ qua URL này → "Custom thumbnail required".
|
|
748
|
+
// Ở đây phải hỏi DOM xem là composer nào (xem vòng bước đăng).
|
|
749
|
+
const REEL_OVERLAY_URL_RE = /^https?:\/\/(www\.)?facebook\.com\/reel\/(\d+\/?)?($|\?|#)/i;
|
|
750
|
+
function isReelOverlayUrl(url) {
|
|
751
|
+
return REEL_OVERLAY_URL_RE.test(String(url || ''));
|
|
752
|
+
}
|
|
744
753
|
// Chữ trên ô "tải ảnh bìa của mình lên" — khớp NGUYÊN chuỗi (sau khi gộp
|
|
745
754
|
// khoảng trắng) để không dính caption hay nhãn mục "Hình thu nhỏ".
|
|
746
755
|
const THUMB_PICKER_TEXT_RE = /^(?:Chọn hình thu nhỏ|Chọn ảnh thu nhỏ|Thêm hình thu nhỏ|Tải hình thu nhỏ lên|(?:Choose|Select|Add|Upload) (?:a )?(?:custom )?thumbnail)$/i;
|
|
756
|
+
// Biến thể thứ ba (đo thật 23/09, page Gà Vườn Mỗi Ngày, wizard
|
|
757
|
+
// /reels/create/?surface=ADDL_PROFILE_PLUS): KHÔNG có hàng ô, ảnh bìa là một
|
|
758
|
+
// nút nhỏ 72x20 chữ "Ảnh bìa", aria-label "Chọn ảnh bìa", có ở cả bước
|
|
759
|
+
// "Chỉnh sửa" lẫn "Cài đặt". Tách regex riêng: nút này không nằm trong hàng ô
|
|
760
|
+
// nào, đi luồng riêng (applyCoverButtonThumb) và xác minh bằng nội dung ảnh.
|
|
761
|
+
const COVER_BUTTON_RE = /^(?:Chọn ảnh bìa|(?:Choose|Select|Add|Edit) (?:a )?cover(?: photo| image)?)$/i;
|
|
762
|
+
// Tiêu đề hộp thoại có thể mở ra từ nút đó — chưa thấy tận mắt, nhận rộng.
|
|
763
|
+
const COVER_HEAD_RE = /^(?:Chọn ảnh bìa|Ảnh bìa|Chỉnh sửa ảnh bìa|(?:Choose |Select |Edit )?(?:a )?cover(?: photo| image)?)$/i;
|
|
747
764
|
|
|
748
765
|
// Đọc hàng ô ảnh bìa của composer mới. Trả về ảnh chụp trạng thái để so
|
|
749
766
|
// trước/sau khi dán (xem newComposerThumbVerdict), và gắn cờ DOM cho:
|
|
@@ -753,7 +770,7 @@ const THUMB_PICKER_TEXT_RE = /^(?:Chọn hình thu nhỏ|Chọn ảnh thu nhỏ|
|
|
|
753
770
|
// __fbpw_thumb_tile__ — ô thứ `tagIndex` (dùng khi phải bấm chọn lại)
|
|
754
771
|
// Không bao giờ ném: đọc hỏng trả null, như isCustomThumbApplied.
|
|
755
772
|
async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {}) {
|
|
756
|
-
return page.evaluate(({ pickSrc, diag, tagIndex }) => {
|
|
773
|
+
return page.evaluate(({ pickSrc, coverSrc, diag, tagIndex }) => {
|
|
757
774
|
const PICK = new RegExp(pickSrc, 'i');
|
|
758
775
|
const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim();
|
|
759
776
|
const vis = (el) => {
|
|
@@ -880,6 +897,23 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
|
|
|
880
897
|
}
|
|
881
898
|
const customMarker = [...document.querySelectorAll('[aria-label]')].some((el) =>
|
|
882
899
|
/Gỡ hình thu nhỏ tùy chỉnh|Remove( your)? custom thumbnail/i.test(el.getAttribute('aria-label') || '') && vis(el));
|
|
900
|
+
if (!row && !pickLeaf) {
|
|
901
|
+
// Không có ô "Chọn hình thu nhỏ" → thử nút "Chọn ảnh bìa" (biến thể wizard).
|
|
902
|
+
const COVER = new RegExp(coverSrc, 'i');
|
|
903
|
+
for (const root of roots) {
|
|
904
|
+
const btn = [...root.querySelectorAll('[aria-label]')].find((el) => vis(el)
|
|
905
|
+
&& COVER.test(norm(el.getAttribute('aria-label'))) && !el.closest('[__fbpw_thumb_modal__]'));
|
|
906
|
+
if (!btn) continue;
|
|
907
|
+
let target = btn;
|
|
908
|
+
for (let n = btn, d = 0; n && d < 3; n = n.parentElement, d++) {
|
|
909
|
+
if (n.matches("[role='button'], button, a, [tabindex]")) { target = n; break; }
|
|
910
|
+
}
|
|
911
|
+
target.setAttribute('__fbpw_thumb_pick__', '1');
|
|
912
|
+
const r = btn.getBoundingClientRect();
|
|
913
|
+
return { found: false, pickerPresent: true, mode: 'cover-button', roots: roots.length, customMarker,
|
|
914
|
+
cover: { aria: norm(btn.getAttribute('aria-label')), text: norm(btn.innerText).slice(0, 30), size: `${Math.round(r.width)}x${Math.round(r.height)}` } };
|
|
915
|
+
}
|
|
916
|
+
}
|
|
883
917
|
if (!row) {
|
|
884
918
|
const miss = { found: false, pickerPresent: !!pickLeaf, roots: roots.length, customMarker };
|
|
885
919
|
if (diag) {
|
|
@@ -954,7 +988,7 @@ async function readNewComposerThumbRow(page, { diag = false, tagIndex = -1 } = {
|
|
|
954
988
|
});
|
|
955
989
|
}
|
|
956
990
|
return res;
|
|
957
|
-
}, { pickSrc: THUMB_PICKER_TEXT_RE.source, diag, tagIndex }).catch(() => null);
|
|
991
|
+
}, { pickSrc: THUMB_PICKER_TEXT_RE.source, coverSrc: COVER_BUTTON_RE.source, diag, tagIndex }).catch(() => null);
|
|
958
992
|
}
|
|
959
993
|
|
|
960
994
|
// Ảnh bìa tự chọn đã VÀO composer mới chưa? So hàng ô trước và sau khi dán
|
|
@@ -997,6 +1031,7 @@ function newComposerThumbVerdict(before, after) {
|
|
|
997
1031
|
// Tóm tắt một hàng ô cho log: "5 ô [chọn-ảnh, *khung, khung…] chọn=1 input=image/*".
|
|
998
1032
|
function describeThumbRow(snap) {
|
|
999
1033
|
if (!snap) return 'không đọc được';
|
|
1034
|
+
if (snap.mode === 'cover-button') return `không có hàng ô — nút ảnh bìa "${snap.cover.text || snap.cover.aria}" (aria "${snap.cover.aria}", ${snap.cover.size})`;
|
|
1000
1035
|
if (!snap.found) return `không thấy hàng ô (composer=${snap.roots || 0}, ô chọn-ảnh=${snap.pickerPresent ? 'có' : 'không'})`;
|
|
1001
1036
|
const cells = (snap.tiles || []).map((t) => `${t.selected ? '*' : ''}${t.placeholder ? 'chọn-ảnh' : (t.img ? (/^(blob|data):/.test(t.img) ? 'ảnh-local' : 'khung') : '?')}`);
|
|
1002
1037
|
return `${cells.length} ô [${cells.join(', ')}] chọn=${snap.selectedIndex ?? '?'} input=${snap.input ? snap.input.accept || '?' : 'không'} via=${snap.via}`;
|
|
@@ -1047,8 +1082,10 @@ async function confirmNewComposerThumbDialog(page, log) {
|
|
|
1047
1082
|
// Gắn cờ: __fbpw_thumb_modal__ (khối), _upload__ (nút Tải lên), _save__ (mọi
|
|
1048
1083
|
// nút Lưu, đánh số từ cuối), _input__ (input ảnh). Trả null nếu không thấy.
|
|
1049
1084
|
async function findThumbPickerModal(page) {
|
|
1050
|
-
return page.evaluate(({ pickSrc }) => {
|
|
1085
|
+
return page.evaluate(({ pickSrc, coverHeadSrc }) => {
|
|
1051
1086
|
const PICK = new RegExp(pickSrc, 'i');
|
|
1087
|
+
const COVER_HEAD = new RegExp(coverHeadSrc, 'i');
|
|
1088
|
+
const isHead = (t) => PICK.test(t) || COVER_HEAD.test(t);
|
|
1052
1089
|
const norm = (s) => String(s || '').replace(/\s+/g, ' ').trim();
|
|
1053
1090
|
const vis = (el) => {
|
|
1054
1091
|
const r = el.getBoundingClientRect();
|
|
@@ -1061,17 +1098,18 @@ async function findThumbPickerModal(page) {
|
|
|
1061
1098
|
}
|
|
1062
1099
|
const says = (el, re) => re.test(norm(el.getAttribute('aria-label'))) || re.test(norm(el.innerText));
|
|
1063
1100
|
const UP = /^(Tải lên|Tải ảnh lên|Upload|Upload image)$/i;
|
|
1064
|
-
const SAVE = /^(Lưu|Save)$/i;
|
|
1101
|
+
const SAVE = /^(Lưu|Save|Xong|Done|Áp dụng|Apply)$/i;
|
|
1065
1102
|
const POST = /^(Đăng|Post|Share now)$/i;
|
|
1066
|
-
const heads = [...document.querySelectorAll('[aria-label]')].filter((el) =>
|
|
1103
|
+
const heads = [...document.querySelectorAll('[aria-label]')].filter((el) => isHead(norm(el.getAttribute('aria-label'))) && vis(el));
|
|
1067
1104
|
const tw = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
1068
1105
|
for (let n = tw.nextNode(); n; n = tw.nextNode()) {
|
|
1069
|
-
if (!/hình|thumbnail/i.test(n.data)) continue;
|
|
1106
|
+
if (!/hình|thumbnail|ảnh bìa|cover/i.test(n.data)) continue;
|
|
1070
1107
|
const el = n.parentElement;
|
|
1071
|
-
if (el && vis(el) &&
|
|
1108
|
+
if (el && vis(el) && isHead(norm(el.innerText))) heads.push(el);
|
|
1072
1109
|
}
|
|
1073
1110
|
for (const h of heads) {
|
|
1074
1111
|
if (h.closest('[__fbpw_thumb_row__]')) continue; // chính ô chọn-ảnh trong hàng
|
|
1112
|
+
if (h.closest('[__fbpw_thumb_pick__]')) continue; // chính nút "Chọn ảnh bìa"
|
|
1075
1113
|
let box = h;
|
|
1076
1114
|
for (let d = 0; d < 10 && box && box !== document.body; d++, box = box.parentElement) {
|
|
1077
1115
|
const btns = [...box.querySelectorAll("[role='button'], button")].filter(vis);
|
|
@@ -1102,7 +1140,7 @@ async function findThumbPickerModal(page) {
|
|
|
1102
1140
|
}
|
|
1103
1141
|
}
|
|
1104
1142
|
return null;
|
|
1105
|
-
}, { pickSrc: THUMB_PICKER_TEXT_RE.source }).catch(() => null);
|
|
1143
|
+
}, { pickSrc: THUMB_PICKER_TEXT_RE.source, coverHeadSrc: COVER_HEAD_RE.source }).catch(() => null);
|
|
1106
1144
|
}
|
|
1107
1145
|
|
|
1108
1146
|
// Ảnh xem trước trong hộp thoại có đúng là ảnh bìa của MÌNH không? Thu cả hai
|
|
@@ -1195,13 +1233,18 @@ async function uploadViaThumbPickerModal(page, thumbPath, log, modal, { previewM
|
|
|
1195
1233
|
while (Date.now() - t0 < previewMs) {
|
|
1196
1234
|
await pause(page, 1500);
|
|
1197
1235
|
m = await findThumbPickerModal(page);
|
|
1198
|
-
if (!m) return { ok: true, via, reason: 'hộp thoại tự đóng sau khi nhận ảnh' };
|
|
1236
|
+
if (!m) return { ok: true, via, verified: false, reason: 'hộp thoại tự đóng sau khi nhận ảnh' };
|
|
1199
1237
|
if (!m.preview || m.preview === beforePreview) continue;
|
|
1200
1238
|
// URL đổi rồi — so NỘI DUNG. Lệch nhiều = FB đổi khung chứ chưa phải ảnh
|
|
1201
1239
|
// của mình: chờ tiếp. Không so được (ảnh CDN khoá canvas) thì rơi về luật
|
|
1202
1240
|
// đổi-URL, kèm ghi rõ vào log. Ảnh chưa nạp xong thì chờ nhịp sau.
|
|
1203
1241
|
lastCmp = await previewMatchesThumb(page, thumbDataUrl);
|
|
1204
1242
|
if (lastCmp.err === 'preview-not-loaded') continue;
|
|
1243
|
+
if (lastCmp.err === 'tainted') {
|
|
1244
|
+
// Ảnh xem trước từ CDN → tải về phía Node mà so, không đoán theo URL.
|
|
1245
|
+
const rm = await remoteImagesMatchThumb(page, [m.preview], thumbDataUrl);
|
|
1246
|
+
if (rm) lastCmp = { diff: rm.diff, via: 'cdn' };
|
|
1247
|
+
}
|
|
1205
1248
|
if (lastCmp.diff !== undefined && lastCmp.diff > THUMB_PREVIEW_MAX_DIFF) {
|
|
1206
1249
|
if (!loggedMismatch) log('info', `[fb-pw] composer mới — ảnh xem trước đổi nhưng KHÔNG giống ảnh bìa của mình (lệch ${lastCmp.diff}/255) — chờ tiếp`);
|
|
1207
1250
|
loggedMismatch = true;
|
|
@@ -1232,23 +1275,203 @@ async function uploadViaThumbPickerModal(page, thumbPath, log, modal, { previewM
|
|
|
1232
1275
|
while (Date.now() - t1 < closeMs) {
|
|
1233
1276
|
await pause(page, 1500);
|
|
1234
1277
|
if (await findThumbPickerModal(page)) { gone = 0; continue; }
|
|
1235
|
-
if (++gone >= 2) return { ok: true, via };
|
|
1278
|
+
if (++gone >= 2) return { ok: true, via, verified: !!(lastCmp && lastCmp.diff !== undefined) };
|
|
1236
1279
|
}
|
|
1237
1280
|
log('warn', `[fb-pw] composer mới — hộp thoại "Chọn hình thu nhỏ" chưa đóng ${Math.round(closeMs / 1000)}s sau khi bấm Lưu`);
|
|
1238
1281
|
return { ok: false, reason: 'modal-not-closed' };
|
|
1239
1282
|
}
|
|
1240
1283
|
|
|
1284
|
+
// Bấm nút/ô chọn ảnh bìa (đã gắn cờ __fbpw_thumb_pick__) rồi chờ MỘT trong
|
|
1285
|
+
// hai: hộp chọn file (biến thể mở thẳng) hoặc hộp thoại chọn ảnh (biến thể đo
|
|
1286
|
+
// thật 23/09). Trả { via, verified, failed, reason }: `via` rỗng = chưa đưa được
|
|
1287
|
+
// file vào (người gọi còn đường dự phòng); `failed` = hộp thoại đã mở mà dán hỏng.
|
|
1288
|
+
async function clickPickerAndUpload(page, thumbPath, log) {
|
|
1289
|
+
let chooser = null;
|
|
1290
|
+
const chooserP = page.waitForEvent('filechooser', { timeout: 8000 }).then((c) => { chooser = c; }).catch(() => {});
|
|
1291
|
+
let clickErr = '';
|
|
1292
|
+
await page.locator("[__fbpw_thumb_pick__='1']").first().click({ timeout: 5000 }).catch((e) => { clickErr = String(e && e.message || e).slice(0, 80); });
|
|
1293
|
+
let modal = null;
|
|
1294
|
+
const tClick = Date.now();
|
|
1295
|
+
while (!chooser && !modal && Date.now() - tClick < 8000) {
|
|
1296
|
+
await pause(page, 800);
|
|
1297
|
+
modal = await findThumbPickerModal(page);
|
|
1298
|
+
}
|
|
1299
|
+
void chooserP;
|
|
1300
|
+
if (chooser) {
|
|
1301
|
+
try {
|
|
1302
|
+
await chooser.setFiles(thumbPath);
|
|
1303
|
+
return { via: 'hộp chọn file', verified: false };
|
|
1304
|
+
} catch (e) {
|
|
1305
|
+
log('info', `[fb-pw] composer mới — hộp chọn file từ chối ảnh: ${String(e && e.message || e).slice(0, 80)}`);
|
|
1306
|
+
return { via: '' };
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (modal) {
|
|
1310
|
+
const r = await uploadViaThumbPickerModal(page, thumbPath, log, modal);
|
|
1311
|
+
if (!r.ok) {
|
|
1312
|
+
log('warn', `[fb-pw] composer mới — dán qua hộp thoại chọn ảnh bìa KHÔNG xong (${r.reason})`);
|
|
1313
|
+
return { via: '', failed: true, reason: r.reason };
|
|
1314
|
+
}
|
|
1315
|
+
return { via: r.via + (r.reason ? ` (${r.reason})` : ''), verified: !!r.verified };
|
|
1316
|
+
}
|
|
1317
|
+
log('info', `[fb-pw] composer mới — bấm chỗ chọn ảnh bìa không mở hộp chọn file lẫn hộp thoại${clickErr ? `: ${clickErr}` : ''}`);
|
|
1318
|
+
return { via: '' };
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// So ảnh nạp từ CDN của Facebook (khác nguồn → canvas bị khoá) với ảnh bìa:
|
|
1322
|
+
// tải bytes bằng page.request phía Node (không qua CORS), đưa lại vào trang
|
|
1323
|
+
// dưới dạng data: URL (cùng nguồn) rồi so 16x16 như previewMatchesThumb. Cần
|
|
1324
|
+
// cho lượt Gà Vườn 23/09 12:55Z: hộp thoại nhận ảnh xong, composer hiện ảnh bìa
|
|
1325
|
+
// bằng URL scontent → "đọc được 0 ảnh, khoá 4" → không so được → chặn oan.
|
|
1326
|
+
// `cache` (Map src→diff) để vòng chờ không tải lại cùng một ảnh mỗi nhịp.
|
|
1327
|
+
async function remoteImagesMatchThumb(page, srcs, thumbDataUrl, cache = new Map()) {
|
|
1328
|
+
let best = null;
|
|
1329
|
+
for (const src of [...new Set(srcs || [])].slice(0, 6)) {
|
|
1330
|
+
if (!/^https?:/i.test(src)) continue;
|
|
1331
|
+
let diff = cache.has(src) ? cache.get(src) : undefined;
|
|
1332
|
+
if (diff === undefined) {
|
|
1333
|
+
diff = null;
|
|
1334
|
+
try {
|
|
1335
|
+
const resp = await page.request.get(src, { timeout: 10_000 });
|
|
1336
|
+
const buf = resp.ok() ? await resp.body() : null;
|
|
1337
|
+
if (buf && buf.length && buf.length < 8 * 1024 * 1024) {
|
|
1338
|
+
const ct = String(resp.headers()['content-type'] || 'image/jpeg').split(';')[0];
|
|
1339
|
+
diff = await page.evaluate(async ({ a, b }) => {
|
|
1340
|
+
const load = (u) => new Promise((res) => { const i = new Image(); i.onload = () => res(i); i.onerror = () => res(null); i.src = u; });
|
|
1341
|
+
const [ia, ib] = await Promise.all([load(a), load(b)]);
|
|
1342
|
+
if (!ia || !ib) return null;
|
|
1343
|
+
const sig = (img) => {
|
|
1344
|
+
const c = document.createElement('canvas');
|
|
1345
|
+
c.width = 16; c.height = 16;
|
|
1346
|
+
const x = c.getContext('2d');
|
|
1347
|
+
x.drawImage(img, 0, 0, 16, 16);
|
|
1348
|
+
return x.getImageData(0, 0, 16, 16).data;
|
|
1349
|
+
};
|
|
1350
|
+
const p = sig(ia), q = sig(ib);
|
|
1351
|
+
let d = 0;
|
|
1352
|
+
for (let k = 0; k < p.length; k += 4) d += Math.abs(p[k] - q[k]) + Math.abs(p[k + 1] - q[k + 1]) + Math.abs(p[k + 2] - q[k + 2]);
|
|
1353
|
+
return Math.round(d / (256 * 3));
|
|
1354
|
+
}, { a: `data:${ct};base64,${buf.toString('base64')}`, b: thumbDataUrl }).catch(() => null);
|
|
1355
|
+
}
|
|
1356
|
+
} catch { /* tải hỏng = không tính ảnh này */ }
|
|
1357
|
+
cache.set(src, diff);
|
|
1358
|
+
}
|
|
1359
|
+
if (diff !== null && diff !== undefined && (!best || diff < best.diff)) best = { diff, src: src.slice(0, 80) };
|
|
1360
|
+
}
|
|
1361
|
+
return best;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Ảnh bìa của MÌNH có đang hiện trong composer không? Quét mọi ảnh ≥40px trong
|
|
1365
|
+
// composer (dialog Reel + vùng main của trang tạo), trừ hộp thoại chọn ảnh, so
|
|
1366
|
+
// nội dung 16x16 với file ảnh bìa như previewMatchesThumb. Dùng cho biến thể nút
|
|
1367
|
+
// "Chọn ảnh bìa": không có hàng ô để so trước/sau, nên bằng chứng duy nhất đứng
|
|
1368
|
+
// vững là thấy chính ảnh đó. Ảnh CDN khác nguồn khoá canvas → không tính.
|
|
1369
|
+
async function composerShowsThumb(page, thumbDataUrl) {
|
|
1370
|
+
return page.evaluate(async ({ ours }) => {
|
|
1371
|
+
const vis = (el) => {
|
|
1372
|
+
const r = el.getBoundingClientRect();
|
|
1373
|
+
if (r.width < 8 || r.height < 8) return false;
|
|
1374
|
+
const cs = getComputedStyle(el);
|
|
1375
|
+
return cs.visibility !== 'hidden' && cs.display !== 'none' && cs.opacity !== '0';
|
|
1376
|
+
};
|
|
1377
|
+
const roots = [...document.querySelectorAll("[role='dialog']")].filter((d) => vis(d)
|
|
1378
|
+
&& /thước phim|reel/i.test(`${d.getAttribute('aria-label') || ''}\n${(d.innerText || '').slice(0, 600)}`));
|
|
1379
|
+
const main = document.querySelector("[role='main'], main");
|
|
1380
|
+
if (main && vis(main)) roots.push(main);
|
|
1381
|
+
const sig = (img) => {
|
|
1382
|
+
const c = document.createElement('canvas');
|
|
1383
|
+
c.width = 16; c.height = 16;
|
|
1384
|
+
const x = c.getContext('2d');
|
|
1385
|
+
x.drawImage(img, 0, 0, 16, 16);
|
|
1386
|
+
return x.getImageData(0, 0, 16, 16).data;
|
|
1387
|
+
};
|
|
1388
|
+
const mine = await new Promise((res) => { const i = new Image(); i.onload = () => res(i); i.onerror = () => res(null); i.src = ours; });
|
|
1389
|
+
if (!mine) return { err: 'thumb-not-decodable' };
|
|
1390
|
+
const b = sig(mine);
|
|
1391
|
+
const seen = new Set();
|
|
1392
|
+
let readable = 0, locked = 0, best = null;
|
|
1393
|
+
const lockedSrcs = [];
|
|
1394
|
+
for (const root of roots) {
|
|
1395
|
+
for (const im of root.querySelectorAll('img')) {
|
|
1396
|
+
if (seen.has(im) || im.closest('[__fbpw_thumb_modal__]')) continue;
|
|
1397
|
+
seen.add(im);
|
|
1398
|
+
const r = im.getBoundingClientRect();
|
|
1399
|
+
if (r.width < 40 || r.height < 40 || !vis(im) || !im.complete || !im.naturalWidth) continue;
|
|
1400
|
+
let a;
|
|
1401
|
+
try { a = sig(im); } catch { locked++; lockedSrcs.push(String(im.currentSrc || im.src || '')); continue; }
|
|
1402
|
+
readable++;
|
|
1403
|
+
let d = 0;
|
|
1404
|
+
for (let k = 0; k < a.length; k += 4) d += Math.abs(a[k] - b[k]) + Math.abs(a[k + 1] - b[k + 1]) + Math.abs(a[k + 2] - b[k + 2]);
|
|
1405
|
+
d = Math.round(d / (256 * 3));
|
|
1406
|
+
if (!best || d < best.diff) best = { diff: d, size: `${Math.round(r.width)}x${Math.round(r.height)}` };
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return { readable, locked, best, lockedSrcs: lockedSrcs.slice(0, 8) };
|
|
1410
|
+
}, { ours: thumbDataUrl }).catch((e) => ({ err: String(e && e.message || e).slice(0, 60) }));
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// Biến thể nút "Chọn ảnh bìa" (wizard, không có hàng ô). Chỉ nhận là đã dán khi:
|
|
1414
|
+
// a) hộp thoại chọn ảnh đã cho thấy ĐÚNG ảnh của mình (so nội dung) rồi mới
|
|
1415
|
+
// Lưu và đóng, hoặc
|
|
1416
|
+
// b) sau khi dán, composer hiện một ảnh giống ảnh bìa của mình (lệch ≤ ngưỡng)
|
|
1417
|
+
// hai nhịp liền, hoặc nút "Gỡ hình thu nhỏ tùy chỉnh" hiện.
|
|
1418
|
+
// Không thì trả không-dán, cổng trước Đăng chặn như mọi biến thể khác.
|
|
1419
|
+
async function applyCoverButtonThumb(page, thumbPath, log, snap, { verifyMs = 30_000 } = {}) {
|
|
1420
|
+
log('info', `[fb-pw] composer mới — ${describeThumbRow(snap)} → dán ảnh bìa qua nút này`);
|
|
1421
|
+
let thumbDataUrl = '';
|
|
1422
|
+
try { thumbDataUrl = `data:image/png;base64,${fs.readFileSync(thumbPath).toString('base64')}`; } catch { /* composerShowsThumb sẽ báo thumb-not-decodable */ }
|
|
1423
|
+
const base = await composerShowsThumb(page, thumbDataUrl);
|
|
1424
|
+
const up = await clickPickerAndUpload(page, thumbPath, log);
|
|
1425
|
+
if (up.failed || !up.via) {
|
|
1426
|
+
const d = await readNewComposerThumbRow(page, { diag: true });
|
|
1427
|
+
log('warn', `[fb-pw] composer mới — nút ảnh bìa: KHÔNG đưa được file vào (${up.reason || 'không mở được chỗ chọn ảnh'})${d && d.inventory ? ` nút=${JSON.stringify(d.inventory)}` : ''}`);
|
|
1428
|
+
return { applied: false, reason: up.reason || 'no-upload' };
|
|
1429
|
+
}
|
|
1430
|
+
if (up.verified) {
|
|
1431
|
+
log('info', `[fb-pw] composer mới — ảnh bìa đã vào qua ${up.via}: hộp thoại cho thấy đúng ảnh của mình rồi mới Lưu`);
|
|
1432
|
+
return { applied: true, reason: 'hộp thoại xác nhận đúng ảnh' };
|
|
1433
|
+
}
|
|
1434
|
+
log('info', `[fb-pw] composer mới — đã đưa file ảnh bìa vào qua ${up.via}, tìm chính ảnh đó trong composer…`);
|
|
1435
|
+
let streak = 0;
|
|
1436
|
+
let scan = null;
|
|
1437
|
+
let remote = null;
|
|
1438
|
+
const remoteCache = new Map();
|
|
1439
|
+
const deadline = Date.now() + verifyMs;
|
|
1440
|
+
while (Date.now() < deadline) {
|
|
1441
|
+
await pause(page, 2500);
|
|
1442
|
+
if (await isCustomThumbApplied(page)) { if (++streak >= 2) break; continue; }
|
|
1443
|
+
scan = await composerShowsThumb(page, thumbDataUrl);
|
|
1444
|
+
if (scan.best && scan.best.diff <= THUMB_PREVIEW_MAX_DIFF) { if (++streak >= 2) break; continue; }
|
|
1445
|
+
// Ảnh CDN khoá canvas → tải về phía Node mà so (xem remoteImagesMatchThumb).
|
|
1446
|
+
if (scan.lockedSrcs && scan.lockedSrcs.length) {
|
|
1447
|
+
remote = await remoteImagesMatchThumb(page, scan.lockedSrcs, thumbDataUrl, remoteCache);
|
|
1448
|
+
if (remote && remote.diff <= THUMB_PREVIEW_MAX_DIFF) { if (++streak >= 2) break; continue; }
|
|
1449
|
+
}
|
|
1450
|
+
streak = 0;
|
|
1451
|
+
}
|
|
1452
|
+
const fmt = (x) => (x && !x.err ? `đọc được ${x.readable} ảnh, khoá ${x.locked}, gần nhất lệch ${x.best ? `${x.best.diff}/255 (${x.best.size})` : '—'}${x === scan && remote ? `, ảnh CDN tải về gần nhất lệch ${remote.diff}/255` : ''}` : `lỗi ${x && x.err}`);
|
|
1453
|
+
if (streak >= 2) {
|
|
1454
|
+
log('info', `[fb-pw] composer mới — ảnh bìa đã vào: composer hiện đúng ảnh của mình (${fmt(scan)}; trước khi dán: ${fmt(base)})`);
|
|
1455
|
+
return { applied: true, reason: 'composer hiện đúng ảnh' };
|
|
1456
|
+
}
|
|
1457
|
+
const d = await readNewComposerThumbRow(page, { diag: true });
|
|
1458
|
+
log('warn', `[fb-pw] composer mới — nút ảnh bìa: KHÔNG thấy ảnh của mình trong composer sau ${Math.round(verifyMs / 1000)}s (${fmt(scan)}; trước khi dán: ${fmt(base)})${d && d.inventory ? ` nút=${JSON.stringify(d.inventory)}` : ''}`);
|
|
1459
|
+
return { applied: false, reason: 'cover-not-shown' };
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1241
1462
|
// Dán ảnh bìa vào composer mới. Trả { applied, reason }. Không ném: thất bại
|
|
1242
1463
|
// thì cờ customThumbDone không bật và cổng trước Đăng chặn như cũ.
|
|
1243
1464
|
async function applyNewComposerThumb(page, thumbPath, log, { waitPickerMs = 20_000, verifyMs = 30_000 } = {}) {
|
|
1244
1465
|
const t0 = Date.now();
|
|
1245
1466
|
const ready = (s) => !!(s && s.found && s.pickerPresent);
|
|
1467
|
+
const coverMode = (s) => !!(s && s.mode === 'cover-button');
|
|
1246
1468
|
let snap = await readNewComposerThumbRow(page);
|
|
1247
1469
|
// Hàng ảnh bìa chỉ hiện sau khi FB cắt xong khung từ video.
|
|
1248
|
-
while (!ready(snap) && Date.now() - t0 < waitPickerMs) {
|
|
1470
|
+
while (!ready(snap) && !coverMode(snap) && Date.now() - t0 < waitPickerMs) {
|
|
1249
1471
|
await pause(page, 2500);
|
|
1250
1472
|
snap = await readNewComposerThumbRow(page);
|
|
1251
1473
|
}
|
|
1474
|
+
if (coverMode(snap)) return applyCoverButtonThumb(page, thumbPath, log, snap, { verifyMs });
|
|
1252
1475
|
if (!ready(snap)) {
|
|
1253
1476
|
const d = await readNewComposerThumbRow(page, { diag: true });
|
|
1254
1477
|
log('info', `[fb-pw] composer mới — chưa thấy ô "Chọn hình thu nhỏ" sau ${Math.round((Date.now() - t0) / 1000)}s: ${describeThumbRow(d)}${d && d.skeleton ? ` skeleton=${JSON.stringify(d.skeleton)}` : ''}${d && d.inventory ? ` nút=${JSON.stringify(d.inventory)}` : ''}`);
|
|
@@ -1267,34 +1490,9 @@ async function applyNewComposerThumb(page, thumbPath, log, { waitPickerMs = 20_0
|
|
|
1267
1490
|
const before = snap;
|
|
1268
1491
|
log('info', `[fb-pw] composer mới — hàng ảnh bìa trước khi dán: ${describeThumbRow(before)}`);
|
|
1269
1492
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
let chooser = null;
|
|
1274
|
-
const chooserP = page.waitForEvent('filechooser', { timeout: 8000 }).then((c) => { chooser = c; }).catch(() => {});
|
|
1275
|
-
let clickErr = '';
|
|
1276
|
-
await page.locator("[__fbpw_thumb_pick__='1']").first().click({ timeout: 5000 }).catch((e) => { clickErr = String(e && e.message || e).slice(0, 80); });
|
|
1277
|
-
let modal = null;
|
|
1278
|
-
const tClick = Date.now();
|
|
1279
|
-
while (!chooser && !modal && Date.now() - tClick < 8000) {
|
|
1280
|
-
await pause(page, 800);
|
|
1281
|
-
modal = await findThumbPickerModal(page);
|
|
1282
|
-
}
|
|
1283
|
-
if (chooser) {
|
|
1284
|
-
await chooser.setFiles(thumbPath).then(() => { via = 'hộp chọn file'; })
|
|
1285
|
-
.catch((e) => log('info', `[fb-pw] composer mới — hộp chọn file từ chối ảnh: ${String(e && e.message || e).slice(0, 80)}`));
|
|
1286
|
-
} else if (modal) {
|
|
1287
|
-
const r = await uploadViaThumbPickerModal(page, thumbPath, log, modal);
|
|
1288
|
-
if (!r.ok) {
|
|
1289
|
-
const d = await readNewComposerThumbRow(page, { diag: true });
|
|
1290
|
-
log('warn', `[fb-pw] composer mới — dán qua hộp thoại "Chọn hình thu nhỏ" KHÔNG xong (${r.reason}) — hàng ô: ${describeThumbRow(d)}`);
|
|
1291
|
-
return { applied: false, reason: r.reason };
|
|
1292
|
-
}
|
|
1293
|
-
via = r.via + (r.reason ? ` (${r.reason})` : '');
|
|
1294
|
-
} else {
|
|
1295
|
-
log('info', `[fb-pw] composer mới — bấm ô "Chọn hình thu nhỏ" không mở hộp chọn file lẫn hộp thoại${clickErr ? `: ${clickErr}` : ''}`);
|
|
1296
|
-
}
|
|
1297
|
-
void chooserP;
|
|
1493
|
+
const up = await clickPickerAndUpload(page, thumbPath, log);
|
|
1494
|
+
let via = up.via;
|
|
1495
|
+
if (up.failed) return { applied: false, reason: up.reason };
|
|
1298
1496
|
if (!via) {
|
|
1299
1497
|
await readNewComposerThumbRow(page); // cú bấm có thể làm FB dựng lại DOM → gắn cờ lại
|
|
1300
1498
|
const input = page.locator("[__fbpw_thumb_input__='1']").first();
|
|
@@ -1861,7 +2059,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
1861
2059
|
} = payload || {};
|
|
1862
2060
|
if (!video_url) throw new Error('No video_url provided');
|
|
1863
2061
|
|
|
1864
|
-
log('info', '[fb-pw] selectors version=2026.09.23-
|
|
2062
|
+
log('info', '[fb-pw] selectors version=2026.09.23-reel-overlay-cdn');
|
|
1865
2063
|
|
|
1866
2064
|
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
1867
2065
|
|
|
@@ -2730,7 +2928,16 @@ async function runOnce({ page, payload, log }) {
|
|
|
2730
2928
|
// thu nhỏ" trong hàng khung, không có nút "Chỉnh sửa hình thu nhỏ". Xem
|
|
2731
2929
|
// applyNewComposerThumb. Chạy ở mọi bước cho tới khi dán được: form một
|
|
2732
2930
|
// trang chỉ có một bước, wizard có thể để hàng ô ở bước sau.
|
|
2733
|
-
|
|
2931
|
+
// Ở /reel/ (composer phủ lên feed Reels) hỏi DOM: có ô "Chọn hình thu
|
|
2932
|
+
// nhỏ"/nút "Chọn ảnh bìa" thì là composer mới, không thì khối cũ lo.
|
|
2933
|
+
const onReelOverlay = isReelOverlayUrl(page.url());
|
|
2934
|
+
let reelOverlayNew = false;
|
|
2935
|
+
if (!customThumbDone && thumbPath && onReelOverlay) {
|
|
2936
|
+
const probe = await readNewComposerThumbRow(page);
|
|
2937
|
+
reelOverlayNew = !!(probe && probe.pickerPresent);
|
|
2938
|
+
log('info', `[fb-pw] composer phủ trên /reel/ — ${reelOverlayNew ? 'có dấu hiệu composer mới → luồng mới' : 'composer kiểu cũ → khối ảnh bìa cũ'}`);
|
|
2939
|
+
}
|
|
2940
|
+
if (!customThumbDone && thumbPath && (isNewComposerUrl(page.url()) || reelOverlayNew)) {
|
|
2734
2941
|
const nt = await applyNewComposerThumb(page, thumbPath, log);
|
|
2735
2942
|
if (nt.applied) customThumbDone = true;
|
|
2736
2943
|
}
|
|
@@ -2739,8 +2946,8 @@ async function runOnce({ page, payload, log }) {
|
|
|
2739
2946
|
// video preview → opens "Chỉnh sửa hình thu nhỏ" modal → "Tải lên" →
|
|
2740
2947
|
// file picker → "Lưu"). Per user screenshots, this is the only way to
|
|
2741
2948
|
// attach a custom thumbnail in the Page-wall composer.
|
|
2742
|
-
if (!customThumbDone && thumbPath && /^https?:\/\/(www\.)?facebook\.com\/?($|\?|#)/.test(page.url())) {
|
|
2743
|
-
const findThumbBtn = () => page.evaluate(() => {
|
|
2949
|
+
if (!customThumbDone && thumbPath && (/^https?:\/\/(www\.)?facebook\.com\/?($|\?|#)/.test(page.url()) || (onReelOverlay && !reelOverlayNew))) {
|
|
2950
|
+
const findThumbBtn = () => page.evaluate((guardNew) => {
|
|
2744
2951
|
// Look for the thumbnail-edit button inside the REEL COMPOSER dialog
|
|
2745
2952
|
// only — scanning all dialogs risked matching a button on the
|
|
2746
2953
|
// page-wall photo composer (which would post a separate "Tin dạng
|
|
@@ -2760,6 +2967,11 @@ async function runOnce({ page, payload, log }) {
|
|
|
2760
2967
|
if (r.width < 8 || r.height < 8) continue;
|
|
2761
2968
|
const cs = getComputedStyle(dlg);
|
|
2762
2969
|
if (cs.visibility === 'hidden' || cs.display === 'none') continue;
|
|
2970
|
+
// Trên /reel/: composer MỚI có nút "Chỉnh sửa" riêng trên ô video —
|
|
2971
|
+
// bộ dò narrow-pill dưới đây sẽ bấm nhầm vào trình sửa video. Thấy
|
|
2972
|
+
// dấu hiệu composer mới thì không đụng (luồng mới sẽ lo ở bước sau).
|
|
2973
|
+
if (guardNew && (/Chọn hình thu nhỏ|File phương tiện đã tải lên/i.test(dlg.innerText || '')
|
|
2974
|
+
|| dlg.querySelector("[aria-label='Chọn ảnh bìa']"))) continue;
|
|
2763
2975
|
const cands = dlg.querySelectorAll("[role='button'], button, [aria-label]");
|
|
2764
2976
|
for (const el of cands) {
|
|
2765
2977
|
const t = (el.innerText || '').trim();
|
|
@@ -2782,7 +2994,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
2782
2994
|
}
|
|
2783
2995
|
}
|
|
2784
2996
|
return null;
|
|
2785
|
-
}).catch(() => null);
|
|
2997
|
+
}, onReelOverlay).catch(() => null);
|
|
2786
2998
|
let editBtn = await findThumbBtn();
|
|
2787
2999
|
// The "Chỉnh sửa hình thu nhỏ" pill appears AFTER FB finishes processing
|
|
2788
3000
|
// the video AND, on the Page-wall composer, only on a LATER step (the
|
|
@@ -3795,7 +4007,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
3795
4007
|
// any "Chia sẻ" text on the page falls through to a feed-item's share
|
|
3796
4008
|
// button or sidebar nav, navigating AWAY from the composer and
|
|
3797
4009
|
// breaking the flow (iter-38 ended up on /profile.php?sk=reels_tab).
|
|
3798
|
-
const onPageWall = /^https?:\/\/(www\.)?facebook\.com\/?($|\?|#)/.test(page.url());
|
|
4010
|
+
const onPageWall = /^https?:\/\/(www\.)?facebook\.com\/?($|\?|#)/.test(page.url()) || isReelOverlayUrl(page.url());
|
|
3799
4011
|
if (step >= 2 && !published && !onPageWall) {
|
|
3800
4012
|
log('info', '[fb-pw] Tiếp ineffective — trying side-wizard jump to "Chia sẻ" step (BS only)');
|
|
3801
4013
|
const jumpBbox = await page.evaluate(() => {
|
|
@@ -4340,6 +4552,13 @@ module.exports.__testables = {
|
|
|
4340
4552
|
uploadViaThumbPickerModal,
|
|
4341
4553
|
previewMatchesThumb,
|
|
4342
4554
|
THUMB_PREVIEW_MAX_DIFF,
|
|
4555
|
+
COVER_BUTTON_RE,
|
|
4556
|
+
COVER_HEAD_RE,
|
|
4557
|
+
composerShowsThumb,
|
|
4558
|
+
applyCoverButtonThumb,
|
|
4559
|
+
remoteImagesMatchThumb,
|
|
4560
|
+
isReelOverlayUrl,
|
|
4561
|
+
clickPickerAndUpload,
|
|
4343
4562
|
// Dùng chung cho comment_facebook: script nào trượt giữa chừng cũng cần chụp
|
|
4344
4563
|
// lại màn hình, viết bản thứ hai là sớm muộn hai bên lệch nhau.
|
|
4345
4564
|
dumpFailure,
|