philverify-api / extension /background.js
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
}
})