// Facebook Post Tracker Extension // Uses API_BASE_URL from config.js const EXTENSION_VERSION = '1.1.0'; const PROCESSED_ATTR = 'data-fb-tracker-processed'; const API_URL = `${API_BASE_URL}/api`; console.log(`[FB Tracker v${EXTENSION_VERSION}] Extension loaded, API URL:`, API_URL); // Profile state helpers async function fetchBackendProfileNumber() { try { const response = await fetch(`${API_URL}/profile-state`); if (!response.ok) { return null; } const data = await response.json(); if (data && data.profile_number) { return data.profile_number; } } catch (error) { console.warn('[FB Tracker] Failed to fetch profile state from backend:', error); } return null; } function storeProfileNumberLocally(profileNumber) { chrome.storage.sync.set({ profileNumber }); } async function getProfileNumber() { const backendProfile = await fetchBackendProfileNumber(); if (backendProfile) { storeProfileNumberLocally(backendProfile); console.log('[FB Tracker] Profile number (backend):', backendProfile); return backendProfile; } return new Promise((resolve) => { chrome.storage.sync.get(['profileNumber'], (result) => { const profile = result.profileNumber || 1; console.log('[FB Tracker] Profile number (local):', profile); resolve(profile); }); }); } // Extract post URL from post element function cleanPostUrl(rawUrl) { if (!rawUrl) { return ''; } const cftIndex = rawUrl.indexOf('__cft__'); let trimmed = cftIndex !== -1 ? rawUrl.slice(0, cftIndex) : rawUrl; trimmed = trimmed.replace(/[?&]$/, ''); return trimmed; } function toAbsoluteFacebookUrl(rawUrl) { if (!rawUrl) { return null; } const cleaned = cleanPostUrl(rawUrl); let url; try { url = new URL(cleaned); } catch (error) { try { url = new URL(cleaned, window.location.origin); } catch (innerError) { return null; } } const host = url.hostname.toLowerCase(); if (!host.endsWith('facebook.com')) { return null; } return url; } function isValidFacebookPostUrl(url) { if (!url) { return false; } const path = url.pathname.toLowerCase(); const searchParams = url.searchParams; const postPathPatterns = [ '/posts/', '/permalink/', '/photos/', '/videos/', '/reel/' ]; if (postPathPatterns.some(pattern => path.includes(pattern))) { return true; } if (path.startsWith('/photo') && searchParams.has('fbid')) { return true; } if (path.startsWith('/photo.php') && searchParams.has('fbid')) { return true; } return false; } function formatFacebookPostUrl(url) { if (!url) { return ''; } let search = url.search; if (search.endsWith('?') || search.endsWith('&')) { search = search.slice(0, -1); } return `${url.origin}${url.pathname}${search}`; } function extractPostUrlCandidate(rawUrl) { const absoluteUrl = toAbsoluteFacebookUrl(rawUrl); if (!absoluteUrl || !isValidFacebookPostUrl(absoluteUrl)) { return ''; } return formatFacebookPostUrl(absoluteUrl); } function getPostUrl(postElement) { console.log('[FB Tracker] Extracting URL from post element'); const attributionLinks = postElement.querySelectorAll('a[attributionsrc*="/privacy_sandbox/comet/"][href]'); for (const link of attributionLinks) { const candidate = extractPostUrlCandidate(link.href); if (candidate) { console.log('[FB Tracker] Found post URL via attribution link:', candidate); return candidate; } } const links = postElement.querySelectorAll('a[href]'); for (const link of links) { const candidate = extractPostUrlCandidate(link.href); if (candidate) { console.log('[FB Tracker] Found post URL via fallback patterns:', candidate); return candidate; } } const fallbackCandidate = extractPostUrlCandidate(window.location.href); if (fallbackCandidate) { console.log('[FB Tracker] Using fallback URL:', fallbackCandidate); return fallbackCandidate; } console.log('[FB Tracker] No valid post URL found'); return ''; } // Check if post is already tracked async function checkPostStatus(postUrl) { try { console.log('[FB Tracker] Checking post status for:', postUrl); const response = await fetch(`${API_URL}/posts/by-url?url=${encodeURIComponent(postUrl)}`); if (response.ok) { const data = await response.json(); console.log('[FB Tracker] Post status:', data); return data; } console.log('[FB Tracker] Post not tracked yet'); return null; } catch (error) { console.error('[FB Tracker] Error checking post status:', error); return null; } } // Add post to tracking async function markPostChecked(postId, profileNumber) { try { console.log('[FB Tracker] Marking post as checked:', postId, 'Profile:', profileNumber); const response = await fetch(`${API_URL}/posts/${postId}/check`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ profile_number: profileNumber }) }); if (response.ok) { const data = await response.json(); console.log('[FB Tracker] Post marked as checked:', data); return data; } if (response.status === 409) { console.log('[FB Tracker] Post already checked by this profile'); return null; } console.error('[FB Tracker] Failed to mark post as checked:', response.status); return null; } catch (error) { console.error('[FB Tracker] Error marking post as checked:', error); return null; } } async function addPostToTracking(postUrl, targetCount, profileNumber, options = {}) { try { console.log('[FB Tracker] Adding post:', postUrl, 'Target:', targetCount, 'Profile:', profileNumber); const response = await fetch(`${API_URL}/posts`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url: postUrl, target_count: targetCount, profile_number: profileNumber }) }); if (response.ok) { const data = await response.json(); console.log('[FB Tracker] Post added successfully:', data); if (data && data.id) { const checkedData = await markPostChecked(data.id, profileNumber); await captureAndUploadScreenshot(data.id, options.postElement || null); if (checkedData) { return checkedData; } } return data; } else { console.error('[FB Tracker] Failed to add post:', response.status); return null; } } catch (error) { console.error('[FB Tracker] Error adding post:', error); return null; } } function normalizeButtonLabel(button) { const aria = button.getAttribute('aria-label'); if (aria) { return aria.trim().toLowerCase(); } const title = button.getAttribute('title'); if (title) { return title.trim().toLowerCase(); } return (button.textContent || '').trim().toLowerCase(); } const LIKE_LABEL_KEYWORDS = ['gefällt mir', 'like', 'mag ich']; const COMMENT_LABEL_KEYWORDS = ['kommentieren', 'comment', 'kommentar', 'antwort hinterlassen']; const SHARE_LABEL_KEYWORDS = ['teilen', 'share', 'senden', 'posten', 'profil posten']; const REPLY_LABEL_KEYWORDS = ['antworten', 'reply']; const LIKE_ROLE_KEYWORDS = ['gefällt mir', 'like']; const COMMENT_ROLE_KEYWORDS = ['comment']; const SHARE_ROLE_KEYWORDS = ['share']; const REPLY_ROLE_KEYWORDS = ['reply']; function matchesKeyword(label, keywords) { return keywords.some((keyword) => label.includes(keyword)); } function collectButtonMeta(button) { const textParts = []; const roleParts = []; const label = normalizeButtonLabel(button); if (label) { textParts.push(label); } const collectRole = (value) => { if (!value) { return; } const lower = value.toLowerCase(); roleParts.push(lower); const tokens = lower.split(/[_\-\s]+/); tokens.forEach((token) => { if (token) { roleParts.push(token); } }); }; collectRole(button.getAttribute('data-ad-rendering-role')); const descendantRoles = button.querySelectorAll('[data-ad-rendering-role]'); descendantRoles.forEach((el) => { collectRole(el.getAttribute('data-ad-rendering-role')); if (el.textContent) { textParts.push(normalizeButtonLabel(el)); } }); return { text: textParts.join(' '), roles: roleParts.join(' ') }; } function buttonClassification(button) { const meta = collectButtonMeta(button); const text = meta.text; const roles = meta.roles; const combined = `${text} ${roles}`; return { isLike: matchesKeyword(combined, LIKE_LABEL_KEYWORDS) || matchesKeyword(roles, LIKE_ROLE_KEYWORDS), isComment: matchesKeyword(combined, COMMENT_LABEL_KEYWORDS) || matchesKeyword(roles, COMMENT_ROLE_KEYWORDS), isShare: matchesKeyword(combined, SHARE_LABEL_KEYWORDS) || matchesKeyword(roles, SHARE_ROLE_KEYWORDS), isReply: matchesKeyword(combined, REPLY_LABEL_KEYWORDS) || matchesKeyword(roles, REPLY_ROLE_KEYWORDS) }; } function findShareButtonForArticle(article) { const direct = article.querySelector('[data-ad-rendering-role="share_button"]'); if (direct) { return direct; } const shareButtons = document.querySelectorAll('[data-ad-rendering-role="share_button"]'); for (const button of shareButtons) { const owningArticle = button.closest('[role="article"]'); if (owningArticle === article) { return button; } } return null; } function hasInteractionButtons(container) { if (!container) { return false; } const buttons = container.querySelectorAll('[role="button"], button'); if (!buttons.length) { return false; } let hasLike = false; let hasComment = false; let hasShare = false; let hasReply = false; for (const button of buttons) { const info = buttonClassification(button); if (info.isLike) { hasLike = true; } if (info.isComment) { hasComment = true; } if (info.isShare) { hasShare = true; } if (info.isReply) { hasReply = true; } } if (hasLike || hasComment || hasShare) { console.log('[FB Tracker] Container analysis', { tag: container.tagName, classes: container.className, hasLike, hasComment, hasShare, hasReply, buttonCount: buttons.length }); } if (!hasLike || !hasComment) { return false; } if (hasShare) { return true; } return !hasReply; } function findButtonBar(postElement) { const shareAnchor = findShareButtonForArticle(postElement); if (shareAnchor) { let container = shareAnchor.closest('[role="button"]') || shareAnchor.closest('button'); for (let i = 0; i < 4 && container; i++) { if (hasInteractionButtons(container)) { console.log('[FB Tracker] Found button bar via share anchor'); return container; } container = container.parentElement; } } // Gather all accessible buttons inside the article const buttons = Array.from(postElement.querySelectorAll('[role="button"], button')); const interactionButtons = buttons.filter((button) => { const info = buttonClassification(button); return info.isLike || info.isComment || info.isShare; }); const anchorElements = [ postElement.querySelector('[data-ad-rendering-role="share_button"]'), postElement.querySelector('[data-ad-rendering-role="comment_button"]'), postElement.querySelector('[data-ad-rendering-role*="gefällt"]') ].filter(Boolean); const candidates = new Set(interactionButtons); anchorElements.forEach((el) => { const buttonContainer = el.closest('[role="button"]') || el.closest('button'); if (buttonContainer) { candidates.add(buttonContainer); } }); const seen = new Set(); for (const button of candidates) { const info = buttonClassification(button); console.log('[FB Tracker] Candidate button', { label: normalizeButtonLabel(button), hasLike: info.isLike, hasComment: info.isComment, hasShare: info.isShare, hasReply: info.isReply, classes: button.className }); let current = button; while (current && current !== postElement && current !== document.body) { if (seen.has(current)) { current = current.parentElement; continue; } seen.add(current); const hasBar = hasInteractionButtons(current); if (hasBar) { console.log('[FB Tracker] Found button bar'); return current; } current = current.parentElement; } } let sibling = postElement.nextElementSibling; for (let i = 0; i < 6 && sibling; i++) { if (!seen.has(sibling) && hasInteractionButtons(sibling)) { console.log('[FB Tracker] Found button bar via next sibling'); return sibling; } sibling = sibling.nextElementSibling; } sibling = postElement.previousElementSibling; for (let i = 0; i < 3 && sibling; i++) { if (!seen.has(sibling) && hasInteractionButtons(sibling)) { console.log('[FB Tracker] Found button bar via previous sibling'); return sibling; } sibling = sibling.previousElementSibling; } let parent = postElement.parentElement; for (let depth = 0; depth < 4 && parent; depth++) { if (!seen.has(parent) && hasInteractionButtons(parent)) { console.log('[FB Tracker] Found button bar via parent'); return parent; } let next = parent.nextElementSibling; for (let i = 0; i < 4 && next; i++) { if (!seen.has(next) && hasInteractionButtons(next)) { console.log('[FB Tracker] Found button bar via parent sibling'); return next; } next = next.nextElementSibling; } let prev = parent.previousElementSibling; for (let i = 0; i < 3 && prev; i++) { if (!seen.has(prev) && hasInteractionButtons(prev)) { console.log('[FB Tracker] Found button bar via parent previous sibling'); return prev; } prev = prev.previousElementSibling; } parent = parent.parentElement; } console.log('[FB Tracker] Button bar not found'); return null; } function findPostContainers() { const containers = []; const seen = new Set(); const likeButtons = document.querySelectorAll('[data-ad-rendering-role="gefällt mir_button"], [data-ad-rendering-role*="gefällt" i]'); likeButtons.forEach((likeButton) => { const container = likeButton.closest('div[aria-posinset]'); if (!container) { return; } if (container.getAttribute(PROCESSED_ATTR) === '1') { return; } if (container.querySelector('.fb-tracker-ui')) { return; } const commentButton = container.querySelector('[data-ad-rendering-role="comment_button"], [data-ad-rendering-role*="comment" i]'); const shareButton = container.querySelector('[data-ad-rendering-role="share_button"], [data-ad-rendering-role*="share" i], [data-ad-rendering-role*="teilen" i]'); if (!commentButton || !shareButton) { return; } if (seen.has(container)) { return; } seen.add(container); containers.push({ container, likeButton, commentButton, shareButton }); }); return containers; } function captureScreenshot(screenshotRect) { return new Promise((resolve) => { chrome.runtime.sendMessage({ type: 'captureScreenshot', screenshotRect }, (response) => { if (chrome.runtime.lastError) { console.warn('[FB Tracker] Screenshot capture failed:', chrome.runtime.lastError.message); resolve(null); return; } if (!response || response.error) { if (response && response.error) { console.warn('[FB Tracker] Screenshot capture reported error:', response.error); } resolve(null); return; } if (!response.imageData) { console.warn('[FB Tracker] Screenshot capture returned no data'); resolve(null); return; } (async () => { try { let result = response.imageData; if (screenshotRect) { const cropped = await cropScreenshot(result, screenshotRect); if (cropped) { result = cropped; } } resolve(result); } catch (error) { console.warn('[FB Tracker] Screenshot processing failed:', error); resolve(response.imageData); } })(); }); }); } async function uploadScreenshot(postId, imageData) { try { const response = await fetch(`${API_URL}/posts/${postId}/screenshot`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageData }) }); if (!response.ok) { console.warn('[FB Tracker] Screenshot upload failed:', response.status); } } catch (error) { console.error('[FB Tracker] Screenshot upload error:', error); } } async function captureAndUploadScreenshot(postId, postElement) { const imageData = await captureElementScreenshot(postElement); if (!imageData) { return; } const optimized = await maybeDownscaleScreenshot(imageData); await uploadScreenshot(postId, optimized); } async function captureElementScreenshot(element) { if (!element) { return await captureScreenshot(); } const horizontalMargin = 32; const verticalMargin = 96; const maxSegments = 12; const delayBetweenScrolls = 200; const originalScrollX = window.scrollX; const originalScrollY = window.scrollY; const devicePixelRatio = window.devicePixelRatio || 1; const stickyOffset = getStickyHeaderHeight(); const segments = []; const elementRect = element.getBoundingClientRect(); const elementTop = elementRect.top + window.scrollY; const elementBottom = elementRect.bottom + window.scrollY; const documentHeight = document.documentElement.scrollHeight; const startY = Math.max(0, elementTop - verticalMargin - stickyOffset); const endY = Math.min(documentHeight, elementBottom + verticalMargin); const baseDocTop = Math.max(0, elementTop - verticalMargin); try { let iteration = 0; let targetScroll = startY; while (iteration < maxSegments) { iteration += 1; window.scrollTo({ top: targetScroll, left: window.scrollX, behavior: 'auto' }); await delay(delayBetweenScrolls); const rect = element.getBoundingClientRect(); const viewportHeight = window.innerHeight; const captureTop = Math.max(0, rect.top - verticalMargin - stickyOffset); const captureBottom = Math.min(viewportHeight, rect.bottom + verticalMargin); const captureHeight = captureBottom - captureTop; if (captureHeight <= 0) { break; } const captureRect = { left: rect.left - horizontalMargin, top: captureTop, width: rect.width + horizontalMargin * 2, height: captureHeight, devicePixelRatio }; const segmentData = await captureScreenshot(captureRect); if (!segmentData) { break; } const docTop = Math.max(0, window.scrollY + captureTop); const docBottom = docTop + captureHeight; segments.push({ data: segmentData, docTop, docBottom }); const reachedBottom = docBottom >= endY - 4; if (reachedBottom) { break; } const nextScroll = docBottom - Math.max(0, (viewportHeight - stickyOffset) * 0.5); const maxScroll = Math.max(0, endY - viewportHeight); targetScroll = Math.min(nextScroll, maxScroll); if (targetScroll <= window.scrollY + 1) { targetScroll = window.scrollY + Math.max(160, viewportHeight * 0.6); } if (targetScroll <= window.scrollY + 1 || targetScroll >= endY) { break; } } } finally { window.scrollTo({ top: originalScrollY, left: originalScrollX, behavior: 'auto' }); } if (!segments.length) { return await captureScreenshot(); } const stitched = await stitchScreenshotSegments(segments, devicePixelRatio, baseDocTop); return stitched; } async function stitchScreenshotSegments(segments, devicePixelRatio, baseDocTop) { const images = []; let maxDocBottom = baseDocTop; for (const segment of segments) { const img = await loadImage(segment.data); if (!img) { continue; } images.push({ img, docTop: segment.docTop, docBottom: segment.docBottom }); if (segment.docBottom > maxDocBottom) { maxDocBottom = segment.docBottom; } } if (!images.length) { return null; } const width = images.reduce((max, item) => Math.max(max, item.img.width), 0); const totalHeightPx = Math.max(1, Math.round((maxDocBottom - baseDocTop) * devicePixelRatio)); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = totalHeightPx; const ctx = canvas.getContext('2d'); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, width, totalHeightPx); for (const { img, docTop } of images) { const offsetY = Math.round(Math.max(0, docTop - baseDocTop) * devicePixelRatio); ctx.drawImage(img, 0, offsetY); } return canvas.toDataURL('image/jpeg', 0.85); } async function cropScreenshot(imageData, rect) { if (!rect) { return imageData; } try { const image = await loadImage(imageData); if (!image) { return imageData; } const ratio = rect.devicePixelRatio || window.devicePixelRatio || 1; const rawLeft = (rect.left || 0) * ratio; const rawTop = (rect.top || 0) * ratio; const rawWidth = (rect.width || image.width) * ratio; const rawHeight = (rect.height || image.height) * ratio; const rawRight = rawLeft + rawWidth; const rawBottom = rawTop + rawHeight; const left = Math.max(0, Math.floor(rawLeft)); const top = Math.max(0, Math.floor(rawTop)); const right = Math.min(image.width, Math.ceil(rawRight)); const bottom = Math.min(image.height, Math.ceil(rawBottom)); const width = Math.max(1, right - left); const height = Math.max(1, bottom - top); const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(image, left, top, width, height, 0, 0, width, height); return canvas.toDataURL('image/jpeg', 0.85); } catch (error) { console.warn('[FB Tracker] Failed to crop screenshot:', error); return imageData; } } async function maybeDownscaleScreenshot(imageData) { try { const maxWidth = 1600; const current = await loadImage(imageData); if (!current) { return imageData; } if (current.width <= maxWidth) { return imageData; } const scale = maxWidth / current.width; const canvas = document.createElement('canvas'); canvas.width = Math.round(current.width * scale); canvas.height = Math.round(current.height * scale); const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; ctx.drawImage(current, 0, 0, canvas.width, canvas.height); return canvas.toDataURL('image/jpeg', 0.8); } catch (error) { console.warn('[FB Tracker] Failed to downscale screenshot:', error); return imageData; } } function getStickyHeaderHeight() { try { const banner = document.querySelector('[role="banner"], header[role="banner"]'); if (!banner) { return 0; } const rect = banner.getBoundingClientRect(); if (!rect || !rect.height) { return 0; } return Math.min(rect.height, 160); } catch (error) { console.warn('[FB Tracker] Failed to determine sticky header height:', error); return 0; } } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function loadImage(dataUrl) { return new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = () => resolve(null); img.src = dataUrl; }); } // Create the tracking UI async function createTrackerUI(postElement, buttonBar) { // Check if UI already exists if (postElement.querySelector('.fb-tracker-ui')) { console.log('[FB Tracker] UI already exists for this post'); return; } const postUrl = getPostUrl(postElement); if (!postUrl) { console.log('[FB Tracker] No URL found for post'); return; } console.log('[FB Tracker] Creating tracker UI for:', postUrl); // Create UI container const container = document.createElement('div'); container.className = 'fb-tracker-ui'; container.style.cssText = ` padding: 12px 16px; background-color: #f0f2f5; border-top: 1px solid #e4e6eb; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 14px; `; // Check current status const profileNumber = await getProfileNumber(); const postData = await checkPostStatus(postUrl); if (postData) { const checkedCount = postData.checked_count ?? (Array.isArray(postData.checks) ? postData.checks.length : 0); const statusText = `${checkedCount}/${postData.target_count}`; const completed = checkedCount >= postData.target_count; const lastCheck = Array.isArray(postData.checks) && postData.checks.length ? new Date(postData.checks[postData.checks.length - 1].checked_at).toLocaleString('de-DE') : null; container.innerHTML = `
Tracker: ${statusText}${completed ? ' ✓ Ziel erreicht' : ''}
${lastCheck ? `
Letzte Bestätigung: ${lastCheck}
` : ''} `; console.log('[FB Tracker] Showing status:', statusText); } else { // Post not tracked - show add UI const selectId = `tracker-select-${Date.now()}`; container.innerHTML = ` `; // Add click handler for the button const addButton = container.querySelector('.fb-tracker-add-btn'); const selectElement = container.querySelector('select'); selectElement.value = '2'; addButton.addEventListener('click', async () => { const targetCount = parseInt(selectElement.value, 10); console.log('[FB Tracker] Add button clicked, target:', targetCount); addButton.disabled = true; addButton.textContent = 'Wird hinzugefügt...'; postElement.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'nearest' }); await delay(220); const result = await addPostToTracking(postUrl, targetCount, profileNumber, { postElement }); if (result) { const checks = Array.isArray(result.checks) ? result.checks : []; const checkedCount = typeof result.checked_count === 'number' ? result.checked_count : checks.length; const targetTotal = result.target_count || targetCount; const statusText = `${checkedCount}/${targetTotal}`; const completed = checkedCount >= targetTotal; const lastCheck = checks.length ? new Date(checks[checks.length - 1].checked_at).toLocaleString('de-DE') : null; let statusHtml = `
Tracker: ${statusText}${completed ? ' ✓ Ziel erreicht' : ' ✓ Erfolgreich hinzugefügt'}
`; if (lastCheck) { statusHtml += `
Letzte Bestätigung: ${lastCheck}
`; } container.innerHTML = statusHtml; } else { // Error addButton.disabled = false; addButton.textContent = 'Fehler - Erneut versuchen'; addButton.style.backgroundColor = '#e74c3c'; } }); console.log('[FB Tracker] UI created for new post'); } // Insert UI below the button bar if available, otherwise append at the end if (buttonBar && buttonBar.parentElement) { buttonBar.parentElement.insertBefore(container, buttonBar.nextSibling); console.log('[FB Tracker] UI inserted after button bar'); } else { postElement.appendChild(container); console.log('[FB Tracker] UI inserted into article (fallback)'); } } // Check if article is a main post (not a comment) function isMainPost(article, buttonBar) { if (buttonBar) { const shareIndicator = buttonBar.querySelector('[data-ad-rendering-role="share_button"]'); if (shareIndicator) { return true; } } // Comments are usually nested inside other articles or comment sections // Check if this article is inside another article (likely a comment) let parent = article.parentElement; while (parent && parent !== document.body) { if (parent.getAttribute('role') === 'article') { // This article is inside another article - it's a comment return false; } parent = parent.parentElement; } // Additional check: Main posts usually have a link with /posts/ or /permalink/ const postLinks = article.querySelectorAll('a[href*="/posts/"], a[href*="/permalink/"], a[href*="/photo"], a[href*="/videos/"], a[href*="/reel/"]'); if (postLinks.length === 0) { if (article.querySelector('[data-ad-rendering-role="share_button"]')) { return true; } const nextSibling = article.nextElementSibling; if (nextSibling && nextSibling.querySelector('[data-ad-rendering-role="share_button"]')) { const nextButtons = nextSibling.querySelectorAll('[role="button"]'); if (nextButtons.length > 30) { return true; } } // No post-type links found - likely a comment return false; } return true; } // Find all Facebook posts on the page function findPosts() { console.log('[FB Tracker] Scanning for posts...'); const postContainers = findPostContainers(); console.log('[FB Tracker] Found', postContainers.length, 'candidate containers'); let processed = 0; for (const { container } of postContainers) { if (container.getAttribute(PROCESSED_ATTR) === '1') { continue; } const buttonBar = findButtonBar(container); if (!buttonBar) { console.log('[FB Tracker] Skipping container without full button bar'); continue; } container.setAttribute(PROCESSED_ATTR, '1'); processed++; console.log('[FB Tracker] Adding tracker to post #' + processed); createTrackerUI(container, buttonBar); } console.log('[FB Tracker] Total processed posts:', processed); } // Initialize console.log('[FB Tracker] Initializing...'); // Run multiple times to catch loading posts setTimeout(findPosts, 2000); setTimeout(findPosts, 4000); setTimeout(findPosts, 6000); // Debounced scan function let scanTimeout = null; function scheduleScan() { if (scanTimeout) { clearTimeout(scanTimeout); } scanTimeout = setTimeout(() => { console.log('[FB Tracker] Scheduled scan triggered'); findPosts(); }, 1000); } // Watch for new posts being added to the page const observer = new MutationObserver((mutations) => { scheduleScan(); }); // Start observing observer.observe(document.body, { childList: true, subtree: true }); // Trigger scan on scroll (for infinite scroll) let scrollTimeout = null; window.addEventListener('scroll', () => { if (scrollTimeout) { clearTimeout(scrollTimeout); } scrollTimeout = setTimeout(() => { console.log('[FB Tracker] Scroll detected, scanning...'); findPosts(); }, 1000); }); console.log('[FB Tracker] Observer and scroll listener started');