diggylab 0.1.51 → 0.1.52
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/README.md +228 -228
- package/cli.js +759 -23
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -8,7 +8,12 @@ import { hostname, homedir } from "node:os";
|
|
|
8
8
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
|
|
11
|
-
// ../../packages/shared/
|
|
11
|
+
// ../../packages/shared/dist/comment-publishing.js
|
|
12
|
+
var COMMENT_TEXT_MAX_LENGTH = 2e3;
|
|
13
|
+
var COMMENT_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
14
|
+
var COMMENT_IMAGE_QUOTA_BYTES = 500 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
// ../../packages/shared/dist/index.js
|
|
12
17
|
var DESKTOP_AGENT_PROTOCOL_VERSION = 1;
|
|
13
18
|
var DESKTOP_POST_COMMENTS_MAX_LIMIT = 200;
|
|
14
19
|
var DESKTOP_POST_COMMENTS_REQUEST_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
@@ -18,17 +23,22 @@ function isFacebookDomain(domain) {
|
|
|
18
23
|
return Boolean(domain && FACEBOOK_HOST.test(domain.replace(/^\./, "")));
|
|
19
24
|
}
|
|
20
25
|
function normalizeSameSite(value) {
|
|
21
|
-
if (typeof value !== "string")
|
|
26
|
+
if (typeof value !== "string")
|
|
27
|
+
return void 0;
|
|
22
28
|
const lower = value.toLowerCase().replace(/[^a-z]/g, "");
|
|
23
|
-
if (lower === "lax")
|
|
24
|
-
|
|
25
|
-
if (lower === "
|
|
29
|
+
if (lower === "lax")
|
|
30
|
+
return "Lax";
|
|
31
|
+
if (lower === "strict")
|
|
32
|
+
return "Strict";
|
|
33
|
+
if (lower === "none" || lower === "norestriction")
|
|
34
|
+
return "None";
|
|
26
35
|
return void 0;
|
|
27
36
|
}
|
|
28
37
|
function toCookie(value) {
|
|
29
38
|
const name = typeof value.name === "string" ? value.name.trim() : "";
|
|
30
39
|
const cookieValue = typeof value.value === "string" ? value.value : "";
|
|
31
|
-
if (!name || !cookieValue)
|
|
40
|
+
if (!name || !cookieValue)
|
|
41
|
+
return null;
|
|
32
42
|
return {
|
|
33
43
|
domain: typeof value.domain === "string" ? value.domain : void 0,
|
|
34
44
|
expires: typeof value.expires === "number" ? value.expires : typeof value.expirationDate === "number" ? value.expirationDate : void 0,
|
|
@@ -44,7 +54,8 @@ function parseCookieHeader(input) {
|
|
|
44
54
|
const header = input.replace(/^\s*cookie\s*:/i, "");
|
|
45
55
|
return header.split(/[;\n]/).map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
46
56
|
const separator = part.indexOf("=");
|
|
47
|
-
if (separator <= 0)
|
|
57
|
+
if (separator <= 0)
|
|
58
|
+
return null;
|
|
48
59
|
return { name: part.slice(0, separator).trim(), value: part.slice(separator + 1) };
|
|
49
60
|
}).filter((cookie) => cookie !== null);
|
|
50
61
|
}
|
|
@@ -54,14 +65,16 @@ function parseCookieTable(input) {
|
|
|
54
65
|
const columns = line.split(" ");
|
|
55
66
|
const name = columns[0]?.trim() ?? "";
|
|
56
67
|
const value = columns[1];
|
|
57
|
-
if (!value || !name || name === "Name")
|
|
68
|
+
if (!value || !name || name === "Name")
|
|
69
|
+
continue;
|
|
58
70
|
cookies.push({ domain: columns[2]?.trim(), name, value });
|
|
59
71
|
}
|
|
60
72
|
return cookies;
|
|
61
73
|
}
|
|
62
74
|
function parseCookieInput(input) {
|
|
63
75
|
const trimmed = input.trim();
|
|
64
|
-
if (!trimmed)
|
|
76
|
+
if (!trimmed)
|
|
77
|
+
return [];
|
|
65
78
|
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
66
79
|
return trimmed.includes(" ") ? parseCookieTable(trimmed) : parseCookieHeader(trimmed);
|
|
67
80
|
}
|
|
@@ -762,6 +775,370 @@ function parseCommentDepth(label) {
|
|
|
762
775
|
return /\brepl(?:y|ies)\b|trả lời|phản hồi/i.test(parentPart) ? 2 : 1;
|
|
763
776
|
}
|
|
764
777
|
|
|
778
|
+
// ../../packages/facebook-scanner/dist/comment-publish-evidence.js
|
|
779
|
+
function readCommentPublishEvidence(body, parseComments) {
|
|
780
|
+
const evidence = { comments: [], declinedIds: /* @__PURE__ */ new Set(), createdIds: /* @__PURE__ */ new Set() };
|
|
781
|
+
const visit2 = (value, depth = 0) => {
|
|
782
|
+
if (!value || typeof value !== "object" || depth > 60)
|
|
783
|
+
return;
|
|
784
|
+
const node = value;
|
|
785
|
+
const creation = node.comment_create;
|
|
786
|
+
const createdId = commentId(creation?.comment?.legacy_fbid) ?? commentId(creation?.comment?.id);
|
|
787
|
+
if (createdId)
|
|
788
|
+
evidence.createdIds.add(createdId);
|
|
789
|
+
const id = commentId(node.legacy_fbid) ?? commentId(node.id);
|
|
790
|
+
if (id) {
|
|
791
|
+
const groupInfo = node.group_comment_info;
|
|
792
|
+
if (groupInfo?.declined_feedback_text != null)
|
|
793
|
+
evidence.declinedIds.add(id);
|
|
794
|
+
if (node.body || node.comment_body || node.preferred_body) {
|
|
795
|
+
evidence.comments.push(...parseComments(JSON.stringify({ ...node, __typename: "Comment", id })));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
for (const child of Object.values(node))
|
|
799
|
+
visit2(child, depth + 1);
|
|
800
|
+
};
|
|
801
|
+
for (const line of body.split(/\r?\n/)) {
|
|
802
|
+
try {
|
|
803
|
+
visit2(JSON.parse(line.trim().replace(/^for\s*\(;;\);/, "")));
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return evidence;
|
|
808
|
+
}
|
|
809
|
+
function commentId(value) {
|
|
810
|
+
if (typeof value !== "string")
|
|
811
|
+
return void 0;
|
|
812
|
+
if (/^\d{1,100}$/.test(value))
|
|
813
|
+
return value;
|
|
814
|
+
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
815
|
+
return /^comment:\d+_(\d{1,100})$/.exec(decoded)?.[1];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ../../packages/facebook-scanner/dist/comment-publisher.js
|
|
819
|
+
var SUBMIT_TIMEOUT_MS = 15e3;
|
|
820
|
+
var COMPOSER_TIMEOUT_MS = 1e4;
|
|
821
|
+
var IMAGE_UPLOAD_TIMEOUT_MS = 15e3;
|
|
822
|
+
var APPROVAL_PATTERN = /pending approval|awaiting approval|chờ duyệt|đang chờ duyệt|chờ quản trị viên/i;
|
|
823
|
+
var EXTENSION = { "image/jpeg": "jpg", "image/png": "png", "image/webp": "webp" };
|
|
824
|
+
async function publishCommentOnPage(page, options, config) {
|
|
825
|
+
const started = Date.now();
|
|
826
|
+
const diagnostic = {
|
|
827
|
+
stage: "COMPOSER",
|
|
828
|
+
mutationResponses: 0,
|
|
829
|
+
graphqlResponses: 0,
|
|
830
|
+
readErrors: 0,
|
|
831
|
+
comments: 0,
|
|
832
|
+
createdIds: 0,
|
|
833
|
+
declinedIds: 0,
|
|
834
|
+
reload: "NOT_STARTED",
|
|
835
|
+
durationMs: 0
|
|
836
|
+
};
|
|
837
|
+
try {
|
|
838
|
+
const result = await publishWithDiagnostic(page, options, config, diagnostic);
|
|
839
|
+
diagnostic.outcome = result.outcome;
|
|
840
|
+
diagnostic.errorCode = result.errorCode;
|
|
841
|
+
return result;
|
|
842
|
+
} catch (error) {
|
|
843
|
+
diagnostic.errorCode = options.signal?.aborted ? "PUBLISH_ABORTED" : "PUBLISH_EXCEPTION";
|
|
844
|
+
throw error;
|
|
845
|
+
} finally {
|
|
846
|
+
diagnostic.durationMs = Date.now() - started;
|
|
847
|
+
try {
|
|
848
|
+
options.onDiagnostic?.({ ...diagnostic });
|
|
849
|
+
} catch {
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async function publishWithDiagnostic(page, options, config, diagnostic) {
|
|
854
|
+
const composer = await waitForComposer(page);
|
|
855
|
+
if (!composer)
|
|
856
|
+
return failed("COMPOSER_NOT_FOUND");
|
|
857
|
+
const fileInput = options.image ? await findFileInput(page, composer) : null;
|
|
858
|
+
if (options.image && !fileInput)
|
|
859
|
+
return failed("IMAGE_NOT_SUPPORTED");
|
|
860
|
+
assertNotAborted(options.signal);
|
|
861
|
+
const allowed = await options.beforeSubmit();
|
|
862
|
+
assertNotAborted(options.signal);
|
|
863
|
+
if (!allowed)
|
|
864
|
+
return failed("START_NOT_ALLOWED");
|
|
865
|
+
if (options.content) {
|
|
866
|
+
diagnostic.stage = "FILL";
|
|
867
|
+
await composer.fill(options.content);
|
|
868
|
+
}
|
|
869
|
+
{
|
|
870
|
+
if (options.image && fileInput) {
|
|
871
|
+
const currentFileInput = await findFileInput(page, composer);
|
|
872
|
+
if (!currentFileInput)
|
|
873
|
+
return failed("IMAGE_NOT_SUPPORTED");
|
|
874
|
+
const uploadDeadline = Date.now() + IMAGE_UPLOAD_TIMEOUT_MS;
|
|
875
|
+
const uploadResponse = page.waitForResponse((response2) => {
|
|
876
|
+
const url = new URL(response2.url());
|
|
877
|
+
return (url.hostname === "facebook.com" || url.hostname.endsWith(".facebook.com")) && url.pathname === "/ajax/ufi/upload/" && response2.request().method() === "POST";
|
|
878
|
+
}, { timeout: IMAGE_UPLOAD_TIMEOUT_MS }).catch(() => null);
|
|
879
|
+
await currentFileInput.setInputFiles({
|
|
880
|
+
buffer: options.image.buffer,
|
|
881
|
+
mimeType: options.image.contentType,
|
|
882
|
+
name: `comment.${EXTENSION[options.image.contentType]}`
|
|
883
|
+
});
|
|
884
|
+
const response = await uploadResponse;
|
|
885
|
+
if (!response || !response.ok())
|
|
886
|
+
return failed("IMAGE_UPLOAD_FAILED");
|
|
887
|
+
try {
|
|
888
|
+
let timer;
|
|
889
|
+
const body = await Promise.race([
|
|
890
|
+
response.text(),
|
|
891
|
+
new Promise((_resolve, reject) => {
|
|
892
|
+
timer = setTimeout(() => reject(new Error("IMAGE_UPLOAD_FAILED")), Math.max(0, uploadDeadline - Date.now()));
|
|
893
|
+
})
|
|
894
|
+
]).finally(() => clearTimeout(timer));
|
|
895
|
+
const uploaded = JSON.parse(body.replace(/^for\s*\(;;\);/, ""));
|
|
896
|
+
const id = uploaded.payload?.fbid;
|
|
897
|
+
if (uploaded.error || !(typeof id === "number" && Number.isSafeInteger(id) && id > 0 || typeof id === "string" && /^[1-9]\d{0,99}$/.test(id)))
|
|
898
|
+
return failed("IMAGE_UPLOAD_FAILED");
|
|
899
|
+
} catch {
|
|
900
|
+
return failed("IMAGE_UPLOAD_FAILED");
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const submit = await findSubmit(page, composer);
|
|
904
|
+
if (!submit)
|
|
905
|
+
return failed("SUBMIT_NOT_FOUND");
|
|
906
|
+
const evidence = { comments: [], declinedIds: /* @__PURE__ */ new Set(), createdIds: /* @__PURE__ */ new Set() };
|
|
907
|
+
const onResponse = (response) => {
|
|
908
|
+
if (!response.url().includes("/api/graphql/"))
|
|
909
|
+
return;
|
|
910
|
+
diagnostic.graphqlResponses += 1;
|
|
911
|
+
const name = new URLSearchParams(response.request().postData() ?? "").get("fb_api_req_friendly_name");
|
|
912
|
+
if (name !== "useCometUFICreateCommentMutation")
|
|
913
|
+
return;
|
|
914
|
+
diagnostic.mutationResponses += 1;
|
|
915
|
+
diagnostic.httpStatus = response.status();
|
|
916
|
+
if (response.status() >= 400)
|
|
917
|
+
return;
|
|
918
|
+
void response.text().then((body) => {
|
|
919
|
+
try {
|
|
920
|
+
diagnostic.stage = "READ_RESPONSE";
|
|
921
|
+
const received = readCommentPublishEvidence(body, config.parseGraphqlComments);
|
|
922
|
+
evidence.comments.push(...received.comments);
|
|
923
|
+
for (const id of received.declinedIds)
|
|
924
|
+
evidence.declinedIds.add(id);
|
|
925
|
+
for (const id of received.createdIds)
|
|
926
|
+
evidence.createdIds.add(id);
|
|
927
|
+
diagnostic.comments = evidence.comments.length;
|
|
928
|
+
diagnostic.createdIds = evidence.createdIds.size;
|
|
929
|
+
diagnostic.declinedIds = evidence.declinedIds.size;
|
|
930
|
+
} catch {
|
|
931
|
+
diagnostic.readErrors += 1;
|
|
932
|
+
}
|
|
933
|
+
}).catch(() => {
|
|
934
|
+
diagnostic.readErrors += 1;
|
|
935
|
+
});
|
|
936
|
+
};
|
|
937
|
+
page.on("response", onResponse);
|
|
938
|
+
try {
|
|
939
|
+
diagnostic.stage = "SUBMIT";
|
|
940
|
+
await submit.click({ timeout: 1e4 });
|
|
941
|
+
if (options.skipVerification) {
|
|
942
|
+
diagnostic.reload = "SKIPPED";
|
|
943
|
+
return { outcome: "UNKNOWN", submitAttempted: true, errorCode: "VERIFICATION_SKIPPED" };
|
|
944
|
+
}
|
|
945
|
+
if (diagnostic.stage === "SUBMIT")
|
|
946
|
+
diagnostic.stage = "WAITING_RESPONSE";
|
|
947
|
+
return await waitForEvidence(page, {
|
|
948
|
+
evidence,
|
|
949
|
+
parseComments: config.parseGraphqlComments,
|
|
950
|
+
diagnostic,
|
|
951
|
+
content: options.content,
|
|
952
|
+
hasImage: Boolean(options.image),
|
|
953
|
+
postUrl: options.postUrl
|
|
954
|
+
});
|
|
955
|
+
} finally {
|
|
956
|
+
page.off("response", onResponse);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async function waitForComposer(page) {
|
|
961
|
+
const deadline = Date.now() + COMPOSER_TIMEOUT_MS;
|
|
962
|
+
for (; ; ) {
|
|
963
|
+
const composer = await findComposer(page);
|
|
964
|
+
if (composer)
|
|
965
|
+
return composer;
|
|
966
|
+
if (Date.now() >= deadline)
|
|
967
|
+
return null;
|
|
968
|
+
await page.mouse.wheel(0, 800).catch(() => void 0);
|
|
969
|
+
await page.waitForTimeout(400);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
async function findComposer(page) {
|
|
973
|
+
const handle = await page.evaluateHandle(() => {
|
|
974
|
+
const boxes = Array.from(document.querySelectorAll('[contenteditable="true"][role="textbox"]')).filter((element2) => {
|
|
975
|
+
const style = window.getComputedStyle(element2);
|
|
976
|
+
const label = element2.getAttribute("aria-label") ?? "";
|
|
977
|
+
return style.display !== "none" && style.visibility !== "hidden" && !/reply|trả lời/i.test(label);
|
|
978
|
+
});
|
|
979
|
+
const composer = boxes.find((element2) => /comment|bình luận/i.test(element2.getAttribute("aria-label") ?? "")) ?? boxes[0];
|
|
980
|
+
return composer ?? null;
|
|
981
|
+
});
|
|
982
|
+
const element = handle.asElement();
|
|
983
|
+
if (!element)
|
|
984
|
+
return null;
|
|
985
|
+
return element;
|
|
986
|
+
}
|
|
987
|
+
async function findFileInput(page, composer) {
|
|
988
|
+
const handle = await composer.evaluateHandle((element2) => {
|
|
989
|
+
let node = element2;
|
|
990
|
+
for (let depth = 0; depth < 8 && node; depth += 1, node = node.parentElement) {
|
|
991
|
+
const input = node.querySelector('input[type="file"][accept*="image"]');
|
|
992
|
+
if (input) {
|
|
993
|
+
return input;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
});
|
|
998
|
+
const element = handle.asElement();
|
|
999
|
+
return element;
|
|
1000
|
+
}
|
|
1001
|
+
async function findSubmit(page, composer) {
|
|
1002
|
+
const handle = await composer.evaluateHandle((element2) => {
|
|
1003
|
+
let node = element2;
|
|
1004
|
+
for (let depth = 0; depth < 8 && node; depth += 1, node = node.parentElement) {
|
|
1005
|
+
const button = Array.from(node.querySelectorAll('[role="button"], button')).find((candidate) => {
|
|
1006
|
+
const name = (candidate.getAttribute("aria-label") ?? candidate.textContent ?? "").trim();
|
|
1007
|
+
return /^(comment|post comment|bình luận|post|đăng|gửi)$/i.test(name) && candidate.getAttribute("aria-disabled") !== "true" && !candidate.disabled;
|
|
1008
|
+
});
|
|
1009
|
+
if (button) {
|
|
1010
|
+
return button;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return null;
|
|
1014
|
+
});
|
|
1015
|
+
const element = handle.asElement();
|
|
1016
|
+
if (!element)
|
|
1017
|
+
return null;
|
|
1018
|
+
return element;
|
|
1019
|
+
}
|
|
1020
|
+
async function waitForEvidence(page, state) {
|
|
1021
|
+
const deadline = Date.now() + SUBMIT_TIMEOUT_MS;
|
|
1022
|
+
while (Date.now() < deadline) {
|
|
1023
|
+
const declinedCreatedId = [...state.evidence.createdIds].find((id) => state.evidence.declinedIds.has(id));
|
|
1024
|
+
if (declinedCreatedId) {
|
|
1025
|
+
state.diagnostic.commentId = declinedCreatedId;
|
|
1026
|
+
return {
|
|
1027
|
+
outcome: "UNKNOWN",
|
|
1028
|
+
submitAttempted: true,
|
|
1029
|
+
errorCode: "FACEBOOK_COMMENT_DECLINED",
|
|
1030
|
+
facebookCommentId: declinedCreatedId,
|
|
1031
|
+
commentUrl: `${state.postUrl}?comment_id=${encodeURIComponent(declinedCreatedId)}`
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
const match = state.evidence.comments.find((comment) => /^\d{1,100}$/.test(comment.id ?? "") && (!state.evidence.createdIds.size || state.evidence.createdIds.has(comment.id ?? "")) && (state.content ? normalize(comment.text) === normalize(state.content) : true) && (!state.hasImage || (comment.attachmentImages?.length ?? 0) > 0));
|
|
1035
|
+
const submittedId = match?.id ?? (state.evidence.createdIds.size === 1 ? state.evidence.createdIds.values().next().value : void 0);
|
|
1036
|
+
if (submittedId) {
|
|
1037
|
+
state.diagnostic.commentId = submittedId;
|
|
1038
|
+
const identity = {
|
|
1039
|
+
commentUrl: `${state.postUrl}?comment_id=${encodeURIComponent(submittedId)}`,
|
|
1040
|
+
facebookCommentId: submittedId
|
|
1041
|
+
};
|
|
1042
|
+
if (state.evidence.declinedIds.has(submittedId)) {
|
|
1043
|
+
return { ...identity, outcome: "UNKNOWN", submitAttempted: true, errorCode: "FACEBOOK_COMMENT_DECLINED" };
|
|
1044
|
+
}
|
|
1045
|
+
try {
|
|
1046
|
+
state.diagnostic.stage = "RELOAD";
|
|
1047
|
+
const verification = await verifyAfterReload(page, state, submittedId);
|
|
1048
|
+
state.diagnostic.reload = verification === "PENDING" ? "MISSING" : verification;
|
|
1049
|
+
if (verification === "DECLINED") {
|
|
1050
|
+
return { ...identity, outcome: "UNKNOWN", submitAttempted: true, errorCode: "FACEBOOK_COMMENT_DECLINED" };
|
|
1051
|
+
}
|
|
1052
|
+
if (verification === "PENDING") {
|
|
1053
|
+
return { ...identity, outcome: "UNKNOWN", submitAttempted: true, errorCode: "FACEBOOK_APPROVAL_PENDING" };
|
|
1054
|
+
}
|
|
1055
|
+
if (verification === "CONFIRMED") {
|
|
1056
|
+
return { ...identity, outcome: "SUCCEEDED", submitAttempted: true };
|
|
1057
|
+
}
|
|
1058
|
+
} catch {
|
|
1059
|
+
state.diagnostic.reload = "ERROR";
|
|
1060
|
+
}
|
|
1061
|
+
return { ...identity, outcome: "UNKNOWN", submitAttempted: true, errorCode: "SUBMIT_UNCONFIRMED" };
|
|
1062
|
+
}
|
|
1063
|
+
const pending = await page.getByText(APPROVAL_PATTERN).count().catch(() => 0);
|
|
1064
|
+
if (pending > 0) {
|
|
1065
|
+
return { outcome: "UNKNOWN", submitAttempted: true, errorCode: "FACEBOOK_APPROVAL_PENDING" };
|
|
1066
|
+
}
|
|
1067
|
+
await page.waitForTimeout(500);
|
|
1068
|
+
}
|
|
1069
|
+
return { outcome: "UNKNOWN", submitAttempted: true, errorCode: "SUBMIT_UNCONFIRMED" };
|
|
1070
|
+
}
|
|
1071
|
+
async function verifyAfterReload(page, state, submittedId) {
|
|
1072
|
+
const received = [];
|
|
1073
|
+
let observing = true;
|
|
1074
|
+
let timer;
|
|
1075
|
+
const onResponse = (response) => {
|
|
1076
|
+
if (!response.url().includes("/api/graphql/") || response.status() >= 400)
|
|
1077
|
+
return;
|
|
1078
|
+
void response.text().then((body) => {
|
|
1079
|
+
if (observing)
|
|
1080
|
+
received.push(readCommentPublishEvidence(body, state.parseComments));
|
|
1081
|
+
}).catch(() => void 0);
|
|
1082
|
+
};
|
|
1083
|
+
page.on("response", onResponse);
|
|
1084
|
+
try {
|
|
1085
|
+
const commentUrl = new URL(state.postUrl);
|
|
1086
|
+
commentUrl.searchParams.set("comment_id", submittedId);
|
|
1087
|
+
await page.goto(commentUrl.href, { waitUntil: "domcontentloaded", timeout: 15e3 });
|
|
1088
|
+
const deadline = Date.now() + 3e3;
|
|
1089
|
+
const inspect = async () => {
|
|
1090
|
+
while (observing && Date.now() < deadline) {
|
|
1091
|
+
const visible = await page.evaluate(({ id, postUrl }) => {
|
|
1092
|
+
const target = new URL(postUrl);
|
|
1093
|
+
const found = Array.from(document.querySelectorAll('a[href*="comment_id="]')).some((link) => {
|
|
1094
|
+
const article = link.closest('[role="article"]');
|
|
1095
|
+
const visibility = { visibilityProperty: true, opacityProperty: true };
|
|
1096
|
+
if (!article || !link.checkVisibility(visibility) || !article.checkVisibility(visibility))
|
|
1097
|
+
return false;
|
|
1098
|
+
const url = new URL(link.href);
|
|
1099
|
+
return url.hostname === target.hostname && url.pathname.replace(/\/$/, "") === target.pathname.replace(/\/$/, "") && url.searchParams.get("comment_id") === id;
|
|
1100
|
+
});
|
|
1101
|
+
return {
|
|
1102
|
+
found,
|
|
1103
|
+
scripts: Array.from(document.querySelectorAll('script[type="application/json"]')).map((script) => script.textContent ?? "").join("\n")
|
|
1104
|
+
};
|
|
1105
|
+
}, { id: submittedId, postUrl: state.postUrl });
|
|
1106
|
+
if (!observing)
|
|
1107
|
+
return "MISSING";
|
|
1108
|
+
const evidence = [...received, readCommentPublishEvidence(visible.scripts, state.parseComments)];
|
|
1109
|
+
if (evidence.some((item) => item.declinedIds.has(submittedId)))
|
|
1110
|
+
return "DECLINED";
|
|
1111
|
+
if (await page.getByText(APPROVAL_PATTERN).count())
|
|
1112
|
+
return "PENDING";
|
|
1113
|
+
if (evidence.some((item) => item.comments.some((comment) => comment.id === submittedId && (!state.hasImage || (comment.attachmentImages?.length ?? 0) > 0))) || !state.hasImage && visible.found)
|
|
1114
|
+
return "CONFIRMED";
|
|
1115
|
+
await page.waitForTimeout(Math.min(200, Math.max(0, deadline - Date.now())));
|
|
1116
|
+
}
|
|
1117
|
+
return "MISSING";
|
|
1118
|
+
};
|
|
1119
|
+
return await Promise.race([
|
|
1120
|
+
inspect(),
|
|
1121
|
+
new Promise((resolve) => {
|
|
1122
|
+
timer = setTimeout(() => resolve("MISSING"), 3e3);
|
|
1123
|
+
})
|
|
1124
|
+
]);
|
|
1125
|
+
} finally {
|
|
1126
|
+
observing = false;
|
|
1127
|
+
clearTimeout(timer);
|
|
1128
|
+
page.off("response", onResponse);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function normalize(value) {
|
|
1132
|
+
return value.replace(/\s+/g, " ").trim();
|
|
1133
|
+
}
|
|
1134
|
+
function failed(errorCode) {
|
|
1135
|
+
return { errorCode, outcome: "FAILED", submitAttempted: false };
|
|
1136
|
+
}
|
|
1137
|
+
function assertNotAborted(signal) {
|
|
1138
|
+
if (signal?.aborted)
|
|
1139
|
+
throw new Error("SCAN_CANCELED");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
765
1142
|
// ../../packages/facebook-scanner/dist/session.js
|
|
766
1143
|
async function detectFacebookSession(page, context) {
|
|
767
1144
|
let url;
|
|
@@ -825,7 +1202,7 @@ var FacebookScanner = class {
|
|
|
825
1202
|
});
|
|
826
1203
|
const results = [];
|
|
827
1204
|
for (const [index, groupUrl] of options.groupUrls.entries()) {
|
|
828
|
-
|
|
1205
|
+
assertNotAborted2(options.signal);
|
|
829
1206
|
const items = await this.scanGroup(browser, options, groupUrl, index);
|
|
830
1207
|
results.push(items);
|
|
831
1208
|
}
|
|
@@ -841,7 +1218,7 @@ var FacebookScanner = class {
|
|
|
841
1218
|
const target = normalizePostUrl(options.postUrl);
|
|
842
1219
|
if (!target)
|
|
843
1220
|
throw new FacebookScannerError("POST_URL_INVALID");
|
|
844
|
-
|
|
1221
|
+
assertNotAborted2(options.signal);
|
|
845
1222
|
this.running = true;
|
|
846
1223
|
let browser = null;
|
|
847
1224
|
try {
|
|
@@ -876,7 +1253,7 @@ var FacebookScanner = class {
|
|
|
876
1253
|
let idleRounds = 0;
|
|
877
1254
|
let rounds = 0;
|
|
878
1255
|
while (comments.length < options.limit && idleRounds < 3 && rounds < 20) {
|
|
879
|
-
|
|
1256
|
+
assertNotAborted2(options.signal);
|
|
880
1257
|
rounds += 1;
|
|
881
1258
|
const previousCount = comments.length;
|
|
882
1259
|
await page.waitForTimeout(1200);
|
|
@@ -893,7 +1270,7 @@ var FacebookScanner = class {
|
|
|
893
1270
|
await page.mouse.wheel(0, 2e3);
|
|
894
1271
|
}
|
|
895
1272
|
}
|
|
896
|
-
|
|
1273
|
+
assertNotAborted2(options.signal);
|
|
897
1274
|
await drain(responseTasks, this.graphqlResponseTimeoutMs);
|
|
898
1275
|
const domComments = await this.extractDomComments(page);
|
|
899
1276
|
comments = mergeFacebookComments([...embeddedComments, ...graphqlComments, ...domComments], options.limit);
|
|
@@ -911,6 +1288,57 @@ var FacebookScanner = class {
|
|
|
911
1288
|
this.running = false;
|
|
912
1289
|
}
|
|
913
1290
|
}
|
|
1291
|
+
async publishComment(options) {
|
|
1292
|
+
if (this.running)
|
|
1293
|
+
throw new FacebookScannerError("SCANNER_BUSY");
|
|
1294
|
+
const target = normalizePostUrl(options.postUrl);
|
|
1295
|
+
if (!target)
|
|
1296
|
+
throw new FacebookScannerError("POST_URL_INVALID");
|
|
1297
|
+
assertNotAborted2(options.signal);
|
|
1298
|
+
this.running = true;
|
|
1299
|
+
let browser = null;
|
|
1300
|
+
try {
|
|
1301
|
+
browser = await this.browserType.launch({
|
|
1302
|
+
headless: true,
|
|
1303
|
+
...this.proxy ? { proxy: this.proxy } : {}
|
|
1304
|
+
});
|
|
1305
|
+
const context = await browser.newContext({ storageState: options.storageState });
|
|
1306
|
+
let page = null;
|
|
1307
|
+
const closeOnAbort = () => {
|
|
1308
|
+
void page?.close().catch(() => void 0);
|
|
1309
|
+
void context.close().catch(() => void 0);
|
|
1310
|
+
void browser?.close().catch(() => void 0);
|
|
1311
|
+
};
|
|
1312
|
+
options.signal?.addEventListener("abort", closeOnAbort, { once: true });
|
|
1313
|
+
if (options.signal?.aborted)
|
|
1314
|
+
closeOnAbort();
|
|
1315
|
+
try {
|
|
1316
|
+
page = await context.newPage();
|
|
1317
|
+
if (options.signal?.aborted)
|
|
1318
|
+
closeOnAbort();
|
|
1319
|
+
await page.route("**/*", (route) => ["font", "media"].includes(route.request().resourceType()) ? route.abort() : route.continue());
|
|
1320
|
+
try {
|
|
1321
|
+
await page.goto(target, { waitUntil: "domcontentloaded", timeout: 45e3 });
|
|
1322
|
+
} catch {
|
|
1323
|
+
throw new FacebookScannerError("NAVIGATION_FAILED");
|
|
1324
|
+
}
|
|
1325
|
+
const condition = await detectFacebookSession(page, context);
|
|
1326
|
+
if (condition !== "READY")
|
|
1327
|
+
throw new FacebookScannerError(condition);
|
|
1328
|
+
return await publishCommentOnPage(page, options, {
|
|
1329
|
+
parseGraphqlComments: this.parseGraphqlComments
|
|
1330
|
+
});
|
|
1331
|
+
} finally {
|
|
1332
|
+
options.signal?.removeEventListener("abort", closeOnAbort);
|
|
1333
|
+
await page?.unroute("**/*").catch(() => void 0);
|
|
1334
|
+
await page?.close().catch(() => void 0);
|
|
1335
|
+
await context.close().catch(() => void 0);
|
|
1336
|
+
}
|
|
1337
|
+
} finally {
|
|
1338
|
+
await browser?.close();
|
|
1339
|
+
this.running = false;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
914
1342
|
async scanGroup(browser, options, groupUrl, groupIndex) {
|
|
915
1343
|
const context = await browser.newContext({ storageState: options.storageState });
|
|
916
1344
|
let page = null;
|
|
@@ -940,7 +1368,7 @@ var FacebookScanner = class {
|
|
|
940
1368
|
let items = [];
|
|
941
1369
|
let idleRounds = 0;
|
|
942
1370
|
while (items.length < options.limit && idleRounds < 3) {
|
|
943
|
-
|
|
1371
|
+
assertNotAborted2(options.signal);
|
|
944
1372
|
const previousCount = items.length;
|
|
945
1373
|
await page.waitForTimeout(1500);
|
|
946
1374
|
await drain(responseTasks, this.graphqlResponseTimeoutMs);
|
|
@@ -958,7 +1386,7 @@ var FacebookScanner = class {
|
|
|
958
1386
|
if (items.length < options.limit)
|
|
959
1387
|
await page.mouse.wheel(0, 2e3);
|
|
960
1388
|
}
|
|
961
|
-
|
|
1389
|
+
assertNotAborted2(options.signal);
|
|
962
1390
|
await drain(responseTasks, this.graphqlResponseTimeoutMs);
|
|
963
1391
|
items = mergeFacebookScanItems([...graphqlItems, ...domItems], options.limit);
|
|
964
1392
|
if (!items.length)
|
|
@@ -974,7 +1402,7 @@ var FacebookScanner = class {
|
|
|
974
1402
|
}
|
|
975
1403
|
}
|
|
976
1404
|
};
|
|
977
|
-
function
|
|
1405
|
+
function assertNotAborted2(signal) {
|
|
978
1406
|
if (signal?.aborted)
|
|
979
1407
|
throw new FacebookScannerError("SCAN_CANCELED");
|
|
980
1408
|
}
|
|
@@ -1620,7 +2048,7 @@ var DesktopAgentSession = class {
|
|
|
1620
2048
|
state: this.reportedState,
|
|
1621
2049
|
facebookReady: this.options.getFacebookReady(),
|
|
1622
2050
|
currentJobId: this.reportedJobId,
|
|
1623
|
-
capabilities: ["POST_COMMENTS", "POST_COMMENTS_MORE"]
|
|
2051
|
+
capabilities: ["POST_COMMENTS", "POST_COMMENTS_MORE", "POST_COMMENT_PUBLISH"]
|
|
1624
2052
|
});
|
|
1625
2053
|
this.handshake = "WELCOME";
|
|
1626
2054
|
return;
|
|
@@ -1664,6 +2092,16 @@ var DesktopAgentSession = class {
|
|
|
1664
2092
|
this.emitJob({ type: "STOP", ...envelope.payload });
|
|
1665
2093
|
return;
|
|
1666
2094
|
}
|
|
2095
|
+
if (envelope.type === "POST_COMMENT_PUBLISH") {
|
|
2096
|
+
const payload = envelope.payload;
|
|
2097
|
+
if (!isCommentPublish(payload)) {
|
|
2098
|
+
this.protocolFailure(socket, stored, generation);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
if (Date.parse(payload.expiresAt) <= Date.now()) return;
|
|
2102
|
+
this.emitJob({ type: "COMMENT_PUBLISH", ...payload });
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
1667
2105
|
if (envelope.type === "POST_COMMENTS_FETCH") {
|
|
1668
2106
|
if (!isPostCommentsFetch(envelope.payload)) {
|
|
1669
2107
|
this.protocolFailure(socket, stored, generation);
|
|
@@ -1823,6 +2261,20 @@ function isPostCommentsFetch(value) {
|
|
|
1823
2261
|
const payload = value;
|
|
1824
2262
|
return typeof payload.requestId === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(payload.requestId) && typeof payload.postUrl === "string" && payload.postUrl.length <= 2e3 && /^https:\/\/[\w.-]*facebook\.com\//i.test(payload.postUrl) && typeof payload.limit === "number" && Number.isInteger(payload.limit) && payload.limit > 0 && payload.limit <= DESKTOP_POST_COMMENTS_MAX_LIMIT;
|
|
1825
2263
|
}
|
|
2264
|
+
function isCommentPublish(value) {
|
|
2265
|
+
if (!value || typeof value !== "object") return false;
|
|
2266
|
+
const payload = value;
|
|
2267
|
+
const image = payload.image;
|
|
2268
|
+
if (typeof payload.requestId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(payload.requestId) || typeof payload.postUrl !== "string" || payload.postUrl.length > 2e3 || !/^https:\/\/[\w.-]*facebook\.com\//i.test(payload.postUrl) || typeof payload.content !== "string" || payload.content.length > COMMENT_TEXT_MAX_LENGTH || payload.skipVerification !== void 0 && typeof payload.skipVerification !== "boolean" || typeof payload.expiresAt !== "string" || !Number.isFinite(Date.parse(payload.expiresAt))) {
|
|
2269
|
+
return false;
|
|
2270
|
+
}
|
|
2271
|
+
const hasImage = image !== null && image !== void 0;
|
|
2272
|
+
if (!payload.content.trim() && !hasImage) return false;
|
|
2273
|
+
if (!hasImage) return true;
|
|
2274
|
+
if (typeof image !== "object" || Array.isArray(image)) return false;
|
|
2275
|
+
const asset = image;
|
|
2276
|
+
return typeof asset.id === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(asset.id) && ["image/jpeg", "image/png", "image/webp"].includes(String(asset.contentType)) && typeof asset.byteSize === "number" && Number.isInteger(asset.byteSize) && asset.byteSize > 0 && asset.byteSize <= COMMENT_IMAGE_MAX_BYTES && typeof asset.sha256 === "string" && /^[0-9a-f]{64}$/i.test(asset.sha256);
|
|
2277
|
+
}
|
|
1826
2278
|
function emptySnapshot(state) {
|
|
1827
2279
|
return {
|
|
1828
2280
|
state,
|
|
@@ -1852,6 +2304,184 @@ function readErrorCode(error) {
|
|
|
1852
2304
|
return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN_ERROR";
|
|
1853
2305
|
}
|
|
1854
2306
|
|
|
2307
|
+
// src/main/desktop-comment-publish-runner.ts
|
|
2308
|
+
var DEFAULT_REPORT_ATTEMPTS = 3;
|
|
2309
|
+
var DesktopCommentPublishRunner = class {
|
|
2310
|
+
options;
|
|
2311
|
+
listeners = /* @__PURE__ */ new Set();
|
|
2312
|
+
stopController = new AbortController();
|
|
2313
|
+
currentController = null;
|
|
2314
|
+
activeRequestId = null;
|
|
2315
|
+
finishedRequestIds = /* @__PURE__ */ new Set();
|
|
2316
|
+
active = null;
|
|
2317
|
+
unsubscribe = null;
|
|
2318
|
+
stopped = false;
|
|
2319
|
+
constructor(options) {
|
|
2320
|
+
this.options = options;
|
|
2321
|
+
}
|
|
2322
|
+
start() {
|
|
2323
|
+
if (this.unsubscribe) return;
|
|
2324
|
+
this.unsubscribe = this.options.session.subscribeJobs((event) => this.handleEvent(event));
|
|
2325
|
+
}
|
|
2326
|
+
stop() {
|
|
2327
|
+
this.stopped = true;
|
|
2328
|
+
this.currentController?.abort();
|
|
2329
|
+
this.stopController.abort();
|
|
2330
|
+
this.unsubscribe?.();
|
|
2331
|
+
this.unsubscribe = null;
|
|
2332
|
+
}
|
|
2333
|
+
isRunning() {
|
|
2334
|
+
return this.active !== null;
|
|
2335
|
+
}
|
|
2336
|
+
subscribe(listener) {
|
|
2337
|
+
this.listeners.add(listener);
|
|
2338
|
+
return () => this.listeners.delete(listener);
|
|
2339
|
+
}
|
|
2340
|
+
async whenIdle() {
|
|
2341
|
+
await this.active;
|
|
2342
|
+
}
|
|
2343
|
+
handleEvent(event) {
|
|
2344
|
+
if (event.type !== "COMMENT_PUBLISH" || this.stopped) return;
|
|
2345
|
+
if (this.activeRequestId === event.requestId || this.finishedRequestIds.has(event.requestId)) return;
|
|
2346
|
+
if (this.active) {
|
|
2347
|
+
void this.reportOnly(event, "AGENT_BUSY", "FAILED");
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
this.active = this.run(event).finally(() => {
|
|
2351
|
+
this.active = null;
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
async run(request) {
|
|
2355
|
+
const controller = new AbortController();
|
|
2356
|
+
const timeout = this.options.setTimer(
|
|
2357
|
+
() => controller.abort(),
|
|
2358
|
+
this.options.publishTimeoutMs ?? 3 * 60 * 1e3
|
|
2359
|
+
);
|
|
2360
|
+
this.currentController = controller;
|
|
2361
|
+
this.activeRequestId = request.requestId;
|
|
2362
|
+
this.emit({ type: "STARTED", requestId: request.requestId, postUrl: request.postUrl });
|
|
2363
|
+
let granted = false;
|
|
2364
|
+
try {
|
|
2365
|
+
if (this.options.isScanActive()) {
|
|
2366
|
+
await this.reportOnly(request, "SCANNER_BUSY", "FAILED");
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
if (this.options.isCommentFetchActive()) {
|
|
2370
|
+
await this.reportOnly(request, "COMMENT_FETCH_ACTIVE", "FAILED");
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
if (this.options.isLoginActive()) {
|
|
2374
|
+
await this.reportOnly(request, "LOGIN_ACTIVE", "FAILED");
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
let image;
|
|
2378
|
+
if (request.image) {
|
|
2379
|
+
try {
|
|
2380
|
+
image = await this.options.http.downloadCommentImage(request.requestId, request.image, controller.signal);
|
|
2381
|
+
} catch {
|
|
2382
|
+
await this.reportOnly(request, "IMAGE_DOWNLOAD_FAILED", "FAILED");
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
const storageState = await this.options.facebookState.load();
|
|
2387
|
+
if (!storageState) {
|
|
2388
|
+
await this.reportOnly(request, "FACEBOOK_SESSION_MISSING", "FAILED");
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
let result;
|
|
2392
|
+
try {
|
|
2393
|
+
result = await this.options.scanner.publishComment({
|
|
2394
|
+
onDiagnostic: (diagnostic) => this.emit({ type: "DIAGNOSTIC", requestId: request.requestId, diagnostic }),
|
|
2395
|
+
beforeSubmit: async () => {
|
|
2396
|
+
const started = await this.options.http.startCommentPublish(request.requestId, controller.signal);
|
|
2397
|
+
granted = started.allowed;
|
|
2398
|
+
return started.allowed;
|
|
2399
|
+
},
|
|
2400
|
+
content: request.content,
|
|
2401
|
+
skipVerification: request.skipVerification,
|
|
2402
|
+
...image && request.image ? { image: { buffer: image, contentType: request.image.contentType } } : {},
|
|
2403
|
+
postUrl: request.postUrl,
|
|
2404
|
+
signal: controller.signal,
|
|
2405
|
+
storageState
|
|
2406
|
+
});
|
|
2407
|
+
} catch {
|
|
2408
|
+
await this.reportOnly(
|
|
2409
|
+
request,
|
|
2410
|
+
granted ? "SUBMIT_UNCONFIRMED" : "PUBLISH_START_FAILED",
|
|
2411
|
+
granted ? "UNKNOWN" : "FAILED"
|
|
2412
|
+
);
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
await this.report(request, {
|
|
2416
|
+
...result.commentUrl ? { commentUrl: result.commentUrl } : {},
|
|
2417
|
+
...result.errorCode ? { errorCode: result.errorCode } : {},
|
|
2418
|
+
...result.facebookCommentId ? { facebookCommentId: result.facebookCommentId } : {},
|
|
2419
|
+
outcome: result.outcome,
|
|
2420
|
+
submitAttempted: result.submitAttempted
|
|
2421
|
+
});
|
|
2422
|
+
} finally {
|
|
2423
|
+
this.options.clearTimer(timeout);
|
|
2424
|
+
if (this.currentController === controller) this.currentController = null;
|
|
2425
|
+
this.activeRequestId = null;
|
|
2426
|
+
this.finishedRequestIds.add(request.requestId);
|
|
2427
|
+
if (this.finishedRequestIds.size > 100) {
|
|
2428
|
+
const oldest = this.finishedRequestIds.values().next().value;
|
|
2429
|
+
if (oldest) this.finishedRequestIds.delete(oldest);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
async reportOnly(request, errorCode, outcome) {
|
|
2434
|
+
await this.report(request, { errorCode, outcome, submitAttempted: outcome === "UNKNOWN" });
|
|
2435
|
+
}
|
|
2436
|
+
async report(request, result) {
|
|
2437
|
+
const attempts = Math.max(1, this.options.reportAttempts ?? DEFAULT_REPORT_ATTEMPTS);
|
|
2438
|
+
let delivered = false;
|
|
2439
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
2440
|
+
try {
|
|
2441
|
+
await this.options.http.reportCommentPublish(request.requestId, result);
|
|
2442
|
+
delivered = true;
|
|
2443
|
+
break;
|
|
2444
|
+
} catch {
|
|
2445
|
+
if (attempt === attempts || this.stopController.signal.aborted) break;
|
|
2446
|
+
try {
|
|
2447
|
+
await this.options.wait(1e3 * 2 ** (attempt - 1), this.stopController.signal);
|
|
2448
|
+
} catch {
|
|
2449
|
+
break;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
if (!delivered) {
|
|
2454
|
+
this.emit({
|
|
2455
|
+
type: "REPORT_FAILED",
|
|
2456
|
+
errorCode: "RESULT_REPORT_FAILED",
|
|
2457
|
+
requestId: request.requestId,
|
|
2458
|
+
postUrl: request.postUrl
|
|
2459
|
+
});
|
|
2460
|
+
} else {
|
|
2461
|
+
if (result.outcome === "SUCCEEDED") {
|
|
2462
|
+
this.emit({ type: "SUCCEEDED", requestId: request.requestId, postUrl: request.postUrl });
|
|
2463
|
+
} else if (result.outcome === "UNKNOWN") {
|
|
2464
|
+
this.emit({
|
|
2465
|
+
type: "UNKNOWN",
|
|
2466
|
+
errorCode: result.errorCode ?? "UNKNOWN",
|
|
2467
|
+
requestId: request.requestId,
|
|
2468
|
+
postUrl: request.postUrl
|
|
2469
|
+
});
|
|
2470
|
+
} else {
|
|
2471
|
+
this.emit({
|
|
2472
|
+
type: "FAILED",
|
|
2473
|
+
errorCode: result.errorCode ?? "FAILED",
|
|
2474
|
+
requestId: request.requestId,
|
|
2475
|
+
postUrl: request.postUrl
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
emit(event) {
|
|
2481
|
+
for (const listener of this.listeners) listener(event);
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
|
|
1855
2485
|
// src/main/desktop-comment-runner.ts
|
|
1856
2486
|
var DEFAULT_FETCH_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
1857
2487
|
var DEFAULT_MAX_QUEUE = 5;
|
|
@@ -2049,7 +2679,60 @@ var DesktopScanHttp = class {
|
|
|
2049
2679
|
signal
|
|
2050
2680
|
);
|
|
2051
2681
|
}
|
|
2682
|
+
startCommentPublish(requestId, signal) {
|
|
2683
|
+
return this.signedPost(
|
|
2684
|
+
`/leads/desktop-agent/comment-publish-requests/${requestId}/start`,
|
|
2685
|
+
{},
|
|
2686
|
+
signal
|
|
2687
|
+
);
|
|
2688
|
+
}
|
|
2689
|
+
reportCommentPublish(requestId, result, signal) {
|
|
2690
|
+
return this.signedPost(
|
|
2691
|
+
`/leads/desktop-agent/comment-publish-requests/${requestId}/result`,
|
|
2692
|
+
result,
|
|
2693
|
+
signal
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
async downloadCommentImage(requestId, metadata, signal) {
|
|
2697
|
+
const response = await this.signedRequest(
|
|
2698
|
+
`/leads/desktop-agent/comment-publish-requests/${requestId}/image`,
|
|
2699
|
+
{},
|
|
2700
|
+
signal,
|
|
2701
|
+
"image"
|
|
2702
|
+
);
|
|
2703
|
+
const lengthHeader = response.headers.get("content-length");
|
|
2704
|
+
if (lengthHeader !== null) {
|
|
2705
|
+
const length = Number(lengthHeader);
|
|
2706
|
+
if (!Number.isFinite(length) || length > COMMENT_IMAGE_MAX_BYTES || length !== metadata.byteSize) {
|
|
2707
|
+
throw new DesktopScanHttpError("IMAGE_INVALID");
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
if (!response.body) throw new DesktopScanHttpError("IMAGE_INVALID");
|
|
2711
|
+
const reader = response.body.getReader();
|
|
2712
|
+
const chunks = [];
|
|
2713
|
+
let size = 0;
|
|
2714
|
+
for (; ; ) {
|
|
2715
|
+
const { done, value } = await reader.read();
|
|
2716
|
+
if (done) break;
|
|
2717
|
+
size += value.byteLength;
|
|
2718
|
+
if (size > COMMENT_IMAGE_MAX_BYTES || size > metadata.byteSize) {
|
|
2719
|
+
await reader.cancel();
|
|
2720
|
+
throw new DesktopScanHttpError("IMAGE_INVALID");
|
|
2721
|
+
}
|
|
2722
|
+
chunks.push(value);
|
|
2723
|
+
}
|
|
2724
|
+
const bytes = Buffer.concat(chunks, size);
|
|
2725
|
+
if (bytes.byteLength !== metadata.byteSize) throw new DesktopScanHttpError("IMAGE_INVALID");
|
|
2726
|
+
if (createHash("sha256").update(bytes).digest("hex") !== metadata.sha256) {
|
|
2727
|
+
throw new DesktopScanHttpError("IMAGE_INVALID");
|
|
2728
|
+
}
|
|
2729
|
+
return bytes;
|
|
2730
|
+
}
|
|
2052
2731
|
async signedPost(path, businessPayload, signal) {
|
|
2732
|
+
const response = await this.signedRequest(path, businessPayload, signal);
|
|
2733
|
+
return readBoundedJson2(response);
|
|
2734
|
+
}
|
|
2735
|
+
async signedRequest(path, businessPayload, signal, responseType = "json") {
|
|
2053
2736
|
const credential = this.getCredential();
|
|
2054
2737
|
if (!credential) throw new DesktopScanHttpError("CREDENTIAL_UNAVAILABLE");
|
|
2055
2738
|
const signedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2083,21 +2766,26 @@ var DesktopScanHttp = class {
|
|
|
2083
2766
|
"content-type": "application/json"
|
|
2084
2767
|
},
|
|
2085
2768
|
method: "POST",
|
|
2769
|
+
redirect: "error",
|
|
2086
2770
|
signal: requestSignal
|
|
2087
2771
|
});
|
|
2088
2772
|
} catch {
|
|
2089
2773
|
throw new DesktopScanHttpError("NETWORK_ERROR", true);
|
|
2090
2774
|
}
|
|
2091
|
-
const contentType = response.headers.get("content-type")?.split(";", 1)[0];
|
|
2092
|
-
if (contentType !== "application/json") throw new DesktopScanHttpError("RESPONSE_INVALID");
|
|
2093
|
-
const value = await readBoundedJson2(response);
|
|
2094
2775
|
if (!response.ok) {
|
|
2095
2776
|
throw new DesktopScanHttpError(
|
|
2096
2777
|
response.status >= 500 ? "SERVER_UNAVAILABLE" : response.status === 413 ? "BATCH_TOO_LARGE" : "BATCH_REJECTED",
|
|
2097
2778
|
response.status >= 500
|
|
2098
2779
|
);
|
|
2099
2780
|
}
|
|
2100
|
-
|
|
2781
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0] ?? "";
|
|
2782
|
+
if (responseType === "json" && contentType !== "application/json") {
|
|
2783
|
+
throw new DesktopScanHttpError("RESPONSE_INVALID");
|
|
2784
|
+
}
|
|
2785
|
+
if (responseType === "image" && !contentType.startsWith("image/")) {
|
|
2786
|
+
throw new DesktopScanHttpError("IMAGE_INVALID");
|
|
2787
|
+
}
|
|
2788
|
+
return response;
|
|
2101
2789
|
}
|
|
2102
2790
|
};
|
|
2103
2791
|
async function readBoundedJson2(response) {
|
|
@@ -2314,7 +3002,7 @@ var DesktopScanRunner = class {
|
|
|
2314
3002
|
return true;
|
|
2315
3003
|
}
|
|
2316
3004
|
async handleEvent(event) {
|
|
2317
|
-
if (event.type === "COMMENTS") return;
|
|
3005
|
+
if (event.type === "COMMENTS" || event.type === "COMMENT_PUBLISH") return;
|
|
2318
3006
|
if (event.type === "AVAILABLE") {
|
|
2319
3007
|
await this.claimIfReady(event.jobId);
|
|
2320
3008
|
return;
|
|
@@ -3696,6 +4384,7 @@ async function commandRun(runtime, io) {
|
|
|
3696
4384
|
browserType: runtime.browserType,
|
|
3697
4385
|
parseGraphqlBody: createGraphqlDumpParser(process.env.FACEBOOK_SCAN_DUMP_DIR)
|
|
3698
4386
|
});
|
|
4387
|
+
let publishRunner = null;
|
|
3699
4388
|
const commentRunner = new DesktopCommentRunner({
|
|
3700
4389
|
clearTimer: (timer) => clearTimeout(timer),
|
|
3701
4390
|
facebookState: runtime.facebook,
|
|
@@ -3715,7 +4404,7 @@ async function commandRun(runtime, io) {
|
|
|
3715
4404
|
clearTimer: (timer) => clearTimeout(timer),
|
|
3716
4405
|
facebookState: runtime.facebook,
|
|
3717
4406
|
http: scanHttp,
|
|
3718
|
-
isCommentFetchActive: () => commentRunner.isRunning(),
|
|
4407
|
+
isCommentFetchActive: () => commentRunner.isRunning() || (publishRunner?.isRunning() ?? false),
|
|
3719
4408
|
isLoginActive: () => false,
|
|
3720
4409
|
now: Date.now,
|
|
3721
4410
|
randomUuid: () => randomUUID3(),
|
|
@@ -3732,6 +4421,22 @@ async function commandRun(runtime, io) {
|
|
|
3732
4421
|
spool: runtime.spool,
|
|
3733
4422
|
wait: abortableDelay
|
|
3734
4423
|
});
|
|
4424
|
+
publishRunner = new DesktopCommentPublishRunner({
|
|
4425
|
+
clearTimer: (timer) => clearTimeout(timer),
|
|
4426
|
+
facebookState: runtime.facebook,
|
|
4427
|
+
http: scanHttp,
|
|
4428
|
+
isCommentFetchActive: () => commentRunner.isRunning(),
|
|
4429
|
+
isLoginActive: () => false,
|
|
4430
|
+
isScanActive: () => scanRunner.isRunning(),
|
|
4431
|
+
scanner,
|
|
4432
|
+
session,
|
|
4433
|
+
setTimer: (callback, delay) => {
|
|
4434
|
+
const timer = setTimeout(callback, delay);
|
|
4435
|
+
timer.unref();
|
|
4436
|
+
return timer;
|
|
4437
|
+
},
|
|
4438
|
+
wait: abortableDelay
|
|
4439
|
+
});
|
|
3735
4440
|
let onReplaced = () => void 0;
|
|
3736
4441
|
const replaced = new Promise((resolve) => {
|
|
3737
4442
|
onReplaced = resolve;
|
|
@@ -3751,6 +4456,7 @@ async function commandRun(runtime, io) {
|
|
|
3751
4456
|
});
|
|
3752
4457
|
scanRunner.start();
|
|
3753
4458
|
commentRunner.start();
|
|
4459
|
+
publishRunner.start();
|
|
3754
4460
|
const unsubscribeComments = commentRunner.subscribe((event) => {
|
|
3755
4461
|
if (event.type === "STARTED") {
|
|
3756
4462
|
io.log(`${stamp()} comments: fetching ${event.postUrl}`);
|
|
@@ -3774,6 +4480,34 @@ async function commandRun(runtime, io) {
|
|
|
3774
4480
|
level: "WARN"
|
|
3775
4481
|
});
|
|
3776
4482
|
});
|
|
4483
|
+
const unsubscribePublish = publishRunner.subscribe((event) => {
|
|
4484
|
+
if (event.type === "DIAGNOSTIC") {
|
|
4485
|
+
const message = JSON.stringify({ requestId: event.requestId, ...event.diagnostic });
|
|
4486
|
+
io.log(`${stamp()} comments: diagnostic ${message}`);
|
|
4487
|
+
recordActivity(message, "COMMENT_PUBLISH_DIAGNOSTIC", { category: "COMMENTS" });
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4490
|
+
if (event.type === "STARTED") {
|
|
4491
|
+
io.log(`${stamp()} comments: publishing on ${event.postUrl}`);
|
|
4492
|
+
recordActivity(`Publishing a comment on ${event.postUrl}.`, "COMMENT_PUBLISH_STARTED", {
|
|
4493
|
+
category: "COMMENTS"
|
|
4494
|
+
});
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
if (event.type === "SUCCEEDED") {
|
|
4498
|
+
io.log(`${stamp()} comments: published`);
|
|
4499
|
+
recordActivity(`Published a comment on ${event.postUrl}.`, "COMMENT_PUBLISH_SUCCEEDED", {
|
|
4500
|
+
category: "COMMENTS"
|
|
4501
|
+
});
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4504
|
+
io.log(`${stamp()} comments: publish ${event.type.toLowerCase()} (${event.errorCode})`);
|
|
4505
|
+
recordActivity(
|
|
4506
|
+
`Comment publish ${event.type.toLowerCase()} (${event.errorCode}) on ${event.postUrl}.`,
|
|
4507
|
+
event.errorCode,
|
|
4508
|
+
{ category: "COMMENTS", level: "WARN" }
|
|
4509
|
+
);
|
|
4510
|
+
});
|
|
3777
4511
|
await scanRunner.recover();
|
|
3778
4512
|
await session.start();
|
|
3779
4513
|
io.log(`Worker running as "${stored.device.name}" (API ${runtime.endpoint.httpOrigin}). Press Ctrl+C to stop.`);
|
|
@@ -3790,7 +4524,9 @@ async function commandRun(runtime, io) {
|
|
|
3790
4524
|
clearInterval(keepAlive);
|
|
3791
4525
|
session.stop();
|
|
3792
4526
|
commentRunner.stop();
|
|
4527
|
+
publishRunner.stop();
|
|
3793
4528
|
unsubscribeComments();
|
|
4529
|
+
unsubscribePublish();
|
|
3794
4530
|
scanRunner.stop();
|
|
3795
4531
|
}
|
|
3796
4532
|
return 0;
|