// f0ckm Uploader - Background Service Worker const DEFAULT_SETTINGS = { uploadMode: "desktop", // "desktop" (f0ckm://) or "direct" (API) apiUrl: "https://f0ckm.com/api/v2/upload", apiKey: "", rating: "", tags: "", visibility: "0", isOc: false, urlType: "post" }; async function getSettings() { return new Promise((resolve) => { chrome.storage.sync.get(DEFAULT_SETTINGS, (items) => { resolve(items || DEFAULT_SETTINGS); }); }); } const badgeCache = {}; function updateBadge(tabId, url) { if (!tabId) return; const isWeb = url && (url.startsWith("http://") || url.startsWith("https://")); const isYT = isWeb && (url.includes("youtube.com") || url.includes("youtu.be")); const targetText = isWeb ? "1" : ""; const targetColor = isYT ? "#ff0000" : "#0066ff"; const cacheKey = `${targetText}:${targetColor}`; if (badgeCache[tabId] === cacheKey) { return; } badgeCache[tabId] = cacheKey; try { if (targetText) { chrome.action.setBadgeText({ text: targetText, tabId: tabId }); chrome.action.setBadgeBackgroundColor({ color: targetColor, tabId: tabId }); } else { chrome.action.setBadgeText({ text: "", tabId: tabId }); } } catch (e) {} } chrome.tabs.onActivated.addListener(async (activeInfo) => { try { const tab = await chrome.tabs.get(activeInfo.tabId); if (tab) updateBadge(activeInfo.tabId, tab.url); } catch (e) {} }); chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { if (tab && (changeInfo.status === "complete" || changeInfo.url)) { updateBadge(tabId, tab.url); } }); function setupContextMenus() { chrome.contextMenus.removeAll(() => { chrome.contextMenus.create({ id: "f0ckm_upload_image", title: "Upload image to f0ckm", contexts: ["image"] }); chrome.contextMenus.create({ id: "f0ckm_upload_video", title: "Upload media to f0ckm", contexts: ["video", "audio"] }); chrome.contextMenus.create({ id: "f0ckm_upload_link", title: "Upload link with f0ckm", contexts: ["link"] }); chrome.contextMenus.create({ id: "f0ckm_upload_selection", title: "Upload selected URL with f0ckm", contexts: ["selection"] }); chrome.contextMenus.create({ id: "f0ckm_upload_page", title: "Upload page URL to f0ckm", contexts: ["page"] }); }); } chrome.runtime.onInstalled.addListener(() => { setupContextMenus(); }); chrome.runtime.onStartup.addListener(() => { setupContextMenus(); }); chrome.contextMenus.onClicked.addListener(async (info, tab) => { let targetUrl = ""; if (info.menuItemId === "f0ckm_upload_image" || info.menuItemId === "f0ckm_upload_video") { targetUrl = info.srcUrl || info.linkUrl; } else if (info.menuItemId === "f0ckm_upload_link") { targetUrl = info.linkUrl; } else if (info.menuItemId === "f0ckm_upload_selection") { targetUrl = info.selectionText ? info.selectionText.trim() : ""; } else if (info.menuItemId === "f0ckm_upload_page") { targetUrl = info.pageUrl || (tab ? tab.url : ""); } if (targetUrl) { await processUpload(targetUrl); } }); chrome.commands.onCommand.addListener(async (command) => { if (command === "upload-page") { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); if (tabs && tabs.length > 0 && tabs[0].url) { await processUpload(tabs[0].url); } } }); function normalizeApiUrl(url) { if (!url) return ""; url = url.trim().replace(/^['"]|['"]$/g, ""); if (!url) return ""; if (!/^https?:\/\//i.test(url)) { url = "http://" + url; } try { const parsed = new URL(url); let path = parsed.pathname.replace(/\/+$/, ""); if (!path || path === "") { return `${parsed.origin}/api/v2/upload`; } else if (path.endsWith("/api/v2")) { return `${parsed.origin}/upload`; } else if (!path.endsWith("/upload")) { return `${parsed.origin}/api/v2/upload`; } return url; } catch (e) { return url; } } async function copyToClipboard(text) { if (!text) return; try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); } } catch (e) {} } async function testConnection(apiUrl, apiKey) { if (!apiUrl || !apiKey) { return { success: false, msg: "API URL and API Key are required." }; } const normUrl = normalizeApiUrl(apiUrl); try { const res = await fetch(normUrl, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": apiKey, "Authorization": `Bearer ${apiKey}` }, credentials: "omit", body: JSON.stringify({ api_key: apiKey, key: apiKey }) }); if (res.status === 200) { return { success: true, msg: "Connection successful! (HTTP 200)" }; } else if (res.status === 401) { return { success: false, msg: "Unauthorized: Invalid API Key." }; } else if (res.status === 400 || res.status === 422) { return { success: true, msg: "Connection successful! (API key verified)" }; } else { let text = `HTTP ${res.status}`; try { const json = await res.json(); if (json.msg || json.error || json.message) text = json.msg || json.error || json.message; } catch (e) {} return { success: false, msg: text }; } } catch (err) { return { success: false, msg: err.message || "Connection failed. Check server URL and network." }; } } async function processUpload(targetUrl) { if (!targetUrl) return; const settings = await getSettings(); if (settings.uploadMode === "desktop") { // 1. Try local loopback HTTP API first (Direct desktop handoff) const localApiUrl = "http://127.0.0.1:18739/upload?url=" + encodeURIComponent(targetUrl); try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1500); const res = await fetch(localApiUrl, { signal: controller.signal }); clearTimeout(timeoutId); if (res.ok) { return; } } catch (err) {} // 2. Fallback: Custom protocol scheme in active tab const customUrl = "f0ckm://upload?url=" + encodeURIComponent(targetUrl); try { chrome.tabs.create({ url: customUrl, active: true }, (newTab) => { if (newTab && newTab.id) { setTimeout(() => { chrome.tabs.remove(newTab.id).catch(() => {}); }, 4500); } }); } catch (e) {} } else { // Direct API Upload mode -> POST JSON payload with url to f0ckm API! showNotification("Uploading...", `Uploading ${targetUrl.substring(0, 45)}...`, "uploading"); try { if (!settings.apiUrl || !settings.apiKey) { showNotification("Upload Error", "API URL and API Key must be configured in extension options.", "error"); return null; } const normalizedUrl = normalizeApiUrl(settings.apiUrl); const payload = { url: targetUrl, tags: settings.tags || "url,upload", visibility: settings.visibility || "0", api_key: settings.apiKey, key: settings.apiKey }; if (settings.rating && settings.rating !== "default" && settings.rating !== "") { const ratingMap = { s: "sfw", q: "nsfw", e: "nsfl", sfw: "sfw", nsfw: "nsfw", nsfl: "nsfl" }; payload.rating = ratingMap[settings.rating.toLowerCase()] || settings.rating; } if (settings.isOc) payload.is_oc = "1"; const uploadRes = await fetch(normalizedUrl, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": settings.apiKey, "Authorization": `Bearer ${settings.apiKey}` }, credentials: "omit", body: JSON.stringify(payload) }); if (!uploadRes.ok) { let errorMsg = `HTTP Error ${uploadRes.status}`; try { const errJson = await uploadRes.json(); if (errJson.msg || errJson.error || errJson.message) { errorMsg = errJson.msg || errJson.error || errJson.message; } } catch (e) {} showNotification("Upload Failed", errorMsg, "error"); return null; } const json = await uploadRes.json(); const postUrl = json.url || json.post_url || json.link || (json.data && (json.data.url || json.data.post_url)) || ""; const directUrl = json.file_url || json.direct_url || json.target_url || json.file || (json.data && (json.data.file_url || json.data.direct_url)) || ""; const resultUrl = (settings.urlType === "direct" && directUrl) ? directUrl : (postUrl || directUrl); if (resultUrl) { await copyToClipboard(resultUrl); showNotification("Upload Successful!", `Link copied to clipboard: ${resultUrl}`, "success"); return resultUrl; } else if (json.success) { showNotification("Upload Successful!", "Upload processed successfully.", "success"); return "success"; } else { const msg = json.msg || json.error || "Upload failed"; showNotification("Upload Failed", msg, "error"); return null; } } catch (err) { showNotification("Upload Error", err.message || "An error occurred during network request", "error"); return null; } } } function showNotification(title, message, iconType = "info") { if (chrome.notifications) { chrome.notifications.create({ type: "basic", iconUrl: "icons/icon128.png", title: title, message: message, priority: 2 }); } } chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === "get_active_tab") { chrome.tabs.query({ active: true, lastFocusedWindow: true }, (tabs) => { if (tabs && tabs.length > 0 && tabs[0].url) { sendResponse({ tab: tabs[0] }); } else { chrome.tabs.query({ active: true, currentWindow: true }, (tabs2) => { if (tabs2 && tabs2.length > 0 && tabs2[0].url) { sendResponse({ tab: tabs2[0] }); } else { chrome.tabs.query({ active: true }, (tabs3) => { sendResponse({ tab: (tabs3 && tabs3.length > 0) ? tabs3[0] : null }); }); } }); } }); return true; } else if (request.action === "upload_url") { processUpload(request.url).then((resUrl) => { sendResponse({ status: "ok", resultUrl: resUrl || "" }); }); return true; } else if (request.action === "test_connection") { testConnection(request.apiUrl, request.apiKey).then((res) => { sendResponse(res); }); return true; } });