Spaces:
Running
Running
Ryan Christian D. Deniega
Fix: Overhaul Facebook post detection to filtered comments using [role='feed'] strategy and improve button positioning
7e55328 | /** | |
| * PhilVerify β Background Service Worker (Manifest V3) | |
| * | |
| * Responsibilities: | |
| * - Proxy API calls to the PhilVerify FastAPI backend | |
| * - File-based cache via chrome.storage.local (24-hour TTL, max 50 entries) | |
| * - Maintain personal verification history | |
| * - Respond to messages from content.js and popup.js | |
| * | |
| * Message types handled: | |
| * VERIFY_TEXT { text } β VerificationResponse | |
| * VERIFY_URL { url } β VerificationResponse | |
| * GET_HISTORY {} β { history: HistoryEntry[] } | |
| * GET_SETTINGS {} β { apiBase, autoScan } | |
| * SAVE_SETTINGS { apiBase, autoScan } β {} | |
| */ | |
| const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours | |
| const MAX_HISTORY = 50 | |
| // ββ Default settings ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const DEFAULT_SETTINGS = { | |
| apiBase: 'https://philverify.web.app/api', | |
| autoScan: true, // Automatically scan Facebook feed posts | |
| } | |
| // ββ Utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** Validate that a string is a safe http/https URL */ | |
| function isHttpUrl(str) { | |
| if (!str || typeof str !== 'string') return false | |
| try { | |
| const u = new URL(str) | |
| return u.protocol === 'http:' || u.protocol === 'https:' | |
| } catch { return false } | |
| } | |
| async function sha256prefix(text, len = 16) { | |
| const buf = await crypto.subtle.digest( | |
| 'SHA-256', | |
| new TextEncoder().encode(text.trim().toLowerCase()), | |
| ) | |
| return Array.from(new Uint8Array(buf)) | |
| .map(b => b.toString(16).padStart(2, '0')) | |
| .join('') | |
| .slice(0, len) | |
| } | |
| async function getSettings() { | |
| const stored = await chrome.storage.local.get('settings') | |
| return { ...DEFAULT_SETTINGS, ...(stored.settings ?? {}) } | |
| } | |
| // ββ Cache helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function getCached(key) { | |
| const stored = await chrome.storage.local.get(key) | |
| const entry = stored[key] | |
| if (!entry) return null | |
| if (Date.now() - entry.timestamp > CACHE_TTL_MS) { | |
| await chrome.storage.local.remove(key) | |
| return null | |
| } | |
| return entry.result | |
| } | |
| async function setCached(key, result, preview) { | |
| await chrome.storage.local.set({ | |
| [key]: { result, timestamp: Date.now() }, | |
| }) | |
| // Prepend to history list | |
| const { history = [] } = await chrome.storage.local.get('history') | |
| const entry = { | |
| id: key, | |
| timestamp: new Date().toISOString(), | |
| text_preview: preview.slice(0, 80), | |
| verdict: result.verdict, | |
| final_score: result.final_score, | |
| } | |
| const updated = [entry, ...history.filter(h => h.id !== key)].slice(0, MAX_HISTORY) | |
| await chrome.storage.local.set({ history: updated }) | |
| } | |
| // ββ API calls βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function verifyText(text, imageUrl) { | |
| const key = 'txt_' + await sha256prefix(text) | |
| const hit = await getCached(key) | |
| if (hit) return { ...hit, _fromCache: true } | |
| const { apiBase } = await getSettings() | |
| // Build payload β include imageUrl for multimodal (text + image) analysis | |
| const payload = { text } | |
| if (imageUrl && isHttpUrl(imageUrl)) payload.image_url = imageUrl | |
| const res = await fetch(`${apiBase}/verify/text`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(payload), | |
| }) | |
| if (!res.ok) { | |
| const body = await res.json().catch(() => ({})) | |
| throw new Error(body.detail ?? `API error ${res.status}`) | |
| } | |
| const result = await res.json() | |
| await setCached(key, result, text) | |
| return result | |
| } | |
| async function verifyUrl(url) { | |
| const key = 'url_' + await sha256prefix(url) | |
| const hit = await getCached(key) | |
| if (hit) return { ...hit, _fromCache: true } | |
| const { apiBase } = await getSettings() | |
| const res = await fetch(`${apiBase}/verify/url`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ url }), | |
| }) | |
| if (!res.ok) { | |
| const body = await res.json().catch(() => ({})) | |
| throw new Error(body.detail ?? `API error ${res.status}`) | |
| } | |
| const result = await res.json() | |
| await setCached(key, result, url) | |
| return result | |
| } | |
| async function verifyImageUrl(imageUrl) { | |
| const key = 'img_' + await sha256prefix(imageUrl) | |
| const hit = await getCached(key) | |
| if (hit) return { ...hit, _fromCache: true } | |
| const { apiBase } = await getSettings() | |
| const imgRes = await fetch(imageUrl) | |
| if (!imgRes.ok) throw new Error(`Could not fetch image: ${imgRes.status}`) | |
| const blob = await imgRes.blob() | |
| const ct = imgRes.headers.get('content-type') ?? 'image/jpeg' | |
| const ext = ct.includes('png') ? 'png' : ct.includes('webp') ? 'webp' : 'jpg' | |
| const formData = new FormData() | |
| formData.append('file', blob, `image.${ext}`) | |
| const res = await fetch(`${apiBase}/verify/image`, { method: 'POST', body: formData }) | |
| if (!res.ok) { | |
| const body = await res.json().catch(() => ({})) | |
| throw new Error(body.detail ?? `API error ${res.status}`) | |
| } | |
| const result = await res.json() | |
| await setCached(key, result, imageUrl) | |
| return result | |
| } | |
| // ββ Side panel ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Open the side panel when the toolbar icon is clicked (stays open while browsing) | |
| chrome.action.onClicked.addListener((tab) => { | |
| chrome.sidePanel.open({ windowId: tab.windowId }) | |
| }) | |
| // ββ Message handler βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { | |
| switch (msg.type) { | |
| case 'VERIFY_TEXT': | |
| verifyText(msg.text, msg.imageUrl) | |
| .then(r => sendResponse({ ok: true, result: r })) | |
| .catch(e => sendResponse({ ok: false, error: e.message })) | |
| return true // keep message channel open for async response | |
| case 'VERIFY_URL': | |
| if (!isHttpUrl(msg.url)) { | |
| sendResponse({ ok: false, error: 'Invalid URL: only http/https allowed' }) | |
| return false | |
| } | |
| verifyUrl(msg.url) | |
| .then(r => sendResponse({ ok: true, result: r })) | |
| .catch(e => sendResponse({ ok: false, error: e.message })) | |
| return true | |
| case 'VERIFY_IMAGE_URL': | |
| if (!isHttpUrl(msg.imageUrl)) { | |
| sendResponse({ ok: false, error: 'Invalid image URL' }) | |
| return false | |
| } | |
| verifyImageUrl(msg.imageUrl) | |
| .then(r => sendResponse({ ok: true, result: r })) | |
| .catch(e => sendResponse({ ok: false, error: e.message })) | |
| return true | |
| case 'GET_HISTORY': | |
| chrome.storage.local.get('history') | |
| .then(({ history = [] }) => sendResponse({ history })) | |
| return true | |
| case 'GET_SETTINGS': | |
| getSettings().then(s => sendResponse(s)) | |
| return true | |
| case 'SAVE_SETTINGS': { | |
| const incoming = msg.settings ?? {} | |
| // Validate apiBase is a safe URL before persisting | |
| if (incoming.apiBase && !isHttpUrl(incoming.apiBase)) { | |
| sendResponse({ ok: false, error: 'Invalid API URL: only http/https allowed' }) | |
| return false | |
| } | |
| chrome.storage.local | |
| .set({ settings: incoming }) | |
| .then(() => sendResponse({ ok: true })) | |
| return true | |
| } | |
| default: | |
| break | |
| } | |
| }) | |