/** * Rules Engine * * Wertet Community-Regeln aus und wendet automatische Aktionen an. * Wird beim Import neuer Interaktionen aufgerufen. */ import type { Payload } from 'payload' interface RuleAction { action: string value?: string targetUser?: number | { id: number } targetTemplate?: number | { id: number } } interface RuleTrigger { type: string keywords?: { keyword: string; matchType: string }[] sentimentValues?: string[] influencerMinFollowers?: number } interface Rule { id: number name: string priority: number isActive: boolean platforms?: Array<{ id: number } | number> channel?: { id: number } | number trigger: RuleTrigger actions: RuleAction[] stats?: { timesTriggered?: number lastTriggeredAt?: string } } interface Interaction { id: number platform?: { id: number } | number message?: string author?: { name?: string subscriberCount?: number isSubscriber?: boolean } analysis?: { sentiment?: string suggestedTemplate?: number } flags?: { isMedicalQuestion?: boolean requiresEscalation?: boolean isSpam?: boolean isFromInfluencer?: boolean } priority?: string linkedContent?: { id: number } | number } interface EvaluationResult { appliedRules: string[] changes: Record } export class RulesEngine { private payload: Payload constructor(payload: Payload) { this.payload = payload } /** * Wertet alle aktiven Regeln für eine Interaktion aus */ async evaluateRules(interactionId: number): Promise { const appliedRules: string[] = [] const changes: Record = {} // Interaktion laden const interaction = (await this.payload.findByID({ collection: 'community-interactions', id: interactionId, depth: 2, })) as Interaction | null if (!interaction) { throw new Error('Interaction not found') } // Aktive Regeln nach Priorität sortiert laden const rules = await this.payload.find({ collection: 'community-rules', where: { isActive: { equals: true }, }, sort: 'priority', limit: 100, }) for (const doc of rules.docs) { const rule = doc as unknown as Rule // Platform-Filter prüfen if (rule.platforms && rule.platforms.length > 0) { const platformIds = rule.platforms.map((p) => (typeof p === 'object' ? p.id : p)) const interactionPlatformId = typeof interaction.platform === 'object' ? interaction.platform.id : interaction.platform if (!platformIds.includes(interactionPlatformId as number)) { continue } } // Trigger prüfen const triggered = await this.checkTrigger(rule, interaction) if (triggered) { // Aktionen anwenden const ruleChanges = await this.applyActions(rule, interaction) Object.assign(changes, ruleChanges) appliedRules.push(rule.name) // Stats aktualisieren await this.payload.update({ collection: 'community-rules', id: rule.id, data: { stats: { timesTriggered: (rule.stats?.timesTriggered || 0) + 1, lastTriggeredAt: new Date().toISOString(), }, }, }) } } // Änderungen an der Interaktion speichern if (Object.keys(changes).length > 0) { await this.payload.update({ collection: 'community-interactions', id: interactionId, data: changes, }) } return { appliedRules, changes } } /** * Prüft ob ein Trigger zutrifft */ private async checkTrigger(rule: Rule, interaction: Interaction): Promise { const { trigger } = rule const message = interaction.message?.toLowerCase() || '' switch (trigger.type) { case 'keyword': if (!trigger.keywords?.length) return false return trigger.keywords.some(({ keyword, matchType }) => { const kw = keyword.toLowerCase() switch (matchType) { case 'exact': return message === kw case 'regex': try { return new RegExp(keyword, 'i').test(interaction.message || '') } catch { return false } case 'contains': default: return message.includes(kw) } }) case 'sentiment': if (!trigger.sentimentValues?.length) return false return trigger.sentimentValues.includes(interaction.analysis?.sentiment || '') case 'influencer': const minFollowers = trigger.influencerMinFollowers || 10000 return (interaction.author?.subscriberCount || 0) >= minFollowers case 'medical': return interaction.flags?.isMedicalQuestion === true case 'new_subscriber': return interaction.author?.isSubscriber === true case 'negative_sentiment': return interaction.analysis?.sentiment === 'negative' case 'positive_sentiment': return interaction.analysis?.sentiment === 'positive' case 'all': // Immer auslösen (für Default-Regeln) return true default: return false } } /** * Wendet die Aktionen einer Regel an */ private async applyActions( rule: Rule, interaction: Interaction ): Promise> { const changes: Record = {} for (const action of rule.actions) { switch (action.action) { case 'set_priority': if (action.value) { changes.priority = action.value } break case 'assign_to': const userId = typeof action.targetUser === 'object' ? action.targetUser.id : action.targetUser if (userId) { changes.assignedTo = userId } break case 'apply_template': const templateId = typeof action.targetTemplate === 'object' ? action.targetTemplate.id : action.targetTemplate if (templateId) { // Nested update für analysis.suggestedTemplate const currentAnalysis = interaction.analysis || {} changes.analysis = { ...currentAnalysis, suggestedTemplate: templateId, } } break case 'flag': // Flags werden als separate Objekte aktualisiert const currentFlags = interaction.flags || {} if (action.value === 'medical') { changes.flags = { ...currentFlags, isMedicalQuestion: true } } else if (action.value === 'escalation') { changes.flags = { ...currentFlags, requiresEscalation: true } } else if (action.value === 'spam') { changes.flags = { ...currentFlags, isSpam: true } } else if (action.value === 'influencer') { changes.flags = { ...currentFlags, isFromInfluencer: true } } break case 'set_status': if (action.value) { changes.status = action.value } break case 'notify': await this.sendNotification(action, interaction, rule) break case 'auto_reply': // Auto-Reply könnte hier implementiert werden // Für jetzt nur loggen console.log(`[RulesEngine] Auto-reply triggered for interaction ${interaction.id}`) break } } return changes } /** * Sendet eine Benachrichtigung */ private async sendNotification( action: RuleAction, interaction: Interaction, rule: Rule ): Promise { const userId = typeof action.targetUser === 'object' ? action.targetUser.id : action.targetUser if (!userId) return try { // Prüfen ob yt-notifications Collection existiert await this.payload.create({ collection: 'yt-notifications', data: { user: userId, type: 'community_rule_triggered', title: `🔔 Rule "${rule.name}" triggered`, message: `New interaction from ${interaction.author?.name || 'Unknown'}: "${(interaction.message || '').substring(0, 100)}..."`, relatedContent: typeof interaction.linkedContent === 'object' ? interaction.linkedContent.id : interaction.linkedContent, priority: interaction.priority === 'urgent' ? 'urgent' : 'normal', isRead: false, }, }) } catch (error) { // Collection existiert möglicherweise nicht - nur loggen, nicht crashen console.warn('[RulesEngine] Failed to create notification:', error) } } } /** * Singleton-Instanz für einfachen Zugriff */ let rulesEngineInstance: RulesEngine | null = null export function getRulesEngine(payload: Payload): RulesEngine { if (!rulesEngineInstance) { rulesEngineInstance = new RulesEngine(payload) } return rulesEngineInstance }