import { DyNTS_SingletonService } from '../../../_services/base/singleton.service'; import { DyFM_Error, DyFM_Log } from '@futdevpro/fsm-dynamo'; import { DyFM_Msg_Message, DyFM_Msg_Conversation, DyFM_Msg_Status, DyFM_Msg_Reaction, DyFM_Msg_Participant, DyFM_Msg_ParticipantRole, DyFM_Msg_Type, DyFM_Msg_ConversationType, } from '@futdevpro/fsm-dynamo/messaging'; import { DyNTS_global_settings } from '../../../_collections/global-settings.const'; import { DyNTS_Msg_Message_DataService } from './msg-message.data-service'; import { DyNTS_Msg_Conversation_DataService } from './msg-conversation.data-service'; import { DyNTS_Msg_Events_Service } from './msg-events.service'; /** * Main Messaging Control Service * Handles all business logic for messaging operations */ export class DyNTS_Msg_Main_ControlService extends DyNTS_SingletonService { static getInstance(): DyNTS_Msg_Main_ControlService { return DyNTS_Msg_Main_ControlService.getSingletonInstance(); } private eventsService = DyNTS_Msg_Events_Service.getInstance(); /** * Send a message */ async sendMessage( conversationId: string, messageData: Partial, senderId: string, issuer: string ): Promise { try { if (!messageData?.content) { throw new Error('Message content is required'); } // Create message const message = new DyFM_Msg_Message({ conversationId: conversationId, senderId: senderId, content: messageData.content, type: messageData.type || DyFM_Msg_Type.text, status: DyFM_Msg_Status.sent, sentAt: new Date(), __createdBy: issuer, __lastModifiedBy: issuer, ...messageData, }); // Save message const messageService = new DyNTS_Msg_Message_DataService({ message, issuer, }); await messageService.saveData(messageService.data, false, false); // Update conversation last message const conversationService = new DyNTS_Msg_Conversation_DataService({ issuer, }); await conversationService.updateLastMessage( conversationId, messageService.data._id!, message.content.substring(0, 100) ); // Emit event this.eventsService.emitMessageSent(messageService.data, conversationId); return messageService.data; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('sendMessage', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-SM`, userMessage: 'Failed to send message.', }); } } /** * Edit a message */ async editMessage( messageId: string, content: string, userId: string, issuer: string ): Promise { try { const messageService = new DyNTS_Msg_Message_DataService({ issuer, }); const message = await messageService.getDataById(messageId); if (!message) { throw new Error('Message not found'); } if (message.senderId !== userId) { throw new Error('Unauthorized: Only the message sender can edit it'); } message.content = content; message.editedAt = new Date(); message.status = DyFM_Msg_Status.sent; // Keep as sent since edited is not a status message.__lastModifiedBy = issuer; await messageService.saveData(message, false, false); // Emit event this.eventsService.emitMessageUpdated(messageService.data, message.conversationId); return messageService.data; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('editMessage', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-EM`, userMessage: 'Failed to edit message.', }); } } async modifyMessage(message: DyFM_Msg_Message, issuer: string): Promise { try { const messageService = new DyNTS_Msg_Message_DataService({ issuer, }); await messageService.saveData(message); // Emit event this.eventsService.emitMessageUpdated(messageService.data, message.conversationId); return messageService.data; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('modifyMessage', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-MM`, userMessage: 'Failed to modify message.', }); } } /** * Delete a message */ async deleteMessage( messageId: string, userId: string, issuer: string ): Promise { try { const messageService = new DyNTS_Msg_Message_DataService({ issuer, }); const message = await messageService.getDataById(messageId); if (!message) { throw new Error('Message not found'); } if (message.senderId !== userId) { throw new Error('Unauthorized: Only the message sender can delete it'); } const conversationId = message.conversationId; await messageService.deleteData(messageId); // Emit event this.eventsService.emitMessageDeleted(messageId, conversationId); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('deleteMessage', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-DM`, userMessage: 'Failed to delete message.', }); } } /** * Mark messages as read */ async markMessagesAsRead( messageIds: string[], userId: string, issuer: string ): Promise<{ success: boolean; count: number }> { try { const messageService = new DyNTS_Msg_Message_DataService({ issuer, }); for (const messageId of messageIds) { await messageService.markAsRead(messageId, userId); // Get message to get conversationId for event const message = await messageService.getDataById(messageId); this.eventsService.emitMessageRead(messageId, userId, message.conversationId); } return { success: true, count: messageIds.length }; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('markMessagesAsRead', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-MAR`, userMessage: 'Failed to mark messages as read.', }); } } /** * Add reaction to message */ async addReaction( messageId: string, emoji: string, userId: string, issuer: string ): Promise { try { const messageService = new DyNTS_Msg_Message_DataService({ issuer, }); const message = await messageService.getDataById(messageId); if (!message) { throw new Error('Message not found'); } if (!message.reactions) { message.reactions = []; } // Check if user already reacted with this emoji const existingReaction = message.reactions.find((r: any) => r.emoji === emoji && r.userId === userId); if (!existingReaction) { const newReaction = { emoji, userId, reactedAt: new Date(), } as any; message.reactions.push(newReaction); await messageService.saveData(message, false, false); // Emit event this.eventsService.emitReactionAdded(messageId, newReaction); } return messageService.data; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('addReaction', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-AR`, userMessage: 'Failed to add reaction.', }); } } /** * Remove reaction from message */ async removeReaction( messageId: string, emoji: string, userId: string, issuer: string ): Promise { try { const messageService = new DyNTS_Msg_Message_DataService({ issuer, }); const message = await messageService.getDataById(messageId); if (!message) { throw new Error('Message not found'); } if (!message.reactions) { message.reactions = []; } // Remove user's reaction with this emoji const initialLength = message.reactions.length; message.reactions = message.reactions.filter((r: any) => !(r.emoji === emoji && r.userId === userId)); if (message.reactions.length < initialLength) { await messageService.saveData(message, false, false); // Emit event this.eventsService.emitReactionRemoved(messageId, userId, emoji); } return messageService.data; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('removeReaction', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-RR`, userMessage: 'Failed to remove reaction.', }); } } /** * Create conversation */ async createConversation( conversationData: Partial, creatorId: string, issuer: string ): Promise { try { // Ensure creator is in participants if (!conversationData.participants?.some(p => p.userId === creatorId)) { if (!conversationData.participants) { conversationData.participants = []; } conversationData.participants.push({ userId: creatorId, role: DyFM_Msg_ParticipantRole.owner, joinedAt: new Date(), }); } const conversation = new DyFM_Msg_Conversation({ type: conversationData.type || DyFM_Msg_ConversationType.direct, participants: conversationData.participants || [], __createdBy: issuer, __lastModifiedBy: issuer, ...conversationData, }); const conversationService = new DyNTS_Msg_Conversation_DataService({ conversation, issuer, }); await conversationService.saveData(conversationService.data, false, false); // Emit event const participantIds = conversationService.data.participants?.map(p => p.userId) || []; this.eventsService.emitConversationCreated(conversationService.data, participantIds); return conversationService.data; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('createConversation', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-CC`, userMessage: 'Failed to create conversation.', }); } } /** * Update conversation */ async updateConversation( conversationId: string, updateData: Partial, userId: string, issuer: string ): Promise { try { const conversationService = new DyNTS_Msg_Conversation_DataService({ issuer, }); const conversation = await conversationService.getDataById(conversationId); if (!conversation) { throw new Error('Conversation not found'); } // Check if user is participant const isParticipant = conversation.participants?.some(p => p.userId === userId); if (!isParticipant) { throw new Error('Unauthorized: User is not a participant in this conversation'); } Object.assign(conversation, updateData); conversation.__lastModifiedBy = issuer; await conversationService.saveData(conversation, false, false); // Emit event this.eventsService.emitConversationUpdated(conversationService.data); return conversationService.data; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('updateConversation', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-UC`, userMessage: 'Failed to update conversation.', }); } } /** * Delete conversation */ async deleteConversation( conversationId: string, userId: string, issuer: string ): Promise { try { const conversationService = new DyNTS_Msg_Conversation_DataService({ issuer, }); const conversation = await conversationService.getDataById(conversationId); if (!conversation) { throw new Error('Conversation not found'); } // Check if user is owner or admin const isOwnerOrAdmin = conversation.participants?.some(p => p.userId === userId && (p.role === DyFM_Msg_ParticipantRole.owner || p.role === DyFM_Msg_ParticipantRole.admin) ); if (!isOwnerOrAdmin) { throw new Error('Unauthorized: Only owners or admins can delete conversations'); } await conversationService.deleteData(conversationId); // Emit event this.eventsService.emitConversationDeleted(conversationId); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('deleteConversation', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-DC`, userMessage: 'Failed to delete conversation.', }); } } /** * Add participant to conversation */ async addParticipant( conversationId: string, newUserId: string, role: DyFM_Msg_ParticipantRole = DyFM_Msg_ParticipantRole.member, requesterId: string, issuer: string ): Promise { try { const conversationService = new DyNTS_Msg_Conversation_DataService({ issuer, }); const conversation = await conversationService.getDataById(conversationId); if (!conversation) { throw new Error('Conversation not found'); } // Check if requester is owner or admin const isOwnerOrAdmin = conversation.participants?.some(p => p.userId === requesterId && (p.role === DyFM_Msg_ParticipantRole.owner || p.role === DyFM_Msg_ParticipantRole.admin) ); if (!isOwnerOrAdmin) { throw new Error('Unauthorized: Only owners or admins can add participants'); } const newParticipant: DyFM_Msg_Participant = { userId: newUserId, role, joinedAt: new Date(), }; await conversationService.addParticipant(conversationId, newParticipant); // Emit event this.eventsService.emitParticipantAdded(conversationId, newUserId); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('addParticipant', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-AP`, userMessage: 'Failed to add participant.', }); } } /** * Remove participant from conversation */ async removeParticipant( conversationId: string, userIdToRemove: string, requesterId: string, issuer: string ): Promise { try { const conversationService = new DyNTS_Msg_Conversation_DataService({ issuer, }); const conversation = await conversationService.getDataById(conversationId); if (!conversation) { throw new Error('Conversation not found'); } // Check if requester is owner or admin, or removing themselves const isOwnerOrAdmin = conversation.participants?.some(p => p.userId === requesterId && (p.role === DyFM_Msg_ParticipantRole.owner || p.role === DyFM_Msg_ParticipantRole.admin) ); const isSelfRemoval = requesterId === userIdToRemove; if (!isOwnerOrAdmin && !isSelfRemoval) { throw new Error('Unauthorized: Only owners, admins, or the user themselves can remove participants'); } await conversationService.removeParticipant(conversationId, userIdToRemove); // Emit event this.eventsService.emitParticipantRemoved(conversationId, userIdToRemove); } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('removeParticipant', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-RP`, userMessage: 'Failed to remove participant.', }); } } /** * Get agent process steps for a message */ async getAgentProcessSteps( messageId: string, issuer: string ): Promise { try { const messageService = new DyNTS_Msg_Message_DataService({ issuer, }); const message = await messageService.getDataById(messageId); if (!message) { throw new Error('Message not found'); } if (!message.agentProcessSteps) { throw new Error('No agent process steps found for this message'); } return message.agentProcessSteps; } catch (error) { throw new DyFM_Error({ ...this.getDefaultErrorSettings('getAgentProcessSteps', error, issuer), errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-MSG-GAPS`, userMessage: 'Failed to get agent process steps.', }); } } }