mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 19:44:12 +00:00
Community Management Phase 1 completion: - Add Community Inbox admin view with filters, stats, and reply functionality - Add Rules Engine service for automated interaction processing - Add YouTube OAuth flow (auth, callback, token refresh) - Add Comment Sync cron job (every 15 minutes) - Add Community Export API (PDF/Excel/CSV) - Fix database schema for community_rules hasMany fields - Fix access control in communityAccess.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
207 lines
5.6 KiB
TypeScript
207 lines
5.6 KiB
TypeScript
/**
|
||
* Sync All Comments Job
|
||
*
|
||
* Synchronisiert Kommentare für alle aktiven YouTube-Accounts.
|
||
* Kann als Cron-Job oder manuell ausgeführt werden.
|
||
*/
|
||
|
||
import { getPayload, Payload } from 'payload'
|
||
import config from '@payload-config'
|
||
import { refreshAccessToken, isTokenExpired } from '@/lib/integrations/youtube/oauth'
|
||
|
||
interface SocialAccount {
|
||
id: number
|
||
displayName?: string
|
||
isActive?: boolean
|
||
externalId?: string
|
||
credentials?: {
|
||
accessToken?: string
|
||
refreshToken?: string
|
||
tokenExpiresAt?: string
|
||
}
|
||
syncSettings?: {
|
||
autoSyncEnabled?: boolean
|
||
syncIntervalMinutes?: number
|
||
syncComments?: boolean
|
||
}
|
||
stats?: {
|
||
lastSyncedAt?: string
|
||
}
|
||
}
|
||
|
||
interface SyncResult {
|
||
accountId: number
|
||
displayName: string
|
||
success: boolean
|
||
created: number
|
||
updated: number
|
||
errors: string[]
|
||
}
|
||
|
||
/**
|
||
* Führt den Comment-Sync für alle aktiven Accounts durch
|
||
*/
|
||
export async function runSyncAllCommentsJob(payload: Payload): Promise<{
|
||
totalAccounts: number
|
||
successfulSyncs: number
|
||
totalNew: number
|
||
totalUpdated: number
|
||
errors: string[]
|
||
}> {
|
||
console.log('🔄 Starting scheduled comment sync...')
|
||
console.log(` Time: ${new Date().toISOString()}`)
|
||
|
||
// Aktive Accounts mit Auto-Sync laden
|
||
const accounts = await payload.find({
|
||
collection: 'social-accounts',
|
||
where: {
|
||
and: [
|
||
{ isActive: { equals: true } },
|
||
{ 'syncSettings.autoSyncEnabled': { equals: true } },
|
||
{ 'credentials.accessToken': { exists: true } },
|
||
],
|
||
},
|
||
limit: 100,
|
||
depth: 2,
|
||
})
|
||
|
||
console.log(`📊 Found ${accounts.docs.length} accounts to sync`)
|
||
|
||
let totalNew = 0
|
||
let totalUpdated = 0
|
||
let successfulSyncs = 0
|
||
const errors: string[] = []
|
||
const results: SyncResult[] = []
|
||
|
||
for (const doc of accounts.docs) {
|
||
const account = doc as unknown as SocialAccount
|
||
|
||
try {
|
||
console.log(`\n📺 Syncing: ${account.displayName || account.id}`)
|
||
|
||
// Token-Ablauf prüfen und ggf. erneuern
|
||
if (isTokenExpired(account.credentials?.tokenExpiresAt)) {
|
||
console.log(' ⏰ Token expired, refreshing...')
|
||
|
||
if (!account.credentials?.refreshToken) {
|
||
console.log(' ❌ No refresh token available')
|
||
errors.push(`${account.displayName}: No refresh token available`)
|
||
continue
|
||
}
|
||
|
||
try {
|
||
const newCredentials = await refreshAccessToken(account.credentials.refreshToken)
|
||
|
||
await payload.update({
|
||
collection: 'social-accounts',
|
||
id: account.id,
|
||
data: {
|
||
credentials: {
|
||
accessToken: newCredentials.access_token || undefined,
|
||
refreshToken: newCredentials.refresh_token || account.credentials.refreshToken,
|
||
tokenExpiresAt: newCredentials.expiry_date
|
||
? new Date(newCredentials.expiry_date).toISOString()
|
||
: undefined,
|
||
},
|
||
},
|
||
})
|
||
|
||
console.log(' ✅ Token refreshed')
|
||
} catch (refreshError) {
|
||
const msg = refreshError instanceof Error ? refreshError.message : 'Unknown error'
|
||
console.error(' ❌ Token refresh failed:', msg)
|
||
errors.push(`${account.displayName}: Token refresh failed - ${msg}`)
|
||
continue
|
||
}
|
||
}
|
||
|
||
// CommentsSyncService dynamisch importieren
|
||
const { CommentsSyncService } = await import(
|
||
'@/lib/integrations/youtube/CommentsSyncService'
|
||
)
|
||
const syncService = new CommentsSyncService(payload)
|
||
|
||
const result = await syncService.syncCommentsForAccount(account.id, {
|
||
maxComments: 100,
|
||
analyzeWithAI: true,
|
||
})
|
||
|
||
const created = result.created || 0
|
||
const updated = result.updated || 0
|
||
|
||
totalNew += created
|
||
totalUpdated += updated
|
||
successfulSyncs++
|
||
|
||
results.push({
|
||
accountId: account.id,
|
||
displayName: account.displayName || `Account ${account.id}`,
|
||
success: true,
|
||
created,
|
||
updated,
|
||
errors: result.errors || [],
|
||
})
|
||
|
||
console.log(` ✅ New: ${created}, Updated: ${updated}`)
|
||
|
||
if (result.errors?.length > 0) {
|
||
errors.push(...result.errors.map((e: string) => `${account.displayName}: ${e}`))
|
||
}
|
||
} catch (accountError: unknown) {
|
||
const msg = accountError instanceof Error ? accountError.message : 'Unknown error'
|
||
console.error(` ❌ Failed: ${msg}`)
|
||
errors.push(`${account.displayName}: ${msg}`)
|
||
|
||
results.push({
|
||
accountId: account.id,
|
||
displayName: account.displayName || `Account ${account.id}`,
|
||
success: false,
|
||
created: 0,
|
||
updated: 0,
|
||
errors: [msg],
|
||
})
|
||
}
|
||
}
|
||
|
||
// Zusammenfassung
|
||
console.log('\n' + '='.repeat(50))
|
||
console.log('📊 SYNC SUMMARY')
|
||
console.log('='.repeat(50))
|
||
console.log(` Accounts processed: ${accounts.docs.length}`)
|
||
console.log(` Successful syncs: ${successfulSyncs}`)
|
||
console.log(` New comments: ${totalNew}`)
|
||
console.log(` Updated: ${totalUpdated}`)
|
||
console.log(` Errors: ${errors.length}`)
|
||
|
||
if (errors.length > 0) {
|
||
console.log('\n⚠️ Errors:')
|
||
errors.forEach((e) => console.log(` - ${e}`))
|
||
}
|
||
|
||
return {
|
||
totalAccounts: accounts.docs.length,
|
||
successfulSyncs,
|
||
totalNew,
|
||
totalUpdated,
|
||
errors,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Standalone-Ausführung für Cron-Job
|
||
*/
|
||
async function main() {
|
||
try {
|
||
const payload = await getPayload({ config })
|
||
await runSyncAllCommentsJob(payload)
|
||
process.exit(0)
|
||
} catch (error) {
|
||
console.error('❌ Sync job failed:', error)
|
||
process.exit(1)
|
||
}
|
||
}
|
||
|
||
// Wenn direkt ausgeführt (nicht importiert)
|
||
if (require.main === module) {
|
||
main()
|
||
}
|