haixunMaster/extension/haixun-threads-sync/service-worker.js

119 lines
3.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { buildStorageState } from "./storage-state.js";
const CONTENT_SCRIPT_ID = "haixun-bridge";
async function getHaixunSessionToken(serverOrigin) {
const cookie = await chrome.cookies.get({
url: serverOrigin,
name: "haixun_session",
});
return cookie?.value ?? null;
}
async function ensureContentScript(serverOrigin) {
const pattern = `${serverOrigin}/*`;
const existing = await chrome.scripting.getRegisteredContentScripts();
if (existing.some((script) => script.id === CONTENT_SCRIPT_ID)) {
return;
}
await chrome.scripting.registerContentScripts([
{
id: CONTENT_SCRIPT_ID,
matches: [pattern],
js: ["content-haixun.js"],
runAt: "document_idle",
},
]);
}
async function requestHostPermission(serverOrigin) {
const granted = await chrome.permissions.contains({
origins: [`${serverOrigin}/*`],
});
if (granted) return true;
return chrome.permissions.request({ origins: [`${serverOrigin}/*`] });
}
export async function syncThreadsSession(serverUrl, accountId) {
const origin = new URL(serverUrl).origin;
const sessionToken = await getHaixunSessionToken(origin);
if (!sessionToken) {
throw new Error(`請先在 Chrome 開啟並登入 ${origin}`);
}
const storageState = await buildStorageState();
const res = await fetch(`${origin}/api/session/import`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: `haixun_session=${sessionToken}`,
},
body: JSON.stringify({
storageState,
...(accountId ? { accountId } : {}),
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.error ?? data.message ?? `匯入失敗HTTP ${res.status}`);
}
return data;
}
async function resolveServerUrl(partial) {
if (partial) return new URL(partial).origin;
const stored = await chrome.storage.sync.get(["serverUrl"]);
if (stored.serverUrl) return new URL(stored.serverUrl).origin;
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const activeUrl = tabs[0]?.url;
if (activeUrl && !activeUrl.includes("threads.com") && !activeUrl.includes("threads.net")) {
try {
return new URL(activeUrl).origin;
} catch {
// ignore
}
}
throw new Error("請在擴充功能選項設定巡樓網址,或先開啟巡樓分頁");
}
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.action !== "sync") return undefined;
(async () => {
try {
const serverUrl = await resolveServerUrl(message.serverUrl);
const allowed = await requestHostPermission(serverUrl);
if (!allowed) {
throw new Error("需要授權存取巡樓網站才能同步");
}
await ensureContentScript(serverUrl);
const result = await syncThreadsSession(serverUrl, message.accountId);
sendResponse({ success: true, ...result });
} catch (error) {
sendResponse({
success: false,
valid: false,
message: error instanceof Error ? error.message : "同步失敗",
});
}
})();
return true;
});
chrome.runtime.onInstalled.addListener(async () => {
const { serverUrl } = await chrome.storage.sync.get(["serverUrl"]);
if (!serverUrl) {
await chrome.storage.sync.set({ serverUrl: "http://localhost:3000" });
}
});