mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 20:54:11 +00:00
feat(community): Phase 2.4 - Unified Sync Service
Implements a unified sync service that orchestrates comment synchronization across all social media platforms. UnifiedSyncService: - Platform-agnostic sync orchestration - Support for YouTube, Facebook, and Instagram - Parallel platform detection and account grouping - Progress tracking with live status updates - Aggregated results per platform - Error handling with partial results support New API Endpoints: - GET/POST /api/cron/community-sync - Cron endpoint for scheduled multi-platform sync - Query params: platforms, accountIds, analyzeWithAI, maxItems - HEAD for monitoring status - GET /api/community/sync-status - Live sync status for dashboard - Platform overview with account details - Interaction statistics (total, today, unanswered) - Last sync result summary Configuration: - vercel.json updated to use community-sync cron - 15-minute sync interval maintained Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b107d60183
commit
89882a545b
4 changed files with 916 additions and 1 deletions
158
src/app/(payload)/api/community/sync-status/route.ts
Normal file
158
src/app/(payload)/api/community/sync-status/route.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// src/app/(payload)/api/community/sync-status/route.ts
|
||||
// Sync Status API für Dashboard
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { getUnifiedSyncStatus } from '@/lib/jobs/UnifiedSyncService'
|
||||
import { getSyncStatus as getYouTubeSyncStatus } from '@/lib/jobs/syncAllComments'
|
||||
|
||||
/**
|
||||
* GET /api/community/sync-status
|
||||
* Gibt den aktuellen Sync-Status für alle Plattformen zurück
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// Authentifizierung prüfen
|
||||
const { user } = await payload.auth({ headers: request.headers })
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Sync-Status abrufen
|
||||
const unifiedStatus = getUnifiedSyncStatus()
|
||||
const youtubeStatus = getYouTubeSyncStatus()
|
||||
|
||||
// Account-Statistiken laden
|
||||
const accounts = await payload.find({
|
||||
collection: 'social-accounts',
|
||||
where: {
|
||||
isActive: { equals: true },
|
||||
},
|
||||
limit: 100,
|
||||
depth: 2,
|
||||
})
|
||||
|
||||
// Nach Plattform aggregieren
|
||||
const platformStats: Record<
|
||||
string,
|
||||
{
|
||||
platform: string
|
||||
accountCount: number
|
||||
accounts: Array<{
|
||||
id: number
|
||||
name: string
|
||||
lastSyncedAt: string | null
|
||||
followers: number
|
||||
totalPosts: number
|
||||
}>
|
||||
}
|
||||
> = {}
|
||||
|
||||
for (const account of accounts.docs) {
|
||||
const platformDoc = account.platform as { slug?: string; name?: string } | undefined
|
||||
const platformSlug = platformDoc?.slug || 'unknown'
|
||||
const platformName = platformDoc?.name || platformSlug
|
||||
|
||||
if (!platformStats[platformSlug]) {
|
||||
platformStats[platformSlug] = {
|
||||
platform: platformName,
|
||||
accountCount: 0,
|
||||
accounts: [],
|
||||
}
|
||||
}
|
||||
|
||||
const stats = account.stats as {
|
||||
lastSyncedAt?: string
|
||||
followers?: number
|
||||
totalPosts?: number
|
||||
} | undefined
|
||||
|
||||
platformStats[platformSlug].accountCount++
|
||||
platformStats[platformSlug].accounts.push({
|
||||
id: account.id as number,
|
||||
name: (account.displayName as string) || `Account ${account.id}`,
|
||||
lastSyncedAt: stats?.lastSyncedAt || null,
|
||||
followers: stats?.followers || 0,
|
||||
totalPosts: stats?.totalPosts || 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Interaktions-Statistiken
|
||||
const interactionStats = await payload.find({
|
||||
collection: 'community-interactions',
|
||||
limit: 0, // Nur Count
|
||||
})
|
||||
|
||||
// Neue Interaktionen heute
|
||||
const todayStart = new Date()
|
||||
todayStart.setHours(0, 0, 0, 0)
|
||||
|
||||
const todayInteractions = await payload.find({
|
||||
collection: 'community-interactions',
|
||||
where: {
|
||||
createdAt: { greater_than: todayStart.toISOString() },
|
||||
},
|
||||
limit: 0,
|
||||
})
|
||||
|
||||
// Unbeantwortete Interaktionen
|
||||
const unansweredInteractions = await payload.find({
|
||||
collection: 'community-interactions',
|
||||
where: {
|
||||
status: { in: ['new', 'in_review'] },
|
||||
},
|
||||
limit: 0,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
// Live Sync-Status
|
||||
syncStatus: {
|
||||
isRunning: unifiedStatus.isRunning || youtubeStatus.isRunning,
|
||||
currentPlatform: unifiedStatus.currentPlatform,
|
||||
currentAccount: unifiedStatus.currentAccount,
|
||||
progress: unifiedStatus.progress,
|
||||
lastRunAt: unifiedStatus.lastRunAt || youtubeStatus.lastRunAt,
|
||||
},
|
||||
|
||||
// Plattform-Übersicht
|
||||
platforms: Object.values(platformStats),
|
||||
|
||||
// Interaktions-Statistiken
|
||||
interactions: {
|
||||
total: interactionStats.totalDocs,
|
||||
today: todayInteractions.totalDocs,
|
||||
unanswered: unansweredInteractions.totalDocs,
|
||||
},
|
||||
|
||||
// Letztes Sync-Ergebnis
|
||||
lastSyncResult: unifiedStatus.lastResult
|
||||
? {
|
||||
success: unifiedStatus.lastResult.success,
|
||||
startedAt: unifiedStatus.lastResult.startedAt,
|
||||
completedAt: unifiedStatus.lastResult.completedAt,
|
||||
duration: unifiedStatus.lastResult.duration,
|
||||
platforms: unifiedStatus.lastResult.platforms,
|
||||
totalNewComments: unifiedStatus.lastResult.results.reduce(
|
||||
(sum, r) => sum + r.newComments,
|
||||
0
|
||||
),
|
||||
totalUpdatedComments: unifiedStatus.lastResult.results.reduce(
|
||||
(sum, r) => sum + r.updatedComments,
|
||||
0
|
||||
),
|
||||
totalErrors: unifiedStatus.lastResult.errors.length,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.error('[API] sync-status error:', error)
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
203
src/app/(payload)/api/cron/community-sync/route.ts
Normal file
203
src/app/(payload)/api/cron/community-sync/route.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
// src/app/(payload)/api/cron/community-sync/route.ts
|
||||
// Unified Community Sync Cron Endpoint
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import {
|
||||
runUnifiedSync,
|
||||
getUnifiedSyncStatus,
|
||||
type SupportedPlatform,
|
||||
type UnifiedSyncOptions,
|
||||
} from '@/lib/jobs/UnifiedSyncService'
|
||||
|
||||
// Geheimer Token für Cron-Authentifizierung
|
||||
const CRON_SECRET = process.env.CRON_SECRET
|
||||
|
||||
/**
|
||||
* GET /api/cron/community-sync
|
||||
* Wird von externem Cron-Job aufgerufen (z.B. Vercel Cron, cron-job.org)
|
||||
*
|
||||
* Query Parameters:
|
||||
* - platforms: Komma-separierte Liste (youtube,facebook,instagram)
|
||||
* - accountIds: Komma-separierte Account-IDs
|
||||
* - analyzeWithAI: true/false (default: true)
|
||||
* - maxItems: Maximale Items pro Account (default: 100)
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
// Auth prüfen wenn CRON_SECRET gesetzt
|
||||
if (CRON_SECRET) {
|
||||
const authHeader = request.headers.get('authorization')
|
||||
|
||||
if (authHeader !== `Bearer ${CRON_SECRET}`) {
|
||||
console.warn('[Cron] Unauthorized request to community-sync')
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
}
|
||||
|
||||
// Query-Parameter parsen
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const options: UnifiedSyncOptions = {}
|
||||
|
||||
// Plattform-Filter
|
||||
const platformsParam = searchParams.get('platforms')
|
||||
if (platformsParam) {
|
||||
const platforms = platformsParam.split(',').map((p) => p.trim().toLowerCase())
|
||||
const validPlatforms = platforms.filter((p): p is SupportedPlatform =>
|
||||
['youtube', 'facebook', 'instagram'].includes(p)
|
||||
)
|
||||
if (validPlatforms.length > 0) {
|
||||
options.platforms = validPlatforms
|
||||
}
|
||||
}
|
||||
|
||||
// Account-ID Filter
|
||||
const accountIdsParam = searchParams.get('accountIds')
|
||||
if (accountIdsParam) {
|
||||
const ids = accountIdsParam
|
||||
.split(',')
|
||||
.map((id) => parseInt(id.trim(), 10))
|
||||
.filter((id) => !isNaN(id))
|
||||
if (ids.length > 0) {
|
||||
options.accountIds = ids
|
||||
}
|
||||
}
|
||||
|
||||
// AI-Analyse
|
||||
const analyzeParam = searchParams.get('analyzeWithAI')
|
||||
if (analyzeParam !== null) {
|
||||
options.analyzeWithAI = analyzeParam !== 'false'
|
||||
}
|
||||
|
||||
// Max Items
|
||||
const maxItemsParam = searchParams.get('maxItems')
|
||||
if (maxItemsParam) {
|
||||
const maxItems = parseInt(maxItemsParam, 10)
|
||||
if (!isNaN(maxItems) && maxItems > 0) {
|
||||
options.maxItemsPerAccount = maxItems
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Cron] Starting community sync', { options })
|
||||
|
||||
const result = await runUnifiedSync(options)
|
||||
|
||||
if (result.success) {
|
||||
// Gesamtstatistiken berechnen
|
||||
let totalNew = 0
|
||||
let totalUpdated = 0
|
||||
let totalErrors = 0
|
||||
|
||||
for (const platform of Object.values(result.platforms)) {
|
||||
if (platform) {
|
||||
totalNew += platform.totalNewComments
|
||||
totalUpdated += platform.totalUpdatedComments
|
||||
totalErrors += platform.totalErrors
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Sync completed: ${totalNew} new, ${totalUpdated} updated comments across ${result.results.length} accounts`,
|
||||
duration: result.duration,
|
||||
platforms: result.platforms,
|
||||
results: result.results,
|
||||
})
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
errors: result.errors,
|
||||
partialResults: result.results,
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/cron/community-sync
|
||||
* Manueller Sync-Trigger mit erweiterten Optionen
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
// Auth prüfen
|
||||
if (CRON_SECRET) {
|
||||
const authHeader = request.headers.get('authorization')
|
||||
|
||||
if (authHeader !== `Bearer ${CRON_SECRET}`) {
|
||||
console.warn('[Cron] Unauthorized POST to community-sync')
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const options: UnifiedSyncOptions = {}
|
||||
|
||||
// Optionen aus Body übernehmen
|
||||
if (body.platforms && Array.isArray(body.platforms)) {
|
||||
options.platforms = body.platforms.filter((p: string): p is SupportedPlatform =>
|
||||
['youtube', 'facebook', 'instagram'].includes(p)
|
||||
)
|
||||
}
|
||||
|
||||
if (body.accountIds && Array.isArray(body.accountIds)) {
|
||||
options.accountIds = body.accountIds.filter(
|
||||
(id: unknown) => typeof id === 'number' && id > 0
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof body.analyzeWithAI === 'boolean') {
|
||||
options.analyzeWithAI = body.analyzeWithAI
|
||||
}
|
||||
|
||||
if (typeof body.maxItemsPerAccount === 'number' && body.maxItemsPerAccount > 0) {
|
||||
options.maxItemsPerAccount = body.maxItemsPerAccount
|
||||
}
|
||||
|
||||
if (body.sinceDate) {
|
||||
const date = new Date(body.sinceDate)
|
||||
if (!isNaN(date.getTime())) {
|
||||
options.sinceDate = date
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Cron] Manual community sync triggered', { options })
|
||||
|
||||
const result = await runUnifiedSync(options)
|
||||
|
||||
return NextResponse.json({
|
||||
success: result.success,
|
||||
startedAt: result.startedAt,
|
||||
completedAt: result.completedAt,
|
||||
duration: result.duration,
|
||||
platforms: result.platforms,
|
||||
results: result.results,
|
||||
errors: result.errors,
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
console.error('[Cron] POST error:', error)
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HEAD /api/cron/community-sync
|
||||
* Status-Check für Monitoring
|
||||
*/
|
||||
export async function HEAD() {
|
||||
const status = getUnifiedSyncStatus()
|
||||
|
||||
return new NextResponse(null, {
|
||||
status: status.isRunning ? 423 : 200, // 423 = Locked (running)
|
||||
headers: {
|
||||
'X-Sync-Running': status.isRunning.toString(),
|
||||
'X-Current-Platform': status.currentPlatform || 'none',
|
||||
'X-Current-Account': status.currentAccount || 'none',
|
||||
'X-Progress': `${status.progress.percentage}%`,
|
||||
'X-Last-Run': status.lastRunAt || 'never',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const maxDuration = 300 // 5 Minuten max (Vercel)
|
||||
554
src/lib/jobs/UnifiedSyncService.ts
Normal file
554
src/lib/jobs/UnifiedSyncService.ts
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
// src/lib/jobs/UnifiedSyncService.ts
|
||||
// Unified Sync Service für alle Social Media Plattformen
|
||||
|
||||
import { getPayload, Payload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { CommentsSyncService } from '../integrations/youtube/CommentsSyncService'
|
||||
import { FacebookSyncService } from '../integrations/meta/FacebookSyncService'
|
||||
import { InstagramSyncService } from '../integrations/meta/InstagramSyncService'
|
||||
import { JobLogger } from './JobLogger'
|
||||
|
||||
// =============================================================================
|
||||
// Types
|
||||
// =============================================================================
|
||||
|
||||
export type SupportedPlatform = 'youtube' | 'facebook' | 'instagram'
|
||||
|
||||
export interface PlatformSyncResult {
|
||||
platform: SupportedPlatform
|
||||
accountId: number
|
||||
accountName: string
|
||||
success: boolean
|
||||
newComments: number
|
||||
updatedComments: number
|
||||
newMessages: number
|
||||
postsProcessed: number
|
||||
errors: string[]
|
||||
duration: number
|
||||
}
|
||||
|
||||
export interface UnifiedSyncResult {
|
||||
success: boolean
|
||||
startedAt: Date
|
||||
completedAt: Date
|
||||
duration: number
|
||||
platforms: {
|
||||
[key in SupportedPlatform]?: {
|
||||
accountsProcessed: number
|
||||
totalNewComments: number
|
||||
totalUpdatedComments: number
|
||||
totalErrors: number
|
||||
}
|
||||
}
|
||||
results: PlatformSyncResult[]
|
||||
errors: string[]
|
||||
}
|
||||
|
||||
export interface UnifiedSyncOptions {
|
||||
/** Nur bestimmte Plattformen synchronisieren */
|
||||
platforms?: SupportedPlatform[]
|
||||
/** Nur bestimmte Account-IDs synchronisieren */
|
||||
accountIds?: number[]
|
||||
/** AI-Analyse aktivieren */
|
||||
analyzeWithAI?: boolean
|
||||
/** Maximale Items pro Account */
|
||||
maxItemsPerAccount?: number
|
||||
/** Nur seit diesem Datum synchronisieren */
|
||||
sinceDate?: Date
|
||||
}
|
||||
|
||||
export interface SyncStatus {
|
||||
isRunning: boolean
|
||||
currentPlatform: SupportedPlatform | null
|
||||
currentAccount: string | null
|
||||
progress: {
|
||||
totalAccounts: number
|
||||
processedAccounts: number
|
||||
percentage: number
|
||||
}
|
||||
lastRunAt: string | null
|
||||
lastResult: UnifiedSyncResult | null
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Global State
|
||||
// =============================================================================
|
||||
|
||||
let syncStatus: SyncStatus = {
|
||||
isRunning: false,
|
||||
currentPlatform: null,
|
||||
currentAccount: null,
|
||||
progress: {
|
||||
totalAccounts: 0,
|
||||
processedAccounts: 0,
|
||||
percentage: 0,
|
||||
},
|
||||
lastRunAt: null,
|
||||
lastResult: null,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Platform Detection
|
||||
// =============================================================================
|
||||
|
||||
function detectPlatform(platformDoc: any): SupportedPlatform | null {
|
||||
const slug = platformDoc?.slug?.toLowerCase()
|
||||
const apiType = platformDoc?.apiConfig?.apiType
|
||||
|
||||
if (slug === 'youtube' || apiType === 'youtube_v3') {
|
||||
return 'youtube'
|
||||
}
|
||||
if (slug === 'facebook' || apiType === 'facebook_graph' || apiType === 'meta_graph') {
|
||||
return 'facebook'
|
||||
}
|
||||
if (slug === 'instagram' || apiType === 'instagram_graph') {
|
||||
return 'instagram'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Unified Sync Service
|
||||
// =============================================================================
|
||||
|
||||
export class UnifiedSyncService {
|
||||
private payload: Payload
|
||||
private logger: JobLogger
|
||||
private youtubeSyncService: CommentsSyncService
|
||||
private facebookSyncService: FacebookSyncService
|
||||
private instagramSyncService: InstagramSyncService
|
||||
|
||||
constructor(payload: Payload) {
|
||||
this.payload = payload
|
||||
this.logger = new JobLogger('unified-sync')
|
||||
this.youtubeSyncService = new CommentsSyncService(payload)
|
||||
this.facebookSyncService = new FacebookSyncService(payload)
|
||||
this.instagramSyncService = new InstagramSyncService(payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen vollständigen Sync aller Plattformen durch
|
||||
*/
|
||||
async runFullSync(options: UnifiedSyncOptions = {}): Promise<UnifiedSyncResult> {
|
||||
const {
|
||||
platforms,
|
||||
accountIds,
|
||||
analyzeWithAI = true,
|
||||
maxItemsPerAccount = 100,
|
||||
sinceDate,
|
||||
} = options
|
||||
|
||||
const startedAt = new Date()
|
||||
const results: PlatformSyncResult[] = []
|
||||
const errors: string[] = []
|
||||
|
||||
// Prüfen ob bereits ein Sync läuft
|
||||
if (syncStatus.isRunning) {
|
||||
return {
|
||||
success: false,
|
||||
startedAt,
|
||||
completedAt: new Date(),
|
||||
duration: 0,
|
||||
platforms: {},
|
||||
results: [],
|
||||
errors: ['Sync already running'],
|
||||
}
|
||||
}
|
||||
|
||||
syncStatus.isRunning = true
|
||||
syncStatus.progress = { totalAccounts: 0, processedAccounts: 0, percentage: 0 }
|
||||
|
||||
try {
|
||||
this.logger.info('Starting unified sync', { platforms, accountIds })
|
||||
|
||||
// Alle aktiven Accounts laden
|
||||
const accounts = await this.loadActiveAccounts(platforms, accountIds)
|
||||
syncStatus.progress.totalAccounts = accounts.length
|
||||
|
||||
this.logger.info(`Found ${accounts.length} active accounts to sync`)
|
||||
|
||||
if (accounts.length === 0) {
|
||||
return this.createResult(startedAt, results, errors)
|
||||
}
|
||||
|
||||
// Accounts nach Plattform gruppieren
|
||||
const accountsByPlatform = this.groupAccountsByPlatform(accounts)
|
||||
|
||||
// Jede Plattform synchronisieren
|
||||
for (const [platform, platformAccounts] of Object.entries(accountsByPlatform)) {
|
||||
syncStatus.currentPlatform = platform as SupportedPlatform
|
||||
|
||||
for (const account of platformAccounts) {
|
||||
syncStatus.currentAccount = account.displayName || `Account ${account.id}`
|
||||
|
||||
try {
|
||||
const result = await this.syncAccount(
|
||||
platform as SupportedPlatform,
|
||||
account,
|
||||
{
|
||||
analyzeWithAI,
|
||||
maxItems: maxItemsPerAccount,
|
||||
sinceDate,
|
||||
}
|
||||
)
|
||||
results.push(result)
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
errors.push(`${platform}/${account.id}: ${errorMsg}`)
|
||||
this.logger.error(`Failed to sync account ${account.id}`, error)
|
||||
}
|
||||
|
||||
syncStatus.progress.processedAccounts++
|
||||
syncStatus.progress.percentage = Math.round(
|
||||
(syncStatus.progress.processedAccounts / syncStatus.progress.totalAccounts) * 100
|
||||
)
|
||||
|
||||
// Rate Limiting zwischen Accounts
|
||||
await this.sleep(500)
|
||||
}
|
||||
}
|
||||
|
||||
const finalResult = this.createResult(startedAt, results, errors)
|
||||
syncStatus.lastResult = finalResult
|
||||
syncStatus.lastRunAt = startedAt.toISOString()
|
||||
|
||||
this.logger.summary({
|
||||
success: finalResult.success,
|
||||
processed: results.length,
|
||||
errors: errors.length,
|
||||
})
|
||||
|
||||
return finalResult
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
this.logger.error('Unified sync failed', error)
|
||||
errors.push(errorMsg)
|
||||
return this.createResult(startedAt, results, errors)
|
||||
} finally {
|
||||
syncStatus.isRunning = false
|
||||
syncStatus.currentPlatform = null
|
||||
syncStatus.currentAccount = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle aktiven Social Accounts
|
||||
*/
|
||||
private async loadActiveAccounts(
|
||||
platforms?: SupportedPlatform[],
|
||||
accountIds?: number[]
|
||||
): Promise<any[]> {
|
||||
const where: any = {
|
||||
and: [{ isActive: { equals: true } }],
|
||||
}
|
||||
|
||||
// Account-ID Filter
|
||||
if (accountIds && accountIds.length > 0) {
|
||||
where.and.push({
|
||||
id: { in: accountIds },
|
||||
})
|
||||
}
|
||||
|
||||
const accounts = await this.payload.find({
|
||||
collection: 'social-accounts',
|
||||
where,
|
||||
limit: 100,
|
||||
depth: 2,
|
||||
})
|
||||
|
||||
// Plattform-Filter anwenden
|
||||
if (platforms && platforms.length > 0) {
|
||||
return accounts.docs.filter((account) => {
|
||||
const platform = detectPlatform(account.platform)
|
||||
return platform && platforms.includes(platform)
|
||||
})
|
||||
}
|
||||
|
||||
// Nur Accounts mit unterstützter Plattform zurückgeben
|
||||
return accounts.docs.filter((account) => {
|
||||
const platform = detectPlatform(account.platform)
|
||||
return platform !== null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gruppiert Accounts nach Plattform
|
||||
*/
|
||||
private groupAccountsByPlatform(accounts: any[]): Record<SupportedPlatform, any[]> {
|
||||
const grouped: Record<SupportedPlatform, any[]> = {
|
||||
youtube: [],
|
||||
facebook: [],
|
||||
instagram: [],
|
||||
}
|
||||
|
||||
for (const account of accounts) {
|
||||
const platform = detectPlatform(account.platform)
|
||||
if (platform) {
|
||||
grouped[platform].push(account)
|
||||
}
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronisiert einen einzelnen Account
|
||||
*/
|
||||
private async syncAccount(
|
||||
platform: SupportedPlatform,
|
||||
account: any,
|
||||
options: {
|
||||
analyzeWithAI: boolean
|
||||
maxItems: number
|
||||
sinceDate?: Date
|
||||
}
|
||||
): Promise<PlatformSyncResult> {
|
||||
const { analyzeWithAI, maxItems, sinceDate } = options
|
||||
const accountId = account.id as number
|
||||
const accountName = (account.displayName as string) || `Account ${accountId}`
|
||||
const startTime = Date.now()
|
||||
|
||||
this.logger.info(`Syncing ${platform} account: ${accountName}`)
|
||||
|
||||
// Default since date: letzte 7 Tage oder letzter Sync
|
||||
const stats = account.stats as { lastSyncedAt?: string } | undefined
|
||||
const effectiveSinceDate =
|
||||
sinceDate ||
|
||||
(stats?.lastSyncedAt
|
||||
? new Date(stats.lastSyncedAt)
|
||||
: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000))
|
||||
|
||||
try {
|
||||
let result: PlatformSyncResult
|
||||
|
||||
switch (platform) {
|
||||
case 'youtube':
|
||||
result = await this.syncYouTubeAccount(accountId, accountName, {
|
||||
sinceDate: effectiveSinceDate,
|
||||
maxItems,
|
||||
analyzeWithAI,
|
||||
})
|
||||
break
|
||||
|
||||
case 'facebook':
|
||||
result = await this.syncFacebookAccount(accountId, accountName, {
|
||||
sinceDate: effectiveSinceDate,
|
||||
maxItems,
|
||||
analyzeWithAI,
|
||||
})
|
||||
break
|
||||
|
||||
case 'instagram':
|
||||
result = await this.syncInstagramAccount(accountId, accountName, {
|
||||
sinceDate: effectiveSinceDate,
|
||||
maxItems,
|
||||
analyzeWithAI,
|
||||
})
|
||||
break
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`)
|
||||
}
|
||||
|
||||
result.duration = Date.now() - startTime
|
||||
|
||||
this.logger.info(
|
||||
`${platform}/${accountName}: ${result.newComments} new, ${result.updatedComments} updated`
|
||||
)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
platform,
|
||||
accountId,
|
||||
accountName,
|
||||
success: false,
|
||||
newComments: 0,
|
||||
updatedComments: 0,
|
||||
newMessages: 0,
|
||||
postsProcessed: 0,
|
||||
errors: [errorMsg],
|
||||
duration: Date.now() - startTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* YouTube Account synchronisieren
|
||||
*/
|
||||
private async syncYouTubeAccount(
|
||||
accountId: number,
|
||||
accountName: string,
|
||||
options: { sinceDate: Date; maxItems: number; analyzeWithAI: boolean }
|
||||
): Promise<PlatformSyncResult> {
|
||||
const syncResult = await this.youtubeSyncService.syncComments({
|
||||
socialAccountId: accountId,
|
||||
sinceDate: options.sinceDate,
|
||||
maxComments: options.maxItems,
|
||||
analyzeWithAI: options.analyzeWithAI,
|
||||
})
|
||||
|
||||
return {
|
||||
platform: 'youtube',
|
||||
accountId,
|
||||
accountName,
|
||||
success: syncResult.success,
|
||||
newComments: syncResult.newComments,
|
||||
updatedComments: syncResult.updatedComments,
|
||||
newMessages: 0,
|
||||
postsProcessed: 0,
|
||||
errors: syncResult.errors,
|
||||
duration: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Facebook Account synchronisieren
|
||||
*/
|
||||
private async syncFacebookAccount(
|
||||
accountId: number,
|
||||
accountName: string,
|
||||
options: { sinceDate: Date; maxItems: number; analyzeWithAI: boolean }
|
||||
): Promise<PlatformSyncResult> {
|
||||
const syncResult = await this.facebookSyncService.syncComments({
|
||||
socialAccountId: accountId,
|
||||
sinceDate: options.sinceDate,
|
||||
maxItems: options.maxItems,
|
||||
analyzeWithAI: options.analyzeWithAI,
|
||||
syncComments: true,
|
||||
syncMessages: false,
|
||||
})
|
||||
|
||||
return {
|
||||
platform: 'facebook',
|
||||
accountId,
|
||||
accountName,
|
||||
success: syncResult.errors.length === 0,
|
||||
newComments: syncResult.newComments,
|
||||
updatedComments: syncResult.updatedComments,
|
||||
newMessages: syncResult.newMessages,
|
||||
postsProcessed: syncResult.postsProcessed,
|
||||
errors: syncResult.errors,
|
||||
duration: syncResult.duration,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instagram Account synchronisieren
|
||||
*/
|
||||
private async syncInstagramAccount(
|
||||
accountId: number,
|
||||
accountName: string,
|
||||
options: { sinceDate: Date; maxItems: number; analyzeWithAI: boolean }
|
||||
): Promise<PlatformSyncResult> {
|
||||
const syncResult = await this.instagramSyncService.syncComments({
|
||||
socialAccountId: accountId,
|
||||
sinceDate: options.sinceDate,
|
||||
maxItems: options.maxItems,
|
||||
analyzeWithAI: options.analyzeWithAI,
|
||||
syncComments: true,
|
||||
syncMentions: true,
|
||||
})
|
||||
|
||||
return {
|
||||
platform: 'instagram',
|
||||
accountId,
|
||||
accountName,
|
||||
success: syncResult.errors.length === 0,
|
||||
newComments: syncResult.newComments,
|
||||
updatedComments: syncResult.updatedComments,
|
||||
newMessages: syncResult.newMessages,
|
||||
postsProcessed: syncResult.postsProcessed,
|
||||
errors: syncResult.errors,
|
||||
duration: syncResult.duration,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt das finale Sync-Ergebnis
|
||||
*/
|
||||
private createResult(
|
||||
startedAt: Date,
|
||||
results: PlatformSyncResult[],
|
||||
errors: string[]
|
||||
): UnifiedSyncResult {
|
||||
const completedAt = new Date()
|
||||
|
||||
// Ergebnisse nach Plattform aggregieren
|
||||
const platforms: UnifiedSyncResult['platforms'] = {}
|
||||
|
||||
for (const result of results) {
|
||||
if (!platforms[result.platform]) {
|
||||
platforms[result.platform] = {
|
||||
accountsProcessed: 0,
|
||||
totalNewComments: 0,
|
||||
totalUpdatedComments: 0,
|
||||
totalErrors: 0,
|
||||
}
|
||||
}
|
||||
|
||||
platforms[result.platform]!.accountsProcessed++
|
||||
platforms[result.platform]!.totalNewComments += result.newComments
|
||||
platforms[result.platform]!.totalUpdatedComments += result.updatedComments
|
||||
platforms[result.platform]!.totalErrors += result.errors.length
|
||||
}
|
||||
|
||||
return {
|
||||
success: errors.length === 0 && results.every((r) => r.success),
|
||||
startedAt,
|
||||
completedAt,
|
||||
duration: completedAt.getTime() - startedAt.getTime(),
|
||||
platforms,
|
||||
results,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Convenience Functions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Führt einen vollständigen Sync aus (Singleton-Pattern)
|
||||
*/
|
||||
export async function runUnifiedSync(
|
||||
options: UnifiedSyncOptions = {}
|
||||
): Promise<UnifiedSyncResult> {
|
||||
const payload = await getPayload({ config })
|
||||
const service = new UnifiedSyncService(payload)
|
||||
return service.runFullSync(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den aktuellen Sync-Status zurück
|
||||
*/
|
||||
export function getUnifiedSyncStatus(): SyncStatus {
|
||||
return { ...syncStatus }
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt den Sync-Status zurück (für Tests)
|
||||
*/
|
||||
export function resetUnifiedSyncStatus(): void {
|
||||
syncStatus = {
|
||||
isRunning: false,
|
||||
currentPlatform: null,
|
||||
currentAccount: null,
|
||||
progress: {
|
||||
totalAccounts: 0,
|
||||
processedAccounts: 0,
|
||||
percentage: 0,
|
||||
},
|
||||
lastRunAt: null,
|
||||
lastResult: null,
|
||||
}
|
||||
}
|
||||
|
||||
export default UnifiedSyncService
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"$schema": "https://openapi.vercel.sh/vercel.json",
|
||||
"crons": [
|
||||
{
|
||||
"path": "/api/cron/youtube-sync",
|
||||
"path": "/api/cron/community-sync",
|
||||
"schedule": "*/15 * * * *"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue