haixunMaster/lib/services/threads-api-sync.ts

143 lines
4.2 KiB
TypeScript

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,
};
}