haixunMaster/lib/threads-api/publish.ts

109 lines
3.1 KiB
TypeScript

import { THREADS_MAX_CHARS } from "@/lib/utils";
import { threadsGraphGet, threadsGraphPost, ThreadsApiError } from "./client";
import type { ThreadsApiCredentials, ThreadsPublishInput, ThreadsPublishResult } from "./types";
const POLL_INTERVAL_MS = 3000;
const POLL_MAX_ATTEMPTS = 20;
export async function publishViaThreadsApi(
credentials: ThreadsApiCredentials,
input: ThreadsPublishInput
): Promise<ThreadsPublishResult> {
if (input.text.length > THREADS_MAX_CHARS) {
return { success: false, error: `貼文超過 ${THREADS_MAX_CHARS} 字上限`, method: "api" };
}
try {
const containerParams: Record<string, string> = {
access_token: credentials.accessToken,
text: input.text,
};
if (input.imageUrl) {
containerParams.media_type = "IMAGE";
containerParams.image_url = input.imageUrl;
} else {
containerParams.media_type = "TEXT";
}
if (input.replyToId) {
containerParams.reply_to_id = input.replyToId;
}
const container = await threadsGraphPost<{ id: string }>(
`/${credentials.userId}/threads`,
containerParams
);
if (!container.id) {
return { success: false, error: "無法建立發布容器", method: "api" };
}
await waitForContainerReady(container.id, credentials.accessToken);
const published = await threadsGraphPost<{ id: string }>(
`/${credentials.userId}/threads_publish`,
{
access_token: credentials.accessToken,
creation_id: container.id,
}
);
if (!published.id) {
return { success: false, error: "發布請求未回傳貼文 ID", method: "api" };
}
const permalink = await fetchPermalink(published.id, credentials.accessToken);
return {
success: true,
mediaId: published.id,
permalink: permalink ?? undefined,
method: "api",
};
} catch (error) {
const message =
error instanceof ThreadsApiError
? error.message
: error instanceof Error
? error.message
: "Threads API 發布失敗";
return { success: false, error: message, method: "api" };
}
}
async function waitForContainerReady(containerId: string, accessToken: string) {
for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
if (attempt > 0) {
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
const status = await threadsGraphGet<{ status?: string; error_message?: string }>(
`/${containerId}`,
{
access_token: accessToken,
fields: "status,error_message",
}
);
if (status.status === "FINISHED") return;
if (status.status === "ERROR") {
throw new ThreadsApiError(status.error_message ?? "媒體處理失敗");
}
}
// Meta 建議至少等 30 秒,若狀態查不到仍嘗試發布
}
async function fetchPermalink(mediaId: string, accessToken: string): Promise<string | null> {
try {
const media = await threadsGraphGet<{ permalink?: string }>(`/${mediaId}`, {
access_token: accessToken,
fields: "permalink",
});
return media.permalink ?? null;
} catch {
return null;
}
}