const GRAPH_BASE = "https://graph.threads.net/v1.0"; export class ThreadsApiError extends Error { constructor( message: string, public readonly code?: number, public readonly subcode?: number ) { super(message); this.name = "ThreadsApiError"; } } async function parseJson(res: Response): Promise> { const text = await res.text(); try { return JSON.parse(text) as Record; } catch { throw new ThreadsApiError(text || `HTTP ${res.status}`, res.status); } } export async function threadsGraphGet( path: string, params: Record ): Promise { const url = new URL(`${GRAPH_BASE}${path}`); for (const [key, value] of Object.entries(params)) { url.searchParams.set(key, value); } const res = await fetch(url.toString(), { method: "GET" }); const json = await parseJson(res); if (!res.ok) { throw new ThreadsApiError( String(json.error_message ?? json.error ?? "Threads API 請求失敗"), typeof json.code === "number" ? json.code : res.status ); } return json as T; } export async function threadsGraphPost( path: string, params: Record ): Promise { const body = new URLSearchParams(params); const res = await fetch(`${GRAPH_BASE}${path}`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, }); const json = await parseJson(res); if (!res.ok) { throw new ThreadsApiError( String(json.error_message ?? json.error ?? "Threads API 請求失敗"), typeof json.code === "number" ? json.code : res.status ); } return json as T; }