// content.js – Telegram Media/File Downloader con supporto multi-tipo e timestamp migliorati
(function () {
if (window.__telegramDownloaderInjected) return;
window.__telegramDownloaderInjected = true;
/*********** STATE ***********/
let isPro = false;
let maxDownloads = 50;
let downloadCount = 0;
let isDownloading = false;
let dateFrom = null;
let dateTo = null;
let downloadAsZip = false;
let scanMode = 'auto';
let downloadTypes = []; // Array di tipi: ['images', 'videos', 'documents', 'audio']
let zipFiles = [];
let downloadedURLs = new Set();
let downloadedFiles = new Set(); // Traccia file già scaricati (timestamp + filename)
const LABELS = {
MEDIA: ['Media','Foto','Photos','Immagini','Imágenes','Média','Medien','Фотографии'],
FILES: ['Files','Documenti','Documentos','Dateien','Файлы'],
INFO: ['Info','Profile','Dettagli','Details','Profilo']
};
// Mapping estensioni per categorizzazione
const FILE_TYPES = {
images: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'ico', 'heic', 'heif', 'tiff', 'tif'],
videos: ['mp4', 'webm', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'm4v', '3gp', 'mpg', 'mpeg', 'ogv'],
documents: [
// Office
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
// Text
'txt', 'rtf', 'odt', 'ods', 'odp', 'csv',
// Code
'js', 'jsx', 'ts', 'tsx', 'html', 'htm', 'css', 'scss', 'sass', 'json', 'xml', 'yml', 'yaml',
'py', 'java', 'cpp', 'c', 'h', 'cs', 'php', 'rb', 'go', 'rs', 'swift', 'kt',
// Archives
'zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz',
// Other
'epub', 'mobi', 'azw', 'azw3'
],
audio: ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'aac', 'wma', 'opus', 'oga', 'ape', 'alac']
};
/*********** UTILS ***********/
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const pad = n => (n<10?'0':'') + n;
const fmtTs = (ts) => {
const d = new Date(ts || Date.now());
return d.getFullYear()+''+pad(d.getMonth()+1)+''+pad(d.getDate())+'_'+pad(d.getHours())+pad(d.getMinutes())+pad(d.getSeconds());
};
const guessExtFromUrl = (u, fallback='bin') => {
try {
const clean = (u||'').split('?')[0].split('#')[0];
const m = clean.match(/\.(\w{1,5})$/i);
return (m ? m[1] : fallback).toLowerCase();
} catch { return fallback; }
};
function getFileCategory(ext) {
ext = ext.toLowerCase();
for (const [category, extensions] of Object.entries(FILE_TYPES)) {
if (extensions.includes(ext)) return category;
}
return null;
}
function shouldDownloadFile(ext) {
const category = getFileCategory(ext);
return category && downloadTypes.includes(category);
}
function showNotification(message, type='info') {
const n = document.createElement('div');
n.className = 'tgdl-toast';
n.textContent = message;
n.style.cssText = `
position:fixed;z-index:2147483647;inset:20px 20px auto auto;background:${type==='error'?'#e53935':type==='success'?'#43a047':'#1e88e5'};
color:#fff;padding:12px 14px;border-radius:10px;box-shadow:0 6px 20px rgba(0,0,0,.25);
font:13px/1.4 system-ui,-apple-system,Segoe UI,Roboto,Arial;max-width:360px;pointer-events:none;opacity:.98;
transition:transform .25s ease,opacity .25s ease;
`;
document.body.appendChild(n);
setTimeout(()=>{ n.style.opacity='0'; n.style.transform='translateY(-6px)'; setTimeout(()=>n.remove(),300); },2400);
}
function setProMode(value) {
isPro = !!value;
maxDownloads = isPro ? 1e9 : 25;
}
function incrementDownloadCount() {
if (downloadCount >= maxDownloads) return;
downloadCount++;
chrome.runtime?.sendMessage?.({ type: 'download' });
if (downloadCount >= maxDownloads) {
chrome.runtime?.sendMessage?.({ type: 'limitReached' });
}
}
function isVisible(el) {
const rect = el.getBoundingClientRect();
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
const cs = window.getComputedStyle(el);
return cs.display !== 'none' && cs.visibility !== 'hidden' && cs.opacity !== '0';
}
function isChatSelected() {
// Telegram A
// - .messages-layout
// - .messages-container
// - .chat-info-wrapper
// Telegram K
// - .chat-utils
// - .chat-info-container
const selectors = [
'.messages-layout',
'.messages-container',
'.chat-info-wrapper',
'.chat-utils',
'.chat-info-container',
];
for (const sel of selectors) {
const el = document.querySelector(sel);
if (el && isVisible(el)) return true;
}
return false;
}
function findMessagesContainer() {
const guesses = [
'.messages-container','.bubbles-inner','.chat-background','.scrollable',
'.chat-content .scrollable','.MessageList','[class*="MessageList"]'
];
let best=null, score=-1;
const isScrollable = el => {
const st = getComputedStyle(el);
return /(auto|scroll)/.test(st.overflowY) && el.scrollHeight>el.clientHeight;
};
const msgCount = el => el.querySelectorAll('.message,.Message,.bubble,[class*="bubble"]').length;
// IMPORTANTE: escludi sidebar delle chat
const isSidebar = el => {
// Telegram K: sidebar ha classi come chatlist, folders-tabs, etc
// Telegram A: sidebar ha classe .chat-list o simili
return el.closest('.chatlist, [class*="chatlist"], .chat-list, [class*="chat-list"], .folders-tabs, .sidebar, [class*="sidebar"]') !== null;
};
for (const sel of guesses) {
const el = document.querySelector(sel);
if (el && isScrollable(el) && !isSidebar(el)) {
const c = msgCount(el); if (c>score){best=el;score=c;}
}
}
if (best) return best;
document.querySelectorAll('div').forEach(el=>{
if (isScrollable(el) && !isSidebar(el)) {
const c = msgCount(el); if (c>score){best=el;score=c;}
}
});
return best;
}
function extractDateFromMessageGroup(messageEl) {
// Cerca il parent .message-date-group e la data nel .sticky-date
let current = messageEl;
let attempts = 0;
const maxAttempts = 10;
while (current && attempts < maxAttempts) {
if (current.classList && current.classList.contains('message-date-group')) {
const stickyDate = current.querySelector('.sticky-date span');
if (stickyDate) {
const dateText = stickyDate.textContent.trim();
console.log('�� Trovata data dal sticky-date:', dateText);
const parsed = parseDateText(dateText);
if (parsed) {
console.log('✅ Data parsata:', new Date(parsed).toLocaleString());
return parsed;
}
}
break;
}
current = current.parentElement;
attempts++;
}
return null;
}
function parseDateText(text) {
if (!text) return null;
const lowerText = text.toLowerCase().trim();
// 1. Gestisci "Oggi" / "Today"
if (/^(oggi|today|heute|hoy|aujourd'hui)$/i.test(lowerText)) {
const today = new Date();
today.setHours(0, 0, 0, 0);
console.log('�� Parsed "Oggi/Today":', new Date(today).toLocaleString());
return today.getTime();
}
// 2. Gestisci "Ieri" / "Yesterday"
if (/^(ieri|yesterday|gestern|ayer|hier)$/i.test(lowerText)) {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
console.log('�� Parsed "Ieri/Yesterday":', new Date(yesterday).toLocaleString());
return yesterday.getTime();
}
// 3. Gestisci giorni della settimana (Lunedì, Martedì, etc.)
const weekdayMap = {
// Italiano
'lunedì': 1, 'lunedi': 1, 'martedì': 2, 'martedi': 2, 'mercoledì': 3, 'mercoledi': 3,
'giovedì': 4, 'giovedi': 4, 'venerdì': 5, 'venerdi': 5, 'sabato': 6, 'domenica': 0,
// Inglese
'monday': 1, 'tuesday': 2, 'wednesday': 3, 'thursday': 4, 'friday': 5, 'saturday': 6, 'sunday': 0,
// Francese
'lundi': 1, 'mardi': 2, 'mercredi': 3, 'jeudi': 4, 'vendredi': 5, 'samedi': 6, 'dimanche': 0,
// Tedesco
'montag': 1, 'dienstag': 2, 'mittwoch': 3, 'donnerstag': 4, 'freitag': 5, 'samstag': 6, 'sonntag': 0,
// Spagnolo
'lunes': 1, 'martes': 2, 'miércoles': 3, 'miercoles': 3, 'jueves': 4, 'viernes': 5, 'sábado': 6, 'sabado': 6, 'domingo': 0
};
const targetDay = weekdayMap[lowerText];
if (targetDay !== undefined) {
const today = new Date();
const currentDay = today.getDay(); // 0 = Domenica, 1 = Lunedì, etc.
// Calcola quanti giorni indietro è il giorno target
let daysAgo = currentDay - targetDay;
if (daysAgo <= 0) {
// Se il giorno è nel futuro questa settimana, vai alla settimana scorsa
daysAgo += 7;
}
const targetDate = new Date(today);
targetDate.setDate(today.getDate() - daysAgo);
targetDate.setHours(0, 0, 0, 0);
console.log(`�� Parsed weekday "${text}" (${targetDay}) as:`, new Date(targetDate).toLocaleString(), `(${daysAgo} giorni fa)`);
return targetDate.getTime();
}
// 4. Parse date formats like "5 settembre 2024", "15 October 2024", etc.
const monthNames = {
'january': 0, 'gennaio': 0, 'janvier': 0, 'januar': 0, 'enero': 0,
'february': 1, 'febbraio': 1, 'février': 1, 'februar': 1, 'febrero': 1,
'march': 2, 'marzo': 2, 'mars': 2, 'märz': 2,
'april': 3, 'aprile': 3, 'avril': 3, 'abril': 3,
'may': 4, 'maggio': 4, 'mai': 4, 'mayo': 4,
'june': 5, 'giugno': 5, 'juin': 5, 'juni': 5, 'junio': 5,
'july': 6, 'luglio': 6, 'juillet': 6, 'juli': 6, 'julio': 6,
'august': 7, 'agosto': 7, 'août': 7, 'agosto': 7,
'september': 8, 'settembre': 8, 'septembre': 8, 'september': 8, 'septiembre': 8,
'october': 9, 'ottobre': 9, 'octobre': 9, 'oktober': 9, 'octubre': 9,
'november': 10, 'novembre': 10, 'novembre': 10, 'november': 10, 'noviembre': 10,
'december': 11, 'dicembre': 11, 'décembre': 11, 'dezember': 11, 'diciembre': 11
};
// Pattern con anno: "5 settembre 2024" o "15 October 2024"
const matchWithYear = text.match(/(\d{1,2})\s+(\w+)\s+(\d{4})/i);
if (matchWithYear) {
const day = parseInt(matchWithYear[1]);
const monthStr = matchWithYear[2].toLowerCase();
const year = parseInt(matchWithYear[3]);
const month = monthNames[monthStr];
if (month !== undefined) {
const date = new Date(year, month, day, 0, 0, 0, 0);
return date.getTime();
}
}
// Pattern SENZA anno: "26 giugno" o "15 October"
const matchNoYear = text.match(/^(\d{1,2})\s+(\w{4,})\s*$/i);
if (matchNoYear) {
const day = parseInt(matchNoYear[1]);
const monthStr = matchNoYear[2].toLowerCase();
const month = monthNames[monthStr];
if (month !== undefined) {
const currentYear = new Date().getFullYear();
const date = new Date(currentYear, month, day, 0, 0, 0, 0);
// Se la data è nel futuro, usa anno precedente
if (date.getTime() > Date.now()) {
date.setFullYear(currentYear - 1);
}
console.log(`�� Parsed date without year "${text}" as:`, new Date(date).toLocaleString());
return date.getTime();
}
}
return null;
}
function combineDateTime(dateTimestamp, timeText) {
// Combina una data (timestamp) con un orario (es. "11:39")
if (!dateTimestamp || !timeText) return null;
const timeMatch = timeText.match(/(\d{1,2}):(\d{2})/);
if (!timeMatch) return null;
const hours = parseInt(timeMatch[1]);
const minutes = parseInt(timeMatch[2]);
const date = new Date(dateTimestamp);
date.setHours(hours, minutes, 0, 0);
return date.getTime();
}
function extractTimestampFromElement(el) {
// 0. PRIMA DI TUTTO: cerca la data dal message-date-group
const groupDate = extractDateFromMessageGroup(el);
// Se abbiamo trovato una data dal gruppo, cerchiamo l'orario nel messaggio
if (groupDate) {
// Cerca .message-time nel messaggio
const timeEl = el.querySelector('.message-time, [class*="time"], [class*="Time"]');
if (timeEl) {
const timeText = timeEl.textContent.trim();
const fullTimestamp = combineDateTime(groupDate, timeText);
if (fullTimestamp) {
console.log('�� Timestamp completo da sticky-date + message-time:', new Date(fullTimestamp).toLocaleString());
return fullTimestamp;
}
}
}
// 1. data-timestamp / data-date (vari formati)
const tsAttrs = ['data-timestamp', 'data-date', 'data-time', 'timestamp', 'date'];
for (const attr of tsAttrs) {
const val = el.getAttribute(attr) || el.dataset?.[attr.replace('data-', '')];
if (val && !isNaN(Number(val))) {
const ts = Number(val);
const converted = ts > 10000000000 ? ts : ts * 1000;
// Valida: deve essere tra 2000 e oggi + 1 anno
if (converted > 946684800000 && converted < Date.now() + 31536000000) {
console.log(`✅ Trovato ${attr}:`, new Date(converted).toLocaleString(), '(' + converted + ')');
return converted;
} else {
console.warn(`⚠️ ${attr} fuori range:`, new Date(converted).toLocaleString(), '- ignorato');
}
}
}
// 2. data-mid (message ID di Telegram)
const dataMid = el.getAttribute('data-mid') || el.dataset?.mid || el.id;
if (dataMid && typeof dataMid === 'string') {
// Pattern 1: "123456789" (solo numero)
if (/^\d+$/.test(dataMid)) {
const ts = parseInt(dataMid);
if (ts > 1000000000) {
const converted = ts > 10000000000 ? ts : ts * 1000;
if (converted > 946684800000 && converted < Date.now() + 31536000000) {
console.log('✅ Trovato data-mid (numero):', new Date(converted).toLocaleString(), '(' + converted + ')');
return converted;
}
}
}
// Pattern 2: "timestamp_random"
const parts = dataMid.split('_');
if (parts.length > 0) {
const ts = parseInt(parts[0]);
if (!isNaN(ts) && ts > 1000000000) {
const converted = ts > 10000000000 ? ts : ts * 1000;
if (converted > 946684800000 && converted < Date.now() + 31536000000) {
console.log('✅ Trovato data-mid (split):', new Date(converted).toLocaleString(), '(' + converted + ')');
return converted;
}
}
}
}
// 3. Cerca in TUTTI i discendenti time/span con classi time/date
const timeElements = el.querySelectorAll('time, [class*="time"], [class*="Time"], [class*="date"], [class*="Date"], span.i');
for (const timeEl of timeElements) {
// Attributi datetime/title
for (const attr of ['datetime', 'title', 'data-timestamp', 'data-content']) {
const dt = timeEl.getAttribute(attr);
if (dt) {
const parsed = Date.parse(dt);
if (!isNaN(parsed) && parsed > 946684800000 && parsed < Date.now() + 31536000000) {
console.log(`✅ Trovato time[${attr}]:`, new Date(parsed).toLocaleString(), '(' + parsed + ')');
return parsed;
}
}
}
// TextContent solo se breve
const text = (timeEl.textContent || '').trim();
if (text && text.length < 30 && text.length > 3) {
const parsed = parseTimeText(text);
if (parsed && parsed > 946684800000 && parsed < Date.now() + 31536000000) {
console.log('✅ Trovato time text:', new Date(parsed).toLocaleString(), '(' + parsed + ') da:', text);
return parsed;
}
}
}
// 4. aria-label (spesso contiene data completa)
const ariaLabel = el.getAttribute('aria-label');
if (ariaLabel && ariaLabel.length < 200) {
const parsed = extractDateFromText(ariaLabel);
if (parsed && parsed > 946684800000 && parsed < Date.now() + 31536000000) {
console.log('✅ Trovato aria-label:', new Date(parsed).toLocaleString(), '(' + parsed + ')');
return parsed;
}
}
return null;
}
function parseTimeText(text) {
if (!text) return null;
text = text.trim();
// Pattern di oggi/ieri - usa solo se c'è anche un orario
const timeMatch = text.match(/(\d{1,2}):(\d{2})/);
if (timeMatch) {
const hours = parseInt(timeMatch[1]);
const minutes = parseInt(timeMatch[2]);
// Se trova "oggi" o "today"
if (/\b(oggi|today)\b/i.test(text)) {
const today = new Date();
today.setHours(hours, minutes, 0, 0);
return today.getTime();
}
// Se trova "ieri" o "yesterday"
if (/\b(ieri|yesterday)\b/i.test(text)) {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(hours, minutes, 0, 0);
return yesterday.getTime();
}
// Non assumere nulla se c'è solo l'ora senza contesto
return null;
}
// Pattern "5 apr" o "12 gen" (senza anno) - usa anno corrente
const shortDateMatch = text.match(/^(\d{1,2})\s+(gen|feb|mar|apr|mag|giu|lug|ago|set|ott|nov|dic|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)/i);
if (shortDateMatch) {
const day = parseInt(shortDateMatch[1]);
const monthAbbr = shortDateMatch[2].toLowerCase();
const monthMap = {
'gen': 0, 'feb': 1, 'mar': 2, 'apr': 3, 'mag': 4, 'giu': 5,
'lug': 6, 'ago': 7, 'set': 8, 'ott': 9, 'nov': 10, 'dic': 11,
'jan': 0, 'feb': 1, 'mar': 2, 'apr': 3, 'may': 4, 'jun': 5,
'jul': 6, 'aug': 7, 'sep': 8, 'oct': 9, 'nov': 10, 'dec': 11
};
const month = monthMap[monthAbbr];
if (month !== undefined) {
const currentYear = new Date().getFullYear();
const date = new Date(currentYear, month, day, 0, 0, 0, 0);
// Se la data è nel futuro (es. siamo a ottobre e troviamo novembre), usa anno precedente
if (date.getTime() > Date.now()) {
date.setFullYear(currentYear - 1);
}
console.log(`�� Parsed short date "${text}" as:`, new Date(date).toLocaleString());
return date.getTime();
}
}
// Prova parsing diretto ISO solo DOPO aver provato i pattern specifici
const directParse = Date.parse(text);
if (!isNaN(directParse) && directParse > 946684800000) {
// Verifica che non sia troppo nel passato (< 2020)
if (directParse < new Date('2020-01-01').getTime()) {
console.warn('⚠️ Date.parse ha restituito una data troppo vecchia:', new Date(directParse).toLocaleString(), 'da testo:', text);
return null;
}
return directParse;
}
return null;
}
function extractDateFromText(text) {
if (!text || text.length > 200) return null; // Evita testi troppo lunghi
// Pattern 1: "12 ottobre 2025, 14:30" o "12 October 2025, 14:30"
const monthsIT = {
'gennaio': 0, 'febbraio': 1, 'marzo': 2, 'aprile': 3, 'maggio': 4, 'giugno': 5,
'luglio': 6, 'agosto': 7, 'settembre': 8, 'ottobre': 9, 'novembre': 10, 'dicembre': 11
};
const monthsEN = {
'january': 0, 'february': 1, 'march': 2, 'april': 3, 'may': 4, 'june': 5,
'july': 6, 'august': 7, 'september': 8, 'october': 9, 'november': 10, 'december': 11
};
const lowerText = text.toLowerCase();
// Cerca pattern "giorno mese anno"
const datePattern = /(\d{1,2})\s+(\w+)\s+(\d{4})/i;
const match = text.match(datePattern);
if (match) {
const day = parseInt(match[1]);
const monthStr = match[2].toLowerCase();
const year = parseInt(match[3]);
let month = monthsIT[monthStr] ?? monthsEN[monthStr];
if (month !== undefined && year >= 2000 && year <= 2100 && day >= 1 && day <= 31) {
// Cerca anche l'orario se presente
const timeMatch = text.match(/(\d{1,2}):(\d{2})/);
const hours = timeMatch ? parseInt(timeMatch[1]) : 12;
const minutes = timeMatch ? parseInt(timeMatch[2]) : 0;
const date = new Date(year, month, day, hours, minutes, 0, 0);
return date.getTime();
}
}
// Pattern 2: ISO format "2025-10-12"
const isoMatch = text.match(/(\d{4})-(\d{1,2})-(\d{1,2})/);
if (isoMatch) {
const parsed = Date.parse(isoMatch[0]);
if (!isNaN(parsed) && parsed > 946684800000) return parsed;
}
// Pattern 3: "12/10/2025" o "12.10.2025"
const dateSlashMatch = text.match(/(\d{1,2})[\/\.](\d{1,2})[\/\.](\d{4})/);
if (dateSlashMatch) {
const d = parseInt(dateSlashMatch[1]);
const m = parseInt(dateSlashMatch[2]);
const y = parseInt(dateSlashMatch[3]);
if (y >= 2000 && y <= 2100 && m >= 1 && m <= 12 && d >= 1 && d <= 31) {
const date = new Date(y, m - 1, d, 12, 0, 0, 0);
return date.getTime();
}
}
return null;
}
function parseDayHeaderText(txt) {
if (!txt) return null;
const t = txt.trim();
// Try native Date
const d1 = Date.parse(t);
if (!isNaN(d1)) return d1;
// Try common i18n formats
const mapIT = {'gennaio':0,'febbraio':1,'marzo':2,'aprile':3,'maggio':4,'giugno':5,'luglio':6,'agosto':7,'settembre':8,'ottobre':9,'novembre':10,'dicembre':11};
const m1 = t.toLowerCase().match(/(\d{1,2})\s+([a-zàéìòù]+)\s+(\d{4})/i);
if (m1 && mapIT[m1[2]]!==undefined) {
const d = new Date(Number(m1[3]), mapIT[m1[2]], Number(m1[1]));
return d.getTime();
}
const m2 = t.match(/(\d{1,2})\s+([A-Za-z]{3,})\s+(\d{4})/);
if (m2) {
const d = Date.parse(`${m2[1]} ${m2[2]} ${m2[3]}`);
if (!isNaN(d)) return d;
}
return null;
}
function nearestDayHeaderBefore(el, maxSteps=80) {
let cur = el;
let steps = 0;
while (cur && steps<maxSteps) {
cur = cur.previousElementSibling;
steps++;
if (!cur) break;
if (cur.matches?.('.message-list-day,[class*="Day"],[role="separator"],.date-separator')) {
const text = cur.textContent || '';
const ts = parseDayHeaderText(text);
if (ts) return ts;
}
}
return null;
}
function getMessageTimestamp(msgEl) {
if (!msgEl) {
console.warn('⚠️ msgEl è null');
return Date.now();
}
console.log('�� Cercando timestamp in:', msgEl.className, 'tag:', msgEl.tagName);
// STRATEGIA: cerca dal più specifico al più generico
// 1. Prova nel messaggio stesso
let ts = extractTimestampFromElement(msgEl);
if (ts && ts > 946684800000 && ts < Date.now() + 86400000) {
console.log('✅ Timestamp trovato nel messaggio:', new Date(ts).toLocaleString(), '(' + ts + ')');
return ts;
}
// 2. Cerca nel parent immediato
if (msgEl.parentElement) {
console.log('�� Provo parent:', msgEl.parentElement.className);
ts = extractTimestampFromElement(msgEl.parentElement);
if (ts && ts > 946684800000 && ts < Date.now() + 86400000) {
console.log('✅ Timestamp trovato nel parent:', new Date(ts).toLocaleString(), '(' + ts + ')');
return ts;
}
}
// 3. Cerca nel contenitore del messaggio (bubble/message wrapper)
const selectors = [
'.bubble',
'.message-container',
'.Message',
'.message-date-group',
'[class*="message"]',
'[class*="Message"]',
'[class*="bubble"]',
'[data-mid]'
];
for (const sel of selectors) {
const bubble = msgEl.closest(sel);
if (bubble && bubble !== msgEl) {
console.log('�� Provo bubble:', sel);
ts = extractTimestampFromElement(bubble);
if (ts && ts > 946684800000 && ts < Date.now() + 86400000) {
console.log('✅ Timestamp trovato nel bubble:', new Date(ts).toLocaleString(), '(' + ts + ')');
return ts;
}
}
}
// 4. Cerca nel day header sopra (max 100 elementi prima)
console.log('�� Cerco nel day header...');
ts = nearestDayHeaderBefore(msgEl, 100);
if (ts && ts > 946684800000 && ts < Date.now() + 86400000) {
console.log('⚠️ Timestamp trovato nel day header:', new Date(ts).toLocaleString(), '(' + ts + ')');
return ts;
}
// 5. ULTIMO RESORT: usa data corrente ma logga warning
// console.error('❌ Nessun timestamp valido trovato, uso data corrente');
console.log('�� HTML elemento:', msgEl.outerHTML.substring(0, 500));
return Date.now();
}
function passDateFilter(ts) {
if (!dateFrom && !dateTo) return true;
console.log('�� Check date filter:', {
messageDate: new Date(ts).toLocaleString(),
messageTime: ts,
dateFrom: dateFrom ? new Date(dateFrom).toLocaleString() : 'none',
dateTo: dateTo ? new Date(dateTo).toLocaleString() : 'none'
});
// Confronto diretto con i timestamp (già includono ore)
if (dateFrom && ts < dateFrom) {
console.log('❌ REJECT: ts < dateFrom');
return false;
}
if (dateTo && ts > dateTo) {
console.log('❌ REJECT: ts > dateTo');
return false;
}
console.log('✅ ACCEPT: dentro range');
return true;
}
function shouldStopScanning(ts) {
// NON fermare MAI basandosi su una singola data!
// Gli elementi nel DOM non sono in ordine cronologico,
// quindi potremmo trovare prima un elemento vecchio e poi uno recente.
//
// Lo scanning si ferma solo quando:
// - Lo scroll non può più andare indietro
// - La posizione scroll rimane stabile per troppo tempo
//
// Il filtro date viene applicato in passDateFilter()
return false;
}
/*** DOWNLOAD CORE ***/
function downloadFile(url, filename, addToZip=false) {
if (!url) return false;
if (downloadedURLs.has(url)) return false;
if (downloadCount >= maxDownloads) return false;
downloadedURLs.add(url);
if (downloadAsZip || addToZip) {
zipFiles.push({ url, filename });
incrementDownloadCount();
} else {
// Download diretto con anchor tag
const a = document.createElement('a');
a.href = url;
a.download = filename || '';
document.body.appendChild(a);
a.click();
a.remove();
incrementDownloadCount();
}
return true;
}
async function createAndDownloadZip() {
if (!zipFiles.length) return;
showNotification(`�� Creating ZIP (${zipFiles.length} files)…`, 'info');
const files = zipFiles.slice();
zipFiles = [];
try {
const response = await chrome.runtime.sendMessage({
type: 'createZip',
files: files
});
if (!response.success) {
showNotification('❌ ZIP creation failed: ' + response.error, 'error');
return;
}
const uint8Array = new Uint8Array(response.data);
const zipBlob = new Blob([uint8Array], { type: 'application/zip' });
const a = document.createElement('a');
a.href = URL.createObjectURL(zipBlob);
a.download = `telegram_media_${Date.now()}.zip`;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(a.href), 100);
chrome.runtime?.sendMessage?.({ type: 'zipReady' });
showNotification('✅ ZIP ready', 'success');
} catch (error) {
console.error('ZIP creation error:', error);
showNotification('❌ ZIP creation failed', 'error');
}
}
/*********** DETECTION FUNCTIONS ***********/
function isImageElement(el) {
// ESCLUDI elementi dentro .Reactions (emoji di reazione)
if (el.closest('.Reactions, .reactions, .Reaction, .reaction')) {
return false;
}
// ESCLUDI elementi dentro .message-action-buttons (bottoni hover)
if (el.closest('.message-action-buttons, .MessageActionButtons')) {
return false;
}
// ESCLUDI elementi dentro menu popup (emoji/sticker/reazioni picker)
if (el.closest('.menu-container, .Menu, .ReactionPicker, .StickerPicker, .EmojiPicker, .in-portal')) {
return false;
}
// ESCLUDI per classe specifica emoji
if (el.classList && (el.classList.contains('ReactionStaticEmoji') ||
el.classList.contains('ReactionEmoji') ||
el.classList.contains('reaction-emoji'))) {
return false;
}
// ESCLUDI per dimensione (emoji/icone sono piccole: <= 24px)
if (el.tagName === 'IMG') {
const width = el.width || parseInt(el.style.width) || 0;
const height = el.height || parseInt(el.style.height) || 0;
// Se entrambe le dimensioni sono <= 24px, probabilmente è una emoji/icona
if ((width > 0 && width <= 24) && (height > 0 && height <= 24)) {
console.log('⏭️ Skipping small image (likely emoji/icon):', width + 'x' + height);
return false;
}
}
// Immagini: img tag, background-image, picture
if (el.tagName === 'IMG') return true;
if (el.tagName === 'PICTURE') return true;
const style = getComputedStyle(el);
if (style.backgroundImage && style.backgroundImage !== 'none') return true;
return false;
}
function isVideoElement(el) {
// Video: video tag, o elementi con attributi video
if (el.tagName === 'VIDEO') return true;
if (el.querySelector('video')) return true;
// Cerca classi/attributi che indicano video
const videoClasses = ['video', 'Video', 'media-video'];
for (const cls of videoClasses) {
if (el.className && el.className.includes(cls)) {
// Verifica che non sia un'immagine
if (!isImageElement(el)) return true;
}
}
return false;
}
function isDocumentElement(el) {
// Documenti: cerca icone/classi che indicano documenti
const docClasses = ['document', 'Document', 'file', 'File', 'file-info'];
const className = el.className || '';
for (const cls of docClasses) {
if (className.includes(cls)) return true;
}
// Cerca elementi con estensioni documenti visibili
const text = el.textContent || '';
for (const ext of FILE_TYPES.documents) {
if (text.toLowerCase().includes('.' + ext)) return true;
}
return false;
}
function isAudioElement(el) {
// Audio: audio tag o elementi con attributi audio
if (el.tagName === 'AUDIO') return true;
if (el.querySelector('audio')) return true;
const audioClasses = ['audio', 'Audio', 'voice', 'Voice', 'voice-message', 'audio-message'];
const className = el.className || '';
for (const cls of audioClasses) {
if (className.includes(cls)) return true;
}
// Cerca elementi con estensioni audio visibili
const text = el.textContent || '';
for (const ext of FILE_TYPES.audio) {
if (text.toLowerCase().includes('.' + ext)) return true;
}
// Cerca icone audio
const hasAudioIcon = el.querySelector('[class*="audio"]') || el.querySelector('[class*="Audio"]');
if (hasAudioIcon) return true;
return false;
}
/*********** CHAT SCAN ***********/
function getAllChatMediaElements() {
const allElements = Array.from(document.querySelectorAll(
'.media-container, .media-photo, .album-item, .message img, .Message img, .bubble img, ' +
'video, .media-video, ' +
'.document-wrapper, .document-container, .File, .file, ' +
'audio, .audio, .voice, ' +
'picture, [style*="background-image"]'
));
// Filtra elementi non validi
const filtered = allElements.filter(el => {
// ESCLUDI elementi dentro .Reactions (emoji di reazione)
if (el.closest('.Reactions, .reactions, .Reaction, .reaction')) {
return false;
}
// ESCLUDI elementi dentro .message-action-buttons (bottoni hover)
if (el.closest('.message-action-buttons, .MessageActionButtons')) {
return false;
}
// ESCLUDI elementi dentro menu popup (emoji/sticker/reazioni picker)
if (el.closest('.menu-container, .Menu, .ReactionPicker, .StickerPicker, .EmojiPicker, .in-portal')) {
return false;
}
// ESCLUDI per classe specifica emoji
if (el.classList && (el.classList.contains('ReactionStaticEmoji') ||
el.classList.contains('ReactionEmoji') ||
el.classList.contains('reaction-emoji'))) {
return false;
}
const msg = el.closest('.message,.Message,.bubble,[class*="message"]');
if (!msg) return false;
// Se è un sotto-elemento di un document-wrapper già nella lista, escludilo
const parent = el.closest('.document-wrapper, .document-container');
if (parent && parent !== el) return false; // È un figlio, escludilo
return true;
});
console.log('�� Trovati', filtered.length, 'elementi media nella chat');
return filtered;
}
function getElementType(el) {
// Determina il tipo di elemento
if (isVideoElement(el)) return 'videos';
if (isAudioElement(el)) return 'audio';
if (isDocumentElement(el)) return 'documents';
if (isImageElement(el)) return 'images';
// Fallback: controlla i tag
const tag = el.tagName.toLowerCase();
if (tag === 'img' || tag === 'picture') return 'images';
if (tag === 'video') return 'videos';
if (tag === 'audio') return 'audio';
return null;
}
function extractMediaURL(element) {
// Video - cerca in modo più approfondito
if (element.tagName === 'VIDEO') {
const src = element.src || element.currentSrc;
console.log('�� Video tag diretto:', src);
return src;
}
// Cerca video in qualsiasi punto del DOM (non solo querySelector diretto)
const videoEl = element.querySelector('video');
if (videoEl) {
const src = videoEl.src || videoEl.currentSrc;
console.log('�� Video trovato nel DOM:', src);
if (src) return src;
// Cerca source tags
const source = videoEl.querySelector('source[src]');
if (source?.src) {
console.log('�� Video source tag:', source.src);
return source.src;
}
}
// Se l'elemento stesso ha classe video, cerca più in profondità
if (element.className && element.className.includes('video')) {
const allVideos = element.querySelectorAll('video');
for (const v of allVideos) {
const src = v.src || v.currentSrc;
if (src) {
console.log('�� Video trovato in profondità:', src);
return src;
}
}
}
// Cerca attributi data-* per video
const videoDataAttrs = ['data-video-url', 'data-video', 'data-src', 'data-url'];
for (const attr of videoDataAttrs) {
const val = element.getAttribute(attr);
if (val && (val.startsWith('http') || val.startsWith('blob:'))) {
console.log('�� Video da data attribute:', val);
return val;
}
}
// Cerca link a video nel messaggio
const videoLink = element.querySelector('a[href*=".mp4"], a[href*=".webm"], a[href*=".mov"]');
if (videoLink?.href) {
console.log('�� Video da link:', videoLink.href);
return videoLink.href;
}
// Audio
if (element.tagName === 'AUDIO') {
return element.src || element.currentSrc;
}
const audioEl = element.querySelector('audio');
if (audioEl) {
const src = audioEl.src || audioEl.currentSrc;
if (src) return src;
const source = audioEl.querySelector('source[src]');
if (source?.src) return source.src;
}
// Documenti - cerca URL in vari modi
if (isDocumentElement(element)) {
// Strategia 1: Link con href
const link = element.querySelector('a[href]:not([href^="#"]):not([href^="javascript:"])');
if (link?.href) {
// Verifica che sia un URL valido di Telegram
if (link.href.includes('telegram.org') || link.href.includes('t.me') || link.href.startsWith('blob:')) {
return link.href;
}
}
// Strategia 2: Attributi data-*
const dataAttrs = ['data-doc-url', 'data-url', 'data-href', 'data-src'];
for (const attr of dataAttrs) {
const val = element.getAttribute(attr);
if (val && (val.startsWith('http') || val.startsWith('blob:'))) {
return val;
}
}
// Strategia 3: Cerca in sottoelementi
const allLinks = element.querySelectorAll('a[href]');
for (const link of allLinks) {
if (link.href && !link.href.includes('#') && !link.href.includes('javascript:')) {
const ext = guessExtFromUrl(link.href);
// Se l'URL ha un'estensione valida, probabilmente è il file
if (FILE_TYPES.documents.includes(ext) || FILE_TYPES.audio.includes(ext)) {
return link.href;
}
}
}
}
// Immagini
const img = element.tagName === 'IMG' ? element : element.querySelector('img');
if (img) {
const srcset = img.getAttribute('srcset');
if (srcset) {
const list = srcset.split(',').map(p=>p.trim().split(' ')[0]).filter(Boolean);
if (list.length) return list[list.length-1];
}
if (img.src && !img.src.startsWith('data:')) return img.src;
}
if (element.tagName === 'PICTURE') {
const s = element.querySelector('source[srcset]');
if (s?.getAttribute('srcset')) {
const list = s.getAttribute('srcset').split(',').map(p=>p.trim().split(' ')[0]).filter(Boolean);
if (list.length) return list[list.length-1];
}
}
const bg = getComputedStyle(element).backgroundImage;
const m = bg && bg.match(/url\(["']?(.*?)["']?\)/);
if (m && m[1]) return m[1];
return null;
}
function extractFileInfo(fileEl) {
const msg = fileEl.closest('.message,.Message,.bubble,[class*="message"]');
const ts = getMessageTimestamp(msg);
// Cerca il nome del file
const nameSelectors = [
'middle-ellipsis-element', // Telegram K audio
'.audio-title', // Telegram K audio container
'strong',
'.file-name',
'.document-name',
'[class*="filename"]',
'[class*="FileName"]',
'[title]',
'.name'
];
let nameEl = null;
for (const sel of nameSelectors) {
nameEl = fileEl.querySelector(sel);
if (nameEl) break;
}
const baseText = (nameEl?.textContent || nameEl?.getAttribute?.('title') || 'file').trim().replace(/\s+/g,'_');
// Se il nome è solo una durata (es: "10:59" o "0:30"), usa nome generico
const isDuration = /^\d{1,2}:\d{2}$/.test(baseText);
const finalName = isDuration ? 'audio' : baseText;
const safeBase = `${fmtTs(ts)}_${finalName}`;
// Cerca URL del file - MULTIPLI TENTATIVI
let url = null;
// Tentativo 1: extractMediaURL (include strategia documenti)
url = extractMediaURL(fileEl);
// Tentativo 2: Link diretto con href
if (!url) {
const link = fileEl.querySelector('a[href]:not([href^="#"]):not([href^="javascript:"])');
if (link?.href) {
url = link.href;
}
}
// Tentativo 3: data-doc-url o attributi simili
if (!url) {
const urlAttrs = ['data-doc-url', 'data-url', 'data-src', 'data-href', 'data-document-url'];
for (const attr of urlAttrs) {
const val = fileEl.getAttribute(attr) || fileEl.querySelector(`[${attr}]`)?.getAttribute(attr);
if (val && (val.startsWith('http') || val.startsWith('blob:'))) {
url = val;
break;
}
}
}
// Tentativo 4: Cerca in tutti i link dell'elemento
if (!url) {
const allLinks = Array.from(fileEl.querySelectorAll('a[href]'));
for (const link of allLinks) {
if (link.href && !link.href.includes('#') && !link.href.includes('javascript:')) {
// Verifica se l'URL sembra un file
if (link.href.includes('telegram.org') || link.href.includes('.pdf') || link.href.includes('.doc')) {
url = link.href;
break;
}
}
}
}
// Cerca pulsante download
const dlBtnSelectors = [
'a[download]',
'button[aria-label*="Download"]',
'button[aria-label*="download"]',
'button[title*="Download"]',
'button[title*="download"]',
'button.download',
'[class*="download"][role="button"]',
'[class*="Download"][role="button"]',
'i[class*="download"]'
];
let downloadBtn = null;
for (const sel of dlBtnSelectors) {
downloadBtn = fileEl.querySelector(sel);
if (downloadBtn) {
// Verifica che sia effettivamente cliccabile
if (downloadBtn.tagName === 'A' || downloadBtn.tagName === 'BUTTON' || downloadBtn.onclick || downloadBtn.hasAttribute('role')) {
break;
}
downloadBtn = null;
}
}
// Se abbiamo un'icona download, cerca il parent cliccabile
if (!downloadBtn) {
const downloadIcon = fileEl.querySelector('[class*="download"]');
if (downloadIcon) {
let parent = downloadIcon.parentElement;
let depth = 0;
while (parent && depth < 5) {
if (parent.tagName === 'BUTTON' || parent.tagName === 'A' || parent.onclick || parent.hasAttribute('role')) {
downloadBtn = parent;
break;
}
parent = parent.parentElement;
depth++;
}
}
}
console.log('�� ExtractFileInfo risultato:', {
filename: safeBase,
hasUrl: !!url,
urlPreview: url ? url.substring(0, 80) : 'none',
hasDownloadBtn: !!downloadBtn,
btnType: downloadBtn ? downloadBtn.tagName : 'none'
});
return {
filename: safeBase,
url,
downloadBtn,
element: fileEl,
ts
};
}
async function scanChatProgressively() {
console.log('�� === INIZIO scanChatProgressively ===');
const container = findMessagesContainer();
if (!container) {
// console.error('❌ Container messaggi non trovato!');
return { found: 0, scanned: 0 };
}
console.log('✅ Container trovato:', container.className);
const typesStr = downloadTypes.map(t => t.charAt(0).toUpperCase() + t.slice(1)).join(', ');
showNotification(`�� Scanning ${typesStr} in chat…`, 'info');
// CRITICO: Scroll in fondo più volte per forzare il caricamento dei messaggi recenti
console.log('�� Scrolling to bottom to load recent messages...');
for (let i = 0; i < 3; i++) {
container.scrollTop = container.scrollHeight;
await sleep(400);
console.log(`�� Scroll attempt ${i + 1}/3: scrollTop=${container.scrollTop}, scrollHeight=${container.scrollHeight}`);
}
let downloaded = 0;
let scannedCount = 0;
const seen = new WeakSet();
const processedFiles = new Set(); // Traccia filename già processati
let stable = 0, prevTop = null, prevHeight = -1;
// NUOVO: Stop intelligente per filtri date
let consecutiveOutOfRange = 0;
const MAX_CONSECUTIVE_OUT_OF_RANGE = 25; // Se 25 elementi consecutivi fuori range/tipo, stop
console.log('�� Inizio loop scansione...');
while (isDownloading) {
const mediaElements = getAllChatMediaElements();
console.log(`�� Loop iteration: ${mediaElements.length} elementi trovati`);
let foundInRangeThisIteration = false;
let newElementsProcessed = 0; // Conta elementi nuovi (non già visti)
for (const el of mediaElements) {
if (!isDownloading) break;
if (seen.has(el)) continue;
seen.add(el);
newElementsProcessed++; // Elemento nuovo trovato
const elementType = getElementType(el);
if (!elementType) {
console.log('⏭️ Elemento senza tipo riconosciuto');
consecutiveOutOfRange++;
continue;
}
if (!downloadTypes.includes(elementType)) {
console.log(`⏭️ Tipo ${elementType} non selezionato, skip`);
consecutiveOutOfRange++;
continue;
}
console.log(`✅ Elemento valido: ${elementType}`);
scannedCount++;
const msg = el.closest('.message,.Message,.bubble,[class*="message"]');
const ts = getMessageTimestamp(msg);
// Filtra per data range
if (!passDateFilter(ts)) {
const tooRecent = dateTo && ts > dateTo;
const tooOld = dateFrom && ts < dateFrom;
if (tooRecent) {
// Troppo recente: continua a scrollare indietro senza incrementare counter
console.log('⏭️ Messaggio troppo recente (dopo dateTo), continuo a scrollare:', new Date(ts).toLocaleDateString());
continue;
}
if (tooOld) {
// Troppo vecchio: incrementa counter e fermati se troppi consecutivi
consecutiveOutOfRange++;
console.log('⏭️ Messaggio troppo vecchio (prima di dateFrom):', new Date(ts).toLocaleDateString());
if (consecutiveOutOfRange >= 10) {
console.log(`⛔ Stop: ${consecutiveOutOfRange} messaggi consecutivi troppo vecchi`);
showNotification(`⛔ Scrolled past date range`, 'info');
isDownloading = false;
break;
}
continue;
}
// Altri casi (non dovrebbe succedere)
console.log('⏭️ Skipping file outside date range:', new Date(ts).toLocaleDateString());
continue;
}
// Reset contatore se troviamo elemento valido
consecutiveOutOfRange = 0;
foundInRangeThisIteration = true;
// Gestione documenti/file
if (elementType === 'documents' || elementType === 'audio') {
const info = extractFileInfo(el);
// Check duplicati basato su filename + timestamp
const fileKey = `${info.filename}_${info.ts}`;
if (processedFiles.has(fileKey)) {
console.log('⏭️ File già processato, skip:', info.filename);
continue;
}
processedFiles.add(fileKey);
console.log(`${elementType === 'audio' ? '��' : '��'} ${elementType} trovato:`, {
filename: info.filename,
timestamp: new Date(info.ts).toLocaleString()
});
// Usa click destro + "Scarica" per documenti e audio
console.log('�� Uso click destro per scaricare...');
// Click destro sull'elemento
const targetElement = el.querySelector('.document, audio-element') || el;
const contextMenuEvent = new MouseEvent('contextmenu', {
bubbles: true,
cancelable: true,
view: window,
button: 2
});
targetElement.dispatchEvent(contextMenuEvent);
// Aspetta che il menu contestuale appaia (più tempo per audio)
await sleep(700);
// Cerca il MenuItem download - supporta sia Telegram A che K
// Telegram A: .MenuItem[role="menuitem"] con i.icon-download
// Telegram K: .btn-menu-item con span contenente "Download"
let downloadMenuItem = Array.from(document.querySelectorAll('.MenuItem[role="menuitem"]'))
.find(item => item.querySelector('i.icon-download'));
if (!downloadMenuItem) {
// Telegram K
const menuItems = Array.from(document.querySelectorAll('.btn-menu-item'));
downloadMenuItem = menuItems.find(item => {
const text = item.textContent.toLowerCase().trim();
return text.includes('download') || text.includes('scarica') || text.includes('descargar');
});
}
if (downloadMenuItem) {
console.log('��️ Click su Download menu item');
downloadMenuItem.click();
incrementDownloadCount();
downloaded++;
await sleep(800); // Aspetta che il download parta e il menu si chiuda
} else {
console.warn('⚠️ Download menu item non trovato per documento');
// Chiudi menu cliccando fuori
document.body.click();
await sleep(300);
}
}
// Gestione media (immagini/video)
else {
if (elementType === 'videos') {
// Check duplicati per video basato su timestamp
const videoKey = `video_${ts}`;
if (processedFiles.has(videoKey)) {
console.log('⏭️ Video già processato, skip');
continue;
}
processedFiles.add(videoKey);
console.log('�� Video trovato, faccio click destro...');
// Trova il video element
const videoElement = el.querySelector('video') || el;
// Simula click destro (contextmenu)
const contextMenuEvent = new MouseEvent('contextmenu', {
bubbles: true,
cancelable: true,
view: window,
button: 2
});
videoElement.dispatchEvent(contextMenuEvent);
// Aspetta che il menu contestuale appaia
await sleep(500);
// Cerca il MenuItem download - supporta sia Telegram A che K
// Telegram A: .MenuItem[role="menuitem"] con i.icon-download
// Telegram K: .btn-menu-item con span contenente "Download"
let downloadMenuItem = Array.from(document.querySelectorAll('.MenuItem[role="menuitem"]'))
.find(item => item.querySelector('i.icon-download'));
if (!downloadMenuItem) {
// Telegram K
downloadMenuItem = Array.from(document.querySelectorAll('.btn-menu-item'))
.find(item => {
const text = item.textContent.toLowerCase().trim();
return text.includes('download') || text.includes('scarica') || text.includes('descargar');
});
}
if (downloadMenuItem) {
console.log('��️ Click su Download menu item');
downloadMenuItem.click();
incrementDownloadCount();
downloaded++;
await sleep(800); // Aspetta che il download parta e il menu si chiuda
} else {
console.warn('⚠️ MenuItem con icon-download non trovato');
// Chiudi menu cliccando fuori
document.body.click();
await sleep(300);
}
} else {
// Per immagini usa l'URL normale
const url = extractMediaURL(el);
if (url) {
const ext = guessExtFromUrl(url, 'jpg');
if (shouldDownloadFile(ext)) {
const prefix = 'IMG';
console.log('��️ Immagine trovata:', {
type: elementType,
timestamp: new Date(ts).toLocaleString(),
ext: ext,
url: url.substring(0, 80) + '...'
});
const ok = downloadFile(url, `${prefix}_${fmtTs(ts)}_${downloaded}.${ext}`, downloadAsZip);
if (ok) downloaded++;
}
}
}
}
if (downloadCount >= maxDownloads) {
isDownloading = false;
console.log('⛔ Limite massimo download raggiunto');
break;
}
await sleep(90);
}
// Se non stiamo più scaricando (stop manuale o limite raggiunto), esci
if (!isDownloading) break;
prevTop = container.scrollTop;
container.scrollBy({ top: -Math.floor(container.clientHeight * 0.9) });
await sleep(600);
const sameHeight = container.scrollHeight === prevHeight;
const sameTop = container.scrollTop === prevTop;
prevHeight = container.scrollHeight;
// Check 1: Posizione stabile (non scrolla più) o raggiunto inizio
if (sameHeight && sameTop) stable++; else stable = 0;
if (stable >= 3 || container.scrollTop <= 0) {
console.log('⛔ Stop scroll: raggiunto inizio o posizione stabile');
break;
}
// Check 2: Nessun nuovo elemento processato E posizione non cambia
// (significa che abbiamo già visto tutto ciò che c'è da vedere)
if (newElementsProcessed === 0 && stable >= 1) {
console.log('⛔ Stop: nessun nuovo elemento trovato dopo scroll');
break;
}
}
console.log(`✅ === FINE scanChatProgressively === Scanned: ${scannedCount}, Downloaded: ${downloaded}`);
// Invia stato di completamento al popup
chrome.runtime.sendMessage({
type: 'downloadComplete',
scanned: scannedCount,
downloaded: downloaded
});
if (!downloadAsZip) showNotification(`✅ Chat scan: ${downloaded} files`, 'success');
return { found: downloaded, scanned: scannedCount }; // Ritorna entrambi
}
/*********** MEDIA PANEL HELPERS ***********/
function elContainsText(el, words) {
const t = (el.textContent || '').trim().toLowerCase();
return words.some(w => t.includes(w.toLowerCase()));
}
function findHeaderButtons() {
const scopes = ['header','.topbar','.TopBar','.header','.chat-header','.peer-title','.chat-info'];
const arr = [];
for (const sel of scopes) document.querySelectorAll(sel+' button,'+sel+' [role="button"]').forEach(b=>arr.push(b));
return Array.from(new Set(arr));
}
async function openRightInfoPanel() {
if (document.querySelector('[role="tablist"],.tabs,.profile-tabs')) return true;
const btns = findHeaderButtons();
for (const b of btns) {
const label = (b.getAttribute('aria-label') || b.getAttribute('title') || b.textContent || '').toLowerCase();
if (LABELS.INFO.some(x => label.includes(x.toLowerCase()))) {
b.click(); await sleep(350);
if (document.querySelector('[role="tablist"],.tabs')) return true;
}
}
const headerAreas = ['.chat-header','.TopBar','.topbar','header','.peer-title','.chat-info'];
for (const sel of headerAreas) {
const el = document.querySelector(sel);
if (el) {
el.click();
await sleep(350);
if (document.querySelector('[role="tablist"],.tabs')) return true;
}
}
return !!document.querySelector('[role="tablist"],.tabs');
}
function clickTabByLabel(words) {
const tabs = Array.from(document.querySelectorAll('[role="tab"], .tabs *'));
for (const t of tabs) {
if (elContainsText(t, words)) {
t.click();
return true;
}
}
const side = document.querySelector('.sidebar,.side,.third-column,[class*="Sidebar"],[class*="Profile"]') || document;
const candidates = Array.from(side.querySelectorAll('button,[role="button"],a,.item'));
for (const c of candidates) {
if (elContainsText(c, words)) {
c.click();
return true;
}
}
return false;
}
function findScrollableWithMany(root, selector, minCount=5) {
const list = Array.from(root.querySelectorAll('div,section,main,ul'));
const scrollables = list.filter(el => {
const st = getComputedStyle(el);
return /(auto|scroll)/.test(st.overflowY) && el.scrollHeight > el.clientHeight;
});
let best=null, score=-1;
for (const el of scrollables) {
const c = el.querySelectorAll(selector).length;
if (c>score){best=el;score=c;}
}
return (score>=minCount)?best:null;
}
async function downloadFromViewerOrThumb(thumbEl, idx, expectedType) {
thumbEl.click();
await sleep(320);
const dlSelectors = [
'button[aria-label*="Download"]','button[title*="Download"]','button.download',
'[class*="download"] button','[class*="Download"]'
];
let clicked = false;
for (const sel of dlSelectors) {
const b = document.querySelector(sel);
if (b) {
b.click();
incrementDownloadCount();
clicked = true;
break;
}
}
if (!clicked) {
let url = null;
const viewer = document.querySelector('.modal, .viewer, [class*="Viewer"]');
if (expectedType === 'videos') {
const vid = viewer?.querySelector('video') || document.querySelector('video');
url = vid ? (vid.currentSrc || vid.src) : null;
} else if (expectedType === 'images') {
const bigImg = viewer?.querySelector('img') || document.querySelector('img[loading="eager"]');
url = bigImg ? (bigImg.currentSrc || bigImg.src) : null;
}
if (!url) {
url = extractMediaURL(thumbEl);
}
if (url) {
const ext = guessExtFromUrl(url, expectedType === 'videos' ? 'mp4' : 'jpg');
if (shouldDownloadFile(ext)) {
const prefix = expectedType === 'videos' ? 'VID' : 'IMG';
downloadFile(url, `${prefix}_${fmtTs(Date.now())}_${idx}.${ext}`, downloadAsZip);
}
}
}
const closeBtn = document.querySelector('button[aria-label*="Close"], .modal [class*="close"], .viewer [class*="close"]');
if (closeBtn) closeBtn.click();
else document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
await sleep(160);
}
/*********** MEDIA PANEL SCAN ***********/
async function scanMediaPanel() {
showNotification('��️ Opening Media panel…', 'info');
const ok = await openRightInfoPanel();
if (!ok) {
showNotification('❌ Cannot open Info/Media panel', 'error');
return { found: 0 };
}
if (!clickTabByLabel(LABELS.MEDIA)) {
showNotification('❌ Media tab not found', 'error');
return { found: 0 };
}
await sleep(320);
const panel = document.querySelector('.sidebar,.side,.third-column,[class*="Sidebar"],[class*="Profile"]') || document;
let downloaded = 0;
// Scan per immagini e video se richiesti
if (downloadTypes.includes('images') || downloadTypes.includes('videos')) {
const grid = findScrollableWithMany(panel, 'img, video, picture, [style*="background-image"]', 3);
if (grid) {
showNotification('�� Scanning images/videos in Media panel…', 'info');
const seen = new WeakSet();
grid.scrollTop = grid.scrollHeight;
await sleep(350);
while (isDownloading) {
const thumbs = Array.from(grid.querySelectorAll('img, video, picture, [style*="background-image"]'))
.map(el => el.tagName === 'SOURCE' ? (el.parentElement||el) : el);
for (const t of thumbs) {
if (!isDownloading) break;
if (seen.has(t)) continue;
seen.add(t);
const type = getElementType(t);
if (!type || !downloadTypes.includes(type)) continue;
await downloadFromViewerOrThumb(t, downloaded, type);
downloaded++;
if (downloadCount >= maxDownloads) {
isDownloading = false;
break;
}
await sleep(120);
}
const prev = grid.scrollTop;
grid.scrollBy({ top: -Math.floor(grid.clientHeight*0.9) });
await sleep(600);
if (grid.scrollTop===prev || grid.scrollTop<=0) break;
}
}
}
// Scan per documenti e audio se richiesti
if (downloadTypes.includes('documents') || downloadTypes.includes('audio')) {
if (!clickTabByLabel(LABELS.FILES)) {
clickTabByLabel(LABELS.MEDIA);
await sleep(250);
clickTabByLabel(LABELS.FILES);
}
await sleep(320);
const list = findScrollableWithMany(panel, 'a,button,[class*="file"],[class*="Document"]', 2) || panel;
showNotification('�� Scanning files in Files panel…', 'info');
const seen = new WeakSet();
list.scrollTop = 0;
await sleep(280);
while (isDownloading) {
const rows = Array.from(list.querySelectorAll('.Document,.document,.File,.file,.row,.list-item,[class*="file"],[class*="Document"]'));
for (const r of rows) {
if (!isDownloading) break;
if (seen.has(r)) continue;
seen.add(r);
const type = getElementType(r);
if (!type || !downloadTypes.includes(type)) continue;
const info = extractFileInfo(r);
if (info.url) {
const ext = guessExtFromUrl(info.url, 'bin');
if (shouldDownloadFile(ext)) {
const ok = downloadFile(info.url, `${info.filename}.${ext}`, downloadAsZip);
if (ok) downloaded++;
}
} else if (info.downloadBtn) {
info.downloadBtn.click();
incrementDownloadCount();
downloaded++;
}
if (downloadCount >= maxDownloads) {
isDownloading = false;
break;
}
await sleep(140);
}
const prev = list.scrollTop;
list.scrollBy({ top: list.clientHeight*0.9 });
await sleep(620);
if (list.scrollTop===prev || list.scrollTop+list.clientHeight>=list.scrollHeight-2) break;
}
}
if (!downloadAsZip) showNotification(`✅ Media panel: ${downloaded} files`, 'success');
return { found: downloaded };
}
/*********** ORCHESTRATION ***********/
async function downloadAllMedia() {
console.log('�� START downloadAllMedia');
console.log('�� Config:', {
scanMode,
downloadTypes,
downloadAsZip,
dateFrom: dateFrom ? new Date(dateFrom).toLocaleString() : 'none',
dateTo: dateTo ? new Date(dateTo).toLocaleString() : 'none'
});
// Notifica popup che il download è iniziato
chrome.runtime.sendMessage({ type: 'downloadStarted' });
if (scanMode === 'media') {
console.log('�� Mode: Media panel only');
await scanMediaPanel();
if (downloadAsZip && zipFiles.length) await createAndDownloadZip();
return;
}
if (scanMode === 'chat') {
console.log('�� Mode: Chat only');
await scanChatProgressively();
if (downloadAsZip && zipFiles.length) await createAndDownloadZip();
return;
}
// Auto mode - prova prima la chat, poi il media panel SOLO se NON ha trovato elementi nella chat
console.log('�� Mode: Auto (chat first)');
const chat = await scanChatProgressively();
console.log('�� Chat scan result:', {
downloaded: chat.found,
scanned: chat.scanned
});
// SOLO se NON ha trovato NESSUN elemento valido nella chat (scanned === 0), prova il media panel
// Se ha trovato elementi ma sono stati scartati per data, NON prova il media panel
if (isDownloading && chat.scanned === 0) {
console.log('⚠️ Nessun elemento trovato in chat, provo media panel...');
await scanMediaPanel();
} else if (dateFrom || dateTo) {
// SOLO se c'è un filtro date mostra il messaggio
if (chat.scanned > 0 && chat.found === 0) {
console.log('ℹ️ Trovati', chat.scanned, 'elementi ma tutti fuori dal range date');
showNotification(`ℹ️ Found ${chat.scanned} files but all outside date range`, 'info');
}
} else {
console.log('✅ Scan completato dalla chat');
}
if (downloadAsZip && zipFiles.length) {
console.log('�� Creazione ZIP con', zipFiles.length, 'files');
await createAndDownloadZip();
} else if (zipFiles.length === 0 && chat.found === 0 && chat.scanned > 0) {
console.warn('⚠️ File trovati ma nessun URL disponibile per ZIP');
showNotification('⚠️ Files found but no direct URL available for download', 'warning');
}
console.log('�� FINE downloadAllMedia');
}
/*********** MESSAGES ***********/
chrome.runtime.onMessage.addListener((request) => {
console.log('�� Messaggio ricevuto:', request.type);
if (request.type === 'startDownload') {
console.log('�� START DOWNLOAD request ricevuto');
if (!isChatSelected()) {
// console.error('❌ Chat non selezionata!');
showNotification('❌ Please select a Chat/Group first!', 'error');
chrome.runtime.sendMessage({
type: 'error',
message: '❌ Please select a Chat or Group first!'
});
return;
}
console.log('✅ Chat selezionata, procedo...');
// Notifica popup che il download è iniziato
chrome.runtime.sendMessage({ type: 'downloadStarted' });
// Sync PRO state before starting
chrome.storage?.local?.get(['isFullVersion'], (res) => {
setProMode(!!res?.isFullVersion);
console.log('�� PRO mode:', isPro);
isDownloading = true;
dateFrom = request.dateFrom || null;
dateTo = request.dateTo || null;
downloadAsZip = !!request.downloadAsZip;
scanMode = request.scanMode || 'auto';
downloadTypes = request.downloadTypes || ['images']; // Default a images
console.log('�� Configurazione:', {
dateFrom: dateFrom ? new Date(dateFrom).toLocaleString() : 'none',
dateTo: dateTo ? new Date(dateTo).toLocaleString() : 'none',
downloadAsZip,
scanMode,
downloadTypes
});
zipFiles = [];
downloadedURLs.clear();
downloadAllMedia();
});
} else if (request.type === 'stopDownload') {
console.log('⛔ STOP DOWNLOAD request');
isDownloading = false;
showNotification('⛔ Download stopped', 'info');
if (downloadAsZip && zipFiles.length > 0) {
setTimeout(async () => {
if (!isDownloading && zipFiles.length) {
console.log('�� Creazione ZIP dopo stop:', zipFiles.length, 'files');
await createAndDownloadZip();
}
}, 250);
}
}
});
console.log('✅ Telegram Downloader loaded');
})();
console