channel-worker 2.5.107 → 2.5.109
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
CHANGED
|
@@ -38,7 +38,7 @@ function parseViews(txt) {
|
|
|
38
38
|
// 09/09 tìm được tab để so trước/sau. Bản riêng còn thiếu cả bộ lọc "anchor
|
|
39
39
|
// của bài trong bảng tin" (gốc 2 link page lạ sáng 10/09) nên có thể quét
|
|
40
40
|
// nhầm tab Reels của page KHÁC mà không hay.
|
|
41
|
-
const { discoverReelsTabUrl } = require('./upload_facebook').__testables;
|
|
41
|
+
const { discoverReelsTabUrl, discoverReelsTabUrls, openReelsTabWithTiles } = require('./upload_facebook').__testables;
|
|
42
42
|
|
|
43
43
|
// Số người theo dõi của page, đọc ngay trên chính trang đang đứng (header hiện
|
|
44
44
|
// ở mọi tab của page/profile) — không tốn thêm lượt mở profile nào. Ô
|
|
@@ -507,17 +507,21 @@ async function run({ page, payload, log }) {
|
|
|
507
507
|
const scrolls = Number.isFinite(Number(payload?.scroll)) && Number(payload.scroll) > 0
|
|
508
508
|
? Number(payload.scroll)
|
|
509
509
|
: (mode === 'fresh' ? 2 : 6);
|
|
510
|
-
let
|
|
511
|
-
if (page_url && /sk=reels_tab/.test(page_url))
|
|
512
|
-
if (!
|
|
510
|
+
let candidates = [];
|
|
511
|
+
if (page_url && /sk=reels_tab/.test(page_url)) candidates = [page_url];
|
|
512
|
+
if (!candidates.length) {
|
|
513
513
|
await page.goto(page_url || 'https://www.facebook.com/', { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
514
514
|
await page.waitForTimeout(4000);
|
|
515
|
-
|
|
515
|
+
candidates = await discoverReelsTabUrls(page, log); // có log → in mẫu href khi không khớp
|
|
516
516
|
}
|
|
517
|
-
if (!
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
517
|
+
if (!candidates.length) throw new Error('không tìm được link tab Reels của page');
|
|
518
|
+
// Thử lần lượt tới khi thấy tile: cùng một page có hai dạng URL mà chỉ một
|
|
519
|
+
// dạng render tab Reels. Ca "Chuyện Nghề Nhà Nông" 14/09 — dạng
|
|
520
|
+
// profile.php?id=… ra 0 tile suốt 4 lượt quét (kênh mất trắng số liệu view,
|
|
521
|
+
// và bộ đối chiếu xuôi cũng mù theo), dạng /chuyennghenhanong/ ra 17 tile.
|
|
522
|
+
const opened = await openReelsTabWithTiles(page, candidates, { log, settleMs: 5000, tag: '[fb-stats]' });
|
|
523
|
+
log('info', `[fb-stats] ${mode} · reels_tab: ${String(opened.url || '').slice(0, 100)}`
|
|
524
|
+
+ (candidates.length > 1 ? ` (ứng viên ${opened.tried}/${candidates.length})` : ''));
|
|
521
525
|
|
|
522
526
|
// Follower chỉ đọc ở lượt nền: lượt bám bài chạy 2-3 lần/ngày mà con số này
|
|
523
527
|
// gần như không đổi trong ngày. Đọc hỏng thì bỏ qua — KHÔNG được làm hỏng
|
|
@@ -560,4 +564,4 @@ async function run({ page, payload, log }) {
|
|
|
560
564
|
return { ok: true, mode, tiles: tiles.length, written: d?.written || 0, matched: d?.matched || 0, rising: d?.rising || 0, alerts: d?.alerts || 0, followers };
|
|
561
565
|
}
|
|
562
566
|
|
|
563
|
-
module.exports = { run, __testables: { parseViews, scrapeFollowers, parseExact, parseVnPostTime, runInsights, dumpInsightsFailure, discoverReelsTabUrl } };
|
|
567
|
+
module.exports = { run, __testables: { parseViews, scrapeFollowers, parseExact, parseVnPostTime, runInsights, dumpInsightsFailure, discoverReelsTabUrl, discoverReelsTabUrls, openReelsTabWithTiles } };
|
|
@@ -803,10 +803,10 @@ async function confirmPublishDialog({ firstHit, tagConfirmButton, clickTagged, c
|
|
|
803
803
|
return { confirmed: !pending };
|
|
804
804
|
}
|
|
805
805
|
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
async function
|
|
809
|
-
|
|
806
|
+
// Mọi anchor trên trang hiện tại (href + aria + có nằm trong bảng tin không) —
|
|
807
|
+
// nguyên liệu cho pickReelsTabUrls. Tách riêng để bộ dò chạy một lượt evaluate.
|
|
808
|
+
async function scrapePageAnchors(page) {
|
|
809
|
+
return page.evaluate(() => {
|
|
810
810
|
const out = [];
|
|
811
811
|
for (const a of document.querySelectorAll('a[href]')) {
|
|
812
812
|
const href = a.getAttribute('href') || '';
|
|
@@ -819,13 +819,47 @@ async function discoverReelsTabUrl(page, log = null) {
|
|
|
819
819
|
}
|
|
820
820
|
return out;
|
|
821
821
|
}).catch(() => []);
|
|
822
|
-
|
|
823
|
-
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// MỌI ứng viên URL tab Reels của page, theo thứ tự ưu tiên. Trả danh sách chứ
|
|
825
|
+
// không phải một cái, vì cùng một page có thể có HAI dạng URL mà chỉ MỘT dạng
|
|
826
|
+
// render tile — ca "Chuyện Nghề Nhà Nông" 14/09: `/profile.php?id=615922598169
|
|
827
|
+
// 21&sk=reels_tab` ra 0 tile ở cả 4 lượt quét liên tiếp (kênh mù hẳn, mất luôn
|
|
828
|
+
// số liệu view), trong khi `/chuyennghenhanong/?sk=reels_tab` ra 17 tile. Trước
|
|
829
|
+
// đó nó vẫn chạy đúng bằng dạng handle; anchor trên trang đổi là dạng
|
|
830
|
+
// profile.php thắng chiến lược 2 và cả bộ đo lẫn bộ lấy link cùng mù.
|
|
831
|
+
async function discoverReelsTabUrls(page, log = null) {
|
|
832
|
+
const anchors = await scrapePageAnchors(page);
|
|
833
|
+
const urls = pickReelsTabUrls(anchors);
|
|
834
|
+
if (!urls.length && log) {
|
|
824
835
|
// Chẩn đoán: in vài href "giống của Page" để lần sau thêm đúng chiến lược.
|
|
825
836
|
const sample = anchors.map((a) => a.href).filter((h) => /^\/(?:profile\.php|\d{8,}|[A-Za-z0-9._-]{3,}\/?(?:\?|$))/.test(h)).slice(0, 8);
|
|
826
837
|
log('info', `[fb-pw] tìm tab Reels: ${anchors.length} anchor, không khớp chiến lược nào; mẫu: ${JSON.stringify(sample)}`);
|
|
827
838
|
}
|
|
828
|
-
return
|
|
839
|
+
return urls;
|
|
840
|
+
}
|
|
841
|
+
async function discoverReelsTabUrl(page, log = null) {
|
|
842
|
+
return (await discoverReelsTabUrls(page, log))[0] || null;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Mở tab Reels: thử lần lượt từng ứng viên tới khi thấy tile. `verified` = URL
|
|
846
|
+
// này CHẮC CHẮN render được tab Reels của page — bằng chứng mạnh hơn "dò được
|
|
847
|
+
// một link trông giống", nên lượt lấy link sau khi đăng ưu tiên nó.
|
|
848
|
+
// Page thật sự chưa có reel nào cũng ra verified=false; khi đó ids rỗng là
|
|
849
|
+
// đúng, không phải hỏng.
|
|
850
|
+
async function openReelsTabWithTiles(page, urls, { log = null, settleMs = 4000, wait = null, tag = '[fb-pw]' } = {}) {
|
|
851
|
+
const list = [...new Set((urls || []).filter(Boolean))];
|
|
852
|
+
let lastUrl = null;
|
|
853
|
+
for (let i = 0; i < list.length; i++) {
|
|
854
|
+
const url = list[i];
|
|
855
|
+
lastUrl = url;
|
|
856
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
|
857
|
+
if (wait) await wait(page, settleMs); else await page.waitForTimeout(settleMs);
|
|
858
|
+
const ids = await scrapeReelTileIds(page);
|
|
859
|
+
if (ids.length) return { url, ids, verified: true, tried: i + 1 };
|
|
860
|
+
if (log && i + 1 < list.length) log('warn', `${tag} tab Reels ${url.slice(0, 80)} không có tile nào — thử ứng viên kế (${i + 2}/${list.length})`);
|
|
861
|
+
}
|
|
862
|
+
return { url: lastUrl, ids: [], verified: false, tried: list.length };
|
|
829
863
|
}
|
|
830
864
|
|
|
831
865
|
// Mọi reel ID đang hiện trên tab Reels, theo thứ tự DOM (mới nhất trước).
|
|
@@ -861,7 +895,11 @@ function diffNewReelIds(beforeIds, afterIds) {
|
|
|
861
895
|
// "no reels_tab link found") — trang đó không có link tab Reels, dò lại là
|
|
862
896
|
// trượt và cả so trước/sau lẫn lượt chờ 75s đều bị bỏ → bài lên thật mà thành
|
|
863
897
|
// "mập mờ" (Makeup tại nhà 2 13/09 18:07, thich-lo-mo 12/09). Thuần, test được.
|
|
864
|
-
function pickPostPublishReelsTabUrl({ discovered, remembered }) {
|
|
898
|
+
function pickPostPublishReelsTabUrl({ discovered, remembered, rememberedVerified = false }) {
|
|
899
|
+
// URL đã CHỨNG MINH có tile lúc chụp ảnh nền thắng cả link dò được trên trang
|
|
900
|
+
// hiện tại: cùng một page có hai dạng URL mà chỉ một dạng render tab Reels
|
|
901
|
+
// (xem discoverReelsTabUrls), và dò lại sau khi đăng dễ bốc trúng dạng hỏng.
|
|
902
|
+
if (rememberedVerified && remembered) return { url: remembered, via: 'snapshot-verified' };
|
|
865
903
|
if (discovered) return { url: discovered, via: 'page' };
|
|
866
904
|
if (remembered) return { url: remembered, via: 'snapshot' };
|
|
867
905
|
return { url: null, via: null };
|
|
@@ -892,7 +930,9 @@ function isComposerDialogSignature(text) {
|
|
|
892
930
|
// chụp ảnh nền trước khi đăng) không có anchor profile.php/Dòng thời gian —
|
|
893
931
|
// 2/2 lượt "không tìm được link tab Reels trước khi đăng". Trang chủ của Page
|
|
894
932
|
// có shortcut bên trái trỏ /<id số>/ hoặc /<handle>/, và đôi khi /<id>/reels/.
|
|
895
|
-
function
|
|
933
|
+
function pickReelsTabUrls(anchors) {
|
|
934
|
+
const out = [];
|
|
935
|
+
const add = (u) => { if (u && !out.includes(u)) out.push(u); };
|
|
896
936
|
const list = (anchors || [])
|
|
897
937
|
.map((a) => ({ href: String(a.href || ''), aria: String(a.aria || ''), inFeed: !!a.inFeed }))
|
|
898
938
|
// ⛔ LOẠI anchor CỦA BÀI TRONG BẢNG TIN — đây là gốc của 2 link sai sáng
|
|
@@ -905,27 +945,31 @@ function pickReelsTabUrl(anchors) {
|
|
|
905
945
|
// (tracking của một bài cụ thể) hoặc anchor nằm trong vùng feed/article.
|
|
906
946
|
.filter((a) => !a.inFeed && !/[?&]__cft__|[?&]__tn__/.test(a.href));
|
|
907
947
|
const abs = (h) => (h.startsWith('http') ? h : `https://www.facebook.com${h}`);
|
|
908
|
-
for (const a of list) if (/sk=reels_tab/.test(a.href))
|
|
948
|
+
for (const a of list) if (/sk=reels_tab/.test(a.href)) add(abs(a.href));
|
|
909
949
|
for (const a of list) {
|
|
910
950
|
const m = a.href.match(/\/profile\.php\?id=(\d+)/);
|
|
911
|
-
if (m)
|
|
951
|
+
if (m) add(`https://www.facebook.com/profile.php?id=${m[1]}&sk=reels_tab`);
|
|
912
952
|
}
|
|
913
953
|
for (const a of list) {
|
|
914
954
|
if (!/Dòng thời gian|Timeline/i.test(a.aria)) continue;
|
|
915
955
|
const m = a.href.match(/^(?:https?:\/\/(?:www\.)?facebook\.com)?\/([A-Za-z0-9._-]{3,})\/?(?:\?|$)/);
|
|
916
|
-
if (m && !['profile.php', 'reel', 'video', 'videos', 'reels'].includes(m[1]))
|
|
956
|
+
if (m && !['profile.php', 'reel', 'video', 'videos', 'reels'].includes(m[1])) add(`https://www.facebook.com/${m[1]}/?sk=reels_tab`);
|
|
917
957
|
}
|
|
918
958
|
// 4. /<id số>/reels/ hoặc /<id số>/ — shortcut của Page trên trang chủ.
|
|
919
959
|
for (const a of list) {
|
|
920
960
|
const m = a.href.match(/^(?:https?:\/\/(?:www\.)?facebook\.com)?\/(\d{10,20})(?:\/reels)?\/?(?:\?|$)/);
|
|
921
|
-
if (m)
|
|
961
|
+
if (m) add(`https://www.facebook.com/profile.php?id=${m[1]}&sk=reels_tab`);
|
|
922
962
|
}
|
|
923
963
|
// 5. /<handle>/reels/ — Page có tên riêng.
|
|
924
964
|
for (const a of list) {
|
|
925
965
|
const m = a.href.match(/^(?:https?:\/\/(?:www\.)?facebook\.com)?\/([A-Za-z0-9._-]{3,})\/reels\/?(?:\?|$)/);
|
|
926
|
-
if (m && !['profile.php', 'reel', 'video', 'videos'].includes(m[1]))
|
|
966
|
+
if (m && !['profile.php', 'reel', 'video', 'videos'].includes(m[1])) add(`https://www.facebook.com/${m[1]}/?sk=reels_tab`);
|
|
927
967
|
}
|
|
928
|
-
return
|
|
968
|
+
return out;
|
|
969
|
+
}
|
|
970
|
+
// Ứng viên đầu bảng (giữ cho chỗ gọi cũ / test cũ).
|
|
971
|
+
function pickReelsTabUrl(anchors) {
|
|
972
|
+
return pickReelsTabUrls(anchors)[0] || null;
|
|
929
973
|
}
|
|
930
974
|
|
|
931
975
|
async function hasVisibleReelComposer(page) {
|
|
@@ -960,7 +1004,7 @@ async function runOnce({ page, payload, log }) {
|
|
|
960
1004
|
} = payload || {};
|
|
961
1005
|
if (!video_url) throw new Error('No video_url provided');
|
|
962
1006
|
|
|
963
|
-
log('info', '[fb-pw] selectors version=2026.09.
|
|
1007
|
+
log('info', '[fb-pw] selectors version=2026.09.14-reels-tab-candidates');
|
|
964
1008
|
|
|
965
1009
|
page.on('dialog', (d) => { d.accept().catch(() => {}); });
|
|
966
1010
|
|
|
@@ -1070,14 +1114,16 @@ async function runOnce({ page, payload, log }) {
|
|
|
1070
1114
|
let reelIdsBefore = null;
|
|
1071
1115
|
// GIỮ LẠI URL tab Reels cho sau khi đăng — xem pickPostPublishReelsTabUrl.
|
|
1072
1116
|
let reelsTabUrlBefore = null;
|
|
1117
|
+
let reelsTabVerifiedBefore = false;
|
|
1073
1118
|
try {
|
|
1074
|
-
const
|
|
1075
|
-
if (
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
reelIdsBefore =
|
|
1080
|
-
log('info', `[fb-pw] ảnh nền tab Reels trước khi đăng: ${reelIdsBefore.length} reel`
|
|
1119
|
+
const candidates = await discoverReelsTabUrls(page, log);
|
|
1120
|
+
if (candidates.length) {
|
|
1121
|
+
const opened = await openReelsTabWithTiles(page, candidates, { log, settleMs: 3500, wait: pause });
|
|
1122
|
+
reelsTabUrlBefore = opened.url;
|
|
1123
|
+
reelsTabVerifiedBefore = opened.verified;
|
|
1124
|
+
reelIdsBefore = opened.ids;
|
|
1125
|
+
log('info', `[fb-pw] ảnh nền tab Reels trước khi đăng: ${reelIdsBefore.length} reel`
|
|
1126
|
+
+ (candidates.length > 1 ? ` (ứng viên ${opened.tried}/${candidates.length})` : ''));
|
|
1081
1127
|
await navigateFacebookHome(page, log);
|
|
1082
1128
|
await pause(page, 2500);
|
|
1083
1129
|
} else {
|
|
@@ -3050,10 +3096,13 @@ async function runOnce({ page, payload, log }) {
|
|
|
3050
3096
|
const picked = pickPostPublishReelsTabUrl({
|
|
3051
3097
|
discovered: await discoverReelsTabUrl(page),
|
|
3052
3098
|
remembered: reelsTabUrlBefore,
|
|
3099
|
+
rememberedVerified: reelsTabVerifiedBefore,
|
|
3053
3100
|
});
|
|
3054
3101
|
const reelsTabHref = picked.url;
|
|
3055
3102
|
if (picked.via === 'snapshot') {
|
|
3056
3103
|
log('info', `[fb-pw] trang hiện tại không có link tab Reels (${String(page.url() || '').slice(0, 70)}) — dùng URL đã biết từ lúc chụp ảnh nền`);
|
|
3104
|
+
} else if (picked.via === 'snapshot-verified') {
|
|
3105
|
+
log('info', '[fb-pw] dùng URL tab Reels ĐÃ chứng minh có tile lúc chụp ảnh nền (dạng URL kia của cùng page có thể ra 0 tile)');
|
|
3057
3106
|
}
|
|
3058
3107
|
if (reelsTabHref) {
|
|
3059
3108
|
reelsTabUrl = reelsTabHref;
|
|
@@ -3381,6 +3430,9 @@ module.exports.__testables = {
|
|
|
3381
3430
|
diffNewReelIds,
|
|
3382
3431
|
pickPostPublishReelsTabUrl,
|
|
3383
3432
|
pickReelsTabUrl,
|
|
3433
|
+
pickReelsTabUrls,
|
|
3434
|
+
discoverReelsTabUrls,
|
|
3435
|
+
openReelsTabWithTiles,
|
|
3384
3436
|
networkIdVerdict,
|
|
3385
3437
|
classifyComposerText,
|
|
3386
3438
|
readPageAlerts,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Đọc CAPTION thật của một số reel để API ghép với bài "đăng mập mờ".
|
|
2
|
+
//
|
|
3
|
+
// Vì sao cần (chủ tịch hỏi 14/09): lượt quét tab Reels chỉ lấy post_id + lượt
|
|
4
|
+
// xem, nên bộ đối chiếu xuôi chỉ suy được "reel này MỚI xuất hiện sau lượt đăng
|
|
5
|
+
// đó". Đo 12 ca thật cùng ngày: 2 ca có 2-3 lượt đăng xen giữa hai lượt quét →
|
|
6
|
+
// suy luận thứ tự là đoán bừa. Caption thì chứng minh được: khi đăng, worker
|
|
7
|
+
// ghép "title + hashtag" vào ô mô tả thước phim, nên caption của bài mình LUÔN
|
|
8
|
+
// chứa nguyên tiêu đề.
|
|
9
|
+
//
|
|
10
|
+
// Script này CHỈ ĐỌC — không bấm, không sửa, không đăng gì. Hỏng thì trả về ít
|
|
11
|
+
// caption hơn, API tự biết là không đủ để ghép và để nguyên cho người.
|
|
12
|
+
//
|
|
13
|
+
// Payload: { channel_id, post_ids: ['1234567890', …] }
|
|
14
|
+
// Trả về: { captions: [{ post_id, og, page_text, url }], read, asked }
|
|
15
|
+
//
|
|
16
|
+
// Việc SO SÁNH nằm ở API (services/ReelCaptionMatch) chứ không ở đây: một bảng
|
|
17
|
+
// luật, test offline được, và worker không cần biết tiêu đề của idea.
|
|
18
|
+
|
|
19
|
+
const NAV_TIMEOUT_MS = 45_000;
|
|
20
|
+
// Đủ để khung reel dựng xong; script chỉ đọc nên không cần chờ lâu như lượt đăng.
|
|
21
|
+
const SETTLE_MS = 4000;
|
|
22
|
+
// Trần số reel đọc trong một lượt — mỗi cái một lần mở trang. Bài mập mờ của
|
|
23
|
+
// một kênh hiếm khi quá vài cái; nhiều hơn thì để lượt sau, đừng giữ profile lâu.
|
|
24
|
+
const MAX_POSTS = 8;
|
|
25
|
+
|
|
26
|
+
// og:description là mô tả của ĐÚNG reel đang mở. Văn bản trang là đường lui —
|
|
27
|
+
// trình xem reel cuộn cả reel kế bên vào DOM nên API chỉ tin phần đầu của nó.
|
|
28
|
+
async function readCaption(page) {
|
|
29
|
+
return page.evaluate(() => {
|
|
30
|
+
const meta = (p) => (document.querySelector(`meta[property="${p}"]`) || {}).content || '';
|
|
31
|
+
const main = document.querySelector("[role='main']") || document.body;
|
|
32
|
+
return {
|
|
33
|
+
og: (meta('og:description') || meta('og:title') || '').slice(0, 1000),
|
|
34
|
+
page_text: String((main && main.innerText) || '').replace(/\s+/g, ' ').trim().slice(0, 1500),
|
|
35
|
+
};
|
|
36
|
+
}).catch(() => ({ og: '', page_text: '' }));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function run({ page, payload = {}, log }) {
|
|
40
|
+
const ids = [...new Set((payload.post_ids || []).map((x) => String(x || '').trim()).filter((x) => /^\d{8,20}$/.test(x)))];
|
|
41
|
+
if (!ids.length) throw new Error('payload.post_ids rỗng — không có reel nào để đọc');
|
|
42
|
+
const asked = ids.length;
|
|
43
|
+
const take = ids.slice(0, MAX_POSTS);
|
|
44
|
+
log('info', `[fb-verify] đọc caption ${take.length}/${asked} reel`);
|
|
45
|
+
|
|
46
|
+
const captions = [];
|
|
47
|
+
for (const id of take) {
|
|
48
|
+
const url = `https://www.facebook.com/reel/${id}/`;
|
|
49
|
+
try {
|
|
50
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS });
|
|
51
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
52
|
+
const c = await readCaption(page);
|
|
53
|
+
captions.push({ post_id: id, og: c.og, page_text: c.page_text, url });
|
|
54
|
+
log('info', `[fb-verify] reel ${id}: og ${c.og.length} ký tự, trang ${c.page_text.length} ký tự`
|
|
55
|
+
+ (c.og ? ` — "${c.og.slice(0, 60)}"` : ''));
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// Một reel đọc hỏng KHÔNG được giết cả lượt: các reel còn lại vẫn ghép được.
|
|
58
|
+
log('warn', `[fb-verify] reel ${id} đọc hỏng: ${String(e && e.message || e).slice(0, 90)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!captions.length) throw new Error(`không đọc được caption của reel nào (${take.length} lượt thử)`);
|
|
62
|
+
return { captions, read: captions.length, asked };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { run };
|
|
66
|
+
module.exports.__testables = { readCaption, MAX_POSTS, SETTLE_MS };
|