channel-worker 2.5.104 → 2.5.106

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/lib/api-client.js CHANGED
@@ -68,7 +68,7 @@ class ApiClient {
68
68
  async getNextCommand(workerId) {
69
69
  // Daemon-handled types. `_pw` variants route to the Playwright pipeline
70
70
  // (lib/playwright-runner → scripts/<base>.js) instead of the extension.
71
- const workerTypes = 'launch_profile,close_profile,launch_veo3_profile,set_profile_proxy,save_file,set_thumbnail,set_tags,set_file_input,click_and_upload,type_text,verify_logins,update_extension,sync_youtube_stats,restart_worker,upload_youtube_pw,upload_tiktok_pw,upload_facebook_pw,upload_facebook_photo_pw,warmup_youtube_pw,warmup_facebook_pw,warmup_tiktok_pw,nurture_facebook_pw,fetch_facebook_reel_stats_pw,scrape_affiliate_products,ingest_shopee_product,get_affiliate_link';
71
+ const workerTypes = 'launch_profile,close_profile,launch_veo3_profile,set_profile_proxy,save_file,set_thumbnail,set_tags,set_file_input,click_and_upload,type_text,verify_logins,update_extension,sync_youtube_stats,restart_worker,upload_youtube_pw,upload_tiktok_pw,upload_facebook_pw,upload_facebook_photo_pw,warmup_youtube_pw,warmup_facebook_pw,warmup_tiktok_pw,nurture_facebook_pw,fetch_facebook_reel_stats_pw,comment_facebook_pw,scrape_affiliate_products,ingest_shopee_product,get_affiliate_link';
72
72
  return this.request('GET', `/workers/commands?worker_id=${workerId}&types=${encodeURIComponent(workerTypes)}`);
73
73
  }
74
74
 
@@ -441,7 +441,7 @@ class CommandPoller {
441
441
  //
442
442
  // Scoped to the browse-only scripts. Publish keeps its profile open on
443
443
  // purpose: one profile serves several platform uploads back-to-back.
444
- const CLOSE_AFTER = new Set(['nurture_facebook', 'warmup_facebook', 'warmup_youtube', 'warmup_tiktok', 'fetch_facebook_reel_stats']);
444
+ const CLOSE_AFTER = new Set(['nurture_facebook', 'warmup_facebook', 'warmup_youtube', 'warmup_tiktok', 'fetch_facebook_reel_stats', 'comment_facebook']);
445
445
  // Cờ vừa bị TẮT trên web → bỏ profile khỏi sổ giữ mở, để nó quay về vòng
446
446
  // đời đóng-mở bình thường ngay từ lượt này.
447
447
  if (!keepOpen.enabled && this._keptOpenProfiles) this._keptOpenProfiles.delete(profileId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "channel-worker",
3
- "version": "2.5.104",
3
+ "version": "2.5.106",
4
4
  "description": "Channel Manager worker daemon — runs on remote machines to execute video pipeline jobs",
5
5
  "main": "lib/daemon.js",
6
6
  "bin": {
@@ -0,0 +1,271 @@
1
+ // Thả link affiliate xuống BÌNH LUẬN của một bài Facebook đã đăng, rồi ghim.
2
+ //
3
+ // Vì sao là bình luận chứ không phải caption: bài đang lên view thì không ai đi
4
+ // sửa caption của nó — sửa caption là bài quay lại hàng đợi xét duyệt của
5
+ // Facebook giữa lúc nó đang được phân phối. Bình luận thì ghim được, sửa được,
6
+ // xoá được, không đụng vào bài.
7
+ //
8
+ // Chạy trên chính profile NST của kênh (máy đăng bài) chứ không phải máy
9
+ // Shopee: bình luận phải mang danh Page thì mới ghim được, và một link lạ dưới
10
+ // bài của mình do tài khoản khác thả thì người xem đọc ra là spam.
11
+ //
12
+ // Payload: { post_url, text, link, pin?, idea_id?, channel_id? }
13
+ // post_url: link bài đã XÁC MINH (API chặn sẵn ô rỗng — services/AffComment)
14
+ // text: nguyên câu bình luận sẽ gõ, đã gồm link
15
+ // link: riêng chuỗi link, dùng để ĐỌC LẠI xác minh — xem `verified`
16
+ //
17
+ // Trả về { verified, comment_url, commented_as, pinned, post_url }.
18
+ //
19
+ // ⚠️ LUẬT LÕI CỦA SCRIPT NÀY: `verified` chỉ được true khi đã đọc lại thấy một
20
+ // bình luận THẬT trên trang mang đúng chuỗi link. Gõ xong + bấm Enter KHÔNG
21
+ // phải bằng chứng — đó đúng là cách 39 ca "đăng xong mà post_url rỗng" ra đời:
22
+ // script khai thành công theo thao tác của chính nó, không theo thứ trang web
23
+ // hiện ra. API hạ mọi kết quả thiếu `verified` xuống 'failed'.
24
+
25
+ const { assertAccountUsable, readPublishBlock } = require('./lib/fb-guard');
26
+ const { humanClick, humanType, pause, randInt } = require('./lib/human');
27
+
28
+ const NAV_TIMEOUT_MS = 45_000;
29
+ // Bình luận hiện ra gần như tức thì, nhưng lượt đầu sau khi mở bài thì Facebook
30
+ // còn đang dựng khung bình luận. 20 giây là dư cho trường hợp chậm nhất đo được
31
+ // mà vẫn không treo lệnh.
32
+ const VERIFY_TIMEOUT_MS = 20_000;
33
+
34
+ // Ô soạn bình luận. Facebook thay nhãn theo phiên bản giao diện và theo việc
35
+ // đang bình luận với tư cách ai, nên dò theo NHIỀU nhãn thay vì một selector
36
+ // "đúng nhất" — bản trước chỉ có 'Viết bình luận' và trượt sạch trên giao diện
37
+ // tiếng Anh.
38
+ const BOX_SELECTORS = [
39
+ "[contenteditable='true'][aria-label*='Bình luận']",
40
+ "[contenteditable='true'][aria-label*='bình luận']",
41
+ "[contenteditable='true'][aria-label*='Comment']",
42
+ "[contenteditable='true'][aria-label*='comment']",
43
+ "[contenteditable='true'][aria-placeholder*='bình luận']",
44
+ "[contenteditable='true'][aria-placeholder*='comment']",
45
+ "[role='textbox'][contenteditable='true']",
46
+ ];
47
+
48
+ // Nút mở khung bình luận, khi bài mở ra mà khung chưa dựng.
49
+ const OPEN_COMMENTS_SELECTORS = [
50
+ "[aria-label='Bình luận'], [aria-label='Comment']",
51
+ "[aria-label*='Để lại bình luận'], [aria-label*='Leave a comment']",
52
+ ];
53
+
54
+ function sel(list) { return list.join(', '); }
55
+
56
+ // ─── tìm ô soạn ─────────────────────────────────────────────────────────────
57
+
58
+ async function findCommentBox(page) {
59
+ for (const s of BOX_SELECTORS) {
60
+ const loc = page.locator(s);
61
+ const n = Math.min(await loc.count().catch(() => 0), 6);
62
+ for (let i = 0; i < n; i++) {
63
+ const el = loc.nth(i);
64
+ if (!(await el.isVisible().catch(() => false))) continue;
65
+ // Ô soạn BÀI (tường Page) cũng là contenteditable và cũng khớp
66
+ // `role=textbox` — gõ nhầm vào đó là đăng một bài mới toàn link. Ô bình
67
+ // luận luôn nằm bên trong khối bài; ô soạn bài thì không.
68
+ const inArticle = await el.evaluate((node) => !!node.closest("[role='article'], [role='dialog'], [role='complementary']"))
69
+ .catch(() => false);
70
+ if (!inArticle && !/comment|bình luận/i.test(s)) continue;
71
+ return el;
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+
77
+ async function openCommentsIfNeeded(page, log) {
78
+ for (const s of OPEN_COMMENTS_SELECTORS) {
79
+ const loc = page.locator(s).first();
80
+ if (!(await loc.isVisible().catch(() => false))) continue;
81
+ log('info', `[fb-cmt] chưa thấy ô soạn — bấm mở khung bình luận ("${s}")`);
82
+ await humanClick(page, loc).catch(() => {});
83
+ await pause(page, 1800);
84
+ return true;
85
+ }
86
+ return false;
87
+ }
88
+
89
+ // ─── đọc lại để xác minh ────────────────────────────────────────────────────
90
+
91
+ // Tìm bình luận THẬT mang chuỗi `link`. Bỏ qua mọi thứ nằm trong ô soạn: chữ
92
+ // mình vừa gõ cũng khớp chuỗi tìm kiếm, mà nó chưa phải bình luận.
93
+ //
94
+ // Facebook đặt tên tác giả ngay trong aria-label của khối bình luận
95
+ // ("Bình luận của <Tên>" / "Comment by <Name>") — đọc thẳng ở đó chắc hơn dò
96
+ // anchor đầu tiên, vì trong khối còn có link tới bài, tới trang được nhắc tên.
97
+ async function findPostedComment(page, link) {
98
+ return page.evaluate((needle) => {
99
+ const out = { found: false, author: '', comment_url: '' };
100
+ const nodes = [...document.querySelectorAll("[role='article']")];
101
+ for (const node of nodes) {
102
+ const txt = node.innerText || '';
103
+ if (!txt.includes(needle)) continue;
104
+ // Chính ô soạn (hoặc khối bài) — không tính.
105
+ if (node.querySelector("[contenteditable='true']") && !node.getAttribute('aria-label')) continue;
106
+ const label = node.getAttribute('aria-label') || '';
107
+ const m = /(?:Bình luận của|Comment by)\s+(.+)$/i.exec(label.trim());
108
+ // Không có aria-label dạng bình luận = nhiều khả năng đây là khối BÀI
109
+ // chứa chuỗi link trong caption, không phải bình luận.
110
+ if (!m) continue;
111
+ out.found = true;
112
+ out.author = m[1].trim().slice(0, 120);
113
+ const a = node.querySelector("a[href*='comment_id=']");
114
+ if (a) {
115
+ const href = a.getAttribute('href') || '';
116
+ out.comment_url = href.startsWith('http') ? href : `https://www.facebook.com${href}`;
117
+ }
118
+ return out;
119
+ }
120
+ return out;
121
+ }, link).catch(() => ({ found: false, author: '', comment_url: '' }));
122
+ }
123
+
124
+ async function waitForPostedComment(page, link, log, timeoutMs = VERIFY_TIMEOUT_MS) {
125
+ const deadline = Date.now() + timeoutMs;
126
+ let last = { found: false, author: '', comment_url: '' };
127
+ while (Date.now() < deadline) {
128
+ last = await findPostedComment(page, link);
129
+ if (last.found) return last;
130
+ await page.waitForTimeout(1200);
131
+ }
132
+ log('warn', `[fb-cmt] hết ${Math.round(timeoutMs / 1000)}s vẫn chưa đọc lại thấy bình luận mang link`);
133
+ return last;
134
+ }
135
+
136
+ // ─── ghim ───────────────────────────────────────────────────────────────────
137
+
138
+ // Ghim là việc CÓ CŨNG TỐT, không có cũng xong: link đã nằm dưới bài rồi. Nên
139
+ // mọi lỗi ở đây chỉ ghi log và trả false, KHÔNG ném — hỏng bước ghim mà đánh
140
+ // cả lệnh thành thất bại thì lượt chạy lại sẽ thả bình luận thứ hai.
141
+ async function pinComment(page, link, log) {
142
+ try {
143
+ const menuBtn = await page.evaluateHandle((needle) => {
144
+ const nodes = [...document.querySelectorAll("[role='article']")];
145
+ for (const node of nodes) {
146
+ if (!(node.innerText || '').includes(needle)) continue;
147
+ if (!/(Bình luận của|Comment by)/i.test(node.getAttribute('aria-label') || '')) continue;
148
+ const btn = [...node.querySelectorAll("[role='button'], [aria-haspopup='menu']")]
149
+ .find((b) => /hành động|tùy chọn|tuỳ chọn|options|actions/i.test(b.getAttribute('aria-label') || ''));
150
+ if (btn) { btn.scrollIntoView({ block: 'center' }); return btn; }
151
+ }
152
+ return null;
153
+ }, link);
154
+ const el = menuBtn.asElement();
155
+ if (!el) { log('info', '[fb-cmt] không thấy nút ⋯ của bình luận — bỏ qua bước ghim'); return false; }
156
+ await el.click({ timeout: 5000 }).catch(() => {});
157
+ await pause(page, 1200);
158
+
159
+ const item = page.locator("[role='menuitem'], [role='menu'] [role='button']")
160
+ .filter({ hasText: /Ghim bình luận|Ghim|Pin comment|Pin/i }).first();
161
+ if (!(await item.isVisible().catch(() => false))) {
162
+ log('info', '[fb-cmt] menu không có mục Ghim (thường là chưa bình luận bằng danh Page) — bỏ qua');
163
+ await page.keyboard.press('Escape').catch(() => {});
164
+ return false;
165
+ }
166
+ await item.click({ timeout: 5000 }).catch(() => {});
167
+ await pause(page, 1500);
168
+ log('info', '[fb-cmt] đã bấm Ghim bình luận');
169
+ return true;
170
+ } catch (e) {
171
+ log('warn', `[fb-cmt] ghim hỏng (bỏ qua, link vẫn nằm dưới bài): ${e.message.slice(0, 120)}`);
172
+ return false;
173
+ }
174
+ }
175
+
176
+ // ─── thân ───────────────────────────────────────────────────────────────────
177
+
178
+ async function run({ page, payload = {}, log }) {
179
+ const postUrl = String(payload.post_url || '').trim();
180
+ const text = String(payload.text || '').trim();
181
+ const link = String(payload.link || '').trim();
182
+ if (!postUrl) throw new Error('thiếu post_url — không biết thả bình luận vào bài nào');
183
+ if (!text) throw new Error('thiếu text — không có gì để bình luận');
184
+ if (!link) throw new Error('thiếu link — không có gì để đọc lại xác minh');
185
+ if (!text.includes(link)) throw new Error('text không chứa link — API dựng sai câu bình luận');
186
+
187
+ // Ghé trang chủ TRƯỚC rồi mới sang bài, dù biết thừa link bài.
188
+ //
189
+ // Lý do không kiểm tra phiên ngay trên trang bài: lib/fb-guard đọc chữ trong
190
+ // body để đoán checkpoint/khoá tài khoản, mà trang bài thì đầy BÌNH LUẬN của
191
+ // người lạ — một người bình luận "tài khoản của bạn đã bị vô hiệu hoá" là đủ
192
+ // để script tự kết luận kênh mình chết. Trang chủ có feed thật, fb-guard
193
+ // thấy feed là trả 'ok' ngay, không cần đọc chữ.
194
+ await page.goto('https://www.facebook.com/', { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS }).catch(() => {});
195
+ await pause(page, 2200);
196
+ await assertAccountUsable(page, log);
197
+
198
+ log('info', `[fb-cmt] mở bài ${postUrl}`);
199
+ await page.goto(postUrl, { waitUntil: 'domcontentloaded', timeout: NAV_TIMEOUT_MS });
200
+ await pause(page, 2600);
201
+
202
+ // Trang bài mở ra là trang LỖI / bài đã xoá thì dừng ngay, đừng đi tìm ô soạn
203
+ // rồi báo "không thấy ô bình luận" — hai chuyện khác hẳn nhau khi đọc log.
204
+ const gone = await page.evaluate(() => {
205
+ const t = (document.body?.innerText || '').slice(0, 1500);
206
+ return /(Không có sẵn|nội dung này hiện không có|This content isn't available|Liên kết bị hỏng|Page Not Found)/i.test(t);
207
+ }).catch(() => false);
208
+ if (gone) throw new Error(`Bài không mở được (đã xoá, đổi quyền riêng tư, hoặc link sai): ${postUrl}`);
209
+
210
+ let box = await findCommentBox(page);
211
+ if (!box) {
212
+ await openCommentsIfNeeded(page, log);
213
+ box = await findCommentBox(page);
214
+ }
215
+ if (!box) {
216
+ await assertAccountUsable(page, log);
217
+ throw new Error(`Không tìm thấy ô soạn bình luận trên ${postUrl} (đã thử ${BOX_SELECTORS.length} nhãn) — nhiều khả năng bài tắt bình luận hoặc Facebook đổi giao diện`);
218
+ }
219
+
220
+ await box.scrollIntoViewIfNeeded({ timeout: 5000 }).catch(() => {});
221
+ await humanClick(page, box).catch(async () => { await box.click({ timeout: 5000 }); });
222
+ await pause(page, 900);
223
+
224
+ // typo: 0 — humanType vốn gõ sai rồi tự Backspace cho giống người, nhưng
225
+ // trong câu này có một URL: một ký tự sai giữa mã link làm Facebook dựng thẻ
226
+ // xem trước của link KHÁC ngay lúc gõ, và thẻ đó không rút lại bằng Backspace.
227
+ // Nhịp gõ thì vẫn giữ nguyên chất người.
228
+ await humanType(page, text, { typo: 0 });
229
+ await pause(page, randInt(900, 1800));
230
+
231
+ await page.keyboard.press('Enter');
232
+ log('info', '[fb-cmt] đã gửi bình luận — đang đọc lại để xác minh');
233
+ await pause(page, 2500);
234
+
235
+ // Facebook chặn theo tần suất thì nó nuốt bình luận và hiện chữ đỏ. Không
236
+ // đọc chỗ này là lệnh sẽ báo "không xác minh được" chung chung, và người đọc
237
+ // log đi tìm lỗi giao diện trong khi thủ phạm là cái phanh chống spam.
238
+ const block = await readPublishBlock(page);
239
+ if (block) {
240
+ throw new Error(`FB_COMMENT_RATE_LIMITED: Facebook chặn bình luận vì tần suất/nghi spam — bình luận KHÔNG được nhận. Chuỗi bắt được: "${block}". Dừng thả link trên kênh này cho tới khi người kiểm tra.`);
241
+ }
242
+
243
+ const found = await waitForPostedComment(page, link, log);
244
+ if (!found.found) {
245
+ await assertAccountUsable(page, log);
246
+ // KHÔNG ném lỗi kiểu "có thể đã thả": trả về verified=false để API ghi
247
+ // 'failed' kèm nguyên văn. Ném lỗi ở đây thì lượt chạy lại sẽ gõ thêm một
248
+ // bình luận nữa, mà bình luận trước có thể đã nằm đó rồi.
249
+ log('warn', '[fb-cmt] gõ và gửi xong nhưng KHÔNG đọc lại thấy bình luận — coi như chưa thả, người phải vào kiểm');
250
+ return { verified: false, post_url: postUrl, comment_url: '', commented_as: '', pinned: false };
251
+ }
252
+
253
+ log('info', `[fb-cmt] xác minh được bình luận — danh nghĩa "${found.author}"${found.comment_url ? ` · ${found.comment_url}` : ''}`);
254
+
255
+ let pinned = false;
256
+ if (payload.pin !== false) pinned = await pinComment(page, link, log);
257
+
258
+ return {
259
+ verified: true,
260
+ post_url: postUrl,
261
+ comment_url: found.comment_url || '',
262
+ // Tên hiện trên bình luận. Phải là tên Page — ra tên người thì profile đang
263
+ // bình luận bằng tài khoản cá nhân, link nằm dưới bài mà không ghim được và
264
+ // người xem đọc ra là spam của người lạ.
265
+ commented_as: found.author || '',
266
+ pinned,
267
+ };
268
+ }
269
+
270
+ module.exports = { run };
271
+ module.exports.__testables = { findPostedComment, findCommentBox, BOX_SELECTORS, sel };
@@ -855,6 +855,18 @@ function diffNewReelIds(beforeIds, afterIds) {
855
855
  return (afterIds || []).filter((id) => !before.has(id));
856
856
  }
857
857
 
858
+ // URL tab Reels dùng SAU khi đăng: ưu tiên link dò được trên trang hiện tại,
859
+ // không có thì dùng URL đã tìm được lúc chụp ảnh nền. Sau khi bấm Đăng,
860
+ // Facebook hay đưa về /saved/?cref=28 (đo 04/08→13/09: 48/1829 lượt, 74 lần
861
+ // "no reels_tab link found") — trang đó không có link tab Reels, dò lại là
862
+ // 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
+ // "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 }) {
865
+ if (discovered) return { url: discovered, via: 'page' };
866
+ if (remembered) return { url: remembered, via: 'snapshot' };
867
+ return { url: null, via: null };
868
+ }
869
+
858
870
  // ID từ gói mạng: tab Reels xác nhận chỉ chứng minh reel THUỘC PAGE MÌNH, không
859
871
  // chứng minh nó MỚI. Ca 10/09 16:26 "ẩm thực đồng quê 01": snapshotLen=0 nên cả
860
872
  // 40 ID bắt được đều "mới", ID đầu tiên 1600960624772777 có trên tab (là bài
@@ -948,7 +960,7 @@ async function runOnce({ page, payload, log }) {
948
960
  } = payload || {};
949
961
  if (!video_url) throw new Error('No video_url provided');
950
962
 
951
- log('info', '[fb-pw] selectors version=2026.09.01-thumb-final-cta-race');
963
+ log('info', '[fb-pw] selectors version=2026.09.13-reels-tab-fallback');
952
964
 
953
965
  page.on('dialog', (d) => { d.accept().catch(() => {}); });
954
966
 
@@ -1056,9 +1068,12 @@ async function runOnce({ page, payload, log }) {
1056
1068
  // trước/sau mà tìm reel mới (xem diffNewReelIds). Một lần goto + quay
1057
1069
  // về, ~6-8s. Hỏng thì bỏ qua, không chặn lượt đăng.
1058
1070
  let reelIdsBefore = null;
1071
+ // GIỮ LẠI URL tab Reels cho sau khi đăng — xem pickPostPublishReelsTabUrl.
1072
+ let reelsTabUrlBefore = null;
1059
1073
  try {
1060
1074
  const tabUrl = await discoverReelsTabUrl(page, log);
1061
1075
  if (tabUrl) {
1076
+ reelsTabUrlBefore = tabUrl;
1062
1077
  await page.goto(tabUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });
1063
1078
  await pause(page, 3500);
1064
1079
  reelIdsBefore = await scrapeReelTileIds(page);
@@ -3032,7 +3047,14 @@ async function runOnce({ page, payload, log }) {
3032
3047
  // 2. Extract Page ID from "Dòng thời gian của X" timeline anchor or
3033
3048
  // any /profile.php?id=<id> anchor, then build the reels_tab URL
3034
3049
  // 3. Extract Page slug from any /<handle>/ anchor
3035
- const reelsTabHref = await discoverReelsTabUrl(page);
3050
+ const picked = pickPostPublishReelsTabUrl({
3051
+ discovered: await discoverReelsTabUrl(page),
3052
+ remembered: reelsTabUrlBefore,
3053
+ });
3054
+ const reelsTabHref = picked.url;
3055
+ if (picked.via === 'snapshot') {
3056
+ 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`);
3057
+ }
3036
3058
  if (reelsTabHref) {
3037
3059
  reelsTabUrl = reelsTabHref;
3038
3060
  log('info', `[fb-pw] navigating to page reels tab: ${reelsTabHref.slice(0, 100)}`);
@@ -3313,6 +3335,11 @@ async function runOnce({ page, payload, log }) {
3313
3335
  // post_url để quyết, hai cờ này để người đọc log biết hỏng ở khâu nào.
3314
3336
  confirm_click_failed: confirmClickFailed,
3315
3337
  post_url_captured: !!postUrl,
3338
+ // Ảnh nền tab Reels chụp trước khi mở composer + URL tab. API lưu vào
3339
+ // publish_results[].reels_before để đối chiếu xuôi (PublishReconcile)
3340
+ // biết reel nào là CŨ khi lượt này không lấy được link.
3341
+ reels_before: reelIdsBefore ? reelIdsBefore.slice(0, 300) : undefined,
3342
+ reels_tab_url: reelsTabUrlBefore || reelsTabUrl || undefined,
3316
3343
  video_path_local: videoPath,
3317
3344
  };
3318
3345
  } finally {
@@ -3352,6 +3379,7 @@ module.exports.__testables = {
3352
3379
  resilientClick,
3353
3380
  confirmPublishDialog,
3354
3381
  diffNewReelIds,
3382
+ pickPostPublishReelsTabUrl,
3355
3383
  pickReelsTabUrl,
3356
3384
  networkIdVerdict,
3357
3385
  classifyComposerText,