119 lines
3.3 KiB
JavaScript
119 lines
3.3 KiB
JavaScript
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" });
|
||
}
|
||
}); |