const STORAGE_KEY = "auth_jwt"; const BACKEND_URL = "http://localhost:5173"; // TODO: Backend URL konfigurieren const PARSE_TIMEOUT_MS = 15000; // 15 seconds const SCAN_TIMEOUT_MS = 45000; // 45 seconds (listing with _ipg=240 can be slow) const activeParseRequests = new Map(); // Map const activeScanRequests = new Map(); // Map; /** Aktueller Scan-Fortschritt für GET_SCAN_PROGRESS (Polling durch Web-App) */ let currentScanProgress = null; // Messages from content script (der von der Web-App kommt) chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // Auth messages vom Content Script if (msg?.type === "AUTH_JWT" && msg.jwt) { chrome.storage.local.set({ [STORAGE_KEY]: msg.jwt }).then(() => { sendResponse({ ok: true }); }); return true; // async } if (msg?.type === "AUTH_CLEARED") { chrome.storage.local.remove(STORAGE_KEY).then(() => { sendResponse({ ok: true }); }); return true; } // API calls vom Popup if (msg?.type === "GET_JWT") { getJwt().then(jwt => { sendResponse({ jwt }); }); return true; } if (msg?.type === "CALL_API" && msg.path) { callProtectedApi(msg.path, msg.payload).then(data => { sendResponse({ ok: true, data }); }).catch(err => { sendResponse({ ok: false, error: err.message }); }); return true; } // eBay Parsing Request (from Web App via content script or directly) if (msg?.action === "PARSE_URL" && msg.url) { handleParseRequest(msg.url, sendResponse); return true; // async } // eBay Parsing Response (from eBay content script) if (msg?.action === "PARSE_COMPLETE") { handleParseComplete(sender.tab?.id, msg.data); return true; } }); // Handle messages from external web apps (externally_connectable) chrome.runtime.onMessageExternal.addListener((msg, sender, sendResponse) => { if (msg?.action === "PARSE_URL" && msg.url) { handleParseRequest(msg.url, sendResponse); return true; // async } if (msg?.action === "SCAN_PRODUCTS" && msg.url && msg.accountId) { handleScanProductsRequest(msg.url, msg.accountId, sendResponse); return true; // async } if (msg?.action === "GET_SCAN_PROGRESS") { sendResponse(currentScanProgress ?? { percent: 0, phase: "idle", total: 0, current: 0, complete: false }); return false; } }); /** * Handles eBay URL parsing request * Creates a hidden tab, waits for load, sends parse message to content script */ async function handleParseRequest(url, sendResponse) { try { // Validate URL if (!url || typeof url !== 'string' || !url.toLowerCase().includes('ebay.')) { sendResponse({ ok: false, error: "Invalid eBay URL" }); return; } console.log("[BACKGROUND] Creating tab for parse request:", url); // Create hidden tab const tab = await chrome.tabs.create({ url: url, active: false }); const tabId = tab.id; console.log("[BACKGROUND] Tab created:", tabId); // Set up timeout const timeoutId = setTimeout(() => { cleanupParseRequest(tabId, null, { ok: false, error: "timeout" }); }, PARSE_TIMEOUT_MS); // Store request info activeParseRequests.set(tabId, { timeout: timeoutId, sendResponse: sendResponse }); // Wait for tab to load, then send parse message const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => { if (updatedTabId !== tabId) return; // Tab is fully loaded if (changeInfo.status === 'complete' && updatedTab.url) { chrome.tabs.onUpdated.removeListener(checkTabLoaded); console.log("[BACKGROUND] Tab loaded, sending parse message:", updatedTab.url); // Wait longer for content script to auto-load (if it does) // Content scripts from manifest.json might need more time in hidden tabs setTimeout(() => { sendParseMessageWithRetry(tabId, 0); }, 2000); // 2 seconds delay - give content script time to auto-load } }; chrome.tabs.onUpdated.addListener(checkTabLoaded); } catch (error) { console.error("Error in handleParseRequest:", error); sendResponse({ ok: false, error: error.message || "Unknown error" }); } } /** * Checks if a URL is an eBay URL * @param {string} url - URL to check * @returns {boolean} - true if URL is an eBay URL */ function isEbayUrl(url) { if (!url) return false; const urlLower = url.toLowerCase(); return urlLower.includes('ebay.'); } /** * Injects the eBay content script manually if it's not already loaded * @param {number} tabId - Tab ID * @returns {Promise} - true if injection successful or already loaded */ async function ensureContentScriptInjected(tabId) { try { // First, check if tab URL is an eBay URL const tab = await chrome.tabs.get(tabId); if (!isEbayUrl(tab.url)) { console.log("[BACKGROUND] Tab is not an eBay URL:", tab.url); return false; } // Check if content script is already loaded by sending a ping try { await chrome.tabs.sendMessage(tabId, { action: "PING" }); console.log("[BACKGROUND] Content script already loaded"); return true; } catch (pingError) { // Content script not loaded, try to inject it manually console.log("[BACKGROUND] Content script not found, injecting manually..."); try { // Get tab info to check frames const tabInfo = await chrome.tabs.get(tabId); // Inject full ebay-content-script.js so PING, PARSE_EBAY and PARSE_PRODUCT_LIST are all handled. // The previous inline injection only handled PING+PARSE_EBAY; SCAN sends PARSE_PRODUCT_LIST. try { await chrome.scripting.executeScript({ target: { tabId: tabId, frameIds: [0] }, files: ["ebay-content-script.js"] }); console.log("[BACKGROUND] ebay-content-script.js injected successfully"); } catch (injectError) { console.error("[BACKGROUND] Failed to inject ebay-content-script:", injectError); throw injectError; } // Also try injecting a simple test script to verify injection works try { await chrome.scripting.executeScript({ target: { tabId: tabId, frameIds: [0] }, func: () => { console.log("[TEST-INJECTION] Test script executed in main frame"); } }); } catch (testError) { } // Wait longer for the script to fully initialize and register message listeners // Retry PING to verify the script is ready // Also try sending to frameId 0 explicitly for (let pingAttempt = 0; pingAttempt < 10; pingAttempt++) { await new Promise(resolve => setTimeout(resolve, 300)); // 300ms between pings try { // Try sending to main frame explicitly try { await chrome.tabs.sendMessage(tabId, { action: "PING" }, { frameId: 0 }); } catch (frameErr) { // Fallback: try without frameId await chrome.tabs.sendMessage(tabId, { action: "PING" }); } console.log("[BACKGROUND] Content script ready after injection"); return true; } catch (pingErr) { // Continue to next attempt } } // Even if PING failed, return true - the script might still work console.warn("[BACKGROUND] Content script injected but PING not responding"); return true; } catch (injectError) { console.error("[BACKGROUND] Failed to inject content script:", injectError); return false; } } } catch (error) { console.error("[BACKGROUND] Error checking/injecting content script:", error); return false; } } /** * Sends parse message to content script with retry mechanism * @param {number} tabId - Tab ID * @param {number} attempt - Current attempt number (0-based) */ async function sendParseMessageWithRetry(tabId, attempt) { const maxAttempts = 3; const retryDelay = 500; // 500ms between retries try { console.log(`[BACKGROUND] Sending parse message (attempt ${attempt + 1}/${maxAttempts}) to tab:`, tabId); // On first attempt, ensure content script is injected if (attempt === 0) { const injected = await ensureContentScriptInjected(tabId); if (!injected) { throw new Error("Could not inject content script"); } } // Check if content script is injected by trying to send a ping // If this fails, the content script might not be loaded yet // Try sending to main frame explicitly first let response; try { response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" }, { frameId: 0 }); } catch (frameErr) { // Fallback: try without frameId response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" }); } console.log("[BACKGROUND] Parse response received:", response); if (response && response.ok && response.data) { handleParseComplete(tabId, response.data); } else { cleanupParseRequest(tabId, null, { ok: false, error: response?.error || "Parsing failed" }); } } catch (err) { // Check if error is due to content script not being ready const runtimeError = chrome.runtime.lastError?.message || ""; const isContentScriptError = err.message?.includes("Could not establish connection") || err.message?.includes("Receiving end does not exist") || err.message?.includes("Could not inject content script") || runtimeError.includes("Receiving end does not exist") || runtimeError.includes("Could not establish connection"); console.log(`[BACKGROUND] Error sending parse message (attempt ${attempt + 1}):`, { error: err.message, runtimeError: runtimeError, isContentScriptError: isContentScriptError }); if (isContentScriptError && attempt < maxAttempts) { // Content script not ready yet, retry after delay console.log(`[BACKGROUND] Content script not ready, retrying in ${retryDelay}ms...`); setTimeout(() => { sendParseMessageWithRetry(tabId, attempt + 1); }, retryDelay); } else { // Max retries reached or different error const errorMessage = isContentScriptError ? "Content script konnte nicht geladen werden. Bitte Extension neu laden." : (err.message || runtimeError || "Content script error"); cleanupParseRequest(tabId, null, { ok: false, error: errorMessage }); } } } /** * Handles parse complete response from content script */ function handleParseComplete(tabId, data) { cleanupParseRequest(tabId, data, null); } /** * Cleans up parse request: closes tab, clears timeout, sends response */ async function cleanupParseRequest(tabId, data, error) { const request = activeParseRequests.get(tabId); if (!request) return; // Clear timeout if (request.timeout) { clearTimeout(request.timeout); } // Remove from active requests activeParseRequests.delete(tabId); // Close tab try { await chrome.tabs.remove(tabId); } catch (err) { // Tab might already be closed console.warn("Could not close tab:", err); } // Send response if (request.sendResponse) { if (error) { request.sendResponse(error); } else if (data) { request.sendResponse({ ok: true, data: data }); } else { request.sendResponse({ ok: false, error: "Unknown error" }); } } } /** * Transform account URL (e.g. /str/topmons) to listing URL with 240 items per page. * Example: https://www.ebay.de/str/topmons?... → https://www.ebay.de/sch/i.html?_ssn=topmons&store_name=topmons&_ipg=240 */ function transformAccountUrlTo240ItemsUrl(accountUrl) { try { const url = new URL(accountUrl); const pathname = url.pathname; const strMatch = pathname.match(/\/str\/([^\/\?]+)/); if (!strMatch || !strMatch[1]) { throw new Error("Could not extract account name from URL (expected /str/{name})"); } const accountName = strMatch[1]; const baseUrl = `${url.protocol}//${url.hostname}`; const listingUrl = `${baseUrl}/sch/i.html?_ssn=${encodeURIComponent(accountName)}&store_name=${encodeURIComponent(accountName)}&_ipg=240`; return listingUrl; } catch (e) { throw new Error(`Failed to transform account URL: ${e.message}`); } } /** * Handles eBay product scan request * Transforms account URL to listing URL with _ipg=240, creates hidden tab, parses, then loads each item in separate tab. */ async function handleScanProductsRequest(url, accountId, sendResponse) { try { // Validate URL if (!url || typeof url !== 'string' || !url.toLowerCase().includes('ebay.')) { sendResponse({ ok: false, error: "Invalid eBay URL" }); return; } let targetUrl = url; try { targetUrl = transformAccountUrlTo240ItemsUrl(url); console.log("[BACKGROUND] Scan using listing URL (240 items):", targetUrl); } catch (transformErr) { console.warn("[BACKGROUND] URL transform failed, using original URL:", transformErr.message); // Fallback: use original URL (e.g. already /sch/ or non-/str/) } // Create hidden tab const tab = await chrome.tabs.create({ url: targetUrl, active: false }); const tabId = tab.id; // Set up timeout const timeoutId = setTimeout(() => { cleanupScanRequest(tabId, null, { ok: false, error: "timeout" }); }, SCAN_TIMEOUT_MS); // Store request info activeScanRequests.set(tabId, { timeout: timeoutId, sendResponse: sendResponse }); currentScanProgress = { percent: 0, phase: "listing", total: 0, current: 0, complete: false }; // Wait for tab to load, then send parse message const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => { if (updatedTabId !== tabId) return; // Tab is fully loaded if (changeInfo.status === 'complete' && updatedTab.url) { chrome.tabs.onUpdated.removeListener(checkTabLoaded); // Small delay to ensure DOM is ready setTimeout(() => { sendScanMessageWithRetry(tabId, 0); }, 1000); // 1 second delay for DOM ready } }; chrome.tabs.onUpdated.addListener(checkTabLoaded); } catch (error) { console.error("Error in handleScanProductsRequest:", error); sendResponse({ ok: false, error: error.message || "Unknown error" }); } } /** * Sends scan message to content script with retry mechanism * @param {number} tabId - Tab ID * @param {number} attempt - Current attempt number (0-based) */ async function sendScanMessageWithRetry(tabId, attempt) { const maxAttempts = 3; const retryDelay = 500; // 500ms between retries try { console.log(`[BACKGROUND] Sending scan message (attempt ${attempt + 1}/${maxAttempts}) to tab:`, tabId); // On first attempt, ensure content script is injected if (attempt === 0) { const injected = await ensureContentScriptInjected(tabId); if (!injected) { throw new Error("Could not inject content script"); } } const response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_PRODUCT_LIST" }); if (response && response.ok) { // Prüfe ob items vorhanden und nicht leer const items = response.items || response.data?.items || []; const meta = response.meta || response.data?.meta || {}; // Log meta für Debugging console.log("[SCAN meta]", meta); if (items.length === 0) { // Leere items: behandele als Fehler mit meta cleanupScanRequest(tabId, null, { ok: false, error: "empty_items", meta: meta }); } else { if (currentScanProgress) { currentScanProgress.phase = "details"; currentScanProgress.total = items.length; currentScanProgress.percent = 10; } await handleScanComplete(tabId, { items, meta }); } } else { // Fehler: sende error + meta const meta = response?.meta || {}; console.log("[SCAN meta]", meta); cleanupScanRequest(tabId, null, { ok: false, error: response?.error || "Parsing failed", meta: meta }); } } catch (err) { // Check if error is due to content script not being ready const runtimeError = chrome.runtime.lastError?.message || ""; const isContentScriptError = err.message?.includes("Could not establish connection") || err.message?.includes("Receiving end does not exist") || err.message?.includes("Could not inject content script") || runtimeError.includes("Receiving end does not exist") || runtimeError.includes("Could not establish connection"); console.log(`[BACKGROUND] Error sending scan message (attempt ${attempt + 1}):`, { error: err.message, runtimeError: runtimeError, isContentScriptError: isContentScriptError }); if (isContentScriptError && attempt < maxAttempts) { // Content script not ready yet, retry after delay console.log(`[BACKGROUND] Content script not ready, retrying in ${retryDelay}ms...`); setTimeout(() => { sendScanMessageWithRetry(tabId, attempt + 1); }, retryDelay); } else { // Max retries reached or different error const errorMessage = isContentScriptError ? "Content script konnte nicht geladen werden. Bitte Extension neu laden." : (err.message || runtimeError || "Content script error"); cleanupScanRequest(tabId, null, { ok: false, error: errorMessage, meta: { pageType: "unknown", finalUrl: "", attempts: attempt + 1, reason: "content_script_error" } }); } } } const ITEM_TAB_LOAD_TIMEOUT_MS = 3000; /** * Opens each item URL in a separate background tab, waits for load, parses detail page * (title, price, currency, category, condition), merges into item, then closes tab. */ async function loadAndParseEachItemTab(items) { if (!Array.isArray(items) || items.length === 0) return; const total = items.length; for (let i = 0; i < total; i++) { const item = items[i]; if (!item || !item.url) continue; try { const tab = await chrome.tabs.create({ url: item.url, active: false }); await new Promise((resolve) => { const listener = (tabId, changeInfo) => { if (tabId === tab.id && changeInfo.status === "complete") { chrome.tabs.onUpdated.removeListener(listener); resolve(); } }; chrome.tabs.onUpdated.addListener(listener); setTimeout(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, ITEM_TAB_LOAD_TIMEOUT_MS); }); const injected = await ensureContentScriptInjected(tab.id); if (!injected) { console.warn(`[BACKGROUND] Content script not ready in item tab ${i + 1}, skipping detail parse`); } try { const response = injected ? await chrome.tabs.sendMessage(tab.id, { action: "PARSE_ITEM_DETAIL" }) : null; if (response?.ok && response?.data) { const d = response.data; if (d.title != null && d.title !== "") item.title = d.title; if (d.price != null) item.price = d.price; if (d.currency != null && d.currency !== "") item.currency = d.currency; if (d.category != null && d.category !== "") item.category = d.category; if (d.condition != null && d.condition !== "") item.condition = d.condition; if (d.quantityAvailable != null) item.quantityAvailable = d.quantityAvailable; if (d.quantitySold != null) item.quantitySold = d.quantitySold; if (d.watchCount != null) item.watchCount = d.watchCount; if (d.inCartsCount != null) item.inCartsCount = d.inCartsCount; } } catch (parseErr) { console.warn(`[BACKGROUND] PARSE_ITEM_DETAIL failed for ${item.url}:`, parseErr); } if (currentScanProgress && total > 0) { currentScanProgress.current = i + 1; currentScanProgress.percent = Math.min(99, 10 + Math.round((90 * (i + 1)) / total)); } try { await chrome.tabs.remove(tab.id); } catch (removeErr) { console.warn(`[BACKGROUND] Could not close item tab:`, removeErr); } } catch (e) { console.warn(`[BACKGROUND] Failed to load/parse item tab ${i + 1}/${total}:`, item.url, e); } } } /** * Handles scan complete response from content script */ async function handleScanComplete(tabId, data) { const request = activeScanRequests.get(tabId); if (request && request.timeout) { clearTimeout(request.timeout); request.timeout = null; } const items = data?.items || []; if (items.length > 0) { await loadAndParseEachItemTab(items); } if (currentScanProgress) { currentScanProgress.percent = 100; currentScanProgress.complete = true; currentScanProgress.current = items.length; } await cleanupScanRequest(tabId, data, null); } /** * Cleans up scan request: closes tab, clears timeout, sends response */ async function cleanupScanRequest(tabId, data, error) { const request = activeScanRequests.get(tabId); if (!request) return; // Clear timeout if (request.timeout) { clearTimeout(request.timeout); } // Remove from active requests activeScanRequests.delete(tabId); currentScanProgress = null; // Close tab (always, even on error) try { await chrome.tabs.remove(tabId); } catch (err) { // Tab might already be closed console.warn("Could not close tab:", err); } // Send response if (request.sendResponse) { if (error) { // error kann bereits meta enthalten request.sendResponse(error); } else if (data) { // data kann items + meta enthalten request.sendResponse({ ok: true, data: data }); } else { request.sendResponse({ ok: false, error: "Unknown error" }); } } } export async function getJwt() { const data = await chrome.storage.local.get(STORAGE_KEY); return data[STORAGE_KEY] || ""; } export async function callProtectedApi(path, payload) { const jwt = await getJwt(); if (!jwt) throw new Error("Not authed"); const res = await fetch(`${BACKEND_URL}${path}`, { method: "POST", headers: { "content-type": "application/json", "authorization": `Bearer ${jwt}` }, body: JSON.stringify(payload || {}) }); if (!res.ok) throw new Error(`API error: ${res.status}`); return await res.json(); }