channel-worker 2.5.125 → 2.5.127
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/comment_facebook.js +93 -66
package/package.json
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
// Thả link affiliate xuống BÌNH LUẬN của một bài Facebook đã đăng
|
|
1
|
+
// Thả link affiliate xuống BÌNH LUẬN của một bài Facebook đã đăng.
|
|
2
2
|
//
|
|
3
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
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ì
|
|
6
|
-
//
|
|
5
|
+
// Facebook giữa lúc nó đang được phân phối. Bình luận thì sửa được, xoá được,
|
|
6
|
+
// không đụng vào bài.
|
|
7
7
|
//
|
|
8
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
|
|
10
|
-
// bài của mình do tài khoản khác thả thì
|
|
9
|
+
// Shopee: bình luận phải mang danh Page — Facebook mới gắn nhãn "Author" và ưu
|
|
10
|
+
// tiên hiển thị nó, còn một link lạ dưới bài của mình do tài khoản khác thả thì
|
|
11
|
+
// người xem đọc ra là spam.
|
|
11
12
|
//
|
|
12
|
-
// Payload: { post_url, text, link,
|
|
13
|
+
// Payload: { post_url, text, link, idea_id?, channel_id? }
|
|
13
14
|
// post_url: link bài đã XÁC MINH (API chặn sẵn ô rỗng — services/AffComment)
|
|
14
15
|
// text: nguyên câu bình luận sẽ gõ, đã gồm link
|
|
15
16
|
// link: riêng chuỗi link, dùng để ĐỌC LẠI xác minh — xem `verified`
|
|
16
17
|
//
|
|
17
|
-
// Trả về { verified, comment_url, commented_as,
|
|
18
|
+
// Trả về { verified, comment_url, commented_as, post_url }.
|
|
19
|
+
//
|
|
20
|
+
// KHÔNG có bước ghim. Facebook không cho ghim bình luận dưới reel ở giao diện
|
|
21
|
+
// web — chủ tịch mở tận nơi kiểm 21/09, menu ⋯ của bình luận không có mục đó,
|
|
22
|
+
// kể cả khi bình luận đã mang nhãn "Author" (tức của chính chủ bài). Bản cũ
|
|
23
|
+
// vẫn đi dò nút ấy mỗi lượt rồi ghi một dòng log nghe như mình làm hỏng việc.
|
|
24
|
+
// Bỏ luôn: bình luận của tác giả bài vốn được Facebook ưu tiên hiển thị.
|
|
18
25
|
//
|
|
19
26
|
// ⚠️ LUẬT LÕI CỦA SCRIPT NÀY: `verified` chỉ được true khi đã đọc lại thấy một
|
|
20
27
|
// bình luận THẬT trên trang mang đúng chuỗi link. Gõ xong + bấm Enter KHÔNG
|
|
@@ -68,8 +75,15 @@ const BOX_SELECTORS = [
|
|
|
68
75
|
const OPEN_COMMENTS_SELECTORS = [
|
|
69
76
|
"[aria-label='Bình luận'], [aria-label='Comment']",
|
|
70
77
|
"[aria-label*='Để lại bình luận'], [aria-label*='Leave a comment']",
|
|
78
|
+
// Reel có thanh hành động riêng, nhãn khác hai dạng trên.
|
|
79
|
+
"[aria-label*='Xem bình luận'], [aria-label*='View comments']",
|
|
71
80
|
];
|
|
72
81
|
|
|
82
|
+
// Khung bình luận của reel mở bằng animation. Máy đang chạy nhiều lượt đăng
|
|
83
|
+
// song song thì vẽ chậm hẳn — chờ rộng tay, vì hỏng cả lượt tốn hơn nhiều so
|
|
84
|
+
// với đứng thêm mấy giây.
|
|
85
|
+
const OPEN_WAIT_MS = 12_000;
|
|
86
|
+
|
|
73
87
|
function sel(list) { return list.join(', '); }
|
|
74
88
|
|
|
75
89
|
// ─── tìm ô soạn ─────────────────────────────────────────────────────────────
|
|
@@ -93,16 +107,48 @@ async function findCommentBox(page) {
|
|
|
93
107
|
return null;
|
|
94
108
|
}
|
|
95
109
|
|
|
110
|
+
// Bấm xong PHẢI chờ tới khi ô soạn thật sự dựng, đừng ngủ cứng rồi đi tiếp.
|
|
111
|
+
//
|
|
112
|
+
// Bản đầu bấm nút rồi `pause(1800)` và trả về ngay. Khung bình luận của reel
|
|
113
|
+
// mở bằng animation, nên 1,8s chỉ đủ khi máy rảnh; 21/09 hai lượt trượt sạch
|
|
114
|
+
// đúng lúc máy đang chạy 5 lượt đăng song song, log dừng ở "bấm mở khung
|
|
115
|
+
// bình luận" rồi báo "không tìm thấy ô soạn (đã thử 7 nhãn)" — đọc lên tưởng
|
|
116
|
+
// bài tắt bình luận, thật ra khung chưa kịp vẽ. Các lượt thành công cùng ngày
|
|
117
|
+
// đều là bài có sẵn ô soạn, không qua nhánh này lần nào.
|
|
118
|
+
//
|
|
119
|
+
// Chờ tối đa OPEN_WAIT_MS và thử NÚT KHÁC nếu nút đầu không dựng được khung —
|
|
120
|
+
// trên reel có nhiều thứ mang nhãn "Bình luận" (nút ở thanh hành động, nút
|
|
121
|
+
// dưới bài), bấm trúng cái không mở panel là hỏng cả lượt.
|
|
96
122
|
async function openCommentsIfNeeded(page, log) {
|
|
123
|
+
let clicked = false;
|
|
97
124
|
for (const s of OPEN_COMMENTS_SELECTORS) {
|
|
98
125
|
const loc = page.locator(s).first();
|
|
99
126
|
if (!(await loc.isVisible().catch(() => false))) continue;
|
|
100
127
|
log('info', `[fb-cmt] chưa thấy ô soạn — bấm mở khung bình luận ("${s}")`);
|
|
101
128
|
await humanClick(page, loc).catch(() => {});
|
|
102
|
-
|
|
103
|
-
|
|
129
|
+
clicked = true;
|
|
130
|
+
const box = await waitForCommentBox(page, OPEN_WAIT_MS);
|
|
131
|
+
if (box) {
|
|
132
|
+
log('info', '[fb-cmt] khung bình luận đã dựng');
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
log('warn', `[fb-cmt] bấm "${s}" mà khung chưa dựng sau ${Math.round(OPEN_WAIT_MS / 1000)}s — thử nút khác`);
|
|
136
|
+
}
|
|
137
|
+
return clicked;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Dò lại ô soạn theo nhịp cho tới khi thấy hoặc hết giờ. Dùng vòng lặp chứ
|
|
141
|
+
// không `waitForSelector` vì ô soạn phải qua được bộ lọc của findCommentBox
|
|
142
|
+
// (ô soạn BÀI trên tường Page cũng khớp selector — gõ nhầm vào đó là đăng một
|
|
143
|
+
// bài mới toàn link).
|
|
144
|
+
async function waitForCommentBox(page, timeoutMs) {
|
|
145
|
+
const deadline = Date.now() + timeoutMs;
|
|
146
|
+
for (;;) {
|
|
147
|
+
const box = await findCommentBox(page);
|
|
148
|
+
if (box) return box;
|
|
149
|
+
if (Date.now() >= deadline) return null;
|
|
150
|
+
await pause(page, 700);
|
|
104
151
|
}
|
|
105
|
-
return false;
|
|
106
152
|
}
|
|
107
153
|
|
|
108
154
|
// ─── đọc lại để xác minh ────────────────────────────────────────────────────
|
|
@@ -152,16 +198,28 @@ async function findPostedComment(page, link) {
|
|
|
152
198
|
if (node.querySelector("[contenteditable='true']") && !node.getAttribute('aria-label')) continue;
|
|
153
199
|
const label = node.getAttribute('aria-label') || '';
|
|
154
200
|
const m = /(?:Bình luận của|Comment by)\s+(.+)$/i.exec(label.trim());
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
201
|
+
// Khối bình luận KHÔNG phải lúc nào cũng mang aria-label "Bình luận của
|
|
202
|
+
// <Tên>" — giao diện reel không dùng nhãn đó, nên mọi lượt thả reel đều
|
|
203
|
+
// về `commented_as` rỗng và `comment_url` rỗng (21/09). Bằng chứng chắc
|
|
204
|
+
// chắn khác, không phụ thuộc chữ nghĩa giao diện: khối có link permalink
|
|
205
|
+
// mang `comment_id=`. Khối BÀI không bao giờ có thứ đó, nên vẫn né được
|
|
206
|
+
// đúng cái bẫy mà nhãn kia sinh ra để né — caption bài chứa chuỗi link.
|
|
160
207
|
const a = node.querySelector("a[href*='comment_id=']");
|
|
208
|
+
if (!m && !a) continue;
|
|
209
|
+
out.found = true;
|
|
161
210
|
if (a) {
|
|
162
211
|
const href = a.getAttribute('href') || '';
|
|
163
212
|
out.comment_url = href.startsWith('http') ? href : `https://www.facebook.com${href}`;
|
|
164
213
|
}
|
|
214
|
+
if (m) {
|
|
215
|
+
out.author = m[1].trim().slice(0, 120);
|
|
216
|
+
} else {
|
|
217
|
+
// Facebook gắn nhãn "Author"/"Tác giả" cho bình luận của chính chủ bài
|
|
218
|
+
// — với luồng này thì đó CHÍNH LÀ điều cần biết: bình luận mang danh
|
|
219
|
+
// Page chứ không phải một tài khoản lạ đi rải link dưới bài của kênh.
|
|
220
|
+
const badge = /(^|\s)(Author|Tác giả)(\s|$)/i.test(node.innerText || '');
|
|
221
|
+
out.author = badge ? 'Author' : '';
|
|
222
|
+
}
|
|
165
223
|
return out;
|
|
166
224
|
}
|
|
167
225
|
return out;
|
|
@@ -186,46 +244,6 @@ async function waitForPostedComment(page, link, log, timeoutMs = VERIFY_TIMEOUT_
|
|
|
186
244
|
return { ...meta, found: false, count: n };
|
|
187
245
|
}
|
|
188
246
|
|
|
189
|
-
// ─── ghim ───────────────────────────────────────────────────────────────────
|
|
190
|
-
|
|
191
|
-
// 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
|
|
192
|
-
// mọi lỗi ở đây chỉ ghi log và trả false, KHÔNG ném — hỏng bước ghim mà đánh
|
|
193
|
-
// cả lệnh thành thất bại thì lượt chạy lại sẽ thả bình luận thứ hai.
|
|
194
|
-
async function pinComment(page, link, log) {
|
|
195
|
-
try {
|
|
196
|
-
const menuBtn = await page.evaluateHandle((needle) => {
|
|
197
|
-
const nodes = [...document.querySelectorAll("[role='article']")];
|
|
198
|
-
for (const node of nodes) {
|
|
199
|
-
if (!(node.innerText || '').includes(needle)) continue;
|
|
200
|
-
if (!/(Bình luận của|Comment by)/i.test(node.getAttribute('aria-label') || '')) continue;
|
|
201
|
-
const btn = [...node.querySelectorAll("[role='button'], [aria-haspopup='menu']")]
|
|
202
|
-
.find((b) => /hành động|tùy chọn|tuỳ chọn|options|actions/i.test(b.getAttribute('aria-label') || ''));
|
|
203
|
-
if (btn) { btn.scrollIntoView({ block: 'center' }); return btn; }
|
|
204
|
-
}
|
|
205
|
-
return null;
|
|
206
|
-
}, link);
|
|
207
|
-
const el = menuBtn.asElement();
|
|
208
|
-
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; }
|
|
209
|
-
await el.click({ timeout: 5000 }).catch(() => {});
|
|
210
|
-
await pause(page, 1200);
|
|
211
|
-
|
|
212
|
-
const item = page.locator("[role='menuitem'], [role='menu'] [role='button']")
|
|
213
|
-
.filter({ hasText: /Ghim bình luận|Ghim|Pin comment|Pin/i }).first();
|
|
214
|
-
if (!(await item.isVisible().catch(() => false))) {
|
|
215
|
-
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');
|
|
216
|
-
await page.keyboard.press('Escape').catch(() => {});
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
await item.click({ timeout: 5000 }).catch(() => {});
|
|
220
|
-
await pause(page, 1500);
|
|
221
|
-
log('info', '[fb-cmt] đã bấm Ghim bình luận');
|
|
222
|
-
return true;
|
|
223
|
-
} catch (e) {
|
|
224
|
-
log('warn', `[fb-cmt] ghim hỏng (bỏ qua, link vẫn nằm dưới bài): ${e.message.slice(0, 120)}`);
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
247
|
// ─── thân ───────────────────────────────────────────────────────────────────
|
|
230
248
|
|
|
231
249
|
async function run({ page, payload = {}, log }) {
|
|
@@ -261,13 +279,23 @@ async function run({ page, payload = {}, log }) {
|
|
|
261
279
|
if (gone) throw new Error(`Bài không mở được (đã xoá, đổi quyền riêng tư, hoặc link sai): ${postUrl}`);
|
|
262
280
|
|
|
263
281
|
let box = await findCommentBox(page);
|
|
282
|
+
// Chưa thấy ngay thì chờ một nhịp trước khi đi bấm nút: trang vừa mở, khối
|
|
283
|
+
// bình luận có khi còn đang vẽ.
|
|
284
|
+
if (!box) box = await waitForCommentBox(page, 4000);
|
|
285
|
+
let opened = false;
|
|
264
286
|
if (!box) {
|
|
265
|
-
await openCommentsIfNeeded(page, log);
|
|
287
|
+
opened = await openCommentsIfNeeded(page, log);
|
|
266
288
|
box = await findCommentBox(page);
|
|
267
289
|
}
|
|
268
290
|
if (!box) {
|
|
269
291
|
await assertAccountUsable(page, log);
|
|
270
|
-
|
|
292
|
+
// Nói đúng bệnh. Đã bấm được nút mở khung mà vẫn không có ô soạn thì bài
|
|
293
|
+
// KHÔNG tắt bình luận — nút đó chỉ hiện khi còn bình luận được; bệnh là
|
|
294
|
+
// khung không dựng kịp (máy bận) hoặc Facebook đổi cách dựng panel. Gộp
|
|
295
|
+
// hai chuyện vào một câu là lần sau đọc log đi sai hướng ngay từ đầu.
|
|
296
|
+
throw new Error(opened
|
|
297
|
+
? `Bấm mở khung bình luận rồi mà không có ô soạn sau ${Math.round(OPEN_WAIT_MS / 1000)}s trên ${postUrl} — máy đang bận vẽ chậm, hoặc Facebook đổi cách dựng khung bình luận của reel`
|
|
298
|
+
: `Không tìm thấy ô soạn bình luận trên ${postUrl} (đã thử ${BOX_SELECTORS.length} nhãn, cũng không thấy nút mở khung) — nhiều khả năng bài tắt bình luận hoặc Facebook đổi giao diện`);
|
|
271
299
|
}
|
|
272
300
|
|
|
273
301
|
// Đo NỀN trước khi gõ: link có thể đã nằm sẵn trong caption, hoặc bài đã
|
|
@@ -308,25 +336,24 @@ async function run({ page, payload = {}, log }) {
|
|
|
308
336
|
// '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
|
|
309
337
|
// bình luận nữa, mà bình luận trước có thể đã nằm đó rồi.
|
|
310
338
|
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');
|
|
311
|
-
return { verified: false, post_url: postUrl, comment_url: '', commented_as: ''
|
|
339
|
+
return { verified: false, post_url: postUrl, comment_url: '', commented_as: '' };
|
|
312
340
|
}
|
|
313
341
|
|
|
314
342
|
log('info', `[fb-cmt] xác minh được bình luận — danh nghĩa "${found.author}"${found.comment_url ? ` · ${found.comment_url}` : ''}`);
|
|
315
343
|
|
|
316
|
-
let pinned = false;
|
|
317
|
-
if (payload.pin !== false) pinned = await pinComment(page, link, log);
|
|
318
|
-
|
|
319
344
|
return {
|
|
320
345
|
verified: true,
|
|
321
346
|
post_url: postUrl,
|
|
322
347
|
comment_url: found.comment_url || '',
|
|
323
|
-
// Tên hiện trên bình luận
|
|
324
|
-
//
|
|
325
|
-
// người xem đọc ra là spam của
|
|
348
|
+
// Tên hiện trên bình luận, hoặc 'Author' khi Facebook gắn nhãn tác giả.
|
|
349
|
+
// Ra tên người lạ nghĩa là profile đang bình luận bằng tài khoản cá nhân —
|
|
350
|
+
// người xem đọc ra là spam dưới bài của kênh.
|
|
326
351
|
commented_as: found.author || '',
|
|
327
|
-
pinned,
|
|
328
352
|
};
|
|
329
353
|
}
|
|
330
354
|
|
|
331
355
|
module.exports = { run };
|
|
332
|
-
module.exports.__testables = {
|
|
356
|
+
module.exports.__testables = {
|
|
357
|
+
findPostedComment, countLinkOnPage, waitForPostedComment, findCommentBox, BOX_SELECTORS, sel,
|
|
358
|
+
openCommentsIfNeeded, waitForCommentBox, OPEN_COMMENTS_SELECTORS, OPEN_WAIT_MS,
|
|
359
|
+
};
|