625 lines
No EOL
20 KiB
TypeScript
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
|
|
}
|
|
}
|
|
} |