facebook-comments-client 0.1.4

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/client/FacebookCommentsClient.ts","../src/browser/BrowserManager.ts","../src/browser/ContextFactory.ts","../src/browser/SessionManager.ts","../src/errors/FacebookClientError.ts","../src/errors/FacebookAuthenticationError.ts","../src/errors/FacebookSessionExpiredError.ts","../src/errors/FacebookPostNotFoundError.ts","../src/errors/FacebookAccessDeniedError.ts","../src/errors/FacebookExtractionError.ts","../src/session/StorageStateProvider.ts","../src/session/FileSessionProvider.ts","../src/session/EnvSessionProvider.ts","../src/session/CookieProvider.ts","../src/session/SessionProvider.ts","../src/extractors/ExtractorPipeline.ts","../src/config/selectors.ts","../src/parsers/FacebookResponseParser.ts","../src/extractors/StructuredDataExtractor.ts","../src/comments/DiscussionScope.ts","../src/extractors/domDump.ts","../src/comments/CommentParser.ts","../src/extractors/AccessibleDomExtractor.ts","../src/extractors/FallbackDomExtractor.ts","../src/comments/ReplyFetcher.ts","../src/comments/CommentFetcher.ts","../src/comments/PaginationHandler.ts","../src/parsers/CommentNormalizer.ts","../src/utils/url.ts","../src/parsers/PostParser.ts","../src/utils/logger.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\r\nimport { FacebookCommentsClient } from \"./client/FacebookCommentsClient\";\r\nimport type { SessionConfig } from \"./types/Session\";\r\nimport type { FacebookComment } from \"./types/Comment\";\r\nimport { FacebookClientError } from \"./errors\";\r\n\r\nconst USAGE = `fb-comments — fetch Facebook post comments with an authorized session\r\n\r\nUsage:\r\n fb-comments login [--output <path>] Interactive login (manual, headed browser)\r\n fb-comments comments <post-url> [flags] Fetch comments\r\n\r\nFlags:\r\n --session <path> Storage-state file (default: ./facebook-session.json or $FACEBOOK_STORAGE_STATE)\r\n --replies Include reply threads\r\n --limit <n> Max top-level comments (default 50)\r\n --max-replies <n> Max replies per comment (default 20)\r\n --timeout <ms> Navigation timeout (default 30000)\r\n --headless Run headless even if the client default says otherwise\r\n --json Print raw JSON result\r\n -h, --help Show this help\r\n`;\r\n\r\nfunction flagValue(args: string[], name: string): string | undefined {\r\n const index = args.indexOf(name);\r\n return index !== -1 ? args[index + 1] : undefined;\r\n}\r\n\r\nfunction hasFlag(args: string[], name: string): boolean {\r\n return args.includes(name);\r\n}\r\n\r\nfunction resolveSession(cliSessionPath: string | undefined): SessionConfig {\r\n if (cliSessionPath) return { type: \"file\", path: cliSessionPath };\r\n if (process.env.FACEBOOK_STORAGE_STATE) {\r\n return { type: \"env\", envKey: \"FACEBOOK_STORAGE_STATE\" };\r\n }\r\n if (existsSync(\"facebook-session.json\")) {\r\n return { type: \"file\", path: \"facebook-session.json\" };\r\n }\r\n console.error(\r\n \"No session found. Run `fb-comments login` or set FACEBOOK_STORAGE_STATE.\"\r\n );\r\n process.exit(1);\r\n}\r\n\r\nfunction printHuman(comments: FacebookComment[], hasMore: boolean): void {\r\n if (comments.length === 0) {\r\n console.log(\"No comments found.\");\r\n return;\r\n }\r\n for (const comment of comments) {\r\n const author = comment.author.name ?? \"unknown\";\r\n const time = comment.createdAt ?? \"unknown time\";\r\n const reactions =\r\n comment.reactions.total !== null ? ` [${comment.reactions.total} reactions]` : \"\";\r\n console.log(`\\n${author} (${time})${reactions}\\n ${comment.message.replace(/\\n/g, \"\\n \")}`);\r\n for (const reply of comment.replies ?? []) {\r\n const replyAuthor = reply.author.name ?? \"unknown\";\r\n console.log(` > ${replyAuthor}: ${reply.message.replace(/\\n/g, \"\\n \")}`);\r\n }\r\n }\r\n console.log(`\\n${comments.length} comment(s)${hasMore ? \" — more available, use the next cursor\" : \"\"}`);\r\n}\r\n\r\nasync function run(): Promise<void> {\r\n const argv = process.argv.slice(2);\r\n const [command, ...rest] = argv;\r\n\r\n if (!command || command === \"help\" || command === \"--help\" || command === \"-h\") {\r\n console.log(USAGE);\r\n return;\r\n }\r\n\r\n if (command === \"login\") {\r\n const output = flagValue(rest, \"--output\") ?? \"./facebook-session.json\";\r\n console.log(`Opening Facebook for manual login. Session will be saved to: ${output}`);\r\n await FacebookCommentsClient.login({ outputPath: output });\r\n console.log(\"Session saved.\");\r\n return;\r\n }\r\n\r\n if (command === \"comments\") {\r\n const url = rest.find((arg) => arg.startsWith(\"http\"));\r\n if (!url) {\r\n console.error(\"A post URL is required:\\n fb-comments comments <post-url>\");\r\n process.exit(1);\r\n }\r\n const session = resolveSession(flagValue(rest, \"--session\"));\r\n const client = new FacebookCommentsClient({ session });\r\n try {\r\n const result = await client.getComments({\r\n url,\r\n includeReplies: hasFlag(rest, \"--replies\"),\r\n maxComments: Number(flagValue(rest, \"--limit\") ?? 50),\r\n maxReplies: Number(flagValue(rest, \"--max-replies\") ?? 20),\r\n timeout: Number(flagValue(rest, \"--timeout\") ?? 30_000),\r\n headless: true\r\n });\r\n if (hasFlag(rest, \"--json\")) {\r\n console.log(JSON.stringify(result, null, 2));\r\n } else {\r\n printHuman(result.comments, result.pagination.hasMore);\r\n }\r\n } finally {\r\n await client.close().catch(() => {});\r\n }\r\n return;\r\n }\r\n\r\n console.error(`Unknown command: ${command}\\n`);\r\n console.log(USAGE);\r\n process.exit(1);\r\n}\r\n\r\nrun().catch((error: unknown) => {\r\n if (error instanceof FacebookClientError) {\r\n console.error(`${error.name}: ${error.message}`);\r\n } else {\r\n console.error(error);\r\n }\r\n process.exit(1);\r\n});\r\n","import { mkdir } from \"node:fs/promises\";\r\nimport { dirname } from \"node:path\";\r\nimport { existsSync } from \"node:fs\";\r\nimport { chromium } from \"playwright\";\r\nimport type {\r\n FacebookCommentsClientOptions,\r\n GetCommentsOptions,\r\n GetCommentsResult,\r\n Logger,\r\n LoginOptions\r\n} from \"../types/Client\";\r\nimport type { SessionValidationResult } from \"../types/Session\";\r\nimport type { FacebookComment } from \"../types/Comment\";\r\nimport { BrowserManager } from \"../browser/BrowserManager\";\r\nimport { createLoggedInContext } from \"../browser/ContextFactory\";\r\nimport { checkAuthenticatedOnPage, hasAuthCookies } from \"../browser/SessionManager\";\r\nimport { createSessionProvider } from \"../session/SessionProvider\";\r\nimport { ExtractorPipeline } from \"../extractors/ExtractorPipeline\";\r\nimport { StructuredDataExtractor } from \"../extractors/StructuredDataExtractor\";\r\nimport { AccessibleDomExtractor } from \"../extractors/AccessibleDomExtractor\";\r\nimport { FallbackDomExtractor } from \"../extractors/FallbackDomExtractor\";\r\nimport { CommentFetcher, detectPageProblem } from \"../comments/CommentFetcher\";\r\nimport { decodeCursor, initialCursor, paginate } from \"../comments/PaginationHandler\";\r\nimport { normalizeComment } from \"../parsers/CommentNormalizer\";\r\nimport { parsePost } from \"../parsers/PostParser\";\r\nimport { parseFacebookPostUrl } from \"../utils/url\";\r\nimport { selectors } from \"../config/selectors\";\r\nimport { createConsoleLogger } from \"../utils/logger\";\r\nimport {\r\n FacebookAuthenticationError,\r\n FacebookClientError\r\n} from \"../errors\";\r\n\r\nconst DEFAULT_MAX_COMMENTS = 50;\r\nconst DEFAULT_MAX_REPLIES = 20;\r\n\r\nfunction trimReplies(comment: FacebookComment, maxReplies: number): FacebookComment {\r\n if (!comment.replies || comment.replies.length === 0) return comment;\r\n const replies = comment.replies.slice(0, maxReplies).map((reply) => trimReplies(reply, maxReplies));\r\n return { ...comment, replies };\r\n}\r\n\r\nexport class FacebookCommentsClient {\r\n private readonly browserManager: BrowserManager;\r\n private readonly logger: Logger;\r\n\r\n constructor(private readonly options: FacebookCommentsClientOptions) {\r\n this.logger = options.logger ?? createConsoleLogger(\"warn\");\r\n this.browserManager = new BrowserManager(options.browser);\r\n }\r\n\r\n /**\r\n * Interactive login helper: opens a visible browser, waits for you to log in\r\n * manually, then exports the storage state. Never enters credentials for you.\r\n */\r\n static async login(loginOptions: LoginOptions): Promise<void> {\r\n const timeoutMs = loginOptions.timeoutMs ?? 300_000;\r\n const browser = await chromium.launch({ headless: false });\r\n try {\r\n const context = await browser.newContext();\r\n const page = await context.newPage();\r\n await page.goto(\"https://www.facebook.com/\", { waitUntil: \"domcontentloaded\" });\r\n\r\n const deadline = Date.now() + timeoutMs;\r\n while (Date.now() < deadline) {\r\n const cookies = await context.cookies(\"https://www.facebook.com\");\r\n if (cookies.some((cookie) => cookie.name === \"c_user\")) {\r\n const targetDir = dirname(loginOptions.outputPath);\r\n if (targetDir && !existsSync(targetDir)) {\r\n await mkdir(targetDir, { recursive: true });\r\n }\r\n await context.storageState({ path: loginOptions.outputPath });\r\n return;\r\n }\r\n await page.waitForTimeout(1500);\r\n }\r\n throw new FacebookAuthenticationError(\r\n `Login timed out after ${Math.round(timeoutMs / 1000)}s — no Facebook session was established. Try again and finish logging in within the window.`\r\n );\r\n } finally {\r\n await browser.close().catch(() => {});\r\n }\r\n }\r\n\r\n /** Fetch and normalize comments for one post URL. */\r\n async getComments(options: GetCommentsOptions): Promise<GetCommentsResult> {\r\n const parsedUrl = parseFacebookPostUrl(options.url);\r\n const maxComments = options.maxComments ?? DEFAULT_MAX_COMMENTS;\r\n const maxReplies = options.maxReplies ?? DEFAULT_MAX_REPLIES;\r\n const includeReplies = options.includeReplies ?? false;\r\n const cursor = options.cursor ? decodeCursor(options.cursor) : initialCursor();\r\n const timeoutMs = options.timeout ?? this.options.browser?.timeoutMs ?? 30_000;\r\n\r\n const provider = createSessionProvider(this.options.session);\r\n const storageState = await provider.resolve();\r\n if (!hasAuthCookies(storageState)) {\r\n throw new FacebookAuthenticationError(\r\n \"The configured session contains no Facebook auth cookies (c_user/xs). Capture a fresh session with `fb-comments login`.\"\r\n );\r\n }\r\n\r\n const headless = options.headless ?? this.options.browser?.headless ?? true;\r\n const browser = await this.browserManager.launch(headless);\r\n const context = await createLoggedInContext(browser, { storageState, timeoutMs });\r\n try {\r\n const page = await context.newPage();\r\n this.logger.debug(`Navigating to ${parsedUrl.url}`);\r\n await page.goto(parsedUrl.url, { waitUntil: \"domcontentloaded\", timeout: timeoutMs });\r\n\r\n const problem = await detectPageProblem(page);\r\n if (problem) throw problem;\r\n\r\n // Wait for either comments (article) or the control that reveals them\r\n // (reels keep comments behind an aria-labeled toggle).\r\n await page\r\n .waitForSelector(`${selectors.commentArticle}, ${selectors.commentOpenerControls}`, {\r\n timeout: Math.min(10_000, timeoutMs)\r\n })\r\n .catch(() => null);\r\n\r\n const fetcher = new CommentFetcher(\r\n new ExtractorPipeline([\r\n new AccessibleDomExtractor(),\r\n new StructuredDataExtractor(),\r\n new FallbackDomExtractor()\r\n ])\r\n );\r\n const outcome = await fetcher.fetch(page, {\r\n maxComments,\r\n includeReplies,\r\n maxReplies,\r\n skipped: cursor.skipped,\r\n postId: parsedUrl.postId\r\n });\r\n\r\n const all = outcome.raw.map((raw) => normalizeComment(raw));\r\n const pageResult = paginate(all, cursor, maxComments, outcome.loaderPresent);\r\n this.logger.info(\r\n `Extracted ${pageResult.comments.length} comments (hasMore: ${pageResult.hasMore})`\r\n );\r\n\r\n return {\r\n post: parsePost(page, parsedUrl.url),\r\n comments: includeReplies\r\n ? pageResult.comments.map((comment) => trimReplies(comment, maxReplies))\r\n : pageResult.comments,\r\n pagination: pageResult\r\n };\r\n } finally {\r\n await context.close().catch(() => {});\r\n }\r\n }\r\n\r\n /**\r\n * Cheap session check: resolves the session, verifies auth cookies, and\r\n * confirms Facebook still accepts them. Never throws for session problems —\r\n * inspect the returned flags instead.\r\n */\r\n async validateSession(): Promise<SessionValidationResult> {\r\n let storageState;\r\n try {\r\n storageState = await createSessionProvider(this.options.session).resolve();\r\n } catch (error) {\r\n if (error instanceof FacebookClientError) {\r\n return { valid: false, authenticated: false };\r\n }\r\n throw error;\r\n }\r\n if (!hasAuthCookies(storageState)) {\r\n return { valid: true, authenticated: false };\r\n }\r\n\r\n const browser = await this.browserManager.launch(\r\n this.options.browser?.headless ?? true\r\n );\r\n const context = await createLoggedInContext(browser, {\r\n storageState,\r\n timeoutMs: this.options.browser?.timeoutMs ?? 30_000\r\n });\r\n try {\r\n const page = await context.newPage();\r\n const authenticated = await checkAuthenticatedOnPage(context, page);\r\n return {\r\n valid: true,\r\n authenticated,\r\n user: { name: null }\r\n };\r\n } finally {\r\n await context.close().catch(() => {});\r\n }\r\n }\r\n\r\n async close(): Promise<void> {\r\n await this.browserManager.close();\r\n }\r\n}\r\n","import { chromium, type Browser } from \"playwright\";\r\nimport type { BrowserOptions } from \"../types/Client\";\r\n\r\n/** Lazy chromium lifecycle. Reuses a running browser; relaunches when headless mode changes. */\r\nexport class BrowserManager {\r\n private browser: Browser | null = null;\r\n private currentHeadless: boolean | null = null;\r\n\r\n constructor(private readonly options: BrowserOptions = {}) {}\r\n\r\n async launch(headless: boolean = this.options.headless ?? true): Promise<Browser> {\r\n if (this.browser?.isConnected() && this.currentHeadless === headless) {\r\n return this.browser;\r\n }\r\n await this.close();\r\n this.browser = await chromium.launch({\r\n headless,\r\n executablePath: this.options.executablePath,\r\n args: this.options.args\r\n });\r\n this.currentHeadless = headless;\r\n return this.browser;\r\n }\r\n\r\n async close(): Promise<void> {\r\n if (this.browser) {\r\n await this.browser.close().catch(() => {});\r\n this.browser = null;\r\n this.currentHeadless = null;\r\n }\r\n }\r\n}\r\n","import type { Browser, BrowserContext } from \"playwright\";\r\nimport type { Cookie, StorageState } from \"../types/Session\";\r\n\r\ninterface StrictCookie {\r\n name: string;\r\n value: string;\r\n domain: string;\r\n path: string;\r\n expires: number;\r\n httpOnly: boolean;\r\n secure: boolean;\r\n sameSite: \"Strict\" | \"Lax\" | \"None\";\r\n}\r\n\r\n/** Fill Playwright's required cookie defaults from our tolerant input shape. */\r\nfunction toStrictCookie(cookie: Cookie): StrictCookie {\r\n return {\r\n name: cookie.name,\r\n value: cookie.value,\r\n domain: cookie.domain,\r\n path: cookie.path ?? \"/\",\r\n expires: cookie.expires ?? -1,\r\n httpOnly: cookie.httpOnly ?? false,\r\n secure: cookie.secure ?? true,\r\n sameSite: cookie.sameSite ?? \"Lax\"\r\n };\r\n}\r\n\r\nexport interface CreateContextOptions {\r\n storageState?: StorageState | null;\r\n timeoutMs?: number;\r\n}\r\n\r\n/**\r\n * Creates a context with the authenticated storage state.\r\n * Standard Playwright context only — no fingerprint or evasion tweaks.\r\n */\r\nexport async function createLoggedInContext(\r\n browser: Browser,\r\n options: CreateContextOptions\r\n): Promise<BrowserContext> {\r\n const storageState = options.storageState\r\n ? {\r\n cookies: options.storageState.cookies\r\n .filter((cookie) => cookie.name && cookie.value && cookie.domain)\r\n .map(toStrictCookie),\r\n origins: options.storageState.origins ?? []\r\n }\r\n : undefined;\r\n const context = await browser.newContext({\r\n storageState,\r\n // Wide desktop viewport: Facebook serves its full layout (comments sidebar\r\n // on reels/videos) rather than collapsed theater mode.\r\n viewport: { width: 1680, height: 1050 }\r\n });\r\n context.setDefaultTimeout(options.timeoutMs ?? 30_000);\r\n context.setDefaultNavigationTimeout(options.timeoutMs ?? 30_000);\r\n return context;\r\n}\r\n","import type { BrowserContext, Page } from \"playwright\";\r\nimport type { StorageState } from \"../types/Session\";\r\n\r\n/** Facebook auth cookies whose presence indicates an authenticated session. */\r\nexport const AUTH_COOKIE_NAMES = [\"c_user\", \"xs\"];\r\n\r\n/** True when the storage state contains the cookies Facebook needs for a logged-in session. */\r\nexport function hasAuthCookies(state: StorageState | null): boolean {\r\n if (!state) return false;\r\n const names = new Set(state.cookies.map((cookie) => cookie.name));\r\n return AUTH_COOKIE_NAMES.every((name) => names.has(name));\r\n}\r\n\r\n/**\r\n * Navigate to facebook.com and check whether the session is still accepted.\r\n * Detects the login redirect Facebook serves for expired sessions.\r\n */\r\nexport async function checkAuthenticatedOnPage(context: BrowserContext, page: Page): Promise<boolean> {\r\n await page.goto(\"https://www.facebook.com/\", { waitUntil: \"domcontentloaded\" });\r\n const url = page.url();\r\n if (/(?:^|\\/)login(?:\\.php)?(?:[/?#]|$)/i.test(url)) return false;\r\n if (/(?:^|\\/)recover(?:[/?#]|$)/i.test(url)) return false;\r\n const cookies = await context.cookies(\"https://www.facebook.com\");\r\n const names = new Set(cookies.map((cookie) => cookie.name));\r\n return AUTH_COOKIE_NAMES.every((name) => names.has(name));\r\n}\r\n","export type FacebookErrorCode =\r\n | \"FB_CLIENT\"\r\n | \"FB_AUTH\"\r\n | \"FB_SESSION_EXPIRED\"\r\n | \"FB_POST_NOT_FOUND\"\r\n | \"FB_ACCESS_DENIED\"\r\n | \"FB_EXTRACTION\";\r\n\r\nexport interface FacebookErrorOptions {\r\n retryable?: boolean;\r\n cause?: unknown;\r\n}\r\n\r\n/**\r\n * Base error for all library failures.\r\n * Messages must never contain session cookies, tokens, or storage-state content.\r\n */\r\nexport class FacebookClientError extends Error {\r\n readonly code: FacebookErrorCode;\r\n readonly retryable: boolean;\r\n\r\n constructor(\r\n message: string,\r\n options: FacebookErrorOptions & { code?: FacebookErrorCode } = {}\r\n ) {\r\n super(message);\r\n this.name = new.target.name;\r\n this.code = options.code ?? \"FB_CLIENT\";\r\n this.retryable = options.retryable ?? false;\r\n if (options.cause !== undefined) {\r\n (this as { cause?: unknown }).cause = options.cause;\r\n }\r\n }\r\n}\r\n","import { FacebookClientError } from \"./FacebookClientError\";\r\n\r\n/** Session data is missing, unreadable, malformed, or contains no Facebook auth cookies. */\r\nexport class FacebookAuthenticationError extends FacebookClientError {\r\n constructor(message: string, options: { retryable?: boolean; cause?: unknown } = {}) {\r\n super(message, { ...options, code: \"FB_AUTH\" });\r\n }\r\n}\r\n","import { FacebookClientError } from \"./FacebookClientError\";\r\n\r\n/** Session was structurally valid but Facebook no longer accepts it (login redirect observed). */\r\nexport class FacebookSessionExpiredError extends FacebookClientError {\r\n constructor(message: string, options: { retryable?: boolean; cause?: unknown } = {}) {\r\n super(message, { ...options, code: \"FB_SESSION_EXPIRED\" });\r\n }\r\n}\r\n","import { FacebookClientError } from \"./FacebookClientError\";\r\n\r\n/** Post URL does not exist, was deleted, or is not visible (\"this content isn't available\"). */\r\nexport class FacebookPostNotFoundError extends FacebookClientError {\r\n constructor(message: string, options: { retryable?: boolean; cause?: unknown } = {}) {\r\n super(message, { ...options, code: \"FB_POST_NOT_FOUND\" });\r\n }\r\n}\r\n","import { FacebookClientError } from \"./FacebookClientError\";\r\n\r\n/** Post exists but the account lacks access (e.g. private group the account has not joined). */\r\nexport class FacebookAccessDeniedError extends FacebookClientError {\r\n constructor(message: string, options: { retryable?: boolean; cause?: unknown } = {}) {\r\n super(message, { ...options, code: \"FB_ACCESS_DENIED\" });\r\n }\r\n}\r\n","import { FacebookClientError } from \"./FacebookClientError\";\r\n\r\n/** Page loaded but no extractor could produce usable comment data. Usually a markup change. */\r\nexport class FacebookExtractionError extends FacebookClientError {\r\n constructor(message: string, options: { retryable?: boolean; cause?: unknown } = {}) {\r\n super(message, { ...options, code: \"FB_EXTRACTION\", retryable: options.retryable ?? true });\r\n }\r\n}\r\n","import { z } from \"zod\";\r\nimport type { StorageState } from \"../types/Session\";\r\nimport { FacebookAuthenticationError } from \"../errors\";\r\nimport type { SessionProvider } from \"./SessionProvider\";\r\n\r\n/**\r\n * Loose Zod schema for Playwright storage states. Tolerant on purpose: Facebook\r\n * exports evolve, and missing optional fields must not invalidate an otherwise\r\n * working session. Cookies lacking required Playwright fields are dropped,\r\n * not fatal.\r\n */\r\nexport const CookieSchema = z\r\n .object({\r\n name: z.string(),\r\n value: z.string(),\r\n domain: z.string().optional(),\r\n path: z.string().optional(),\r\n expires: z.number().optional(),\r\n httpOnly: z.boolean().optional(),\r\n secure: z.boolean().optional(),\r\n sameSite: z.enum([\"Strict\", \"Lax\", \"None\"]).optional()\r\n })\r\n .passthrough();\r\n\r\nexport const StorageStateSchema = z.object({\r\n cookies: z.array(CookieSchema).default([]),\r\n origins: z\r\n .array(\r\n z\r\n .object({\r\n origin: z.string(),\r\n localStorage: z\r\n .array(z.object({ name: z.string(), value: z.string() }))\r\n .default([])\r\n })\r\n .passthrough()\r\n )\r\n .default([])\r\n});\r\n\r\n/** Validate unknown data as a Playwright storage state. Throws FacebookAuthenticationError. */\r\nexport function validateStorageState(input: unknown, source: string): StorageState {\r\n const result = StorageStateSchema.safeParse(input);\r\n if (!result.success) {\r\n // Zod issues only reference field paths — no session values leak.\r\n const paths = result.error.issues.map((issue) => issue.path.join(\".\")).slice(0, 5);\r\n throw new FacebookAuthenticationError(\r\n `Session data from ${source} is not a valid storage state (invalid: ${paths.join(\", \")})`,\r\n { cause: undefined }\r\n );\r\n }\r\n const cookies: StorageState[\"cookies\"] = [];\r\n for (const cookie of result.data.cookies) {\r\n if (!cookie.name || !cookie.value || !cookie.domain) continue;\r\n cookies.push({ ...cookie, domain: cookie.domain, path: cookie.path ?? \"/\" });\r\n }\r\n return { cookies, origins: result.data.origins };\r\n}\r\n\r\n/** Session provided inline as a storage-state object. */\r\nexport class StorageStateProvider implements SessionProvider {\r\n readonly type = \"state\";\r\n\r\n constructor(private readonly state: unknown) {}\r\n\r\n async resolve(): Promise<StorageState> {\r\n return validateStorageState(this.state, \"inline session config\");\r\n }\r\n}\r\n","import { readFile } from \"node:fs/promises\";\r\nimport type { StorageState } from \"../types/Session\";\r\nimport { FacebookAuthenticationError } from \"../errors\";\r\nimport type { SessionProvider } from \"./SessionProvider\";\r\nimport { validateStorageState } from \"./StorageStateProvider\";\r\n\r\n/** Loads a Playwright storage-state JSON file from disk. */\r\nexport class FileSessionProvider implements SessionProvider {\r\n readonly type = \"file\";\r\n\r\n constructor(private readonly path: string) {}\r\n\r\n async resolve(): Promise<StorageState> {\r\n let raw: string;\r\n try {\r\n raw = await readFile(this.path, \"utf8\");\r\n } catch (error) {\r\n const code = (error as NodeJS.ErrnoException).code;\r\n if (code === \"ENOENT\") {\r\n throw new FacebookAuthenticationError(\r\n `Session file not found: ${this.path}. Run \\`fb-comments login --output ${this.path}\\` first.`\r\n );\r\n }\r\n throw new FacebookAuthenticationError(`Could not read session file: ${this.path} (${code})`);\r\n }\r\n\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(raw);\r\n } catch {\r\n throw new FacebookAuthenticationError(`Session file is not valid JSON: ${this.path}`);\r\n }\r\n return validateStorageState(parsed, `file ${this.path}`);\r\n }\r\n}\r\n","import type { StorageState } from \"../types/Session\";\r\nimport { FacebookAuthenticationError } from \"../errors\";\r\nimport type { SessionProvider } from \"./SessionProvider\";\r\nimport { validateStorageState } from \"./StorageStateProvider\";\r\n\r\n/**\r\n * Loads a base64(base64url)-encoded storage state from an environment variable:\r\n * env value -> base64 decode -> JSON parse -> Zod validate -> storage state\r\n */\r\nexport class EnvSessionProvider implements SessionProvider {\r\n readonly type = \"env\";\r\n\r\n constructor(private readonly envKey: string) {}\r\n\r\n async resolve(): Promise<StorageState> {\r\n const encoded = process.env[this.envKey];\r\n if (!encoded) {\r\n throw new FacebookAuthenticationError(\r\n `Environment variable ${this.envKey} is not set. Encode a storage state with: ` +\r\n `Buffer.from(JSON.stringify(storageState)).toString(\"base64\")`\r\n );\r\n }\r\n\r\n // Accept both standard base64 and base64url.\r\n const normalized = encoded.replace(/-/g, \"+\").replace(/_/g, \"/\");\r\n let decoded: string;\r\n try {\r\n decoded = Buffer.from(normalized, \"base64\").toString(\"utf8\");\r\n } catch {\r\n throw new FacebookAuthenticationError(\r\n `Environment variable ${this.envKey} is not valid base64.`\r\n );\r\n }\r\n\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(decoded);\r\n } catch {\r\n throw new FacebookAuthenticationError(\r\n `Environment variable ${this.envKey} does not contain valid JSON after base64 decoding.`\r\n );\r\n }\r\n return validateStorageState(parsed, `env ${this.envKey}`);\r\n }\r\n}\r\n","import type { Cookie, StorageState } from \"../types/Session\";\r\nimport { CookieSchema } from \"./StorageStateProvider\";\r\nimport { FacebookAuthenticationError } from \"../errors\";\r\nimport type { SessionProvider } from \"./SessionProvider\";\r\n\r\n/** Wraps a plain cookie array (Playwright cookie format) as a storage state. */\r\nexport class CookieProvider implements SessionProvider {\r\n readonly type = \"cookies\";\r\n\r\n constructor(private readonly cookies: unknown) {}\r\n\r\n async resolve(): Promise<StorageState> {\r\n const result = CookieSchema.array().safeParse(this.cookies);\r\n if (!result.success) {\r\n throw new FacebookAuthenticationError(\r\n \"Cookie session config contains invalid cookies (each needs at least name, value, domain)\"\r\n );\r\n }\r\n const cookies: Cookie[] = [];\r\n for (const cookie of result.data) {\r\n if (!cookie.name || !cookie.value || !cookie.domain) continue;\r\n cookies.push({\r\n ...cookie,\r\n domain: cookie.domain,\r\n path: cookie.path ?? \"/\",\r\n expires: cookie.expires ?? -1\r\n });\r\n }\r\n return { cookies, origins: [] };\r\n }\r\n}\r\n","import type { StorageState } from \"../types/Session\";\r\nimport type { SessionConfig } from \"../types/Session\";\r\nimport { FacebookClientError } from \"../errors\";\r\nimport { StorageStateProvider } from \"./StorageStateProvider\";\r\nimport { FileSessionProvider } from \"./FileSessionProvider\";\r\nimport { EnvSessionProvider } from \"./EnvSessionProvider\";\r\nimport { CookieProvider } from \"./CookieProvider\";\r\n\r\n/** Resolves a SessionConfig into a Playwright storage state. */\r\nexport interface SessionProvider {\r\n readonly type: string;\r\n resolve(): Promise<StorageState>;\r\n}\r\n\r\nexport function createSessionProvider(config: SessionConfig): SessionProvider {\r\n switch (config.type) {\r\n case \"file\":\r\n case \"storage\":\r\n return new FileSessionProvider(config.path);\r\n case \"env\":\r\n return new EnvSessionProvider(config.envKey);\r\n case \"cookies\":\r\n return new CookieProvider(config.cookies);\r\n case \"state\":\r\n return new StorageStateProvider(config.state);\r\n default:\r\n throw new FacebookClientError(\r\n `Unknown session type: ${JSON.stringify((config as { type?: string }).type)}`\r\n );\r\n }\r\n}\r\n","import type { Page } from \"playwright\";\r\nimport type { CommentExtractor } from \"./CommentExtractor\";\r\nimport type { ExtractionOptions, RawFacebookComment } from \"../types/Comment\";\r\nimport type { DiscussionScope } from \"../comments/DiscussionScope\";\r\nimport { FacebookExtractionError } from \"../errors\";\r\n\r\n/** Tries extractors in order and returns the output of the first one that produces comments. */\r\nexport class ExtractorPipeline {\r\n constructor(private readonly extractors: CommentExtractor[]) {}\r\n\r\n async extract(\r\n page: Page,\r\n options: ExtractionOptions,\r\n scope?: DiscussionScope\r\n ): Promise<RawFacebookComment[]> {\r\n const attempted: string[] = [];\r\n for (const extractor of this.extractors) {\r\n try {\r\n if (!(await extractor.canHandle(page, scope))) continue;\r\n attempted.push(extractor.name);\r\n const raw = await extractor.extract(page, options, scope);\r\n if (raw.length > 0) return raw;\r\n } catch {\r\n // Extractor failures fall through to the next strategy.\r\n }\r\n }\r\n throw new FacebookExtractionError(\r\n attempted.length > 0\r\n ? `No extractor produced comments (tried: ${attempted.join(\", \")}). Facebook markup may have changed — see src/config/selectors.ts.`\r\n : \"No extractor could handle this page. Facebook markup may have changed — see src/config/selectors.ts.\"\r\n );\r\n }\r\n}\r\n","/**\r\n * UNSTABLE SURFACE — every Facebook-specific selector and text pattern lives here.\r\n * Facebook rotates markup and class names frequently; when extraction breaks,\r\n * this file is the only place that should need updates.\r\n *\r\n * TODO(markup): patterns below are written against accessibility attributes\r\n * (role/aria-label) because they outlive generated class names. Validate against\r\n * a real capture of a group post whenever extraction quality degrades.\r\n */\r\n\r\nexport const selectors = {\r\n /** Each comment renders as an aria article. The post itself is also an article —\r\n * CommentParser distinguishes comments via reply affordances and aria-labels. */\r\n commentArticle: '[role=\"article\"]',\r\n\r\n /** Real comment articles only (top-level \"Comment by\" + nested \"Reply by\");\r\n * excludes post/feed wrapper articles. */\r\n commentCandidate: '[role=\"article\"][aria-label*=\"comment by\" i], [role=\"article\"][aria-label*=\"reply by\" i]',\r\n\r\n /** Foreground permalink modal — group/photo/video permalinks open the post in\r\n * a dialog while the logged-in feed stays mounted behind it. Comment\r\n * interactions and extraction must be scoped to this dialog, or background\r\n * feed comments leak into results. Plain [role=\"dialog\"] wrappers (without\r\n * aria-modal) are the nesting chrome, not the boundary. */\r\n foregroundModal: '[role=\"dialog\"][aria-modal=\"true\"]',\r\n\r\n /** Profile links inside a comment (author avatar/name). */\r\n authorLink: 'a[href*=\"/people/\"], a[href^=\"/\"], a[href*=\"facebook.com/\"]',\r\n\r\n /** Elements that expand more comments (button or link). Matched by text, see textPatterns.moreComments. */\r\n commentExpander: '[role=\"button\"], [role=\"link\"], a[href=\"#\"]',\r\n\r\n /** Elements that expand reply threads. Matched by text, see textPatterns.viewMoreReplies. */\r\n replyExpander: '[role=\"button\"], [role=\"link\"], a[href=\"#\"]',\r\n\r\n /** Embedded structured data blobs (opportunistic structured extractor). */\r\n structuredDataScripts: 'script[type=\"application/json\"]',\r\n\r\n /** Looser comment containers for the fallback extractor. */\r\n fallbackCommentContainer: '[aria-label*=\"comment by\" i], [data-testid*=\"comment\" i]',\r\n\r\n /** Icon-only comment openers on reels/videos: aria-label carries \"comment\" (\"Comment\",\r\n * \"Comments\", \"Comment, 5\"), while the button text may be empty. */\r\n commentOpenerControls:\r\n '[role=\"button\"][aria-label*=\"comment\" i], [role=\"link\"][aria-label*=\"comment\" i], [aria-label^=\"comment\" i]',\r\n} as const;\r\n\r\n/** Text patterns (regex, case-insensitive) used to classify page elements. */\r\nexport const textPatterns = {\r\n /** aria-label of a comment article, e.g. \"Comment by John Doe\" or \"John Doe · 2h\". */\r\n commentBy: /(?:^|\\b)comment(?:ed)? by\\s+(.+)$/i,\r\n\r\n /** aria-label of a reply article: \"Reply by X to Y's comment 3 days ago\".\r\n * Group modals render replies as flat sibling articles — group 1 is the\r\n * reply author, group 2 the parent comment's author. */\r\n replyBy: /\\breply by\\s+(.+?)\\s+to\\s+(.+?)'s comment\\b/i,\r\n\r\n /** aria-label of a reply article without the parent attribution. */\r\n replyByFallback: /\\breply by\\s+(.+)$/i,\r\n\r\n /** Author · timestamp header, e.g. \"John Doe · 2h\". */\r\n authorTime: /^(.+?)\\s*·\\s*(?:.*?\\d)/,\r\n\r\n /** Reply action affordance. */\r\n reply: /^\\s*(reply|antworten|r\\u00e9pondere|responder)\\s*$/i,\r\n\r\n /** \"View more replies\", \"View 2 replies\", \"Load more replies\", \"Show N previous replies\". */\r\n viewMoreReplies: /\\b\\d*\\s*(?:view|see|show|load)\\s*(?:\\d+\\s*)?(?:more\\s*)?(?:previous\\s*)?repl(?:y|ies)\\b|^\\s*\\d+\\s+repl(?:y|ies)\\s*$/i,\r\n\r\n /** \"View more comments\", \"View 12 comments\", \"Load 25 more comments\", \"View previous comments\". */\r\n moreComments: /\\b(?:view|see|show|load)\\s*(?:\\d+\\s*)?(?:more\\s*)?(?:previous\\s*)?comments?\\b/i,\r\n\r\n /** Comment-panel opener on reels/videos (\"Comments\", \"View comments\", \"View 12 comments\"). */\r\n commentOpener: /^(?:view\\s+)?comments?\\b/i,\r\n\r\n /** Composer boxes (\"Write a comment\") also contain \"comment\" — never click those. */\r\n commentComposer: /^(write|leave|reply\\s+to)\\b/i,\r\n\r\n /** Reaction counts, e.g. \"5\", \"5 reactions\", \"1.2K\". */\r\n reactions: /^(?:[\\d.,]+\\s*[KkMm]?|[\\d.,]+\\s*[KkMm]?\\s*(?:reactions?|likes?))$/,\r\n\r\n /** Post unavailable / deleted. */\r\n unavailable: /this content isn'?t (?:available|viewable)|content you requested cannot|page isn'?t available|this page isn'?t available/i,\r\n\r\n /** Private group wall the account has not joined. */\r\n joinGroup: /ask to join|join (?:this )?group|this group is private|join to see/i,\r\n\r\n /** Login wall shown to logged-out visitors. */\r\n loginWall: /log ?in (?:to continue|to see|or sign up)|you must log in to continue/i,\r\n\r\n /** Relative timestamps: \"2h\", \"5m\", \"3d\", \"12w\", \"2y\", \"just now\", \"5 hours ago\",\r\n * \"a day ago\", \"Yesterday at 3:04 PM\". */\r\n relativeTime:\r\n /\\b(?:\\d+\\s*[smhdyw]\\b|\\d+\\s+(?:seconds?|minutes?|hours?|days?|weeks?|months?|years?)\\b(?:\\s+ago)?|an?\\s+(?:second|minute|hour|day|week|month|year)\\s+ago|just now|yesterday(?:\\s+at\\s+[\\d:]+\\s*(?:am|pm)?)?)/i,\r\n\r\n /** Timestamp glued onto the end of an author name in an aria-label\r\n * (\"Comment by Mjac Cabildo 5 hours ago\") — strip it from the name. */\r\n trailingTimestamp:\r\n /\\s*[·-]?\\s*(?:just now|yesterday(?:\\s+at\\s+\\d{1,2}:\\d{2}\\s*(?:am|pm))?|(?:an?|\\d+)\\s+(?:s|secs?|m|mins?|min(?:utes?)?|h|hrs?|h(?:ours?)?|d|days?|w|wks?|w(?:eeks?)?|mo|months?|y|yrs?|y(?:ears?)?)\\s*(?:ago\\b)?|\\d+\\s*[smhdyw]\\b|\\d{1,2}:\\d{2}\\s*(?:am|pm)?)$/i,\r\n\r\n /** Absolute dates as Facebook renders them in comment headers:\r\n * \"Wednesday, August 26, 2026 at 2:38 PM\", \"August 26, 2026 at 2:38 PM\",\r\n * \"August 26 at 2:38 PM\". */\r\n absoluteTime:\r\n /^(?:[a-z]+day,\\s*)?(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sept?(?:ember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\\s+\\d{1,2}(?:,\\s*\\d{4})?(?:\\s+at\\s+\\d{1,2}:\\d{2}\\s*(?:am|pm)?)?$/i,\r\n\r\n /** Per-comment truncation expander (\"See more\" at the end of long comments). */\r\n seeMore: /^\\s*see more\\s*$/i,\r\n\r\n /** Author badges rendered inside comment articles. */\r\n badge:\r\n /^(?:verified account|top fan|new member|founding member|active member|rising fan|contributor|admin|moderator|analyst)$/i,\r\n\r\n /** Separator-only text lines (\"·\", \"•\", \"…\", \"!\") left between header and body. */\r\n noiseLine: /^[·•|,!?:;.…\\-–—\\s]+$/\r\n} as const;\r\n\r\n/** How many times \"load more\" / \"expand replies\" loops will click before giving up. */\r\nexport const loadMoreLimits = {\r\n commentExpansions: 40,\r\n replyExpansions: 60,\r\n clicksPerRound: 5,\r\n settleMs: 800\r\n} as const;\r\n","/** Safe helpers for walking the JSON blobs Facebook embeds in pages. */\r\n\r\nexport function safeJsonParse(input: string): unknown {\r\n try {\r\n return JSON.parse(input) as unknown;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport interface WalkOptions {\r\n maxDepth: number;\r\n maxNodes: number;\r\n}\r\n\r\n/**\r\n * Depth-first walk over arbitrary JSON. `visit` is called for every node\r\n * (object or array). Bounded so hostile/broken payloads cannot hang the process.\r\n */\r\nexport function walkJson(\r\n root: unknown,\r\n visit: (node: unknown) => void,\r\n options: WalkOptions\r\n): void {\r\n let visited = 0;\r\n const walk = (node: unknown, depth: number): void => {\r\n if (depth > options.maxDepth || visited > options.maxNodes) return;\r\n if (!node || typeof node !== \"object\") return;\r\n visited++;\r\n visit(node);\r\n for (const value of Object.values(node as Record<string, unknown>)) {\r\n walk(value, depth + 1);\r\n }\r\n };\r\n walk(root, 0);\r\n}\r\n","import type { Page } from \"playwright\";\r\nimport type { CommentExtractor } from \"./CommentExtractor\";\r\nimport type { ExtractionOptions, RawFacebookComment } from \"../types/Comment\";\r\nimport { selectors } from \"../config/selectors\";\r\nimport { safeJsonParse, walkJson } from \"../parsers/FacebookResponseParser\";\r\n\r\ninterface CommentShapedNode {\r\n message?: { text?: unknown };\r\n author?: { name?: unknown; id?: unknown; profile_url?: unknown };\r\n created_time?: unknown;\r\n reaction_count?: unknown;\r\n reactions?: { summary?: { total_count?: unknown } };\r\n}\r\n\r\nfunction isCommentNode(node: unknown): node is CommentShapedNode {\r\n if (!node || typeof node !== \"object\") return false;\r\n const candidate = node as CommentShapedNode;\r\n return (\r\n typeof candidate.message?.text === \"string\" &&\r\n typeof candidate.author?.name === \"string\"\r\n );\r\n}\r\n\r\nfunction mapNode(node: CommentShapedNode): RawFacebookComment {\r\n const created = typeof node.created_time === \"number\" ? node.created_time : null;\r\n const reactions =\r\n typeof node.reaction_count === \"number\"\r\n ? node.reaction_count\r\n : typeof node.reactions?.summary?.total_count === \"number\"\r\n ? node.reactions.summary.total_count\r\n : null;\r\n return {\r\n authorId: typeof node.author?.id === \"string\" ? node.author.id : null,\r\n authorName: typeof node.author?.name === \"string\" ? node.author.name : null,\r\n authorProfileUrl:\r\n typeof node.author?.profile_url === \"string\" ? node.author.profile_url : null,\r\n message: String(node.message?.text ?? \"\"),\r\n createdAtRaw:\r\n created !== null && created > 0\r\n ? new Date(created * 1000).toISOString()\r\n : null,\r\n reactionsTotal: reactions,\r\n replies: []\r\n };\r\n}\r\n\r\n/**\r\n * Opportunistic extractor over embedded application/json blobs (GraphQL\r\n * payloads). When present, this yields the cleanest data; when absent or\r\n * compressed, the pipeline falls through to DOM extractors.\r\n *\r\n * TODO(markup): field names follow Facebook's classic comment node shape.\r\n * Validate against a real capture and extend `isCommentNode` if needed.\r\n */\r\nexport class StructuredDataExtractor implements CommentExtractor {\r\n readonly name = \"structured-data\";\r\n\r\n async canHandle(page: Page): Promise<boolean> {\r\n return page.evaluate(\r\n ({ scriptSelector }) =>\r\n Array.from(document.querySelectorAll(scriptSelector)).some((script) => {\r\n const text = script.textContent ?? \"\";\r\n return text.includes(\"\\\"message\\\"\") && text.includes(\"\\\"author\\\"\");\r\n }),\r\n { scriptSelector: selectors.structuredDataScripts }\r\n );\r\n }\r\n\r\n async extract(page: Page, options: ExtractionOptions): Promise<RawFacebookComment[]> {\r\n const blobs = await page.evaluate(\r\n (scriptSelector) =>\r\n Array.from(document.querySelectorAll(scriptSelector))\r\n .map((script) => script.textContent ?? \"\")\r\n .filter((text) => text.length > 2 && text.length < 5_000_000)\r\n .slice(0, 100),\r\n selectors.structuredDataScripts\r\n );\r\n\r\n const out: RawFacebookComment[] = [];\r\n const cap = Math.max(options.maxComments * 3, 100);\r\n for (const blob of blobs) {\r\n const json = safeJsonParse(blob);\r\n if (!json) continue;\r\n walkJson(\r\n json,\r\n (node) => {\r\n if (out.length < cap && isCommentNode(node)) out.push(mapNode(node));\r\n },\r\n { maxNodes: 200_000, maxDepth: 40 }\r\n );\r\n if (out.length >= cap) break;\r\n }\r\n return out;\r\n }\r\n}\r\n","import type { ElementHandle, Locator, Page } from \"playwright\";\r\nimport { selectors } from \"../config/selectors\";\r\n\r\n/**\r\n * All comment interactions and DOM extraction run inside this scope: the\r\n * foreground permalink modal when Facebook renders one (the logged-in feed\r\n * stays mounted behind it and would otherwise leak its own comments in), or\r\n * the whole page for layouts without a modal.\r\n */\r\nexport class DiscussionScope {\r\n private constructor(\r\n readonly page: Page,\r\n private readonly root: Locator | null\r\n ) {}\r\n\r\n static wholePage(page: Page): DiscussionScope {\r\n return new DiscussionScope(page, null);\r\n }\r\n\r\n /** Coalesce an optional scope into a whole-page scope. */\r\n static of(page: Page, scope?: DiscussionScope): DiscussionScope {\r\n return scope ?? DiscussionScope.wholePage(page);\r\n }\r\n\r\n /**\r\n * Resolve the discussion scope for the loaded page. Modal permalinks hydrate\r\n * their dialog shortly after load, so wait briefly before deciding.\r\n *\r\n * Preference order:\r\n * 1. visible foreground modal containing a permalink/link to the requested post\r\n * 2. the single visible foreground modal\r\n * 3. the whole page (non-modal layouts, or ambiguous nesting)\r\n */\r\n static async resolve(page: Page, postId: string | null): Promise<DiscussionScope> {\r\n await page\r\n .waitForSelector(selectors.foregroundModal, { state: \"attached\", timeout: 5000 })\r\n .catch(() => null);\r\n\r\n const modals = page.locator(selectors.foregroundModal);\r\n const count = await modals.count().catch(() => 0);\r\n if (count === 0) return DiscussionScope.wholePage(page);\r\n\r\n const visible: Locator[] = [];\r\n for (let i = 0; i < count; i++) {\r\n if (await modals.nth(i).isVisible().catch(() => false)) {\r\n visible.push(modals.nth(i));\r\n }\r\n }\r\n\r\n if (postId && /^\\d+$/.test(postId) && visible.length > 0) {\r\n const paths = [`/posts/${postId}/`, `/permalink/${postId}/`];\r\n for (const modal of visible) {\r\n const hasPostLink = await modal\r\n .evaluate(\r\n (el: Element, wanted: string[]) =>\r\n Array.from(el.querySelectorAll<HTMLAnchorElement>(\"a[href]\")).some((anchor) => {\r\n try {\r\n const pathname = new URL(anchor.href, window.location.href).pathname;\r\n return wanted.some((segment) => pathname.includes(segment));\r\n } catch {\r\n return false;\r\n }\r\n }),\r\n paths\r\n )\r\n .catch(() => false);\r\n if (hasPostLink) return new DiscussionScope(page, modal);\r\n }\r\n }\r\n\r\n if (visible.length === 1) return new DiscussionScope(page, visible[0]!);\r\n return DiscussionScope.wholePage(page);\r\n }\r\n\r\n /** Locator scoped to the modal when present, else page-wide. */\r\n locator(selector: string): Locator {\r\n return this.root ? this.root.locator(selector) : this.page.locator(selector);\r\n }\r\n\r\n /** The modal element itself, for root-scoped page.evaluate calls. */\r\n async rootElementHandle(): Promise<ElementHandle<Element> | null> {\r\n return this.root ? await this.root.elementHandle().catch(() => null) : null;\r\n }\r\n\r\n /** True when interactions are scoped to a foreground modal. */\r\n get isModal(): boolean {\r\n return this.root !== null;\r\n }\r\n}\r\n","import type { Page } from \"playwright\";\r\nimport type { ArticleDump } from \"./CommentExtractor\";\r\nimport type { DiscussionScope } from \"../comments/DiscussionScope\";\r\n\r\nexport interface DomDumpOptions {\r\n /** Selector matching comment containers (see config/selectors.ts). */\r\n containerSelector: string;\r\n maxArticles: number;\r\n}\r\n\r\n/**\r\n * Dumps the article tree for every top-level match of containerSelector.\r\n * For each element, text/links exclude nested articles so Node-side parsing\r\n * sees only that element's own content. When the discussion scope is a\r\n * foreground modal, only articles inside that modal are dumped — the\r\n * background feed stays out.\r\n */\r\nexport async function buildArticleDumps(\r\n page: Page,\r\n options: DomDumpOptions,\r\n scope?: DiscussionScope\r\n): Promise<ArticleDump[]> {\r\n try {\r\n // The modal is passed as an ElementHandle so the entire collect body can\r\n // stay inside the evaluate callback (closures do not cross the boundary).\r\n const root = scope?.isModal ? await scope.rootElementHandle() : null;\r\n const dumps = await page.evaluate(\r\n ({ containerSelector, maxArticles, root }) => {\r\n const scopeRoot = (root as Element | null) ?? document;\r\n const dump = (el: Element): Record<string, unknown> | null => {\r\n const links: Record<string, unknown>[] = [];\r\n const images: Record<string, unknown>[] = [];\r\n let ownText = \"\";\r\n // Author avatar: a small fbcdn image inside a profile link, without\r\n // descriptive alt text (live markup: <a><svg><image xlink:href=...>).\r\n let avatarUrl: string | null = null;\r\n\r\n const walk = (node: Node, inLink: boolean): void => {\r\n let linkContext = inLink;\r\n if (node.nodeType === Node.ELEMENT_NODE) {\r\n const elem = node as Element;\r\n if (elem !== el && elem.getAttribute(\"role\") === \"article\") return;\r\n if (elem.tagName === \"A\") {\r\n linkContext = true;\r\n const label = (elem.getAttribute(\"aria-label\") ?? elem.textContent ?? \"\").trim() || null;\r\n links.push({ href: elem.getAttribute(\"href\") ?? \"\", label });\r\n const hasImage = Boolean(elem.querySelector(\"img, image\"));\r\n // Hashtags are comment body text (links without images: label is\r\n // the body). Links WITH images (author avatars, linked media)\r\n // contribute their text via child traversal instead — otherwise\r\n // the name inside the link is counted twice.\r\n if (label && !hasImage) ownText += label + \"\\n\";\r\n if (!hasImage) return;\r\n }\r\n // Avatars live inside profile links (skipped above). Keep comment\r\n // images/stickers as attachments; alt remains the text fallback.\r\n const tag = elem.tagName.toUpperCase();\r\n if (tag === \"IMG\" || tag === \"IMAGE\") {\r\n const image = elem as HTMLImageElement;\r\n const rawUrl =\r\n image.currentSrc ||\r\n elem.getAttribute(\"src\") ||\r\n elem.getAttribute(\"href\") ||\r\n elem.getAttribute(\"xlink:href\") ||\r\n \"\";\r\n const alt =\r\n (elem.getAttribute(\"alt\") ?? elem.getAttribute(\"aria-label\") ?? \"\").trim() ||\r\n null;\r\n let attachment = false;\r\n try {\r\n const url = new URL(rawUrl, document.baseURI);\r\n const host = url.hostname.toLowerCase();\r\n const allowed =\r\n url.protocol === \"https:\" &&\r\n (host === \"fbcdn.net\" || host.endsWith(\".fbcdn.net\"));\r\n // Comment media is rendered as a sizable image with descriptive\r\n // alt text. This excludes profile photos and UI sprite images.\r\n attachment =\r\n allowed &&\r\n Boolean(alt) &&\r\n (image.naturalWidth >= 120 ||\r\n image.naturalHeight >= 120 ||\r\n image.width >= 120 ||\r\n image.height >= 120) &&\r\n !/^(?:subscribe to|profile picture|avatar)/i.test(alt ?? \"\");\r\n if (attachment) {\r\n images.push({\r\n url: url.toString(),\r\n alt,\r\n width: image.naturalWidth || image.width || null,\r\n height: image.naturalHeight || image.height || null\r\n });\r\n }\r\n // The author avatar is the first alt-less (or avatar-labeled)\r\n // fbcdn image inside a link — comment media always carries alt.\r\n if (\r\n !avatarUrl &&\r\n linkContext &&\r\n allowed &&\r\n (!alt || /^(?:profile picture|avatar)/i.test(alt))\r\n ) {\r\n avatarUrl = url.toString();\r\n }\r\n } catch {\r\n // Invalid/unsafe image URLs are ignored.\r\n }\r\n if (\r\n !attachment &&\r\n alt &&\r\n !/^https?:/i.test(alt) &&\r\n !/^(?:subscribe to|profile picture|avatar)/i.test(alt)\r\n ) {\r\n ownText += alt + \"\\n\";\r\n }\r\n return;\r\n }\r\n }\r\n if (node.nodeType === Node.TEXT_NODE) {\r\n ownText += (node.textContent ?? \"\") + \"\\n\";\r\n return;\r\n }\r\n for (const child of Array.from(node.childNodes)) walk(child, linkContext);\r\n };\r\n walk(el, false);\r\n\r\n const children: Record<string, unknown>[] = [];\r\n const findArticles = (node: Element): void => {\r\n if (node !== el && node.getAttribute(\"role\") === \"article\") {\r\n const child = dump(node);\r\n if (child) children.push(child);\r\n return;\r\n }\r\n for (const child of Array.from(node.children)) findArticles(child);\r\n };\r\n findArticles(el);\r\n\r\n return {\r\n ariaLabel: el.getAttribute(\"aria-label\"),\r\n domId: el.id || null,\r\n ownText: ownText.trim(),\r\n fullText: (el as HTMLElement).innerText ?? ownText.trim(),\r\n links: links.slice(0, 12),\r\n images: images.slice(0, 12),\r\n avatarUrl,\r\n children\r\n };\r\n };\r\n\r\n const all = Array.from(scopeRoot.querySelectorAll(containerSelector));\r\n // Keep only articles whose nearest matching ancestor is outside the\r\n // scope (or absent) — nested articles become children of their parent.\r\n const tops = all.filter((el) => {\r\n const parent = el.parentElement?.closest(containerSelector) ?? null;\r\n return !parent || !scopeRoot.contains(parent);\r\n });\r\n return tops\r\n .slice(0, maxArticles)\r\n .map(dump)\r\n .filter((d): d is Record<string, unknown> => d !== null);\r\n },\r\n { containerSelector: options.containerSelector, maxArticles: options.maxArticles, root }\r\n );\r\n return (dumps ?? []) as unknown as ArticleDump[];\r\n } catch {\r\n return [];\r\n }\r\n}\r\n","import type { ArticleDump } from \"../extractors/CommentExtractor\";\r\nimport type { RawFacebookComment } from \"../types/Comment\";\r\nimport { textPatterns } from \"../config/selectors\";\r\n\r\nconst NON_PROFILE_SEGMENTS = new Set([\r\n \"groups\", \"help\", \"policies\", \"login\", \"photo\", \"photos\", \"reel\", \"share\", \"watch\",\r\n \"marketplace\", \"hashtag\", \"events\", \"gaming\", \"stories\", \"video\", \"videos\", \"posts\",\r\n \"permalink.php\", \"settings\", \"privacy\", \"legal\", \"notes\", \"login.php\", \"recover\",\r\n \"pages\", \"bookmarks\", \"friends\", \"feed\", \"notifications\", \"helpcenter\"\r\n]);\r\n\r\n/** Lines that are UI affordances, not comment content. */\r\nconst ACTION_LINE =\r\n /^(?:like|reply|share|hide|report|delete|edit|see translation|view translation|most relevant|all comments|see more|show more|by author|liked by author|top sticker|giphy|via giphy|click to view attachment|\\d+\\s*(?:reactions?|likes?|replies?|comments?)|liked by .*|reply\\s*·\\s*.*)$/i;\r\n\r\n/** \"5\" | \"1.2K\" | \"3,402 reactions\" -> number */\r\nexport function parseReactionCount(input: string): number | null {\r\n const match = input.trim().match(/^([\\d.,]+)\\s*([KkMm])?/);\r\n if (!match) return null;\r\n const base = Number.parseFloat(match[1]?.replace(/,/g, \"\") ?? \"\");\r\n if (!Number.isFinite(base)) return null;\r\n const suffix = match[2]?.toLowerCase();\r\n if (suffix === \"k\") return Math.round(base * 1000);\r\n if (suffix === \"m\") return Math.round(base * 1000_000);\r\n return Math.round(base);\r\n}\r\n\r\nfunction isProfileUrl(href: string): boolean {\r\n if (!href || href.startsWith(\"#\") || href.startsWith(\"javascript:\")) return false;\r\n try {\r\n const url = new URL(href, \"https://www.facebook.com\");\r\n if (!/(?:^|\\.)facebook\\.com$/i.test(url.hostname.replace(/^www\\./i, \"\"))) return false;\r\n if (url.pathname.startsWith(\"/people/\")) return true;\r\n if (url.pathname.startsWith(\"/profile.php\")) return true;\r\n const first = url.pathname.split(\"/\").filter(Boolean)[0];\r\n if (!first || NON_PROFILE_SEGMENTS.has(first.toLowerCase())) return false;\r\n return url.pathname.split(\"/\").filter(Boolean).length === 1;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\nfunction absoluteUrl(href: string): string | null {\r\n if (!href) return null;\r\n try {\r\n return new URL(href, \"https://www.facebook.com\").toString();\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nfunction authorIdFromProfileUrl(href: string | null): string | null {\r\n if (!href) return null;\r\n try {\r\n const url = new URL(href);\r\n const people = url.pathname.match(/^\\/people\\/[^/]+\\/(\\d+)/);\r\n if (people?.[1]) return people[1];\r\n const queryId = url.searchParams.get(\"id\");\r\n if (queryId) return queryId;\r\n } catch {\r\n /* ignore */\r\n }\r\n return null;\r\n}\r\n\r\n/** Strip a trailing timestamp from an aria-label author capture\r\n * (\"Mjac Cabildo 5 hours ago\" -> \"Mjac Cabildo\"). */\r\nfunction stripTrailingTimestamp(name: string): string {\r\n let out = name.trim();\r\n for (let i = 0; i < 2; i++) {\r\n out = out.replace(textPatterns.trailingTimestamp, \"\").trim();\r\n }\r\n return out;\r\n}\r\n\r\n/** Decode profile.php?comment_id=BASE64 (\"comment:POSTID_COMMENTID\") when present.\r\n * Gives comments real ids and reveals which post they actually belong to. */\r\nfunction commentRefFromProfileUrl(\r\n href: string | null\r\n): { postRefId: string; commentId: string } | null {\r\n if (!href) return null;\r\n try {\r\n const url = new URL(href);\r\n const encoded = url.searchParams.get(\"comment_id\") ?? url.searchParams.get(\"reply_id\");\r\n if (!encoded) return null;\r\n const decoded = Buffer.from(\r\n encoded.replace(/-/g, \"+\").replace(/_/g, \"/\"),\r\n \"base64\"\r\n ).toString(\"utf8\");\r\n const match = decoded.match(/^(?:comment|reply):(\\d+)_(\\d+)/);\r\n if (!match?.[1] || !match[2]) return null;\r\n return { postRefId: match[1], commentId: match[2] };\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nfunction timestampFrom(lines: string[], ariaLabel: string): string | null {\r\n // Absolute dates (\"Wednesday, August 26, 2026 at 2:38 PM\") are the most\r\n // precise; strip the weekday so parseTimestamp's month-day form applies.\r\n for (const line of lines) {\r\n if (textPatterns.absoluteTime.test(line)) {\r\n return line.replace(/^[a-z]+day,\\s*/i, \"\").trim();\r\n }\r\n }\r\n for (const line of lines) {\r\n if (textPatterns.relativeTime.test(line)) {\r\n // The header line fuses name+time (\"Rita Garcia a day ago\") — keep only\r\n // the timestamp portion.\r\n const matched = line.match(textPatterns.trailingTimestamp)?.[0] ?? line;\r\n return matched.replace(/^[·\\s-]+/, \"\").trim();\r\n }\r\n }\r\n const ariaTime = ariaLabel.split(\"·\").map((part) => part.trim()).find(Boolean);\r\n if (ariaTime && textPatterns.relativeTime.test(ariaTime)) {\r\n return ariaTime.match(textPatterns.trailingTimestamp)?.[0]?.trim() ?? ariaTime;\r\n }\r\n return null;\r\n}\r\n\r\n/**\r\n * Parse one dumped element into a raw comment, or null when it does not look\r\n * like a comment (e.g. the post article itself). Never throws — every field\r\n * degrades to null when Facebook's markup differs from expectations.\r\n */\r\nexport function parseArticleDump(dump: ArticleDump): RawFacebookComment | null {\r\n const ariaLabel = dump.ariaLabel ?? \"\";\r\n const commentByMatch = ariaLabel.match(textPatterns.commentBy);\r\n // \"Reply by X to Y's comment\" — the author is the reply author, not Y.\r\n const replyByMatch =\r\n ariaLabel.match(textPatterns.replyBy) ?? ariaLabel.match(textPatterns.replyByFallback);\r\n const authorTimeMatch = ariaLabel.match(textPatterns.authorTime);\r\n const hasReplyLink = dump.links.some(\r\n (link) => link.label !== null && textPatterns.reply.test(link.label)\r\n );\r\n\r\n const profileLink = dump.links.find((link) => isProfileUrl(link.href));\r\n const profileUrl = profileLink ? absoluteUrl(profileLink.href) : null;\r\n const commentRef = commentRefFromProfileUrl(profileUrl);\r\n\r\n const lines = dump.ownText\r\n .split(/\\n+/)\r\n .map((line) => line.trim())\r\n .filter(Boolean);\r\n\r\n const contentLines: string[] = [];\r\n const droppedLines: string[] = [];\r\n let headerSkipped = false;\r\n for (const line of lines) {\r\n const authorName = stripTrailingTimestamp(\r\n replyByMatch?.[1] ?? commentByMatch?.[1] ?? authorTimeMatch?.[1] ?? \"\"\r\n );\r\n if (\r\n !headerSkipped &&\r\n authorName &&\r\n (line.startsWith(authorName) || textPatterns.authorTime.test(line))\r\n ) {\r\n headerSkipped = true;\r\n continue;\r\n }\r\n if (\r\n ACTION_LINE.test(line) ||\r\n textPatterns.noiseLine.test(line) ||\r\n textPatterns.badge.test(line) ||\r\n textPatterns.reactions.test(line) ||\r\n textPatterns.relativeTime.test(line) ||\r\n textPatterns.absoluteTime.test(line)\r\n ) {\r\n droppedLines.push(line);\r\n continue;\r\n }\r\n contentLines.push(line);\r\n }\r\n\r\n const message = contentLines.join(\"\\n\").trim();\r\n const attachments = dump.images.map((image) => ({\r\n type: /sticker/i.test(image.alt ?? \"\") ? (\"sticker\" as const) : (\"image\" as const),\r\n ...image\r\n }));\r\n\r\n // Must look like a comment: body text or media, plus a comment affordance or\r\n // a comment-ish aria-label. Giant blobs are the post itself or shared content.\r\n const looksLikeComment =\r\n (message.length > 0 || attachments.length > 0) &&\r\n message.length < 3000 &&\r\n (hasReplyLink || commentByMatch !== null || replyByMatch !== null || authorTimeMatch !== null) &&\r\n !(dump.children.length > 0 && contentLines.length > 40);\r\n if (!looksLikeComment) return null;\r\n\r\n // Author name preference: profile-link text (clean) > stripped aria capture.\r\n const authorName =\r\n (profileLink?.label ?? \"\").trim() ||\r\n stripTrailingTimestamp(\r\n replyByMatch?.[1] ?? commentByMatch?.[1] ?? authorTimeMatch?.[1] ?? \"\"\r\n ) ||\r\n null;\r\n\r\n const reactionLine = droppedLines.find((line) => textPatterns.reactions.test(line));\r\n\r\n return {\r\n id: commentRef?.commentId ?? (dump.domId || null),\r\n postRefId: commentRef?.postRefId ?? null,\r\n authorId: authorIdFromProfileUrl(profileUrl),\r\n authorName,\r\n authorProfileUrl: profileUrl,\r\n authorAvatarUrl: dump.avatarUrl ?? null,\r\n message,\r\n attachments: attachments.length > 0 ? attachments : undefined,\r\n createdAtRaw: timestampFrom([...lines], ariaLabel),\r\n reactionsTotal: reactionLine ? parseReactionCount(reactionLine) : null,\r\n isReply: replyByMatch !== null || undefined,\r\n parentAuthorName: replyByMatch?.[2]?.trim() || null,\r\n replies: []\r\n };\r\n}\r\n\r\n/**\r\n * Parse a dumped element tree into raw comments. If the root is not itself a\r\n * comment (the post wrapper), its comment children are hoisted to the top level.\r\n */\r\nexport function parseArticleTree(dump: ArticleDump, parentId: string | null): RawFacebookComment[] {\r\n const parsed = parseArticleDump(dump);\r\n if (parsed) {\r\n parsed.parentId = parentId;\r\n const replies: RawFacebookComment[] = [];\r\n for (const child of dump.children) {\r\n replies.push(...parseArticleTree(child, parsed.id ?? null));\r\n }\r\n parsed.replies = replies;\r\n return [parsed];\r\n }\r\n return dump.children.flatMap((child) => parseArticleTree(child, null));\r\n}\r\n\r\n/**\r\n * Facebook renders some replies as flat sibling articles distinguished only by\r\n * their \"Reply by X to Y's comment\" aria-label. Re-attach those to the nearest\r\n * preceding top-level comment whose author matches Y; unmatched replies stay\r\n * top-level rather than being dropped.\r\n */\r\nexport function attachFlatReplies(comments: RawFacebookComment[]): RawFacebookComment[] {\r\n const tops: RawFacebookComment[] = [];\r\n for (const comment of comments) {\r\n if (comment.isReply) {\r\n const parent = [...tops]\r\n .reverse()\r\n .find((top) => !top.isReply && top.authorName === comment.parentAuthorName);\r\n if (parent) {\r\n comment.parentId = parent.id ?? comment.parentId ?? null;\r\n parent.replies = [...(parent.replies ?? []), comment];\r\n continue;\r\n }\r\n }\r\n tops.push(comment);\r\n }\r\n return tops;\r\n}\r\n","import type { Page } from \"playwright\";\r\nimport type { CommentExtractor } from \"./CommentExtractor\";\r\nimport { DiscussionScope } from \"../comments/DiscussionScope\";\r\nimport type { ExtractionOptions, RawFacebookComment } from \"../types/Comment\";\r\nimport { selectors } from \"../config/selectors\";\r\nimport { buildArticleDumps } from \"./domDump\";\r\nimport { attachFlatReplies, parseArticleTree } from \"../comments/CommentParser\";\r\n\r\n/**\r\n * Primary extractor: aria roles and labels outlive Facebook's generated class\r\n * names. Dumps the article tree in-page, parses it Node-side. On modal\r\n * permalinks only the foreground dialog's articles are dumped.\r\n */\r\nexport class AccessibleDomExtractor implements CommentExtractor {\r\n readonly name = \"accessible-dom\";\r\n\r\n async canHandle(page: Page, scope?: DiscussionScope): Promise<boolean> {\r\n return (await DiscussionScope.of(page, scope).locator(selectors.commentArticle).count()) > 0;\r\n }\r\n\r\n async extract(\r\n page: Page,\r\n options: ExtractionOptions,\r\n scope?: DiscussionScope\r\n ): Promise<RawFacebookComment[]> {\r\n const dumps = await buildArticleDumps(\r\n page,\r\n {\r\n containerSelector: selectors.commentArticle,\r\n // Headroom for the post wrapper plus reply threads.\r\n maxArticles: Math.max(options.maxComments * 3, 50)\r\n },\r\n scope\r\n );\r\n\r\n // Sibling feed units (related posts, suggested reels) can render their own\r\n // comment articles on the same page. Scope to the unit that clearly holds\r\n // the discussion (>= 2x any other unit). Reel panels legitimately split\r\n // comments across several wrapper articles — close counts keep everything.\r\n const perRoot = dumps.map((dump) => parseArticleTree(dump, null));\r\n const counts = perRoot.map((comments) => comments.length).sort((a, b) => b - a);\r\n const max = counts[0] ?? 0;\r\n const second = counts[1] ?? 0;\r\n if (max >= 2 * second && max > 0) {\r\n const best = perRoot.filter((comments) => comments.length === max);\r\n if (best.length === 1) return attachFlatReplies(best[0] ?? []);\r\n }\r\n return attachFlatReplies(perRoot.flat());\r\n }\r\n}\r\n","import type { Page } from \"playwright\";\r\nimport type { CommentExtractor } from \"./CommentExtractor\";\r\nimport { DiscussionScope } from \"../comments/DiscussionScope\";\r\nimport type { ExtractionOptions, RawFacebookComment } from \"../types/Comment\";\r\nimport { selectors } from \"../config/selectors\";\r\nimport { buildArticleDumps } from \"./domDump\";\r\nimport { attachFlatReplies, parseArticleTree } from \"../comments/CommentParser\";\r\n\r\n/**\r\n * Last-resort sweep with looser container selectors. Catches pages where\r\n * role=\"article\" markup is absent but aria-labels or data-testid hints remain.\r\n */\r\nexport class FallbackDomExtractor implements CommentExtractor {\r\n readonly name = \"fallback-dom\";\r\n\r\n async canHandle(page: Page, scope?: DiscussionScope): Promise<boolean> {\r\n return (\r\n (await DiscussionScope.of(page, scope).locator(selectors.fallbackCommentContainer).count()) >\r\n 0\r\n );\r\n }\r\n\r\n async extract(\r\n page: Page,\r\n options: ExtractionOptions,\r\n scope?: DiscussionScope\r\n ): Promise<RawFacebookComment[]> {\r\n const dumps = await buildArticleDumps(\r\n page,\r\n {\r\n containerSelector: selectors.fallbackCommentContainer,\r\n maxArticles: Math.max(options.maxComments * 3, 50)\r\n },\r\n scope\r\n );\r\n return attachFlatReplies(dumps.flatMap((dump) => parseArticleTree(dump, null)));\r\n }\r\n}\r\n","import { loadMoreLimits, selectors, textPatterns } from \"../config/selectors\";\r\nimport type { DiscussionScope } from \"./DiscussionScope\";\r\n\r\n/**\r\n * Click \"View more replies\" / \"N replies\" controls until reply threads are\r\n * expanded or the bounded limits in config/selectors.ts are exhausted. Only\r\n * controls inside the discussion scope are touched — the background feed\r\n * renders its own reply controls that must never be clicked.\r\n */\r\nexport async function expandReplies(scope: DiscussionScope): Promise<void> {\r\n const rounds = Math.ceil(loadMoreLimits.replyExpansions / loadMoreLimits.clicksPerRound);\r\n for (let round = 0; round < rounds; round++) {\r\n const expanders = scope\r\n .locator(selectors.replyExpander)\r\n .filter({ hasText: textPatterns.viewMoreReplies });\r\n const count = Math.min(await expanders.count().catch(() => 0), loadMoreLimits.clicksPerRound);\r\n if (count === 0) return;\r\n for (let i = 0; i < count; i++) {\r\n await expanders\r\n .nth(i)\r\n .click({ timeout: 3000 })\r\n .catch(() => {});\r\n }\r\n await scope.page.waitForTimeout(loadMoreLimits.settleMs).catch(() => {});\r\n }\r\n}\r\n","import type { Page } from \"playwright\";\r\nimport type { RawFacebookComment } from \"../types/Comment\";\r\nimport { loadMoreLimits, selectors, textPatterns } from \"../config/selectors\";\r\nimport { ExtractorPipeline } from \"../extractors/ExtractorPipeline\";\r\nimport { expandReplies } from \"./ReplyFetcher\";\r\nimport { DiscussionScope } from \"./DiscussionScope\";\r\nimport {\r\n FacebookSessionExpiredError,\r\n FacebookPostNotFoundError,\r\n FacebookAccessDeniedError\r\n} from \"../errors\";\r\n\r\nexport interface FetchParams {\r\n maxComments: number;\r\n includeReplies: boolean;\r\n maxReplies: number;\r\n /** Comments already consumed by earlier pages — load this many plus one page. */\r\n skipped: number;\r\n /** Post id from the request URL (when known) — comments whose profile link\r\n * references a different post id are dropped (feed posts rendered on the\r\n * same page otherwise leak in). */\r\n postId: string | null;\r\n}\r\n\r\nexport interface FetchOutcome {\r\n raw: RawFacebookComment[];\r\n /** True when Facebook still offered a \"more comments\" control at the end. */\r\n loaderPresent: boolean;\r\n}\r\n\r\n/** Inspect the loaded page for error states. Returns a fitting error, or null. */\r\nexport async function detectPageProblem(page: Page): Promise<Error | null> {\r\n const url = page.url();\r\n if (/(?:^|\\/)login(?:\\.php)?(?:[/?#]|$)/i.test(url)) {\r\n return new FacebookSessionExpiredError(\r\n \"Facebook redirected to a login page — the session is no longer accepted. Re-run `fb-comments login` to refresh it.\"\r\n );\r\n }\r\n\r\n let bodyText = \"\";\r\n try {\r\n bodyText = (await page.evaluate(() => document.body?.innerText ?? \"\")).slice(0, 20_000);\r\n } catch {\r\n return null;\r\n }\r\n\r\n if (textPatterns.unavailable.test(bodyText)) {\r\n return new FacebookPostNotFoundError(\r\n \"This content is not available. The post may have been deleted or is not visible to this account.\"\r\n );\r\n }\r\n if (textPatterns.joinGroup.test(bodyText)) {\r\n return new FacebookAccessDeniedError(\r\n \"Access denied: this group is private and the account has not joined it.\"\r\n );\r\n }\r\n if (textPatterns.loginWall.test(bodyText)) {\r\n return new FacebookSessionExpiredError(\r\n \"Facebook is showing a login wall — the session appears expired. Re-run `fb-comments login`.\"\r\n );\r\n }\r\n return null;\r\n}\r\n\r\nasync function commentCount(scope: DiscussionScope): Promise<number> {\r\n return scope.locator(selectors.commentCandidate).count().catch(() => 0);\r\n}\r\n\r\nasync function waitForCommentGrowth(\r\n scope: DiscussionScope,\r\n previous: number,\r\n timeoutMs = 5000\r\n): Promise<number> {\r\n const start = Date.now();\r\n while (Date.now() - start < timeoutMs) {\r\n const count = await commentCount(scope);\r\n if (count > previous) return count;\r\n await scope.page.waitForTimeout(250).catch(() => {});\r\n }\r\n return commentCount(scope);\r\n}\r\n\r\n/**\r\n * Load comments until `target` articles are present or Facebook stops offering\r\n * more. Bounded by loadMoreLimits in config/selectors.ts.\r\n */\r\nexport async function expandComments(scope: DiscussionScope, target: number): Promise<boolean> {\r\n let lastCount = await commentCount(scope);\r\n for (let round = 0; round < loadMoreLimits.commentExpansions; round++) {\r\n if (lastCount >= target) return true;\r\n const expanders = scope\r\n .locator(selectors.commentExpander)\r\n .filter({ hasText: textPatterns.moreComments });\r\n const count = Math.min(\r\n await expanders.count().catch(() => 0),\r\n loadMoreLimits.clicksPerRound\r\n );\r\n if (count === 0) return false;\r\n for (let i = 0; i < count; i++) {\r\n await expanders\r\n .nth(i)\r\n .click({ timeout: 3000 })\r\n .catch(() => {});\r\n }\r\n const nextCount = await waitForCommentGrowth(scope, lastCount);\r\n if (nextCount <= lastCount) return false;\r\n lastCount = nextCount;\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * Reels and videos keep comments behind a \"Comments\"/\"View comments\" control —\r\n * often icon-only, so it is labeled via aria-label without matching text.\r\n * Try text-matched and aria-labeled controls (bounded, two rounds); no-op when\r\n * comments are already open.\r\n */\r\nexport async function openCommentsPanel(scope: DiscussionScope): Promise<void> {\r\n // Comments area already rendered (posts, auto-opened sidebars) — nothing to do.\r\n if ((await commentCount(scope)) > 0) return;\r\n\r\n // Reels render the action bar late; give the control a moment to appear.\r\n await scope\r\n .locator(selectors.commentOpenerControls)\r\n .first()\r\n .waitFor({ timeout: 8000 })\r\n .catch(() => null);\r\n\r\n const groups = [\r\n scope.locator(selectors.commentExpander).filter({ hasText: textPatterns.commentOpener }),\r\n scope.locator(selectors.commentOpenerControls)\r\n ];\r\n\r\n // The control is a TOGGLE: one click opens the panel, a second closes it.\r\n // After each click, stop as soon as comment articles appear — never re-click.\r\n for (let attempt = 0; attempt < 3; attempt++) {\r\n if ((await commentCount(scope)) > 0) return;\r\n let clicked = false;\r\n for (const group of groups) {\r\n const count = Math.min(await group.count().catch(() => 0), 8);\r\n for (let i = 0; i < count && !clicked; i++) {\r\n const el = group.nth(i);\r\n const label = (await el.getAttribute(\"aria-label\").catch(() => null)) ?? \"\";\r\n if (textPatterns.commentComposer.test(label)) continue;\r\n if (!(await el.isVisible().catch(() => false))) continue;\r\n clicked = true;\r\n await el.click({ timeout: 3000 }).catch(() => {});\r\n }\r\n if (clicked) break;\r\n }\r\n if (!clicked) return;\r\n // Give the panel time to render (reels hydrate slowly right after load).\r\n await scope\r\n .locator(selectors.commentArticle)\r\n .first()\r\n .waitFor({ timeout: 6000 })\r\n .catch(() => null);\r\n await scope.page.waitForTimeout(loadMoreLimits.settleMs).catch(() => {});\r\n }\r\n}\r\n\r\n/** Wait until the article count stops changing — comments render progressively\r\n * (top comments first) and extracting too early misses the rest. */\r\nasync function waitForStableArticles(scope: DiscussionScope, maxWaitMs = 8000): Promise<void> {\r\n const start = Date.now();\r\n let last = -1;\r\n let stablePolls = 0;\r\n while (Date.now() - start < maxWaitMs) {\r\n const count = await commentCount(scope).catch(() => 0);\r\n if (count === last) {\r\n stablePolls++;\r\n if (stablePolls >= 2 && count > 0) return;\r\n } else {\r\n stablePolls = 0;\r\n last = count;\r\n }\r\n await scope.page.waitForTimeout(700).catch(() => {});\r\n }\r\n}\r\n\r\n/** Click per-comment \"See more\" so long comments are not extracted truncated. */\r\nexport async function expandSeeMore(scope: DiscussionScope): Promise<void> {\r\n for (let round = 0; round < 3; round++) {\r\n const buttons = scope\r\n .locator(selectors.commentExpander)\r\n .filter({ hasText: textPatterns.seeMore });\r\n const count = Math.min(await buttons.count().catch(() => 0), 25);\r\n if (count === 0) return;\r\n for (let i = 0; i < count; i++) {\r\n await buttons.nth(i).click({ timeout: 1500 }).catch(() => {});\r\n }\r\n await scope.page.waitForTimeout(loadMoreLimits.settleMs).catch(() => {});\r\n }\r\n}\r\n\r\n/** Orchestrates load-more, reply expansion, and the extractor pipeline. */\r\nexport class CommentFetcher {\r\n constructor(private readonly pipeline: ExtractorPipeline) {}\r\n\r\n async fetch(page: Page, params: FetchParams): Promise<FetchOutcome> {\r\n const scope = await DiscussionScope.resolve(page, params.postId);\r\n await openCommentsPanel(scope);\r\n await waitForStableArticles(scope);\r\n\r\n const target = params.skipped + params.maxComments;\r\n await expandComments(scope, target);\r\n await expandSeeMore(scope);\r\n if (params.includeReplies) {\r\n await expandReplies(scope);\r\n }\r\n const extracted = await this.pipeline.extract(\r\n page,\r\n {\r\n maxComments: params.maxComments,\r\n includeReplies: params.includeReplies,\r\n maxReplies: params.includeReplies ? params.maxReplies : 0\r\n },\r\n scope\r\n );\r\n\r\n const loaderCount = await scope\r\n .locator(selectors.commentExpander)\r\n .filter({ hasText: textPatterns.moreComments })\r\n .count()\r\n .catch(() => 0);\r\n\r\n const raw = filterWrongPost(extracted, params.postId);\r\n return { raw, loaderPresent: loaderCount > 0 };\r\n }\r\n}\r\n\r\n/** Collapse the same comment rendered twice (feed re-renders, wrapper articles).\r\n * Keyed by comment id when present, else by author+message+time. */\r\nexport function dedupeComments(comments: RawFacebookComment[]): RawFacebookComment[] {\r\n const seenIds = new Set<string>();\r\n const seenIdlessContent = new Set<string>();\r\n const out: RawFacebookComment[] = [];\r\n for (const comment of comments) {\r\n // Same id = same comment. Id-less copies collapse on content only — two\r\n // DISTINCT comments can share text, so never merge when ids exist. The\r\n // timestamp is excluded for long messages: the same comment can render\r\n // twice with different time formats (\"2 days ago\" vs an absolute date).\r\n const message = comment.message ?? \"\";\r\n const mediaKey = (comment.attachments ?? []).map((attachment) => attachment.url).join(\"\\u0001\");\r\n const contentKey =\r\n `${comment.authorName ?? \"\"}\\u0000${message}\\u0000${mediaKey}` +\r\n (message.length < 30 && !mediaKey ? `\\u0000${comment.createdAtRaw ?? \"\"}` : \"\");\r\n if (comment.id != null) {\r\n if (seenIds.has(comment.id)) continue;\r\n seenIds.add(comment.id);\r\n } else {\r\n if (seenIdlessContent.has(contentKey)) continue;\r\n seenIdlessContent.add(contentKey);\r\n }\r\n out.push(comment.replies?.length ? { ...comment, replies: dedupeComments(comment.replies) } : comment);\r\n }\r\n return out;\r\n}\r\n\r\n/** Drop comments that provably belong to a different post rendered on the same\r\n * page (feed units, related content). The owning post id comes from each\r\n * comment's comment_id param — which equals the URL post id on permalinks, but\r\n * is Facebook's internal story id (NOT the URL id) for reels/videos. So: an\r\n * explicit URL-id match always wins; otherwise keep the majority postRefId —\r\n * the permalink's own comments dominate the page. Ties keep everything. */\r\nexport function filterWrongPost(\r\n comments: RawFacebookComment[],\r\n postId: string | null\r\n): RawFacebookComment[] {\r\n const deduped = dedupeComments(comments);\r\n\r\n const counts = new Map<string, number>();\r\n for (const comment of deduped) {\r\n if (!comment.postRefId) continue;\r\n counts.set(comment.postRefId, (counts.get(comment.postRefId) ?? 0) + 1);\r\n }\r\n if (counts.size === 0) return deduped;\r\n\r\n const urlId = postId && /^\\d+$/.test(postId) ? postId : null;\r\n let best: string | null = urlId && counts.has(urlId) ? urlId : null;\r\n\r\n if (!best) {\r\n let bestCount = 0;\r\n let tie = false;\r\n for (const [id, count] of counts) {\r\n if (count > bestCount) {\r\n best = id;\r\n bestCount = count;\r\n tie = false;\r\n } else if (count === bestCount) {\r\n tie = true;\r\n }\r\n }\r\n if (tie || !best) {\r\n if (urlId && counts.has(urlId)) best = urlId;\r\n else return deduped;\r\n }\r\n }\r\n\r\n const belongs = (c: RawFacebookComment): boolean => !c.postRefId || c.postRefId === best;\r\n return deduped.filter(belongs).map((c) => ({ ...c, replies: c.replies?.filter(belongs) }));\r\n}\r\n","import { FacebookClientError } from \"../errors\";\r\nimport type { FacebookComment } from \"../types/Comment\";\r\n\r\ninterface CursorPayload {\r\n v: 1;\r\n /** Top-level comments already returned in earlier pages. */\r\n skipped: number;\r\n lastId: string | null;\r\n}\r\n\r\nconst encodeBase64Url = (input: string): string =>\r\n Buffer.from(input, \"utf8\").toString(\"base64url\");\r\n\r\nexport function encodeCursor(payload: CursorPayload): string {\r\n return encodeBase64Url(JSON.stringify(payload));\r\n}\r\n\r\nexport function decodeCursor(cursor: string): CursorPayload {\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(Buffer.from(cursor, \"base64url\").toString(\"utf8\"));\r\n } catch {\r\n throw new FacebookClientError(\"Invalid pagination cursor.\");\r\n }\r\n const payload = parsed as Partial<CursorPayload>;\r\n if (payload.v !== 1 || typeof payload.skipped !== \"number\" || payload.skipped < 0) {\r\n throw new FacebookClientError(\"Invalid pagination cursor.\");\r\n }\r\n return { v: 1, skipped: payload.skipped, lastId: payload.lastId ?? null };\r\n}\r\n\r\nexport const initialCursor = (): CursorPayload => ({ v: 1, skipped: 0, lastId: null });\r\n\r\nexport interface PaginateResult {\r\n comments: FacebookComment[];\r\n hasMore: boolean;\r\n nextCursor: string | null;\r\n}\r\n\r\n/**\r\n * Slice a normalized comment list into one page. `sourceHasMore` reflects\r\n * whether Facebook still had more comments to load at extraction time.\r\n */\r\nexport function paginate(\r\n all: FacebookComment[],\r\n cursor: CursorPayload,\r\n max: number,\r\n sourceHasMore: boolean\r\n): PaginateResult {\r\n const page = all.slice(cursor.skipped, cursor.skipped + max);\r\n const remaining = all.length - (cursor.skipped + page.length);\r\n const hasMore = remaining > 0 || (sourceHasMore && all.length >= cursor.skipped + max);\r\n return {\r\n comments: page,\r\n hasMore,\r\n nextCursor: hasMore\r\n ? encodeCursor({\r\n v: 1,\r\n skipped: cursor.skipped + page.length,\r\n lastId: page.length > 0 ? (page[page.length - 1]?.id ?? null) : cursor.lastId\r\n })\r\n : null\r\n };\r\n}\r\n","import type { FacebookComment, RawFacebookComment } from \"../types/Comment\";\r\n\r\nconst RELATIVE_UNITS: Array<{ pattern: RegExp; unit: number }> = [\r\n { pattern: /^(\\d+)\\s*s(econds?)?$/i, unit: 1000 },\r\n { pattern: /^(\\d+)\\s*m(in(utes?)?)?$/i, unit: 60_000 },\r\n { pattern: /^(\\d+)\\s*h(ours?)?$/i, unit: 3_600_000 },\r\n { pattern: /^(\\d+)\\s*d(ays?)?$/i, unit: 86_400_000 },\r\n { pattern: /^(\\d+)\\s*w(eeks?)?$/i, unit: 604_800_000 },\r\n { pattern: /^(\\d+)\\s*(months?)$/i, unit: 2_592_000_000 },\r\n { pattern: /^(\\d+)\\s*y(ea?rs?)?$/i, unit: 31_536_000_000 }\r\n];\r\n\r\n/**\r\n * Best-effort timestamp parsing. Understands ISO strings, unix seconds,\r\n * Facebook relative times (\"2h\", \"5m\", \"just now\", \"Yesterday at 3:04 PM\")\r\n * and month-name formats (\"January 5 at 3:04 PM\", \"5 January 2026 at 15:04\").\r\n * Returns null when nothing parses — never throws.\r\n */\r\nexport function parseTimestamp(input: string | null | undefined, now = new Date()): string | null {\r\n if (!input) return null;\r\n // Normalize word forms first: \"5 hours ago\" -> \"5 hours\", \"a day ago\" -> \"1 day\",\r\n // \"Wednesday, August 26, ...\" -> \"August 26, ...\".\r\n const raw = input\r\n .trim()\r\n .replace(/\\s+ago$/i, \"\")\r\n .replace(/^(an?)\\s+/i, \"1 \")\r\n .replace(/^[a-z]+day,\\s*/i, \"\")\r\n .trim();\r\n if (!raw) return null;\r\n\r\n // ISO / RFC strings\r\n if (/\\d{4}-\\d{2}-\\d{2}/.test(raw)) {\r\n const iso = new Date(raw);\r\n if (!Number.isNaN(iso.getTime())) return iso.toISOString();\r\n }\r\n\r\n // Unix seconds\r\n if (/^\\d{10}$/.test(raw)) return new Date(Number(raw) * 1000).toISOString();\r\n\r\n if (/^just now$/i.test(raw)) return now.toISOString();\r\n\r\n if (/^yesterday/i.test(raw)) {\r\n const timeMatch = raw.match(/(\\d{1,2}):(\\d{2})\\s*(am|pm)?/i);\r\n const date = new Date(now);\r\n date.setDate(date.getDate() - 1);\r\n if (timeMatch) {\r\n let hours = Number(timeMatch[1]);\r\n const minutes = Number(timeMatch[2]);\r\n const meridiem = timeMatch[3]?.toLowerCase();\r\n if (meridiem === \"pm\" && hours < 12) hours += 12;\r\n if (meridiem === \"am\" && hours === 12) hours = 0;\r\n date.setHours(hours, minutes, 0, 0);\r\n }\r\n return date.toISOString();\r\n }\r\n\r\n // Relative units: \"2h\", \"5m\", \"3d\", \"12w\", \"2y\", \"7 months\"\r\n for (const { pattern, unit } of RELATIVE_UNITS) {\r\n const match = raw.match(pattern);\r\n if (match?.[1]) {\r\n return new Date(now.getTime() - Number(match[1]) * unit).toISOString();\r\n }\r\n }\r\n\r\n // \"January 5 at 3:04 PM\", \"August 26, 2026 at 2:38 PM\" (comma before year;\r\n // year missing -> assume current, roll back if future)\r\n const monthAt = raw.match(\r\n /^([A-Za-z]+)\\s+(\\d{1,2})(?:,?\\s+(\\d{4}))?(?:\\s+(?:at\\s+)?(\\d{1,2}):(\\d{2})\\s*(am|pm)?)?/i\r\n );\r\n if (monthAt) {\r\n const month = new Date(`${monthAt[1]} 1, 2000`).getMonth();\r\n if (!Number.isNaN(month)) {\r\n const year = monthAt[3] ? Number(monthAt[3]) : now.getFullYear();\r\n const date = new Date(year, month, Number(monthAt[2]), 0, 0, 0, 0);\r\n if (monthAt[4]) {\r\n let hours = Number(monthAt[4]);\r\n const meridiem = monthAt[6]?.toLowerCase();\r\n if (meridiem === \"pm\" && hours < 12) hours += 12;\r\n if (meridiem === \"am\" && hours === 12) hours = 0;\r\n date.setHours(hours, Number(monthAt[5] ?? 0), 0, 0);\r\n }\r\n if (!monthAt[3] && date.getTime() > now.getTime()) date.setFullYear(year - 1);\r\n return date.toISOString();\r\n }\r\n }\r\n\r\n // \"5 January 2026 at 15:04\", \"5 January, 2026 at 3:04 PM\"\r\n const dayMonth = raw.match(\r\n /^(\\d{1,2})\\s+([A-Za-z]+),?\\s+(\\d{4})(?:\\s+(?:at\\s+)?(\\d{1,2}):(\\d{2})\\s*(am|pm)?)?/i\r\n );\r\n if (dayMonth) {\r\n const month = new Date(`${dayMonth[2]} 1, 2000`).getMonth();\r\n if (!Number.isNaN(month)) {\r\n let hours = Number(dayMonth[4] ?? 0);\r\n const meridiem = dayMonth[6]?.toLowerCase();\r\n if (meridiem === \"pm\" && hours < 12) hours += 12;\r\n if (meridiem === \"am\" && hours === 12) hours = 0;\r\n const date = new Date(\r\n Number(dayMonth[3]),\r\n month,\r\n Number(dayMonth[1]),\r\n hours,\r\n Number(dayMonth[5] ?? 0)\r\n );\r\n if (!Number.isNaN(date.getTime())) return date.toISOString();\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/** Raw extractor output -> normalized FacebookComment. Null-safe on every field. */\r\nexport function normalizeComment(\r\n raw: RawFacebookComment,\r\n parentId: string | null = null,\r\n now: Date = new Date()\r\n): FacebookComment {\r\n const replies = (raw.replies ?? []).map((reply) => normalizeComment(reply, raw.id ?? null, now));\r\n return {\r\n id: raw.id ?? null,\r\n author: {\r\n id: raw.authorId ?? null,\r\n name: raw.authorName ?? null,\r\n profileUrl: raw.authorProfileUrl ?? null,\r\n avatarUrl: raw.authorAvatarUrl ?? null\r\n },\r\n message: raw.message ?? \"\",\r\n createdAt: parseTimestamp(raw.createdAtRaw, now),\r\n ...(raw.attachments?.length ? { attachments: raw.attachments } : {}),\r\n reactions: { total: raw.reactionsTotal ?? null },\r\n parentId,\r\n ...(replies.length > 0 ? { replies } : {})\r\n };\r\n}\r\n","import { FacebookClientError } from \"../errors\";\r\n\r\nexport interface ParsedPostUrl {\r\n /** Normalized absolute URL. */\r\n url: string;\r\n /** Best-effort post identifier; null when it cannot be determined from the URL. */\r\n postId: string | null;\r\n /** Group identifier when the post lives in a group. */\r\n groupId: string | null;\r\n}\r\n\r\nconst FB_HOST = /^(?:[a-z0-9-]+\\.)*(?:facebook\\.com|fb\\.com|fb\\.watch)$/i;\r\n\r\n/**\r\n * Validate and parse a Facebook post URL. Throws FacebookClientError for\r\n * non-Facebook or non-http(s) URLs. Id extraction is best-effort — a null postId\r\n * never blocks fetching; extraction falls back to page data.\r\n */\r\nexport function parseFacebookPostUrl(input: string): ParsedPostUrl {\r\n let parsed: URL;\r\n try {\r\n parsed = new URL(input.trim());\r\n } catch {\r\n throw new FacebookClientError(`Invalid URL: \"${input.trim()}\"`);\r\n }\r\n\r\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\r\n throw new FacebookClientError(`URL must use http or https: \"${parsed.protocol}\"`);\r\n }\r\n if (!FB_HOST.test(parsed.hostname)) {\r\n throw new FacebookClientError(`Not a Facebook URL: \"${parsed.hostname}\"`);\r\n }\r\n\r\n const path = parsed.pathname;\r\n const segments = path.split(\"/\").filter(Boolean);\r\n\r\n let postId: string | null = null;\r\n let groupId: string | null = null;\r\n\r\n // /groups/:groupId/posts/:postId | /groups/:groupId/permalink/:postId\r\n if (segments[0] === \"groups\" && segments.length >= 4) {\r\n groupId = segments[1] ?? null;\r\n const kindIndex = segments.findIndex((s) => s === \"posts\" || s === \"permalink\");\r\n if (kindIndex !== -1 && segments.length > kindIndex + 1) {\r\n postId = segments[kindIndex + 1] ?? null;\r\n }\r\n }\r\n // /:user/posts/:postId | /:user/posts/pfbid... | /reel/:id | /videos/:id |\r\n // /share/p|v|r/:token | /photo/:id | /:user/pfbid...\r\n else if (segments.length >= 2) {\r\n const kind = segments[segments.length - 2];\r\n if (\r\n kind &&\r\n [\"posts\", \"videos\", \"reel\", \"reels\", \"p\", \"photo\", \"comments\", \"share\", \"v\", \"r\"].includes(\r\n kind\r\n )\r\n ) {\r\n postId = segments[segments.length - 1] ?? null;\r\n } else if (segments[0] === \"permalink.php\") {\r\n // handled below via query params\r\n postId = null;\r\n } else {\r\n const last = segments[segments.length - 1];\r\n if (last && /^(?:pfbid|\\d{6,})/i.test(last)) postId = last;\r\n }\r\n }\r\n\r\n // /permalink.php?story_fbid=...&id=...\r\n if (parsed.searchParams.has(\"story_fbid\")) {\r\n postId = parsed.searchParams.get(\"story_fbid\");\r\n }\r\n // /photo.php?fbid=... | /photo/?fbid=...\r\n const fbid = parsed.searchParams.get(\"fbid\");\r\n if (!postId && fbid) postId = fbid;\r\n // /watch/?v=... | /video.php?v=...\r\n const videoId = parsed.searchParams.get(\"v\");\r\n if (!postId && videoId) postId = videoId;\r\n\r\n return { url: parsed.toString(), postId, groupId };\r\n}\r\n","import type { Page } from \"playwright\";\r\nimport type { FacebookPost } from \"../types/Post\";\r\nimport { parseFacebookPostUrl } from \"../utils/url\";\r\n\r\n/** Best-effort post metadata from the URL (page URL after navigation preferred). */\r\nexport function parsePost(page: Page, requestedUrl: string): FacebookPost {\r\n const source = page.url().startsWith(\"http\") ? page.url() : requestedUrl;\r\n try {\r\n const parsed = parseFacebookPostUrl(source);\r\n return { id: parsed.postId, url: parsed.url };\r\n } catch {\r\n // Post page redirected somewhere we don't parse — keep the caller's URL.\r\n return { id: null, url: requestedUrl };\r\n }\r\n}\r\n","import type { LogLevel, Logger } from \"../types/Client\";\r\n\r\nconst SENSITIVE_KEY = /cookie|token|secret|password|passwd|storage.?state|authorization|\\bxs\\b|\\bc_user\\b|\\bsb\\b|\\bfr\\b/i;\r\nconst MAX_DEPTH = 6;\r\n\r\n/**\r\n * Recursively replace values of sensitive keys so session data never reaches logs\r\n * or error messages. Known Facebook auth cookie names are always redacted.\r\n */\r\nexport function redact(value: unknown, depth = 0): unknown {\r\n if (depth > MAX_DEPTH) return \"[TRUNCATED]\";\r\n if (Array.isArray(value)) return value.map((item) => redact(item, depth + 1));\r\n if (value && typeof value === \"object\") {\r\n const out: Record<string, unknown> = {};\r\n for (const [key, val] of Object.entries(value as Record<string, unknown>)) {\r\n out[key] = SENSITIVE_KEY.test(key) ? \"[REDACTED]\" : redact(val, depth + 1);\r\n }\r\n return out;\r\n }\r\n if (typeof value === \"string\" && value.length > 2000) {\r\n return `${value.slice(0, 2000)}...[truncated]`;\r\n }\r\n return value;\r\n}\r\n\r\nconst LEVEL_ORDER: Record<LogLevel, number> = {\r\n silent: 0,\r\n error: 1,\r\n warn: 2,\r\n info: 3,\r\n debug: 4\r\n};\r\n\r\n/** Console logger that redacts every interpolated argument. Quiet (warn+) by default. */\r\nexport function createConsoleLogger(level: LogLevel = \"warn\"): Logger {\r\n const threshold = LEVEL_ORDER[level];\r\n const emit = (allowed: number, sink: (args: unknown[]) => void, args: unknown[]) => {\r\n if (threshold >= allowed) sink(args.map((arg) => redact(arg)));\r\n };\r\n return {\r\n debug: (msg, ...args) => emit(LEVEL_ORDER.debug, console.debug, [msg, ...args]),\r\n info: (msg, ...args) => emit(LEVEL_ORDER.info, console.info, [msg, ...args]),\r\n warn: (msg, ...args) => emit(LEVEL_ORDER.warn, console.warn, [msg, ...args]),\r\n error: (msg, ...args) => emit(LEVEL_ORDER.error, console.error, [msg, ...args])\r\n };\r\n}\r\n\r\nexport const silentLogger: Logger = {\r\n debug: () => {},\r\n info: () => {},\r\n warn: () => {},\r\n error: () => {}\r\n};\r\n"],"mappings":"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","names":["existsSync","chromium","root","authorName","chromium","existsSync"]}