import { prisma } from "@/lib/db"; import { getActiveAccountId } from "@/lib/account-context"; import { getMediaInsightsViaThreadsApi, getMediaRepliesViaThreadsApi, getOwnPostsViaThreadsApi, getProfileInsightsViaThreadsApi, type ThreadsInsights, } from "@/lib/threads-api"; import { getActiveThreadsCredentials } from "@/lib/services/threads-credentials"; function normalizeDate(value?: string | null): Date | undefined { if (!value) return undefined; const date = new Date(value); return Number.isNaN(date.getTime()) ? undefined : date; } export async function syncThreadsOwnPostsAndInsights(options?: { postsLimit?: number; repliesLimit?: number; }) { const accountId = await getActiveAccountId(); const credentials = await getActiveThreadsCredentials(); if (!credentials) throw new Error("Threads API 尚未連線"); const emptyInsights: ThreadsInsights = {}; const [ownPosts, profileInsights] = await Promise.all([ getOwnPostsViaThreadsApi(credentials, options?.postsLimit ?? 25), getProfileInsightsViaThreadsApi(credentials).catch(() => emptyInsights), ]); const synced = []; for (const post of ownPosts) { const insights = await getMediaInsightsViaThreadsApi(credentials, post.id).catch( () => emptyInsights ); const existing = await prisma.published.findFirst({ where: { externalId: post.id, ...(accountId ? { accountId } : {}) }, }); const published = existing ? await prisma.published.update({ where: { id: existing.id }, data: { accountId, text: post.text ?? undefined, permalink: post.permalink, views: insights.views, likes: insights.likes, replies: insights.replies, reposts: insights.reposts, quotes: insights.quotes, }, }) : await prisma.published.create({ data: { accountId, externalId: post.id, text: post.text ?? "", permalink: post.permalink, publishedAt: normalizeDate(post.timestamp) ?? new Date(), views: insights.views, likes: insights.likes, replies: insights.replies, reposts: insights.reposts, quotes: insights.quotes, }, }); await prisma.performanceSnapshot.create({ data: { publishedId: published.id, profileId: credentials.userId, views: insights.views, likes: insights.likes, replies: insights.replies, reposts: insights.reposts, quotes: insights.quotes, followers: profileInsights.followers_count, }, }); const replies = await getMediaRepliesViaThreadsApi( credentials, post.id, options?.repliesLimit ?? 25 ).catch(() => []); for (const reply of replies) { if (!reply.id || !reply.text?.trim()) continue; await prisma.inboundReply.upsert({ where: { externalId: reply.id }, create: { publishedId: published.id, externalId: reply.id, parentId: reply.parent_id, text: reply.text.trim(), authorName: reply.username, permalink: reply.permalink, postedAt: normalizeDate(reply.timestamp), likeCount: reply.like_count, }, update: { publishedId: published.id, parentId: reply.parent_id, text: reply.text.trim(), authorName: reply.username, permalink: reply.permalink, postedAt: normalizeDate(reply.timestamp), likeCount: reply.like_count, }, }); } synced.push({ id: published.id, externalId: post.id, replies: replies.length, insights, }); } if (Object.keys(profileInsights).length > 0) { await prisma.performanceSnapshot.create({ data: { profileId: credentials.userId, views: profileInsights.views, likes: profileInsights.likes, replies: profileInsights.replies, reposts: profileInsights.reposts, quotes: profileInsights.quotes, followers: profileInsights.followers_count, }, }); } return { profileInsights, synced, }; }