veza/fixtures/core/generators/conversations.ts
2025-12-03 22:56:50 +01:00

625 lines
No EOL
20 KiB
TypeScript

import { v4 as uuidv4 } from 'uuid'
import { vezaFaker } from '../utils/faker-config'
import { globalConfig } from '../config'
import type {
Conversation,
ConversationType,
Message,
MessageType,
MessageStatus
} from '../schemas/database'
/**
* Conversation generation options
*/
export interface ConversationGenerationOptions {
type?: ConversationType
participantIds?: string[]
createdById?: string
messageCount?: number
isPrivate?: boolean
withAvatar?: boolean
timeSpan?: 'recent' | 'week' | 'month' | 'year'
}
/**
* Message generation options
*/
export interface MessageGenerationOptions {
type?: MessageType
senderId?: string
conversationId?: string
withAttachments?: boolean
withReactions?: boolean
withMentions?: boolean
isReply?: boolean
parentMessageId?: string
timeSpan?: 'recent' | 'week' | 'month'
}
/**
* Conversation scenario for realistic data generation
*/
export interface ConversationScenario {
name: string
participantCount: number
messageCount: number
timeSpan: 'recent' | 'week' | 'month'
messageTypes: MessageType[]
topics: string[]
}
/**
* Advanced conversation and messaging generator for Veza Platform
*/
export class ConversationGenerator {
private static generatedConversations: Map<string, Conversation> = new Map()
private static generatedMessages: Map<string, Message> = new Map()
private static conversationMessages: Map<string, string[]> = new Map()
/**
* Generate a single conversation
*/
static generateConversation(options: ConversationGenerationOptions = {}): Conversation {
const type = options.type || vezaFaker.helpers.arrayElement(['direct', 'group', 'channel'])
const participantIds = options.participantIds || this.generateParticipantIds(type)
const createdById = options.createdById || vezaFaker.helpers.arrayElement(participantIds)
const createdAt = this.generateCreationDate(options.timeSpan)
const conversation: Conversation = {
id: uuidv4(),
name: type === 'direct' ? undefined : vezaFaker.chat.conversationName(type),
description: type === 'channel' ? this.generateChannelDescription() : undefined,
type,
isPrivate: options.isPrivate ?? this.shouldBePrivate(type),
avatarUrl: options.withAvatar !== false && type !== 'direct' ?
vezaFaker.utils.fileUrl('image', 'conversation-avatar.jpg') : undefined,
createdById,
participantIds,
lastMessageId: undefined, // Will be set after generating messages
lastActivityAt: createdAt,
createdAt,
updatedAt: createdAt,
}
// Generate messages for this conversation
if (options.messageCount !== 0) {
const messageCount = options.messageCount || this.getDefaultMessageCount(type)
const messages = this.generateMessages(conversation.id, participantIds, messageCount, {
timeSpan: options.timeSpan
})
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1]
conversation.lastMessageId = lastMessage.id
conversation.lastActivityAt = lastMessage.createdAt
conversation.updatedAt = lastMessage.createdAt
}
}
this.generatedConversations.set(conversation.id, conversation)
return conversation
}
/**
* Generate multiple conversations
*/
static generateBatch(count: number, options: ConversationGenerationOptions = {}): Conversation[] {
const conversations: Conversation[] = []
for (let i = 0; i < count; i++) {
conversations.push(this.generateConversation(options))
}
return conversations
}
/**
* Generate conversations with realistic distribution
*/
static generateWithDistribution(userIds: string[]): Conversation[] {
const config = globalConfig.generation.conversations
const conversations: Conversation[] = []
// Generate direct conversations
for (let i = 0; i < config.directCount; i++) {
const participantIds = vezaFaker.helpers.arrayElements(userIds, 2)
conversations.push(this.generateConversation({
type: 'direct',
participantIds,
createdById: participantIds[0] || uuidv4(),
timeSpan: vezaFaker.helpers.arrayElement(['recent', 'week', 'month'])
}))
}
// Generate group conversations
for (let i = 0; i < config.groupCount; i++) {
const participantCount = vezaFaker.number.int({ min: 3, max: 15 })
const participantIds = vezaFaker.helpers.arrayElements(userIds, participantCount)
conversations.push(this.generateConversation({
type: 'group',
participantIds,
createdById: participantIds[0] || uuidv4(),
timeSpan: vezaFaker.helpers.arrayElement(['week', 'month', 'year'])
}))
}
// Generate channels
for (let i = 0; i < config.channelCount; i++) {
const participantCount = vezaFaker.number.int({ min: 10, max: Math.min(50, userIds.length) })
const participantIds = vezaFaker.helpers.arrayElements(userIds, participantCount)
conversations.push(this.generateConversation({
type: 'channel',
participantIds,
createdById: participantIds[0] || uuidv4(),
isPrivate: vezaFaker.datatype.boolean({ probability: 0.3 }),
timeSpan: 'year'
}))
}
return conversations
}
/**
* Generate messages for a conversation
*/
static generateMessages(
conversationId: string,
participantIds: string[],
count: number,
options: Omit<MessageGenerationOptions, 'conversationId'> = {}
): Message[] {
const messages: Message[] = []
const messageIds: string[] = []
const timeSpan = options.timeSpan || 'month'
const startDate = this.getTimeSpanStart(timeSpan)
const endDate = new Date()
// Generate conversation flow with realistic patterns
let currentSender = vezaFaker.helpers.arrayElement(participantIds)
let lastMessageTime = startDate
let conversationMomentum = 0.5 // How active the conversation is
for (let i = 0; i < count; i++) {
// Determine message timing
const timeSinceLastMessage = this.generateMessageDelay(conversationMomentum, timeSpan)
const messageTime = new Date(lastMessageTime.getTime() + timeSinceLastMessage)
if (messageTime > endDate) break
// Determine sender (realistic conversation flow)
const senderChangeProb = this.getSenderChangeprobability(i, conversationMomentum)
if (vezaFaker.datatype.boolean({ probability: senderChangeProb })) {
currentSender = vezaFaker.helpers.arrayElement(
participantIds.filter(id => id !== currentSender)
)
}
// Generate message
const message = this.generateMessage({
conversationId,
senderId: currentSender,
withAttachments: vezaFaker.datatype.boolean({ probability: 0.1 }),
withReactions: vezaFaker.datatype.boolean({ probability: 0.3 }),
withMentions: participantIds.length > 2 && vezaFaker.datatype.boolean({ probability: 0.15 }),
isReply: i > 0 && vezaFaker.datatype.boolean({ probability: 0.1 }),
parentMessageId: i > 0 && vezaFaker.datatype.boolean({ probability: 0.1 }) ?
vezaFaker.helpers.arrayElement(messageIds) : undefined
})
// Set realistic timestamp
message.createdAt = messageTime
message.updatedAt = messageTime
messages.push(message)
messageIds.push(message.id)
lastMessageTime = messageTime
// Update conversation momentum
conversationMomentum = this.updateConversationMomentum(conversationMomentum, i, count)
}
// Store message IDs for this conversation
this.conversationMessages.set(conversationId, messageIds)
return messages
}
/**
* Generate a single message
*/
static generateMessage(options: MessageGenerationOptions = {}): Message {
const type = options.type || 'text'
const senderId = options.senderId || uuidv4()
const conversationId = options.conversationId || uuidv4()
const createdAt = new Date()
const message: Message = {
id: uuidv4(),
conversationId,
senderId,
content: this.generateMessageContent(type),
type,
status: 'delivered' as MessageStatus,
parentMessageId: options.parentMessageId,
attachments: options.withAttachments ? this.generateAttachments() : [],
reactions: options.withReactions ? this.generateReactions() : [],
mentions: options.withMentions ? this.generateMentions() : [],
isEdited: vezaFaker.datatype.boolean({ probability: 0.05 }),
isDeleted: false,
createdAt,
updatedAt: createdAt,
}
this.generatedMessages.set(message.id, message)
return message
}
/**
* Generate conversation scenarios
*/
static generateScenario(scenario: ConversationScenario, userIds: string[]): Conversation {
const participantIds = vezaFaker.helpers.arrayElements(userIds, scenario.participantCount)
const conversation = this.generateConversation({
type: scenario.participantCount === 2 ? 'direct' : 'group',
participantIds,
messageCount: 0, // We'll generate messages separately
timeSpan: scenario.timeSpan
})
// Generate scenario-specific messages
const messages = this.generateScenarioMessages(
conversation.id,
participantIds,
scenario
)
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1]
conversation.lastMessageId = lastMessage.id
conversation.lastActivityAt = lastMessage.createdAt
conversation.updatedAt = lastMessage.createdAt
}
return conversation
}
/**
* Predefined conversation scenarios
*/
static readonly SCENARIOS: ConversationScenario[] = [
{
name: 'Studio Collaboration',
participantCount: 4,
messageCount: 50,
timeSpan: 'week',
messageTypes: ['text', 'audio', 'file'],
topics: ['recording', 'mixing', 'feedback', 'scheduling']
},
{
name: 'Music Discovery',
participantCount: 6,
messageCount: 30,
timeSpan: 'month',
messageTypes: ['text'],
topics: ['recommendations', 'new releases', 'concerts', 'reviews']
},
{
name: 'Event Planning',
participantCount: 8,
messageCount: 80,
timeSpan: 'month',
messageTypes: ['text', 'image', 'file'],
topics: ['venue', 'lineup', 'promotion', 'logistics']
},
{
name: 'Producer Feedback',
participantCount: 3,
messageCount: 25,
timeSpan: 'week',
messageTypes: ['text', 'audio'],
topics: ['mix notes', 'arrangement', 'sound design', 'mastering']
}
]
/**
* Private helper methods
*/
private static generateParticipantIds(type: ConversationType): string[] {
const counts = {
direct: 2,
group: vezaFaker.number.int({ min: 3, max: 15 }),
channel: vezaFaker.number.int({ min: 10, max: 50 })
}
const count = counts[type]
return Array.from({ length: count }, () => uuidv4())
}
private static shouldBePrivate(type: ConversationType): boolean {
const probabilities = {
direct: 0.8,
group: 0.4,
channel: 0.2
}
return vezaFaker.datatype.boolean({ probability: probabilities[type] })
}
private static getDefaultMessageCount(type: ConversationType): number {
const ranges = {
direct: { min: 10, max: 100 },
group: { min: 20, max: 200 },
channel: { min: 50, max: 500 }
}
const range = ranges[type]
return vezaFaker.number.int(range)
}
private static generateCreationDate(timeSpan?: string): Date {
const spans = {
recent: { days: 7 },
week: { days: 30 },
month: { days: 90 },
year: { days: 365 }
}
const span = spans[timeSpan as keyof typeof spans] || spans.month
return vezaFaker.date.recent(span)
}
private static generateChannelDescription(): string {
const descriptions = [
'Canal pour les discussions générales de la communauté',
'Espace dédié aux annonces importantes',
'Partage de musique et découvertes',
'Discussions techniques et production musicale',
'Feedback et critiques constructives',
'Organisation d\'événements et concerts',
'Collaborations entre artistes',
'Support et entraide communautaire'
]
return vezaFaker.helpers.arrayElement(descriptions)
}
private static generateMessageContent(type: MessageType): string {
if (type === 'system') {
return vezaFaker.chat.messageContent('system')
}
if (type === 'audio') {
return 'Message audio'
}
if (type === 'image') {
return 'Image partagée'
}
if (type === 'file') {
return 'Fichier partagé'
}
return vezaFaker.chat.messageContent('text')
}
private static generateAttachments() {
const attachmentTypes = ['image', 'audio', 'file'] as const
const type = vezaFaker.helpers.arrayElement(attachmentTypes)
return [{
id: uuidv4(),
type,
url: vezaFaker.utils.fileUrl(type === 'image' ? 'image' : 'audio'),
filename: `attachment.${type === 'image' ? 'jpg' : type === 'audio' ? 'mp3' : 'pdf'}`,
size: vezaFaker.number.int({ min: 1024, max: 10485760 }), // 1KB to 10MB
mimeType: type === 'image' ? 'image/jpeg' : type === 'audio' ? 'audio/mpeg' : 'application/pdf'
}]
}
private static generateReactions() {
const reactionCount = vezaFaker.number.int({ min: 1, max: 5 })
const reactions = []
for (let i = 0; i < reactionCount; i++) {
reactions.push({
emoji: vezaFaker.chat.reactionEmoji(),
userId: uuidv4(),
createdAt: vezaFaker.date.recent({ days: 1 })
})
}
return reactions
}
private static generateMentions(): string[] {
const mentionCount = vezaFaker.number.int({ min: 1, max: 3 })
return Array.from({ length: mentionCount }, () => uuidv4())
}
private static getTimeSpanStart(timeSpan: string): Date {
const now = new Date()
const spans = {
recent: 7 * 24 * 60 * 60 * 1000, // 7 days
week: 30 * 24 * 60 * 60 * 1000, // 30 days
month: 90 * 24 * 60 * 60 * 1000 // 90 days
}
const span = spans[timeSpan as keyof typeof spans] || spans.month
return new Date(now.getTime() - span)
}
private static generateMessageDelay(momentum: number, timeSpan: string): number {
const baseDelays = {
recent: { min: 60000, max: 3600000 }, // 1 minute to 1 hour
week: { min: 3600000, max: 86400000 }, // 1 hour to 1 day
month: { min: 86400000, max: 604800000 } // 1 day to 1 week
}
const delays = baseDelays[timeSpan as keyof typeof baseDelays] || baseDelays.month
const momentumFactor = 1 / (momentum + 0.1) // Higher momentum = shorter delays
return vezaFaker.number.int({
min: delays.min * momentumFactor,
max: delays.max * momentumFactor
})
}
private static getSenderChangeProability(messageIndex: number, momentum: number): number {
// Higher momentum and later in conversation = more likely to change sender
const baseProbability = 0.3
const momentumBonus = momentum * 0.3
const indexBonus = Math.min(messageIndex / 10, 0.2)
return Math.min(baseProbability + momentumBonus + indexBonus, 0.8)
}
private static updateConversationMomentum(
currentMomentum: number,
messageIndex: number,
totalMessages: number
): number {
// Conversations tend to be more active in the middle
const progress = messageIndex / totalMessages
const peakActivity = 0.3 + 0.4 * Math.sin(progress * Math.PI)
const randomVariation = vezaFaker.number.float({ min: -0.1, max: 0.1 })
return Math.max(0.1, Math.min(1, peakActivity + randomVariation))
}
private static generateScenarioMessages(
conversationId: string,
participantIds: string[],
scenario: ConversationScenario
): Message[] {
const messages: Message[] = []
const topicMessages = this.getTopicMessages(scenario.topics)
for (let i = 0; i < scenario.messageCount; i++) {
const messageType = vezaFaker.helpers.arrayElement(scenario.messageTypes)
const senderId = vezaFaker.helpers.arrayElement(participantIds)
const content = vezaFaker.helpers.arrayElement(topicMessages)
const message = this.generateMessage({
conversationId,
senderId,
type: messageType,
})
message.content = content
messages.push(message)
}
return messages
}
private static getTopicMessages(topics: string[]): string[] {
const messagesByTopic: Record<string, string[]> = {
recording: [
'On peut programmer la session pour demain ?',
'J\'ai préparé les pistes de base',
'Le studio est libre à partir de 14h',
'N\'oubliez pas vos instruments !'
],
mixing: [
'Le mix sonne vraiment bien maintenant',
'On pourrait baisser un peu les graves ?',
'Cette reverb est parfaite',
'Qu\'est-ce que vous pensez du niveau vocal ?'
],
feedback: [
'Excellent travail sur cette composition !',
'La mélodie est accrocheuse',
'Peut-être ajouter plus de variation dans le refrain ?',
'Cette section groove vraiment bien'
],
recommendations: [
'Vous devez absolument écouter ce nouvel album !',
'Cette artiste va cartonner, j\'en suis sûr',
'Quelqu\'un connaît ce genre musical ?',
'Merci pour la découverte, c\'est génial !'
],
venue: [
'La salle est confirmée pour le 15',
'Capacité de 500 personnes',
'Le matériel son est inclus',
'Parking disponible pour les artistes'
],
'sound design': [
'Ce synthé sonne incroyable',
'Comment tu as fait cet effet ?',
'On pourrait essayer avec plus de distortion',
'Cette texture sonore est parfaite'
]
}
const allMessages: string[] = []
topics.forEach(topic => {
if (messagesByTopic[topic]) {
allMessages.push(...messagesByTopic[topic])
}
})
return allMessages.length > 0 ? allMessages : [vezaFaker.chat.messageContent()]
}
/**
* Utility methods
*/
static getGeneratedConversation(id: string): Conversation | undefined {
return this.generatedConversations.get(id)
}
static getGeneratedMessage(id: string): Message | undefined {
return this.generatedMessages.get(id)
}
static getConversationMessages(conversationId: string): Message[] {
const messageIds = this.conversationMessages.get(conversationId) || []
return messageIds.map(id => this.generatedMessages.get(id)!).filter(Boolean)
}
static getAllGeneratedConversations(): Conversation[] {
return Array.from(this.generatedConversations.values())
}
static getAllGeneratedMessages(): Message[] {
return Array.from(this.generatedMessages.values())
}
static clearCache(): void {
this.generatedConversations.clear()
this.generatedMessages.clear()
this.conversationMessages.clear()
}
static getStats(): {
totalConversations: number
totalMessages: number
byType: Record<ConversationType, number>
averageMessagesPerConversation: number
} {
const conversations = this.getAllGeneratedConversations()
const messages = this.getAllGeneratedMessages()
const byType: Record<ConversationType, number> = {
direct: 0,
group: 0,
channel: 0
}
conversations.forEach(conv => {
byType[conv.type]++
})
return {
totalConversations: conversations.length,
totalMessages: messages.length,
byType,
averageMessagesPerConversation: conversations.length > 0 ?
Math.round(messages.length / conversations.length) : 0
}
}
}