diggylab 0.1.50 → 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 -225
- package/cli.js +921 -134
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -8,6 +8,106 @@ 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/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
|
|
17
|
+
var DESKTOP_AGENT_PROTOCOL_VERSION = 1;
|
|
18
|
+
var DESKTOP_POST_COMMENTS_MAX_LIMIT = 200;
|
|
19
|
+
var DESKTOP_POST_COMMENTS_REQUEST_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
20
|
+
var FACEBOOK_DOMAIN = ".facebook.com";
|
|
21
|
+
var FACEBOOK_HOST = /(^|\.)facebook\.com$/i;
|
|
22
|
+
function isFacebookDomain(domain) {
|
|
23
|
+
return Boolean(domain && FACEBOOK_HOST.test(domain.replace(/^\./, "")));
|
|
24
|
+
}
|
|
25
|
+
function normalizeSameSite(value) {
|
|
26
|
+
if (typeof value !== "string")
|
|
27
|
+
return void 0;
|
|
28
|
+
const lower = value.toLowerCase().replace(/[^a-z]/g, "");
|
|
29
|
+
if (lower === "lax")
|
|
30
|
+
return "Lax";
|
|
31
|
+
if (lower === "strict")
|
|
32
|
+
return "Strict";
|
|
33
|
+
if (lower === "none" || lower === "norestriction")
|
|
34
|
+
return "None";
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
function toCookie(value) {
|
|
38
|
+
const name = typeof value.name === "string" ? value.name.trim() : "";
|
|
39
|
+
const cookieValue = typeof value.value === "string" ? value.value : "";
|
|
40
|
+
if (!name || !cookieValue)
|
|
41
|
+
return null;
|
|
42
|
+
return {
|
|
43
|
+
domain: typeof value.domain === "string" ? value.domain : void 0,
|
|
44
|
+
expires: typeof value.expires === "number" ? value.expires : typeof value.expirationDate === "number" ? value.expirationDate : void 0,
|
|
45
|
+
httpOnly: typeof value.httpOnly === "boolean" ? value.httpOnly : void 0,
|
|
46
|
+
name,
|
|
47
|
+
path: typeof value.path === "string" ? value.path : void 0,
|
|
48
|
+
sameSite: normalizeSameSite(value.sameSite),
|
|
49
|
+
secure: typeof value.secure === "boolean" ? value.secure : void 0,
|
|
50
|
+
value: cookieValue
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function parseCookieHeader(input) {
|
|
54
|
+
const header = input.replace(/^\s*cookie\s*:/i, "");
|
|
55
|
+
return header.split(/[;\n]/).map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
56
|
+
const separator = part.indexOf("=");
|
|
57
|
+
if (separator <= 0)
|
|
58
|
+
return null;
|
|
59
|
+
return { name: part.slice(0, separator).trim(), value: part.slice(separator + 1) };
|
|
60
|
+
}).filter((cookie) => cookie !== null);
|
|
61
|
+
}
|
|
62
|
+
function parseCookieTable(input) {
|
|
63
|
+
const cookies = [];
|
|
64
|
+
for (const line of input.split("\n")) {
|
|
65
|
+
const columns = line.split(" ");
|
|
66
|
+
const name = columns[0]?.trim() ?? "";
|
|
67
|
+
const value = columns[1];
|
|
68
|
+
if (!value || !name || name === "Name")
|
|
69
|
+
continue;
|
|
70
|
+
cookies.push({ domain: columns[2]?.trim(), name, value });
|
|
71
|
+
}
|
|
72
|
+
return cookies;
|
|
73
|
+
}
|
|
74
|
+
function parseCookieInput(input) {
|
|
75
|
+
const trimmed = input.trim();
|
|
76
|
+
if (!trimmed)
|
|
77
|
+
return [];
|
|
78
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
79
|
+
return trimmed.includes(" ") ? parseCookieTable(trimmed) : parseCookieHeader(trimmed);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(trimmed);
|
|
83
|
+
if (Array.isArray(parsed)) {
|
|
84
|
+
return parsed.map((entry) => entry && typeof entry === "object" ? toCookie(entry) : null).filter((cookie) => cookie !== null);
|
|
85
|
+
}
|
|
86
|
+
if (parsed && typeof parsed === "object") {
|
|
87
|
+
const cookies = parsed.cookies;
|
|
88
|
+
if (Array.isArray(cookies)) {
|
|
89
|
+
return cookies.map((entry) => entry && typeof entry === "object" ? toCookie(entry) : null).filter((cookie) => cookie !== null);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
function toFacebookStorageState(cookies) {
|
|
98
|
+
const usable = cookies.filter((cookie) => isFacebookDomain(cookie.domain) || cookie.domain === void 0).map((cookie) => ({
|
|
99
|
+
domain: isFacebookDomain(cookie.domain) ? cookie.domain : FACEBOOK_DOMAIN,
|
|
100
|
+
expires: typeof cookie.expires === "number" && cookie.expires > 0 ? cookie.expires : -1,
|
|
101
|
+
httpOnly: cookie.httpOnly ?? true,
|
|
102
|
+
name: cookie.name,
|
|
103
|
+
path: cookie.path?.startsWith("/") ? cookie.path : "/",
|
|
104
|
+
sameSite: cookie.sameSite ?? "Lax",
|
|
105
|
+
secure: cookie.secure ?? true,
|
|
106
|
+
value: cookie.value
|
|
107
|
+
}));
|
|
108
|
+
return { cookies: usable, origins: [] };
|
|
109
|
+
}
|
|
110
|
+
|
|
11
111
|
// ../../packages/facebook-scanner/dist/parser.js
|
|
12
112
|
var COMMENT_TEXT_LIMIT = 2e3;
|
|
13
113
|
var COMMENT_AUTHOR_LIMIT = 200;
|
|
@@ -675,6 +775,370 @@ function parseCommentDepth(label) {
|
|
|
675
775
|
return /\brepl(?:y|ies)\b|trả lời|phản hồi/i.test(parentPart) ? 2 : 1;
|
|
676
776
|
}
|
|
677
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
|
+
|
|
678
1142
|
// ../../packages/facebook-scanner/dist/session.js
|
|
679
1143
|
async function detectFacebookSession(page, context) {
|
|
680
1144
|
let url;
|
|
@@ -708,6 +1172,7 @@ var FacebookScannerError = class extends Error {
|
|
|
708
1172
|
};
|
|
709
1173
|
var FacebookScanner = class {
|
|
710
1174
|
browserType;
|
|
1175
|
+
proxy;
|
|
711
1176
|
extractDomItems;
|
|
712
1177
|
extractDomComments;
|
|
713
1178
|
parseGraphqlBody;
|
|
@@ -717,6 +1182,7 @@ var FacebookScanner = class {
|
|
|
717
1182
|
running = false;
|
|
718
1183
|
constructor(options) {
|
|
719
1184
|
this.browserType = options.browserType;
|
|
1185
|
+
this.proxy = options.proxy;
|
|
720
1186
|
this.extractDomItems = options.extractDomItems ?? extractFacebookDomItems;
|
|
721
1187
|
this.extractDomComments = options.extractDomComments ?? extractFacebookDomComments;
|
|
722
1188
|
this.parseGraphqlBody = options.parseGraphqlBody ?? parseFacebookGraphqlBody;
|
|
@@ -730,10 +1196,13 @@ var FacebookScanner = class {
|
|
|
730
1196
|
this.running = true;
|
|
731
1197
|
let browser = null;
|
|
732
1198
|
try {
|
|
733
|
-
browser = await this.browserType.launch({
|
|
1199
|
+
browser = await this.browserType.launch({
|
|
1200
|
+
headless: true,
|
|
1201
|
+
...this.proxy ? { proxy: this.proxy } : {}
|
|
1202
|
+
});
|
|
734
1203
|
const results = [];
|
|
735
1204
|
for (const [index, groupUrl] of options.groupUrls.entries()) {
|
|
736
|
-
|
|
1205
|
+
assertNotAborted2(options.signal);
|
|
737
1206
|
const items = await this.scanGroup(browser, options, groupUrl, index);
|
|
738
1207
|
results.push(items);
|
|
739
1208
|
}
|
|
@@ -749,11 +1218,14 @@ var FacebookScanner = class {
|
|
|
749
1218
|
const target = normalizePostUrl(options.postUrl);
|
|
750
1219
|
if (!target)
|
|
751
1220
|
throw new FacebookScannerError("POST_URL_INVALID");
|
|
752
|
-
|
|
1221
|
+
assertNotAborted2(options.signal);
|
|
753
1222
|
this.running = true;
|
|
754
1223
|
let browser = null;
|
|
755
1224
|
try {
|
|
756
|
-
browser = await this.browserType.launch({
|
|
1225
|
+
browser = await this.browserType.launch({
|
|
1226
|
+
headless: true,
|
|
1227
|
+
...this.proxy ? { proxy: this.proxy } : {}
|
|
1228
|
+
});
|
|
757
1229
|
const context = await browser.newContext({ storageState: options.storageState });
|
|
758
1230
|
let page = null;
|
|
759
1231
|
const graphqlComments = [];
|
|
@@ -781,7 +1253,7 @@ var FacebookScanner = class {
|
|
|
781
1253
|
let idleRounds = 0;
|
|
782
1254
|
let rounds = 0;
|
|
783
1255
|
while (comments.length < options.limit && idleRounds < 3 && rounds < 20) {
|
|
784
|
-
|
|
1256
|
+
assertNotAborted2(options.signal);
|
|
785
1257
|
rounds += 1;
|
|
786
1258
|
const previousCount = comments.length;
|
|
787
1259
|
await page.waitForTimeout(1200);
|
|
@@ -798,7 +1270,7 @@ var FacebookScanner = class {
|
|
|
798
1270
|
await page.mouse.wheel(0, 2e3);
|
|
799
1271
|
}
|
|
800
1272
|
}
|
|
801
|
-
|
|
1273
|
+
assertNotAborted2(options.signal);
|
|
802
1274
|
await drain(responseTasks, this.graphqlResponseTimeoutMs);
|
|
803
1275
|
const domComments = await this.extractDomComments(page);
|
|
804
1276
|
comments = mergeFacebookComments([...embeddedComments, ...graphqlComments, ...domComments], options.limit);
|
|
@@ -816,6 +1288,57 @@ var FacebookScanner = class {
|
|
|
816
1288
|
this.running = false;
|
|
817
1289
|
}
|
|
818
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
|
+
}
|
|
819
1342
|
async scanGroup(browser, options, groupUrl, groupIndex) {
|
|
820
1343
|
const context = await browser.newContext({ storageState: options.storageState });
|
|
821
1344
|
let page = null;
|
|
@@ -845,7 +1368,7 @@ var FacebookScanner = class {
|
|
|
845
1368
|
let items = [];
|
|
846
1369
|
let idleRounds = 0;
|
|
847
1370
|
while (items.length < options.limit && idleRounds < 3) {
|
|
848
|
-
|
|
1371
|
+
assertNotAborted2(options.signal);
|
|
849
1372
|
const previousCount = items.length;
|
|
850
1373
|
await page.waitForTimeout(1500);
|
|
851
1374
|
await drain(responseTasks, this.graphqlResponseTimeoutMs);
|
|
@@ -863,7 +1386,7 @@ var FacebookScanner = class {
|
|
|
863
1386
|
if (items.length < options.limit)
|
|
864
1387
|
await page.mouse.wheel(0, 2e3);
|
|
865
1388
|
}
|
|
866
|
-
|
|
1389
|
+
assertNotAborted2(options.signal);
|
|
867
1390
|
await drain(responseTasks, this.graphqlResponseTimeoutMs);
|
|
868
1391
|
items = mergeFacebookScanItems([...graphqlItems, ...domItems], options.limit);
|
|
869
1392
|
if (!items.length)
|
|
@@ -879,7 +1402,7 @@ var FacebookScanner = class {
|
|
|
879
1402
|
}
|
|
880
1403
|
}
|
|
881
1404
|
};
|
|
882
|
-
function
|
|
1405
|
+
function assertNotAborted2(signal) {
|
|
883
1406
|
if (signal?.aborted)
|
|
884
1407
|
throw new FacebookScannerError("SCAN_CANCELED");
|
|
885
1408
|
}
|
|
@@ -1360,13 +1883,6 @@ function isDeviceSummary(value) {
|
|
|
1360
1883
|
|
|
1361
1884
|
// src/main/desktop-agent-session.ts
|
|
1362
1885
|
import { randomUUID } from "node:crypto";
|
|
1363
|
-
|
|
1364
|
-
// ../../packages/shared/src/index.ts
|
|
1365
|
-
var DESKTOP_AGENT_PROTOCOL_VERSION = 1;
|
|
1366
|
-
var DESKTOP_POST_COMMENTS_MAX_LIMIT = 200;
|
|
1367
|
-
var DESKTOP_POST_COMMENTS_REQUEST_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
1368
|
-
|
|
1369
|
-
// src/main/desktop-agent-session.ts
|
|
1370
1886
|
var MESSAGE_FRESHNESS_MS = 5 * 60 * 1e3;
|
|
1371
1887
|
var MAX_REPLAY_IDS = 512;
|
|
1372
1888
|
var DesktopAgentSession = class {
|
|
@@ -1527,11 +2043,12 @@ var DesktopAgentSession = class {
|
|
|
1527
2043
|
envelope.payload.challenge
|
|
1528
2044
|
),
|
|
1529
2045
|
agentVersion: this.options.agentVersion,
|
|
2046
|
+
...this.options.clientType ? { clientType: this.options.clientType } : {},
|
|
1530
2047
|
platform: this.options.platform,
|
|
1531
2048
|
state: this.reportedState,
|
|
1532
2049
|
facebookReady: this.options.getFacebookReady(),
|
|
1533
2050
|
currentJobId: this.reportedJobId,
|
|
1534
|
-
capabilities: ["POST_COMMENTS", "POST_COMMENTS_MORE"]
|
|
2051
|
+
capabilities: ["POST_COMMENTS", "POST_COMMENTS_MORE", "POST_COMMENT_PUBLISH"]
|
|
1535
2052
|
});
|
|
1536
2053
|
this.handshake = "WELCOME";
|
|
1537
2054
|
return;
|
|
@@ -1575,6 +2092,16 @@ var DesktopAgentSession = class {
|
|
|
1575
2092
|
this.emitJob({ type: "STOP", ...envelope.payload });
|
|
1576
2093
|
return;
|
|
1577
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
|
+
}
|
|
1578
2105
|
if (envelope.type === "POST_COMMENTS_FETCH") {
|
|
1579
2106
|
if (!isPostCommentsFetch(envelope.payload)) {
|
|
1580
2107
|
this.protocolFailure(socket, stored, generation);
|
|
@@ -1594,10 +2121,12 @@ var DesktopAgentSession = class {
|
|
|
1594
2121
|
this.clearHeartbeat();
|
|
1595
2122
|
this.heartbeatTimer = this.options.setTimer(() => {
|
|
1596
2123
|
if (generation !== this.generation || socket !== this.socket) return;
|
|
2124
|
+
const metrics = this.options.metrics?.();
|
|
1597
2125
|
this.send(socket, deviceId, "HEARTBEAT", {
|
|
1598
2126
|
state: this.reportedState,
|
|
1599
2127
|
facebookReady: this.options.getFacebookReady(),
|
|
1600
|
-
currentJobId: this.reportedJobId
|
|
2128
|
+
currentJobId: this.reportedJobId,
|
|
2129
|
+
...metrics ? { metrics } : {}
|
|
1601
2130
|
});
|
|
1602
2131
|
this.scheduleHeartbeat(socket, deviceId, interval, generation);
|
|
1603
2132
|
}, interval);
|
|
@@ -1703,9 +2232,9 @@ var DesktopAgentSession = class {
|
|
|
1703
2232
|
if (this.accessRefreshTimer) this.options.clearTimer(this.accessRefreshTimer);
|
|
1704
2233
|
this.accessRefreshTimer = null;
|
|
1705
2234
|
}
|
|
1706
|
-
update(
|
|
1707
|
-
this.snapshot =
|
|
1708
|
-
for (const listener of this.listeners) listener(
|
|
2235
|
+
update(snapshot2) {
|
|
2236
|
+
this.snapshot = snapshot2;
|
|
2237
|
+
for (const listener of this.listeners) listener(snapshot2);
|
|
1709
2238
|
}
|
|
1710
2239
|
};
|
|
1711
2240
|
function parseServerEnvelope(data) {
|
|
@@ -1732,6 +2261,20 @@ function isPostCommentsFetch(value) {
|
|
|
1732
2261
|
const payload = value;
|
|
1733
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;
|
|
1734
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
|
+
}
|
|
1735
2278
|
function emptySnapshot(state) {
|
|
1736
2279
|
return {
|
|
1737
2280
|
state,
|
|
@@ -1761,6 +2304,184 @@ function readErrorCode(error) {
|
|
|
1761
2304
|
return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : "UNKNOWN_ERROR";
|
|
1762
2305
|
}
|
|
1763
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
|
+
|
|
1764
2485
|
// src/main/desktop-comment-runner.ts
|
|
1765
2486
|
var DEFAULT_FETCH_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
1766
2487
|
var DEFAULT_MAX_QUEUE = 5;
|
|
@@ -1958,7 +2679,60 @@ var DesktopScanHttp = class {
|
|
|
1958
2679
|
signal
|
|
1959
2680
|
);
|
|
1960
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
|
+
}
|
|
1961
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") {
|
|
1962
2736
|
const credential = this.getCredential();
|
|
1963
2737
|
if (!credential) throw new DesktopScanHttpError("CREDENTIAL_UNAVAILABLE");
|
|
1964
2738
|
const signedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1992,21 +2766,26 @@ var DesktopScanHttp = class {
|
|
|
1992
2766
|
"content-type": "application/json"
|
|
1993
2767
|
},
|
|
1994
2768
|
method: "POST",
|
|
2769
|
+
redirect: "error",
|
|
1995
2770
|
signal: requestSignal
|
|
1996
2771
|
});
|
|
1997
2772
|
} catch {
|
|
1998
2773
|
throw new DesktopScanHttpError("NETWORK_ERROR", true);
|
|
1999
2774
|
}
|
|
2000
|
-
const contentType = response.headers.get("content-type")?.split(";", 1)[0];
|
|
2001
|
-
if (contentType !== "application/json") throw new DesktopScanHttpError("RESPONSE_INVALID");
|
|
2002
|
-
const value = await readBoundedJson2(response);
|
|
2003
2775
|
if (!response.ok) {
|
|
2004
2776
|
throw new DesktopScanHttpError(
|
|
2005
2777
|
response.status >= 500 ? "SERVER_UNAVAILABLE" : response.status === 413 ? "BATCH_TOO_LARGE" : "BATCH_REJECTED",
|
|
2006
2778
|
response.status >= 500
|
|
2007
2779
|
);
|
|
2008
2780
|
}
|
|
2009
|
-
|
|
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;
|
|
2010
2789
|
}
|
|
2011
2790
|
};
|
|
2012
2791
|
async function readBoundedJson2(response) {
|
|
@@ -2223,7 +3002,7 @@ var DesktopScanRunner = class {
|
|
|
2223
3002
|
return true;
|
|
2224
3003
|
}
|
|
2225
3004
|
async handleEvent(event) {
|
|
2226
|
-
if (event.type === "COMMENTS") return;
|
|
3005
|
+
if (event.type === "COMMENTS" || event.type === "COMMENT_PUBLISH") return;
|
|
2227
3006
|
if (event.type === "AVAILABLE") {
|
|
2228
3007
|
await this.claimIfReady(event.jobId);
|
|
2229
3008
|
return;
|
|
@@ -2542,9 +3321,9 @@ var DesktopScanRunner = class {
|
|
|
2542
3321
|
leaseExpiresAt(job) {
|
|
2543
3322
|
return this.activeAttempt?.jobId === job.jobId && this.activeAttempt.attemptToken === job.attemptToken ? this.activeAttempt.leaseExpiresAt : job.leaseExpiresAt;
|
|
2544
3323
|
}
|
|
2545
|
-
update(
|
|
2546
|
-
this.snapshot =
|
|
2547
|
-
for (const listener of this.listeners) listener(
|
|
3324
|
+
update(snapshot2) {
|
|
3325
|
+
this.snapshot = snapshot2;
|
|
3326
|
+
for (const listener of this.listeners) listener(snapshot2);
|
|
2548
3327
|
}
|
|
2549
3328
|
};
|
|
2550
3329
|
function toDesktopPost(item) {
|
|
@@ -2870,7 +3649,7 @@ var FacebookLoginService = class {
|
|
|
2870
3649
|
await this.finish({ state: "READY", errorCode: null, accountId });
|
|
2871
3650
|
return true;
|
|
2872
3651
|
}
|
|
2873
|
-
async finish(
|
|
3652
|
+
async finish(snapshot2) {
|
|
2874
3653
|
if (this.timer) this.options.clearTimer(this.timer);
|
|
2875
3654
|
this.timer = null;
|
|
2876
3655
|
const page = this.page;
|
|
@@ -2882,12 +3661,12 @@ var FacebookLoginService = class {
|
|
|
2882
3661
|
page?.off("framenavigated", this.navigationHandler);
|
|
2883
3662
|
await context?.close();
|
|
2884
3663
|
await browser?.close();
|
|
2885
|
-
this.update(
|
|
3664
|
+
this.update(snapshot2);
|
|
2886
3665
|
}
|
|
2887
|
-
update(
|
|
2888
|
-
if (
|
|
2889
|
-
this.snapshot =
|
|
2890
|
-
for (const listener of this.listeners) listener(
|
|
3666
|
+
update(snapshot2) {
|
|
3667
|
+
if (snapshot2.state === this.snapshot.state && snapshot2.errorCode === this.snapshot.errorCode && snapshot2.accountId === this.snapshot.accountId) return;
|
|
3668
|
+
this.snapshot = snapshot2;
|
|
3669
|
+
for (const listener of this.listeners) listener(snapshot2);
|
|
2891
3670
|
}
|
|
2892
3671
|
serialize(operation) {
|
|
2893
3672
|
const result = this.pending.catch(() => void 0).then(operation);
|
|
@@ -2943,6 +3722,41 @@ function getFacebookAccountId(state) {
|
|
|
2943
3722
|
return value && /^\d{1,32}$/.test(value) ? value : null;
|
|
2944
3723
|
}
|
|
2945
3724
|
|
|
3725
|
+
// src/shared/resource-metrics.ts
|
|
3726
|
+
import os from "node:os";
|
|
3727
|
+
function snapshot(cpus) {
|
|
3728
|
+
let idle = 0;
|
|
3729
|
+
let total = 0;
|
|
3730
|
+
for (const cpu of cpus) {
|
|
3731
|
+
const times = cpu.times;
|
|
3732
|
+
idle += times.idle;
|
|
3733
|
+
total += times.user + times.nice + times.sys + times.idle + times.irq;
|
|
3734
|
+
}
|
|
3735
|
+
return { idle, total };
|
|
3736
|
+
}
|
|
3737
|
+
function usageBetween(previous, current) {
|
|
3738
|
+
const totalDelta = current.total - previous.total;
|
|
3739
|
+
const idleDelta = current.idle - previous.idle;
|
|
3740
|
+
if (totalDelta <= 0) return 0;
|
|
3741
|
+
return Math.min(100, Math.max(0, (1 - idleDelta / totalDelta) * 100));
|
|
3742
|
+
}
|
|
3743
|
+
function createMetricsCollector() {
|
|
3744
|
+
let previous = null;
|
|
3745
|
+
return () => {
|
|
3746
|
+
const cpus = os.cpus();
|
|
3747
|
+
const current = snapshot(cpus);
|
|
3748
|
+
const cpuPercent = previous ? usageBetween(previous, current) : usageBetween({ idle: 0, total: 0 }, current);
|
|
3749
|
+
previous = current;
|
|
3750
|
+
const memTotalBytes = os.totalmem();
|
|
3751
|
+
return {
|
|
3752
|
+
cpuCount: cpus.length,
|
|
3753
|
+
cpuPercent: Math.round(cpuPercent * 10) / 10,
|
|
3754
|
+
memTotalBytes,
|
|
3755
|
+
memUsedBytes: Math.max(0, memTotalBytes - os.freemem())
|
|
3756
|
+
};
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
|
|
2946
3760
|
// src/main/graphql-dump.ts
|
|
2947
3761
|
import { writeFile as writeFile4 } from "node:fs/promises";
|
|
2948
3762
|
import { join as join3 } from "node:path";
|
|
@@ -3047,90 +3861,6 @@ function browserInstallCommands(platform) {
|
|
|
3047
3861
|
return platform === "linux" ? [["install", "--with-deps", "chromium"], install] : [install];
|
|
3048
3862
|
}
|
|
3049
3863
|
|
|
3050
|
-
// src/cli/cookie-import.ts
|
|
3051
|
-
var FACEBOOK_DOMAIN = ".facebook.com";
|
|
3052
|
-
var FACEBOOK_HOST = /(^|\.)facebook\.com$/i;
|
|
3053
|
-
function isFacebookDomain(domain) {
|
|
3054
|
-
return Boolean(domain && FACEBOOK_HOST.test(domain.replace(/^\./, "")));
|
|
3055
|
-
}
|
|
3056
|
-
function normalizeSameSite(value) {
|
|
3057
|
-
if (typeof value !== "string") return void 0;
|
|
3058
|
-
const lower = value.toLowerCase().replace(/[^a-z]/g, "");
|
|
3059
|
-
if (lower === "lax") return "Lax";
|
|
3060
|
-
if (lower === "strict") return "Strict";
|
|
3061
|
-
if (lower === "none" || lower === "norestriction") return "None";
|
|
3062
|
-
return void 0;
|
|
3063
|
-
}
|
|
3064
|
-
function toCookie(value) {
|
|
3065
|
-
const name = typeof value.name === "string" ? value.name.trim() : "";
|
|
3066
|
-
const cookieValue = typeof value.value === "string" ? value.value : "";
|
|
3067
|
-
if (!name || !cookieValue) return null;
|
|
3068
|
-
return {
|
|
3069
|
-
domain: typeof value.domain === "string" ? value.domain : void 0,
|
|
3070
|
-
expires: typeof value.expires === "number" ? value.expires : typeof value.expirationDate === "number" ? value.expirationDate : void 0,
|
|
3071
|
-
httpOnly: typeof value.httpOnly === "boolean" ? value.httpOnly : void 0,
|
|
3072
|
-
name,
|
|
3073
|
-
path: typeof value.path === "string" ? value.path : void 0,
|
|
3074
|
-
sameSite: normalizeSameSite(value.sameSite),
|
|
3075
|
-
secure: typeof value.secure === "boolean" ? value.secure : void 0,
|
|
3076
|
-
value: cookieValue
|
|
3077
|
-
};
|
|
3078
|
-
}
|
|
3079
|
-
function parseCookieHeader(input) {
|
|
3080
|
-
const header = input.replace(/^\s*cookie\s*:/i, "");
|
|
3081
|
-
return header.split(/[;\n]/).map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
3082
|
-
const separator = part.indexOf("=");
|
|
3083
|
-
if (separator <= 0) return null;
|
|
3084
|
-
return { name: part.slice(0, separator).trim(), value: part.slice(separator + 1) };
|
|
3085
|
-
}).filter((cookie) => cookie !== null);
|
|
3086
|
-
}
|
|
3087
|
-
function parseCookieTable(input) {
|
|
3088
|
-
const cookies = [];
|
|
3089
|
-
for (const line of input.split("\n")) {
|
|
3090
|
-
const columns = line.split(" ");
|
|
3091
|
-
const name = columns[0]?.trim() ?? "";
|
|
3092
|
-
const value = columns[1];
|
|
3093
|
-
if (!value || !name || name === "Name") continue;
|
|
3094
|
-
cookies.push({ domain: columns[2]?.trim(), name, value });
|
|
3095
|
-
}
|
|
3096
|
-
return cookies;
|
|
3097
|
-
}
|
|
3098
|
-
function parseCookieInput(input) {
|
|
3099
|
-
const trimmed = input.trim();
|
|
3100
|
-
if (!trimmed) return [];
|
|
3101
|
-
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
3102
|
-
return trimmed.includes(" ") ? parseCookieTable(trimmed) : parseCookieHeader(trimmed);
|
|
3103
|
-
}
|
|
3104
|
-
try {
|
|
3105
|
-
const parsed = JSON.parse(trimmed);
|
|
3106
|
-
if (Array.isArray(parsed)) {
|
|
3107
|
-
return parsed.map((entry) => entry && typeof entry === "object" ? toCookie(entry) : null).filter((cookie) => cookie !== null);
|
|
3108
|
-
}
|
|
3109
|
-
if (parsed && typeof parsed === "object") {
|
|
3110
|
-
const cookies = parsed.cookies;
|
|
3111
|
-
if (Array.isArray(cookies)) {
|
|
3112
|
-
return cookies.map((entry) => entry && typeof entry === "object" ? toCookie(entry) : null).filter((cookie) => cookie !== null);
|
|
3113
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
} catch {
|
|
3116
|
-
return [];
|
|
3117
|
-
}
|
|
3118
|
-
return [];
|
|
3119
|
-
}
|
|
3120
|
-
function toFacebookStorageState(cookies) {
|
|
3121
|
-
const usable = cookies.filter((cookie) => isFacebookDomain(cookie.domain) || cookie.domain === void 0).map((cookie) => ({
|
|
3122
|
-
domain: isFacebookDomain(cookie.domain) ? cookie.domain : FACEBOOK_DOMAIN,
|
|
3123
|
-
expires: typeof cookie.expires === "number" && cookie.expires > 0 ? cookie.expires : -1,
|
|
3124
|
-
httpOnly: cookie.httpOnly ?? true,
|
|
3125
|
-
name: cookie.name,
|
|
3126
|
-
path: cookie.path?.startsWith("/") ? cookie.path : "/",
|
|
3127
|
-
sameSite: cookie.sameSite ?? "Lax",
|
|
3128
|
-
secure: cookie.secure ?? true,
|
|
3129
|
-
value: cookie.value
|
|
3130
|
-
}));
|
|
3131
|
-
return { cookies: usable, origins: [] };
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
3864
|
// src/cli/file-crypto.ts
|
|
3135
3865
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
3136
3866
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -3184,15 +3914,15 @@ var SERVICE_PATH = `/etc/systemd/system/${SERVICE_NAME}.service`;
|
|
|
3184
3914
|
function quote(value) {
|
|
3185
3915
|
return /\s/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
|
|
3186
3916
|
}
|
|
3187
|
-
function
|
|
3917
|
+
function buildServiceUnit(options) {
|
|
3188
3918
|
return `[Unit]
|
|
3189
|
-
Description
|
|
3919
|
+
Description=${options.description}
|
|
3190
3920
|
After=network-online.target
|
|
3191
3921
|
Wants=network-online.target
|
|
3192
3922
|
|
|
3193
3923
|
[Service]
|
|
3194
3924
|
ExecStart=${options.execStart.map(quote).join(" ")} run
|
|
3195
|
-
Environment="
|
|
3925
|
+
Environment="${options.environmentName}=${options.configDirectory}"
|
|
3196
3926
|
Environment="DIGGYLAB_API_URL=${options.apiOrigin}"
|
|
3197
3927
|
Restart=on-failure
|
|
3198
3928
|
RestartSec=10
|
|
@@ -3201,6 +3931,13 @@ RestartSec=10
|
|
|
3201
3931
|
WantedBy=multi-user.target
|
|
3202
3932
|
`;
|
|
3203
3933
|
}
|
|
3934
|
+
function buildSystemdUnit(options) {
|
|
3935
|
+
return buildServiceUnit({
|
|
3936
|
+
...options,
|
|
3937
|
+
description: "DiggyLab Desktop Agent",
|
|
3938
|
+
environmentName: "DIGGYLAB_AGENT_DIR"
|
|
3939
|
+
});
|
|
3940
|
+
}
|
|
3204
3941
|
|
|
3205
3942
|
// src/cli/cli.ts
|
|
3206
3943
|
var LOGIN_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -3289,17 +4026,17 @@ function waitForFacebookLogin(login, timeoutMs) {
|
|
|
3289
4026
|
reject(new Error("Timed out waiting for the Facebook login."));
|
|
3290
4027
|
}, timeoutMs);
|
|
3291
4028
|
timer.unref();
|
|
3292
|
-
const unsubscribe = login.subscribe((
|
|
3293
|
-
if (
|
|
4029
|
+
const unsubscribe = login.subscribe((snapshot2) => {
|
|
4030
|
+
if (snapshot2.state === "READY") {
|
|
3294
4031
|
clearTimeout(timer);
|
|
3295
4032
|
unsubscribe();
|
|
3296
4033
|
resolve();
|
|
3297
4034
|
return;
|
|
3298
4035
|
}
|
|
3299
|
-
if (
|
|
4036
|
+
if (snapshot2.state === "NOT_READY" && snapshot2.errorCode) {
|
|
3300
4037
|
clearTimeout(timer);
|
|
3301
4038
|
unsubscribe();
|
|
3302
|
-
reject(new Error(`Facebook login failed (${
|
|
4039
|
+
reject(new Error(`Facebook login failed (${snapshot2.errorCode}).`));
|
|
3303
4040
|
}
|
|
3304
4041
|
});
|
|
3305
4042
|
void login.open().catch((error) => {
|
|
@@ -3609,6 +4346,8 @@ async function commandRun(runtime, io) {
|
|
|
3609
4346
|
const facebookReady = await runtime.facebook.isReady();
|
|
3610
4347
|
const session = new DesktopAgentSession({
|
|
3611
4348
|
agentVersion: agentVersion(),
|
|
4349
|
+
clientType: "CLI",
|
|
4350
|
+
metrics: createMetricsCollector(),
|
|
3612
4351
|
clearTimer: (timer) => clearTimeout(timer),
|
|
3613
4352
|
createSocket: (url, accessToken) => {
|
|
3614
4353
|
const socket = new WebSocket(url, {
|
|
@@ -3645,6 +4384,7 @@ async function commandRun(runtime, io) {
|
|
|
3645
4384
|
browserType: runtime.browserType,
|
|
3646
4385
|
parseGraphqlBody: createGraphqlDumpParser(process.env.FACEBOOK_SCAN_DUMP_DIR)
|
|
3647
4386
|
});
|
|
4387
|
+
let publishRunner = null;
|
|
3648
4388
|
const commentRunner = new DesktopCommentRunner({
|
|
3649
4389
|
clearTimer: (timer) => clearTimeout(timer),
|
|
3650
4390
|
facebookState: runtime.facebook,
|
|
@@ -3664,7 +4404,7 @@ async function commandRun(runtime, io) {
|
|
|
3664
4404
|
clearTimer: (timer) => clearTimeout(timer),
|
|
3665
4405
|
facebookState: runtime.facebook,
|
|
3666
4406
|
http: scanHttp,
|
|
3667
|
-
isCommentFetchActive: () => commentRunner.isRunning(),
|
|
4407
|
+
isCommentFetchActive: () => commentRunner.isRunning() || (publishRunner?.isRunning() ?? false),
|
|
3668
4408
|
isLoginActive: () => false,
|
|
3669
4409
|
now: Date.now,
|
|
3670
4410
|
randomUuid: () => randomUUID3(),
|
|
@@ -3681,25 +4421,42 @@ async function commandRun(runtime, io) {
|
|
|
3681
4421
|
spool: runtime.spool,
|
|
3682
4422
|
wait: abortableDelay
|
|
3683
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
|
+
});
|
|
3684
4440
|
let onReplaced = () => void 0;
|
|
3685
4441
|
const replaced = new Promise((resolve) => {
|
|
3686
4442
|
onReplaced = resolve;
|
|
3687
4443
|
});
|
|
3688
|
-
session.subscribe((
|
|
3689
|
-
io.log(`${stamp()} session: ${
|
|
3690
|
-
recordActivity(`Session state changed to ${
|
|
3691
|
-
if (
|
|
4444
|
+
session.subscribe((snapshot2) => {
|
|
4445
|
+
io.log(`${stamp()} session: ${snapshot2.state}${snapshot2.errorCode ? ` (${snapshot2.errorCode})` : ""}`);
|
|
4446
|
+
recordActivity(`Session state changed to ${snapshot2.state}.`, snapshot2.state);
|
|
4447
|
+
if (snapshot2.errorCode === "CONNECTION_REPLACED") {
|
|
3692
4448
|
io.error("Another worker is already running with this device, so this one stopped.");
|
|
3693
4449
|
io.error("Stop the other worker (for example: systemctl stop diggylab), then start this one again.");
|
|
3694
4450
|
onReplaced();
|
|
3695
4451
|
}
|
|
3696
4452
|
});
|
|
3697
|
-
scanRunner.subscribe((
|
|
3698
|
-
io.log(`scanner: ${
|
|
3699
|
-
recordActivity(`Scanner state changed to ${
|
|
4453
|
+
scanRunner.subscribe((snapshot2) => {
|
|
4454
|
+
io.log(`scanner: ${snapshot2.state}${snapshot2.jobId ? ` (job ${snapshot2.jobId})` : ""}`);
|
|
4455
|
+
recordActivity(`Scanner state changed to ${snapshot2.state}.`, snapshot2.state);
|
|
3700
4456
|
});
|
|
3701
4457
|
scanRunner.start();
|
|
3702
4458
|
commentRunner.start();
|
|
4459
|
+
publishRunner.start();
|
|
3703
4460
|
const unsubscribeComments = commentRunner.subscribe((event) => {
|
|
3704
4461
|
if (event.type === "STARTED") {
|
|
3705
4462
|
io.log(`${stamp()} comments: fetching ${event.postUrl}`);
|
|
@@ -3723,6 +4480,34 @@ async function commandRun(runtime, io) {
|
|
|
3723
4480
|
level: "WARN"
|
|
3724
4481
|
});
|
|
3725
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
|
+
});
|
|
3726
4511
|
await scanRunner.recover();
|
|
3727
4512
|
await session.start();
|
|
3728
4513
|
io.log(`Worker running as "${stored.device.name}" (API ${runtime.endpoint.httpOrigin}). Press Ctrl+C to stop.`);
|
|
@@ -3739,7 +4524,9 @@ async function commandRun(runtime, io) {
|
|
|
3739
4524
|
clearInterval(keepAlive);
|
|
3740
4525
|
session.stop();
|
|
3741
4526
|
commentRunner.stop();
|
|
4527
|
+
publishRunner.stop();
|
|
3742
4528
|
unsubscribeComments();
|
|
4529
|
+
unsubscribePublish();
|
|
3743
4530
|
scanRunner.stop();
|
|
3744
4531
|
}
|
|
3745
4532
|
return 0;
|