UNPKG

@tiflux/mcp

Version:

TiFlux MCP Server - Model Context Protocol integration for Claude Code and other AI clients

408 lines (347 loc) 12.4 kB
/** * CommunicationMapper - Transformação de dados entre API e domínio para comunicações internas * * Responsabilidades: * - Converter dados da API TiFlux para formato interno * - Normalizar estruturas de comunicação e anexos * - Mapear dados de autor e timestamps * - Garantir compatibilidade entre versões */ class CommunicationMapper { constructor(container) { this.container = container; this.logger = container.resolve('logger'); this.config = container.resolve('config'); } /** * Mapeia dados de comunicação da API para formato interno */ mapFromAPI(apiData) { if (!apiData) { return null; } try { // A API pode retornar a comunicação dentro de { internal_communication: {...} } ou direto const commData = apiData.internal_communication || apiData.communication || apiData; const mapped = { // Identificadores id: this._extractId(commData.id), ticket_number: this._extractString(commData.ticket_number || commData.ticket_id), // Conteúdo text: this._extractString(commData.text || commData.content || commData.message), html_text: this._extractString(commData.html_text || commData.html_content), // Autor author: this._mapAuthor(commData.author || commData.user || commData.created_by), // Timestamps created_at: this._mapDateTime(commData.created_at), updated_at: this._mapDateTime(commData.updated_at), // Anexos attachments: this._mapAttachments(commData.attachments || commData.files), // Visibilidade e tipo visibility: this._mapVisibility(commData.visibility || commData.type), is_private: this._extractBoolean(commData.is_private || commData.private, true), // Default: privado // Status status: this._extractString(commData.status), // Metadados source: this._extractString(commData.source || 'mcp'), ip_address: this._extractString(commData.ip_address), user_agent: this._extractString(commData.user_agent), // URLs url: this._buildCommunicationURL(commData.ticket_number || commData.ticket_id, commData.id) }; // Remove campos null/undefined return this._cleanObject(mapped); } catch (error) { this.logger.error('Failed to map communication from API', { error: error.message, communicationId: apiData.id || apiData.communication?.id }); throw new Error(`Falha ao mapear dados da comunicação: ${error.message}`); } } /** * Mapeia lista de comunicações da API */ mapListFromAPI(apiResponse) { try { let communications = []; let pagination = null; // A API pode retornar diferentes formatos de lista if (Array.isArray(apiResponse)) { communications = apiResponse; } else if (apiResponse.internal_communications && Array.isArray(apiResponse.internal_communications)) { communications = apiResponse.internal_communications; pagination = apiResponse.pagination || apiResponse.meta; } else if (apiResponse.communications && Array.isArray(apiResponse.communications)) { communications = apiResponse.communications; pagination = apiResponse.pagination || apiResponse.meta; } else if (apiResponse.data && Array.isArray(apiResponse.data)) { communications = apiResponse.data; pagination = apiResponse.meta || apiResponse.pagination; } // Mapeia cada comunicação const mappedCommunications = communications.map(comm => { try { return this.mapFromAPI(comm); } catch (error) { this.logger.warn('Failed to map individual communication in list', { communicationId: comm.id, error: error.message }); // Retorna comunicação com dados básicos em caso de erro return { id: comm.id, text: comm.text || 'Erro ao carregar conteúdo', author: { name: 'Desconhecido' }, created_at: comm.created_at || new Date().toISOString(), attachments: [] }; } }); return { communications: mappedCommunications, pagination: this._mapPagination(pagination), total_count: mappedCommunications.length }; } catch (error) { this.logger.error('Failed to map communication list from API', { error: error.message }); throw new Error(`Falha ao mapear lista de comunicações: ${error.message}`); } } /** * Mapeia dados do autor */ _mapAuthor(authorData) { if (!authorData) return null; if (typeof authorData === 'string') { return { name: authorData }; } return { id: this._extractId(authorData.id), name: this._extractString(authorData.name || authorData.full_name) || 'Autor Desconhecido', email: this._extractEmail(authorData.email), role: this._extractString(authorData.role || authorData.position), department: this._extractString(authorData.department), avatar_url: this._extractString(authorData.avatar_url || authorData.photo_url) }; } /** * Mapeia anexos */ _mapAttachments(attachmentsData) { if (!attachmentsData || !Array.isArray(attachmentsData)) { return []; } return attachmentsData.map(attachment => { try { return { id: this._extractId(attachment.id), filename: this._extractString(attachment.filename || attachment.name || attachment.original_name), original_filename: this._extractString(attachment.original_filename || attachment.original_name), size: this._extractNumber(attachment.size || attachment.file_size), mime_type: this._extractString(attachment.mime_type || attachment.content_type), extension: this._extractFileExtension(attachment.filename || attachment.name), // URLs url: this._extractString(attachment.url || attachment.download_url), preview_url: this._extractString(attachment.preview_url || attachment.thumbnail_url), // Metadados uploaded_at: this._mapDateTime(attachment.uploaded_at || attachment.created_at), uploader: this._mapAuthor(attachment.uploader || attachment.uploaded_by), // Informações adicionais is_image: this._isImageFile(attachment.filename || attachment.name), is_document: this._isDocumentFile(attachment.filename || attachment.name), formatted_size: this._formatFileSize(attachment.size || attachment.file_size) }; } catch (error) { this.logger.warn('Failed to map individual attachment', { attachmentId: attachment.id, error: error.message }); return { id: attachment.id, filename: attachment.filename || 'Arquivo sem nome', size: attachment.size || 0, url: attachment.url || null, formatted_size: this._formatFileSize(attachment.size || 0) }; } }).filter(attachment => attachment !== null); } /** * Mapeia visibilidade da comunicação */ _mapVisibility(visibility) { if (!visibility) return 'internal'; const visibilityStr = visibility.toString().toLowerCase(); const visibilityMap = { 'internal': 'internal', 'private': 'internal', 'public': 'public', 'external': 'public', 'client': 'client' }; return visibilityMap[visibilityStr] || 'internal'; } /** * Mapeia paginação */ _mapPagination(paginationData) { if (!paginationData) return null; return { current_page: this._extractNumber(paginationData.current_page || paginationData.page) || 1, total_pages: this._extractNumber(paginationData.total_pages || paginationData.pages), per_page: this._extractNumber(paginationData.per_page || paginationData.limit) || 20, total_count: this._extractNumber(paginationData.total_count || paginationData.total), has_more: !!paginationData.has_more || !!paginationData.next_page }; } /** * Mapeia data/hora */ _mapDateTime(dateTime) { if (!dateTime) return null; try { const date = new Date(dateTime); if (isNaN(date.getTime())) return null; return date.toISOString(); } catch { return null; } } /** * Constrói URL da comunicação */ _buildCommunicationURL(ticketNumber, communicationId) { if (!ticketNumber || !communicationId) return null; const baseUrl = this.config.get('ui.base_url', 'https://app.tiflux.com'); return `${baseUrl}/tickets/${ticketNumber}#communication-${communicationId}`; } /** * Extrai extensão do arquivo */ _extractFileExtension(filename) { if (!filename) return null; const parts = filename.split('.'); if (parts.length < 2) return null; return parts[parts.length - 1].toLowerCase(); } /** * Verifica se é arquivo de imagem */ _isImageFile(filename) { if (!filename) return false; const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']; const extension = this._extractFileExtension(filename); return extension && imageExtensions.includes(extension); } /** * Verifica se é arquivo de documento */ _isDocumentFile(filename) { if (!filename) return false; const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf', 'odt']; const extension = this._extractFileExtension(filename); return extension && documentExtensions.includes(extension); } /** * Formata tamanho de arquivo */ _formatFileSize(bytes) { if (!bytes || bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Extrai ID numérico */ _extractId(value) { if (value === null || value === undefined) return null; const id = parseInt(value); return isNaN(id) ? null : id; } /** * Extrai string */ _extractString(value) { if (value === null || value === undefined) return null; const str = String(value).trim(); return str === '' ? null : str; } /** * Extrai número */ _extractNumber(value) { if (value === null || value === undefined) return null; const num = parseFloat(value); return isNaN(num) ? null : num; } /** * Extrai boolean com valor padrão */ _extractBoolean(value, defaultValue = false) { if (value === null || value === undefined) return defaultValue; if (typeof value === 'boolean') return value; const str = String(value).toLowerCase(); return ['true', '1', 'yes', 'on'].includes(str); } /** * Valida e normaliza email */ _extractEmail(email) { if (!email) return null; const emailStr = String(email).trim().toLowerCase(); const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(emailStr) ? emailStr : null; } /** * Remove campos null/undefined de um objeto */ _cleanObject(obj) { const cleaned = {}; Object.keys(obj).forEach(key => { const value = obj[key]; if (value !== null && value !== undefined) { if (typeof value === 'object' && !Array.isArray(value)) { const cleanedChild = this._cleanObject(value); if (Object.keys(cleanedChild).length > 0) { cleaned[key] = cleanedChild; } } else { cleaned[key] = value; } } }); return cleaned; } /** * Estatísticas do mapper */ getStats() { return { supported_formats: { api_to_internal: true, list_mapping: true, attachment_mapping: true, author_mapping: true }, transformations: { file_type_detection: true, size_formatting: true, visibility_normalization: true, date_iso_conversion: true, url_generation: true }, features: { attachment_preview_detection: true, image_document_classification: true, author_details_mapping: true, pagination_support: true } }; } } module.exports = CommunicationMapper;