// DIDI Content Extractor - Content Script
// Extracts content from social media posts
// Facebook Extractor
class FacebookExtractor {
constructor() {
this.platform = 'facebook';
}
findPostContainer(element) {
let current = element;
let depth = 0;
const maxDepth = 30;
console.log('[Facebook] Starting search for post container from:', element.tagName);
while (current && depth < maxDepth) {
if (current.matches) {
// Standard Feed posts
if (current.matches('[role="article"]')) {
console.log('[Facebook] Found article container at depth:', depth);
return current;
}
// Groups posts, Stories, alternative layouts
if (current.matches('div[data-pagelet*="FeedUnit"]') ||
current.matches('div[class*="userContentWrapper"]')) {
console.log('[Facebook] Found pagelet container at depth:', depth);
return current;
}
// Look for divs that contain both author and content
// This is more generic and works across different FB layouts
const hasAuthor = current.querySelector('h2 a[role="link"], h3 a[role="link"], h4 a[role="link"]');
const hasContent = current.querySelector('div[dir="auto"]');
if (hasAuthor && hasContent && depth > 3) {
console.log('[Facebook] Found generic post container at depth:', depth);
return current;
}
}
current = current.parentElement;
depth++;
}
console.log('[Facebook] No post container found after searching', maxDepth, 'levels');
return null;
}
extractAuthor(postElement) {
const authorSelectors = [
'h2 a[role="link"]',
'h3 a[role="link"]',
'a[aria-label*="profile"]',
'strong > a'
];
for (const selector of authorSelectors) {
const authorEl = postElement.querySelector(selector);
if (authorEl && authorEl.textContent.trim()) {
const authorName = authorEl.textContent.trim();
console.log('[FB] Found author:', authorName);
return authorName;
}
}
console.log('[FB] Author not found');
return 'Unknown';
}
extractText(postElement) {
const textParts = [];
const seenTexts = new Set();
const textSelectors = [
'div[dir="auto"][style*="text-align"]',
'[data-ad-comet-preview="message"]',
'div[data-ad-preview="message"]'
];
for (const selector of textSelectors) {
const elements = postElement.querySelectorAll(selector);
elements.forEach(el => {
const text = el.textContent.trim();
const isUIText = [
'Like', 'Comment', 'Share', 'Send',
'Vezi mai mult', 'See more', 'Show more',
'Just now', 'min', 'hr', 'hrs',
'·', '•'
].some(noise => text === noise || text.length < 3);
if (!isUIText && !seenTexts.has(text) && text.length > 10) {
const hasNoTextChildren = Array.from(el.children).every(child =>
!child.textContent || child.textContent.trim().length === 0
);
if (hasNoTextChildren || text.length > 50) {
textParts.push(text);
seenTexts.add(text);
}
}
});
}
const fullText = textParts.join('\n\n').trim();
console.log('[FB] Extracted text length:', fullText.length);
return fullText;
}
// Extract ALL images from post (not just primary)
extractAllImages(postElement) {
const images = postElement.querySelectorAll('img[src]');
const extractedImages = [];
console.log('[FB] Found total images:', images.length);
for (const img of images) {
const src = img.src || img.getAttribute('data-src');
const width = img.naturalWidth || img.width;
const height = img.naturalHeight || img.height;
// Filter out emoji, icons, avatars, profile pics
const isValidImage =
src &&
!src.includes('emoji') &&
!src.includes('icon') &&
!src.includes('avatar') &&
!src.includes('profile') &&
width >= 200 &&
height >= 200 &&
!img.closest('a[aria-label*="profile"]');
if (isValidImage) {
extractedImages.push({
url: src,
width: width,
height: height,
alt: img.alt || ''
});
}
}
console.log('[FB] Extracted valid images:', extractedImages.length);
return extractedImages;
}
// Keep for backward compatibility
extractPrimaryImage(postElement) {
const allImages = this.extractAllImages(postElement);
return allImages.length > 0 ? allImages[0] : null;
}
extractTimestamp(postElement) {
const timeSelectors = [
'a[aria-label*="ago"]',
'a[aria-label*="min"]',
'a[aria-label*="hour"]',
'abbr',
'span[title]'
];
for (const selector of timeSelectors) {
const timeEl = postElement.querySelector(selector);
if (timeEl) {
const timestamp = timeEl.getAttribute('title') ||
timeEl.getAttribute('aria-label') ||
timeEl.textContent;
console.log('[FB] Found timestamp:', timestamp);
return timestamp;
}
}
return new Date().toISOString();
}
extractPostUrl(postElement) {
const permalinkSelectors = [
'a[aria-label*="ago"]',
'a[href*="/posts/"]',
'a[href*="/photos/"]',
'a[href*="/videos/"]'
];
for (const selector of permalinkSelectors) {
const link = postElement.querySelector(selector);
if (link && link.href) {
console.log('[FB] Found post URL:', link.href);
return link.href;
}
}
return window.location.href;
}
// Extract all external links from post
extractLinks(postElement) {
const links = [];
const anchorElements = postElement.querySelectorAll('a[href]');
anchorElements.forEach(anchor => {
const href = anchor.href;
// Filter out internal Facebook navigation links
const isExternalLink =
href &&
!href.includes('/profile/') &&
!href.includes('/groups/') &&
!href.includes('/hashtag/') &&
!href.includes('facebook.com/photo') &&
!href.includes('facebook.com/watch') &&
(href.startsWith('http://') || href.startsWith('https://')) &&
!href.includes('facebook.com');
if (isExternalLink && !links.includes(href)) {
links.push(href);
}
});
console.log('[FB] Extracted external links:', links.length);
return links;
}
extract(element) {
console.log('[FB] Starting Facebook extraction...');
const postContainer = this.findPostContainer(element);
if (!postContainer) {
console.log('[FB] No post container found, returning null');
return null;
}
const author = this.extractAuthor(postContainer);
const text = this.extractText(postContainer);
const allImages = this.extractAllImages(postContainer);
const primaryImage = allImages.length > 0 ? allImages[0] : null;
const links = this.extractLinks(postContainer);
const timestamp = this.extractTimestamp(postContainer);
const postUrl = this.extractPostUrl(postContainer);
let contentType = 'text';
if (text && allImages.length > 0) {
contentType = 'text+image';
} else if (allImages.length > 0) {
contentType = 'image';
}
const result = {
platform: this.platform,
type: contentType,
author: author,
text: text,
image: primaryImage, // Keep for backward compatibility
images: allImages, // NEW: All images
links: links, // NEW: External links
timestamp: timestamp,
postUrl: postUrl,
extractedAt: new Date().toISOString()
};
console.log('[FB] Extraction complete:', {
type: result.type,
author: result.author,
textLength: result.text.length,
imageCount: allImages.length,
linkCount: links.length
});
return result;
}
}
// Twitter Extractor
class TwitterExtractor {
constructor() {
this.platform = 'twitter';
}
findPostContainer(element) {
let current = element;
let depth = 0;
const maxDepth = 20;
while (current && depth < maxDepth) {
if (current.matches && current.matches('article[data-testid="tweet"]')) {
console.log('[Twitter] Found tweet container at depth:', depth);
return current;
}
current = current.parentElement;
depth++;
}
console.log('[Twitter] No tweet container found');
return null;
}
extractAuthor(tweetElement) {
const authorSelectors = [
'[data-testid="User-Name"] span',
'a[role="link"] span'
];
for (const selector of authorSelectors) {
const authorEl = tweetElement.querySelector(selector);
if (authorEl && authorEl.textContent.trim() && !authorEl.textContent.includes('@')) {
const authorName = authorEl.textContent.trim();
console.log('[Twitter] Found author:', authorName);
return authorName;
}
}
return 'Unknown';
}
extractText(tweetElement) {
const textEl = tweetElement.querySelector('[data-testid="tweetText"]');
if (textEl) {
const text = textEl.textContent.trim();
console.log('[Twitter] Extracted text length:', text.length);
return text;
}
console.log('[Twitter] No text found');
return '';
}
extractPrimaryImage(tweetElement) {
const imgSelectors = [
'[data-testid="tweetPhoto"] img',
'img[alt*="Image"]'
];
for (const selector of imgSelectors) {
const img = tweetElement.querySelector(selector);
if (img && img.src) {
console.log('[Twitter] Found primary image');
return {
url: img.src,
width: img.naturalWidth || img.width,
height: img.naturalHeight || img.height,
alt: img.alt || ''
};
}
}
console.log('[Twitter] No primary image found');
return null;
}
extractTimestamp(tweetElement) {
const timeEl = tweetElement.querySelector('time');
if (timeEl) {
const timestamp = timeEl.getAttribute('datetime') || timeEl.textContent;
console.log('[Twitter] Found timestamp:', timestamp);
return timestamp;
}
return new Date().toISOString();
}
extractPostUrl(tweetElement) {
const timeLink = tweetElement.querySelector('time');
if (timeLink && timeLink.parentElement && timeLink.parentElement.href) {
console.log('[Twitter] Found tweet URL:', timeLink.parentElement.href);
return timeLink.parentElement.href;
}
return window.location.href;
}
extract(element) {
console.log('[Twitter] Starting Twitter extraction...');
const tweetContainer = this.findPostContainer(element);
if (!tweetContainer) {
console.log('[Twitter] No tweet container found');
return null;
}
const author = this.extractAuthor(tweetContainer);
const text = this.extractText(tweetContainer);
const primaryImage = this.extractPrimaryImage(tweetContainer);
const timestamp = this.extractTimestamp(tweetContainer);
const postUrl = this.extractPostUrl(tweetContainer);
let contentType = 'text';
if (text && primaryImage) {
contentType = 'text+image';
} else if (primaryImage) {
contentType = 'image';
}
const result = {
platform: this.platform,
type: contentType,
author: author,
text: text,
image: primaryImage,
timestamp: timestamp,
postUrl: postUrl,
extractedAt: new Date().toISOString()
};
console.log('[Twitter] Extraction complete:', {
type: result.type,
author: result.author,
textLength: result.text.length,
hasImage: !!result.image
});
return result;
}
}
let selectionBox = null;
let isSelecting = false;
let selectedElement = null;
let currentPlatform = null;
let extractor = null;
// Snipping tool variables
let isSnipping = false;
let snipStartX = 0;
let snipStartY = 0;
let snipBox = null;
console.log('[DIDI] Content script loaded on:', window.location.href);
// Initialize extension
function init() {
console.log('[DIDI] Content script initialized');
createSelectionBox();
// Listen for messages from popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('[DIDI] Message received:', message);
if (message.action === 'activateSelection') {
currentPlatform = message.platform;
console.log('[DIDI] Activating selection for platform:', currentPlatform);
// Initialize platform-specific extractor
if (currentPlatform === 'facebook') {
extractor = new FacebookExtractor();
console.log('[DIDI] Initialized Facebook extractor');
} else if (currentPlatform === 'twitter') {
extractor = new TwitterExtractor();
console.log('[DIDI] Initialized Twitter extractor');
}
activateSelectionMode();
sendResponse({ success: true });
}
if (message.action === 'activateSnipping') {
console.log('[DIDI] Activating snipping mode');
ensureUploadConsent().then((accepted) => {
if (accepted) activateSnippingMode();
sendResponse({ success: accepted });
});
return true;
}
if (message.action === 'activateVideoSnipping') {
console.log('[DIDI] Activating video snipping mode');
ensureUploadConsent().then((accepted) => {
if (accepted) activateVideoSnippingMode();
sendResponse({ success: accepted });
});
return true;
}
if (message.action === 'requestUploadConsent') {
ensureUploadConsent().then((accepted) => sendResponse({ accepted }));
return true;
}
if (message.action === 'showToast') {
showNotification(message.message, message.type || 'info');
sendResponse({ success: true });
}
if (message.action === 'showAnalysis') {
console.log('[DIDI] Showing analysis result modal (pushed from background)');
showAnalysisResult(message.analysis, message.filename);
sendResponse({ success: true });
}
if (message.action === 'analysisFailed') {
console.warn('[DIDI] Analysis failed:', message.error);
showNotification('❌ Analysis failed: ' + (message.error || 'unknown'), 'error');
sendResponse({ success: true });
}
return true; // Keep channel open
});
}
// Create selection box overlay
function createSelectionBox() {
console.log('[DIDI] Creating selection box');
// Remove existing box if any
const existing = document.getElementById('didi-selection-box');
if (existing) {
existing.remove();
}
selectionBox = document.createElement('div');
selectionBox.id = 'didi-selection-box';
selectionBox.style.cssText = `
position: absolute;
border: 3px solid #009198;
background: rgba(0,145,152, 0.1);
pointer-events: none;
z-index: 999999;
display: none;
border-radius: 8px;
box-shadow: 0 0 20px rgba(0,145,152, 0.4);
`;
document.body.appendChild(selectionBox);
console.log('[DIDI] Selection box created');
}
// Activate selection mode
function activateSelectionMode() {
console.log('[DIDI] Selection mode activating...');
isSelecting = true;
document.body.style.cursor = 'crosshair';
// Add event listeners
document.addEventListener('mousemove', handleMouseMove, true);
document.addEventListener('click', handleClick, true);
showNotification('✓ Selection mode active - Hover over any element and click');
console.log('[DIDI] Selection mode activated - cursor should be crosshair');
}
// Deactivate selection mode
function deactivateSelectionMode() {
console.log('[DIDI] Selection mode deactivating...');
isSelecting = false;
document.body.style.cursor = 'default';
if (selectionBox) {
selectionBox.style.display = 'none';
}
document.removeEventListener('mousemove', handleMouseMove, true);
document.removeEventListener('click', handleClick, true);
showNotification('Selection mode deactivated');
}
// Handle mouse movement - highlight post containers
function handleMouseMove(e) {
if (!isSelecting || !extractor) return;
// Find the post container using platform-specific extractor
const postContainer = extractor.findPostContainer(e.target);
if (!postContainer || !selectionBox) return;
// Highlight the entire post container
const rect = postContainer.getBoundingClientRect();
selectionBox.style.display = 'block';
selectionBox.style.top = (rect.top + window.scrollY) + 'px';
selectionBox.style.left = (rect.left + window.scrollX) + 'px';
selectionBox.style.width = rect.width + 'px';
selectionBox.style.height = rect.height + 'px';
selectedElement = postContainer;
}
// Handle click - extract content
function handleClick(e) {
console.log('[DIDI] Click event detected!');
if (!isSelecting || !extractor) {
console.log('[DIDI] Not in selecting mode or no extractor');
return;
}
if (!selectedElement) {
console.log('[DIDI] No element selected');
return;
}
e.preventDefault();
e.stopPropagation();
console.log('[DIDI] Extracting from element:', selectedElement);
// Use platform-specific extractor
const content = extractor.extract(selectedElement);
console.log('[DIDI] Extracted content:', content);
if (content) {
// Store in chrome storage FIRST
chrome.storage.local.set({
[`${currentPlatform}_content`]: content,
lastExtractedContent: content,
lastExtractedPlatform: currentPlatform,
extractedAt: new Date().toISOString()
}, () => {
console.log('[DIDI] Content stored in chrome.storage');
});
// Then send to popup (if it's open)
chrome.runtime.sendMessage({
action: 'contentExtracted',
data: content,
platform: currentPlatform
}, (response) => {
console.log('[DIDI] Message sent to popup, response:', response);
});
// Show detailed notification — background starts the analysis automatically
const summary = `✓ Postare extrasă (${content.type})\n` +
`📝 Text: ${content.text ? content.text.length + ' caractere' : 'fără'}\n` +
`🔗 Linkuri: ${content.links ? content.links.length : 0}\n\n` +
`DiDi analizează… rezultatul apare aici în 30–90s.`;
showNotification(summary, 'success');
} else {
console.log('[DIDI] No content found');
showNotification('No content found', 'error');
}
deactivateSelectionMode();
}
// Legacy extraction function - no longer used
// Platform-specific extraction is now handled by FacebookExtractor and TwitterExtractor classes
// === UPLOAD CONSENT (cerință caiet: consimțământ explicit pentru fișiere/capturi) ===
// Shown once before the first capture/image upload; the choice is persisted.
function ensureUploadConsent() {
return new Promise((resolve) => {
chrome.storage.local.get(['didiUploadConsent'], (data) => {
if (data.didiUploadConsent === true) return resolve(true);
document.querySelectorAll('.didi-consent-modal').forEach((n) => n.remove());
const overlay = document.createElement('div');
overlay.className = 'didi-consent-modal';
overlay.style.cssText = `
position: fixed; inset: 0; z-index: 10000000;
background: rgba(13, 20, 36, 0.55);
display: flex; align-items: center; justify-content: center;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
const card = document.createElement('div');
card.setAttribute('role', 'alertdialog');
card.setAttribute('aria-modal', 'true');
card.setAttribute('aria-label', 'Consimțământ pentru trimiterea capturii');
card.style.cssText = `
background: #fbfbfd; color: #0d1424; max-width: 420px; margin: 16px;
border-radius: 22px; padding: 24px; box-shadow: 0 24px 64px rgba(13,20,36,0.35);
border: 1px solid rgba(13,20,36,0.08); font-size: 14px; line-height: 1.6;
`;
card.innerHTML = `
Consimțământ pentru analiză
Captura sau imaginea selectată va fi trimisă platformei DiDi pentru analiză
anti-dezinformare. Conținutul este folosit exclusiv pentru generarea raportului de analiză,
conform politicii de confidențialitate. Trimite doar fragmentul selectat — nimic în plus.
`;
overlay.appendChild(card);
document.body.appendChild(overlay);
const finish = (accepted) => {
overlay.remove();
if (accepted) {
chrome.storage.local.set({ didiUploadConsent: true, didiUploadConsentAt: new Date().toISOString() });
}
resolve(accepted);
};
card.querySelector('.didi-consent-accept').addEventListener('click', () => finish(true));
card.querySelector('.didi-consent-decline').addEventListener('click', () => finish(false));
overlay.addEventListener('keydown', (e) => { if (e.key === 'Escape') finish(false); });
card.querySelector('.didi-consent-accept').focus();
});
});
}
// Show notification
function showNotification(message, type = 'info') {
console.log('[DIDI] Showing notification:', message, type);
// Remove existing notifications
const existing = document.querySelectorAll('.didi-notification');
existing.forEach(n => n.remove());
const notification = document.createElement('div');
notification.className = 'didi-notification';
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
background: ${type === 'success' ? '#009198' : type === 'error' ? '#E63946' : '#009198'};
color: white;
border-radius: 12px;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
font-weight: 500;
line-height: 1.6;
z-index: 9999999;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
animation: slideIn 0.3s ease;
white-space: pre-wrap;
max-width: 350px;
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// === ANALYSIS RESULT MODAL — uses shared DidiRender module (render.js) ===
function showAnalysisResult(analysis, filename) {
console.log('[DIDI] Showing analysis result modal');
if (!window.DidiRender || typeof window.DidiRender.renderModalHTML !== 'function') {
console.error('[DIDI] DidiRender not loaded — render.js was not injected. Reload the page.');
showNotification('⚠️ Result ready but renderer missing — reload this page and check History', 'error');
return;
}
document.querySelectorAll('.didi-analysis-modal').forEach((n) => n.remove());
saveToLocalHistory(analysis, filename);
const modal = document.createElement('div');
modal.className = 'didi-analysis-modal';
modal.innerHTML = window.DidiRender.renderModalHTML(analysis);
// Inject animation keyframes once
if (!document.getElementById('didi-modal-anim-styles')) {
const animStyle = document.createElement('style');
animStyle.id = 'didi-modal-anim-styles';
animStyle.textContent = '@keyframes didiFadeIn{from{opacity:0}to{opacity:1}}@keyframes didiSlideUp{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}';
document.head.appendChild(animStyle);
}
document.body.appendChild(modal);
// Wire close buttons (modal markup uses class hooks).
const closeBtn = modal.querySelector('.didi-modal-close');
const gotItBtn = modal.querySelector('.didi-modal-gotit');
if (closeBtn) closeBtn.addEventListener('click', () => modal.remove());
if (gotItBtn) gotItBtn.addEventListener('click', () => modal.remove());
// Close on click outside the inner card (= click on the dark overlay).
// The overlay is the modal's first child; the inner card is its grandchild.
// We must compare against the actual overlay element, not "first element child"
// which would also fire when clicking on the inner card's siblings.
const overlay = modal.firstElementChild; // the fullscreen
with rgba(5,5,16,0.88)
if (overlay) {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) modal.remove(); // click hit the dark padding only
});
}
}
// Save analysis to local chrome.storage history
function saveToLocalHistory(analysis, filename) {
chrome.storage.local.get(['didiHistory'], (data) => {
const history = data.didiHistory || [];
history.unshift({
analysis: analysis,
filename: filename,
timestamp: new Date().toISOString(),
url: window.location.href
});
// Keep max 50 items
if (history.length > 50) history.length = 50;
chrome.storage.local.set({ didiHistory: history });
});
}
// ===== SNIPPING TOOL MODE =====
function activateSnippingMode() {
console.log('[DIDI] Snipping mode activating...');
isSnipping = true;
document.body.style.cursor = 'crosshair';
// Create snipping overlay
createSnippingOverlay();
// Add event listeners
document.addEventListener('mousedown', handleSnipStart, true);
document.addEventListener('mousemove', handleSnipDrag, true);
document.addEventListener('mouseup', handleSnipEnd, true);
document.addEventListener('keydown', handleSnipCancel, true);
showNotification('✂️ Snipping mode active - Click and drag to select area (ESC to cancel)', 'info');
}
function createSnippingOverlay() {
// Remove existing overlay
const existing = document.getElementById('didi-snip-overlay');
if (existing) existing.remove();
// Create semi-transparent overlay
const overlay = document.createElement('div');
overlay.id = 'didi-snip-overlay';
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
z-index: 999998;
pointer-events: none;
`;
document.body.appendChild(overlay);
// Create snip box
snipBox = document.createElement('div');
snipBox.id = 'didi-snip-box';
snipBox.style.cssText = `
position: absolute;
border: 3px solid #009198;
background: rgba(0,145,152, 0.15);
z-index: 999999;
display: none;
box-shadow: 0 0 20px rgba(0,145,152, 0.6), inset 0 0 20px rgba(0,145,152, 0.1);
pointer-events: none;
`;
document.body.appendChild(snipBox);
console.log('[DIDI] Snip box created');
}
function handleSnipStart(e) {
if (!isSnipping) return;
e.preventDefault();
e.stopPropagation();
snipStartX = e.pageX;
snipStartY = e.pageY;
snipBox.style.left = snipStartX + 'px';
snipBox.style.top = snipStartY + 'px';
snipBox.style.width = '0px';
snipBox.style.height = '0px';
snipBox.style.display = 'block';
}
function handleSnipDrag(e) {
if (!isSnipping || !snipBox || snipBox.style.display === 'none') return;
e.preventDefault();
const currentX = e.pageX;
const currentY = e.pageY;
const width = Math.abs(currentX - snipStartX);
const height = Math.abs(currentY - snipStartY);
const left = Math.min(currentX, snipStartX);
const top = Math.min(currentY, snipStartY);
snipBox.style.width = width + 'px';
snipBox.style.height = height + 'px';
snipBox.style.left = left + 'px';
snipBox.style.top = top + 'px';
// Add size indicator inside the box
snipBox.innerHTML = `