mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-09 01:49:37 -06:00
feat: add project guidelines and configuration files for development standards
- Introduce AGENTS.md for repository guidelines and project structure - Add core development principles in .cursor/rules/core-development.mdc - Establish project-specific context in .cursor/rules/project-context.mdc - Implement Cursor IDE configuration in .cursor/rules/cursor.json - Create specialized rules for controllers, services, DTOs, guards, routes, and integrations - Update .gitignore to exclude unnecessary files - Enhance CLAUDE.md with project overview and common development commands
This commit is contained in:
parent
805f40c841
commit
7088ad05d2
106
.cursor/rules/README.md
Normal file
106
.cursor/rules/README.md
Normal file
@ -0,0 +1,106 @@
|
||||
# Evolution API Cursor Rules
|
||||
|
||||
Este diretório contém as regras e configurações do Cursor IDE para o projeto Evolution API.
|
||||
|
||||
## Estrutura dos Arquivos
|
||||
|
||||
### Arquivos Principais (alwaysApply: true)
|
||||
- **`core-development.mdc`** - Princípios fundamentais de desenvolvimento
|
||||
- **`project-context.mdc`** - Contexto específico do projeto Evolution API
|
||||
- **`cursor.json`** - Configurações do Cursor IDE
|
||||
|
||||
### Regras Especializadas (alwaysApply: false)
|
||||
Estas regras são ativadas automaticamente quando você trabalha nos arquivos correspondentes:
|
||||
|
||||
#### Camadas da Aplicação
|
||||
- **`specialized-rules/service-rules.mdc`** - Padrões para services (`src/api/services/`)
|
||||
- **`specialized-rules/controller-rules.mdc`** - Padrões para controllers (`src/api/controllers/`)
|
||||
- **`specialized-rules/dto-rules.mdc`** - Padrões para DTOs (`src/api/dto/`)
|
||||
- **`specialized-rules/guard-rules.mdc`** - Padrões para guards (`src/api/guards/`)
|
||||
- **`specialized-rules/route-rules.mdc`** - Padrões para routers (`src/api/routes/`)
|
||||
|
||||
#### Tipos e Validação
|
||||
- **`specialized-rules/type-rules.mdc`** - Definições TypeScript (`src/api/types/`)
|
||||
- **`specialized-rules/validate-rules.mdc`** - Schemas de validação (`src/validate/`)
|
||||
|
||||
#### Utilitários
|
||||
- **`specialized-rules/util-rules.mdc`** - Funções utilitárias (`src/utils/`)
|
||||
|
||||
#### Integrações
|
||||
- **`specialized-rules/integration-channel-rules.mdc`** - Integrações de canal (`src/api/integrations/channel/`)
|
||||
- **`specialized-rules/integration-chatbot-rules.mdc`** - Integrações de chatbot (`src/api/integrations/chatbot/`)
|
||||
- **`specialized-rules/integration-storage-rules.mdc`** - Integrações de storage (`src/api/integrations/storage/`)
|
||||
- **`specialized-rules/integration-event-rules.mdc`** - Integrações de eventos (`src/api/integrations/event/`)
|
||||
|
||||
## Como Usar
|
||||
|
||||
### Referências Cruzadas
|
||||
Os arquivos principais fazem referência aos especializados usando a sintaxe `@specialized-rules/nome-do-arquivo.mdc`. Quando você trabalha em um arquivo específico, o Cursor automaticamente carrega as regras relevantes.
|
||||
|
||||
### Exemplo de Uso
|
||||
Quando você edita um arquivo em `src/api/services/`, o Cursor automaticamente:
|
||||
1. Carrega `core-development.mdc` (sempre ativo)
|
||||
2. Carrega `project-context.mdc` (sempre ativo)
|
||||
3. Carrega `specialized-rules/service-rules.mdc` (ativado pelo glob pattern)
|
||||
|
||||
### Padrões de Código
|
||||
Cada arquivo de regras contém:
|
||||
- **Estruturas padrão** - Como organizar o código
|
||||
- **Padrões de nomenclatura** - Convenções de nomes
|
||||
- **Exemplos práticos** - Código de exemplo
|
||||
- **Anti-padrões** - O que evitar
|
||||
- **Testes** - Como testar o código
|
||||
|
||||
## Configuração do Cursor
|
||||
|
||||
O arquivo `cursor.json` contém:
|
||||
- Configurações de formatação
|
||||
- Padrões de código específicos do Evolution API
|
||||
- Diretórios principais do projeto
|
||||
- Integrações e tecnologias utilizadas
|
||||
|
||||
## Manutenção
|
||||
|
||||
Para manter as regras atualizadas:
|
||||
1. Analise novos padrões no código
|
||||
2. Atualize as regras especializadas correspondentes
|
||||
3. Mantenha os exemplos sincronizados com o código real
|
||||
4. Documente mudanças significativas
|
||||
|
||||
## Tecnologias Cobertas
|
||||
|
||||
- **Backend**: Node.js 20+ + TypeScript 5+ + Express.js
|
||||
- **Database**: Prisma ORM (PostgreSQL/MySQL)
|
||||
- **Cache**: Redis + Node-cache
|
||||
- **Queue**: RabbitMQ + Amazon SQS
|
||||
- **Real-time**: Socket.io
|
||||
- **Storage**: AWS S3 + Minio
|
||||
- **Validation**: class-validator + Joi
|
||||
- **Logging**: Pino
|
||||
- **WhatsApp**: Baileys + Meta Business API
|
||||
- **Integrations**: Chatwoot, Typebot, OpenAI, Dify
|
||||
|
||||
## Estrutura do Projeto
|
||||
|
||||
```
|
||||
src/
|
||||
├── api/
|
||||
│ ├── controllers/ # Controllers (HTTP handlers)
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── dto/ # Data Transfer Objects
|
||||
│ ├── guards/ # Authentication/Authorization
|
||||
│ ├── routes/ # Express routers
|
||||
│ ├── types/ # TypeScript definitions
|
||||
│ └── integrations/ # External integrations
|
||||
│ ├── channel/ # WhatsApp channels (Baileys, Business API)
|
||||
│ ├── chatbot/ # Chatbot integrations
|
||||
│ ├── event/ # Event integrations
|
||||
│ └── storage/ # Storage integrations
|
||||
├── cache/ # Cache implementations
|
||||
├── config/ # Configuration files
|
||||
├── utils/ # Utility functions
|
||||
├── validate/ # Validation schemas
|
||||
└── exceptions/ # Custom exceptions
|
||||
```
|
||||
|
||||
Este sistema de regras garante consistência no código e facilita o desenvolvimento seguindo os padrões estabelecidos do Evolution API.
|
||||
144
.cursor/rules/core-development.mdc
Normal file
144
.cursor/rules/core-development.mdc
Normal file
@ -0,0 +1,144 @@
|
||||
---
|
||||
description: Core development principles and standards for Evolution API development
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Evolution API Development Standards
|
||||
|
||||
## Cross-References
|
||||
- **Project Context**: @project-context.mdc for Evolution API-specific patterns
|
||||
- **Specialized Rules**:
|
||||
- @specialized-rules/service-rules.mdc for service layer patterns
|
||||
- @specialized-rules/controller-rules.mdc for controller patterns
|
||||
- @specialized-rules/dto-rules.mdc for DTO validation patterns
|
||||
- @specialized-rules/guard-rules.mdc for authentication/authorization
|
||||
- @specialized-rules/route-rules.mdc for router patterns
|
||||
- @specialized-rules/type-rules.mdc for TypeScript definitions
|
||||
- @specialized-rules/util-rules.mdc for utility functions
|
||||
- @specialized-rules/validate-rules.mdc for validation schemas
|
||||
- @specialized-rules/integration-channel-rules.mdc for channel integrations
|
||||
- @specialized-rules/integration-chatbot-rules.mdc for chatbot integrations
|
||||
- @specialized-rules/integration-storage-rules.mdc for storage integrations
|
||||
- @specialized-rules/integration-event-rules.mdc for event integrations
|
||||
- **TypeScript/Node.js**: Node.js 20+ + TypeScript 5+ best practices
|
||||
- **Express/Prisma**: Express.js + Prisma ORM patterns
|
||||
- **WhatsApp Integrations**: Baileys + Meta Business API patterns
|
||||
|
||||
## Senior Engineer Context - Evolution API Platform
|
||||
- You are a senior software engineer working on a WhatsApp API platform
|
||||
- Focus on Node.js + TypeScript + Express.js full-stack development
|
||||
- Specialized in real-time messaging, WhatsApp integrations, and event-driven architecture
|
||||
- Apply scalable patterns for multi-tenant API platform
|
||||
- Consider WhatsApp integration workflow implications and performance at scale
|
||||
|
||||
## Fundamental Principles
|
||||
|
||||
### Code Quality Standards
|
||||
- **Simplicity First**: Always prefer simple solutions over complex ones
|
||||
- **DRY Principle**: Avoid code duplication - check for existing similar functionality before implementing
|
||||
- **Single Responsibility**: Each function/class should have one clear purpose
|
||||
- **Readable Code**: Write code that tells a story - clear naming and structure
|
||||
|
||||
### Problem Resolution Approach
|
||||
- **Follow Existing Patterns**: Use established Service patterns, DTOs, and Integration patterns
|
||||
- **Event-Driven First**: Leverage EventEmitter2 for event publishing when adding new features
|
||||
- **Integration Pattern**: Follow existing WhatsApp integration patterns for new channels
|
||||
- **Conservative Changes**: Prefer extending existing services over creating new architecture
|
||||
- **Clean Migration**: Remove deprecated patterns when introducing new ones
|
||||
- **Incremental Changes**: Break large changes into smaller, testable increments with proper migrations
|
||||
|
||||
### File and Function Organization - Node.js/TypeScript Structure
|
||||
- **Services**: Keep services focused and under 200 lines
|
||||
- **Controllers**: Keep controllers thin - only routing and validation
|
||||
- **DTOs**: Use class-validator for all input validation
|
||||
- **Integrations**: Follow `src/api/integrations/` structure for new integrations
|
||||
- **Utils**: Extract common functionality into well-named utilities
|
||||
- **Types**: Define clear TypeScript interfaces and types
|
||||
|
||||
### Code Analysis and Reflection
|
||||
- After writing code, deeply reflect on scalability and maintainability
|
||||
- Provide 1-2 paragraph analysis of code changes
|
||||
- Suggest improvements or next steps based on reflection
|
||||
- Consider performance, security, and maintenance implications
|
||||
|
||||
## Development Standards
|
||||
|
||||
### TypeScript Standards
|
||||
- **Strict Mode**: Always use TypeScript strict mode
|
||||
- **No Any**: Avoid `any` type - use proper typing
|
||||
- **Interfaces**: Define clear contracts with interfaces
|
||||
- **Enums**: Use enums for constants and status values
|
||||
- **Generics**: Use generics for reusable components
|
||||
|
||||
### Error Handling Standards
|
||||
- **HTTP Exceptions**: Use appropriate HTTP status codes
|
||||
- **Logging**: Structured logging with context
|
||||
- **Retry Logic**: Implement retry for external services
|
||||
- **Graceful Degradation**: Handle service failures gracefully
|
||||
|
||||
### Security Standards
|
||||
- **Input Validation**: Validate all inputs with class-validator
|
||||
- **Authentication**: Use API keys and JWT tokens
|
||||
- **Rate Limiting**: Implement rate limiting for APIs
|
||||
- **Data Sanitization**: Sanitize sensitive data in logs
|
||||
|
||||
### Performance Standards
|
||||
- **Caching**: Use Redis for frequently accessed data
|
||||
- **Database**: Optimize Prisma queries with proper indexing
|
||||
- **Memory**: Monitor memory usage and implement cleanup
|
||||
- **Async**: Use async/await properly with error handling
|
||||
|
||||
## Communication Standards
|
||||
|
||||
### Language Requirements
|
||||
- **User Communication**: Always respond in Portuguese (PT-BR)
|
||||
- **Code Comments**: English for technical documentation
|
||||
- **API Documentation**: English for consistency
|
||||
- **Error Messages**: Portuguese for user-facing errors
|
||||
|
||||
### Documentation Standards
|
||||
- **Code Comments**: Document complex business logic
|
||||
- **API Documentation**: Document all public endpoints
|
||||
- **README**: Keep project documentation updated
|
||||
- **Changelog**: Document breaking changes
|
||||
|
||||
## Quality Assurance
|
||||
|
||||
### Testing Standards
|
||||
- **Unit Tests**: Test business logic in services
|
||||
- **Integration Tests**: Test API endpoints
|
||||
- **Mocks**: Mock external dependencies
|
||||
- **Coverage**: Aim for 70%+ test coverage
|
||||
|
||||
### Code Review Standards
|
||||
- **Peer Review**: All code must be reviewed
|
||||
- **Automated Checks**: ESLint, Prettier, TypeScript
|
||||
- **Security Review**: Check for security vulnerabilities
|
||||
- **Performance Review**: Check for performance issues
|
||||
|
||||
## Evolution API Specific Patterns
|
||||
|
||||
### WhatsApp Integration Patterns
|
||||
- **Connection Management**: One connection per instance
|
||||
- **Event Handling**: Proper event listeners for Baileys
|
||||
- **Message Processing**: Queue-based message processing
|
||||
- **Error Recovery**: Automatic reconnection logic
|
||||
|
||||
### Multi-Database Support
|
||||
- **Schema Compatibility**: Support PostgreSQL and MySQL
|
||||
- **Migration Sync**: Keep migrations synchronized
|
||||
- **Type Safety**: Use Prisma generated types
|
||||
- **Connection Pooling**: Proper database connection management
|
||||
|
||||
### Cache Strategy
|
||||
- **Redis Primary**: Use Redis for distributed caching
|
||||
- **Local Fallback**: Node-cache for local fallback
|
||||
- **TTL Strategy**: Appropriate TTL for different data types
|
||||
- **Cache Invalidation**: Proper cache invalidation patterns
|
||||
|
||||
### Event System
|
||||
- **EventEmitter2**: Use for internal events
|
||||
- **WebSocket**: Socket.io for real-time updates
|
||||
- **Queue Systems**: RabbitMQ/SQS for async processing
|
||||
- **Webhook Processing**: Proper webhook validation and processing
|
||||
179
.cursor/rules/cursor.json
Normal file
179
.cursor/rules/cursor.json
Normal file
@ -0,0 +1,179 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"description": "Cursor IDE configuration for Evolution API project",
|
||||
"rules": {
|
||||
"general": {
|
||||
"max_line_length": 120,
|
||||
"indent_size": 2,
|
||||
"end_of_line": "lf",
|
||||
"charset": "utf-8",
|
||||
"trim_trailing_whitespace": true,
|
||||
"insert_final_newline": true
|
||||
},
|
||||
"typescript": {
|
||||
"quotes": "single",
|
||||
"semi": true,
|
||||
"trailing_comma": "es5",
|
||||
"bracket_spacing": true,
|
||||
"arrow_parens": "avoid",
|
||||
"print_width": 120,
|
||||
"tab_width": 2,
|
||||
"use_tabs": false,
|
||||
"single_quote": true,
|
||||
"end_of_line": "lf",
|
||||
"strict": true,
|
||||
"no_implicit_any": true,
|
||||
"strict_null_checks": true
|
||||
},
|
||||
"javascript": {
|
||||
"quotes": "single",
|
||||
"semi": true,
|
||||
"trailing_comma": "es5",
|
||||
"bracket_spacing": true,
|
||||
"arrow_parens": "avoid",
|
||||
"print_width": 120,
|
||||
"tab_width": 2,
|
||||
"use_tabs": false,
|
||||
"single_quote": true,
|
||||
"end_of_line": "lf",
|
||||
"style_guide": "eslint-airbnb"
|
||||
},
|
||||
"json": {
|
||||
"tab_width": 2,
|
||||
"use_tabs": false,
|
||||
"parser": "json"
|
||||
},
|
||||
"ignore": {
|
||||
"files": [
|
||||
"node_modules/**",
|
||||
"dist/**",
|
||||
"build/**",
|
||||
".git/**",
|
||||
"*.min.js",
|
||||
"*.min.css",
|
||||
".env",
|
||||
".env.*",
|
||||
".env.example",
|
||||
"coverage/**",
|
||||
"*.log",
|
||||
"*.lock",
|
||||
"pnpm-lock.yaml",
|
||||
"package-lock.json",
|
||||
"yarn.lock",
|
||||
"log/**",
|
||||
"tmp/**",
|
||||
"instances/**",
|
||||
"public/uploads/**",
|
||||
"*.dump",
|
||||
"*.rdb",
|
||||
"*.mmdb",
|
||||
".DS_Store",
|
||||
"*.swp",
|
||||
"*.swo",
|
||||
"*.un~",
|
||||
".jest-cache",
|
||||
".idea/**",
|
||||
".vscode/**",
|
||||
".yalc/**",
|
||||
"yalc.lock",
|
||||
"*.local",
|
||||
"prisma/migrations/**",
|
||||
"prisma/mysql-migrations/**",
|
||||
"prisma/postgresql-migrations/**"
|
||||
]
|
||||
},
|
||||
"search": {
|
||||
"exclude_patterns": [
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/.git/**",
|
||||
"**/coverage/**",
|
||||
"**/log/**",
|
||||
"**/tmp/**",
|
||||
"**/instances/**",
|
||||
"**/public/uploads/**",
|
||||
"**/*.min.js",
|
||||
"**/*.min.css",
|
||||
"**/*.log",
|
||||
"**/*.lock",
|
||||
"**/pnpm-lock.yaml",
|
||||
"**/package-lock.json",
|
||||
"**/yarn.lock",
|
||||
"**/*.dump",
|
||||
"**/*.rdb",
|
||||
"**/*.mmdb",
|
||||
"**/.DS_Store",
|
||||
"**/*.swp",
|
||||
"**/*.swo",
|
||||
"**/*.un~",
|
||||
"**/.jest-cache",
|
||||
"**/.idea/**",
|
||||
"**/.vscode/**",
|
||||
"**/.yalc/**",
|
||||
"**/yalc.lock",
|
||||
"**/*.local",
|
||||
"**/prisma/migrations/**",
|
||||
"**/prisma/mysql-migrations/**",
|
||||
"**/prisma/postgresql-migrations/**"
|
||||
]
|
||||
},
|
||||
"evolution_api": {
|
||||
"project_type": "nodejs_typescript_api",
|
||||
"backend_framework": "express_prisma",
|
||||
"database": ["postgresql", "mysql"],
|
||||
"cache": ["redis", "node_cache"],
|
||||
"queue": ["rabbitmq", "sqs"],
|
||||
"real_time": "socket_io",
|
||||
"file_storage": ["aws_s3", "minio"],
|
||||
"validation": "class_validator",
|
||||
"logging": "pino",
|
||||
"main_directories": {
|
||||
"source": "src/",
|
||||
"api": "src/api/",
|
||||
"controllers": "src/api/controllers/",
|
||||
"services": "src/api/services/",
|
||||
"integrations": "src/api/integrations/",
|
||||
"dto": "src/api/dto/",
|
||||
"types": "src/api/types/",
|
||||
"guards": "src/api/guards/",
|
||||
"routes": "src/api/routes/",
|
||||
"cache": "src/cache/",
|
||||
"config": "src/config/",
|
||||
"utils": "src/utils/",
|
||||
"exceptions": "src/exceptions/",
|
||||
"validate": "src/validate/",
|
||||
"prisma": "prisma/",
|
||||
"tests": "test/",
|
||||
"docs": "docs/"
|
||||
},
|
||||
"key_patterns": [
|
||||
"whatsapp_integration",
|
||||
"multi_database_support",
|
||||
"instance_management",
|
||||
"event_driven_architecture",
|
||||
"service_layer_pattern",
|
||||
"dto_validation",
|
||||
"webhook_processing",
|
||||
"message_queuing",
|
||||
"real_time_communication",
|
||||
"file_storage_integration"
|
||||
],
|
||||
"whatsapp_integrations": [
|
||||
"baileys",
|
||||
"meta_business_api",
|
||||
"whatsapp_cloud_api"
|
||||
],
|
||||
"external_integrations": [
|
||||
"chatwoot",
|
||||
"typebot",
|
||||
"openai",
|
||||
"dify",
|
||||
"rabbitmq",
|
||||
"sqs",
|
||||
"s3",
|
||||
"minio"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
174
.cursor/rules/project-context.mdc
Normal file
174
.cursor/rules/project-context.mdc
Normal file
@ -0,0 +1,174 @@
|
||||
---
|
||||
description: Evolution API project-specific context and constraints
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Evolution API Project Context
|
||||
|
||||
## Cross-References
|
||||
- **Core Development**: @core-development.mdc for fundamental development principles
|
||||
- **Specialized Rules**: Reference specific specialized rules when working on:
|
||||
- Services: @specialized-rules/service-rules.mdc
|
||||
- Controllers: @specialized-rules/controller-rules.mdc
|
||||
- DTOs: @specialized-rules/dto-rules.mdc
|
||||
- Guards: @specialized-rules/guard-rules.mdc
|
||||
- Routes: @specialized-rules/route-rules.mdc
|
||||
- Types: @specialized-rules/type-rules.mdc
|
||||
- Utils: @specialized-rules/util-rules.mdc
|
||||
- Validation: @specialized-rules/validate-rules.mdc
|
||||
- Channel Integrations: @specialized-rules/integration-channel-rules.mdc
|
||||
- Chatbot Integrations: @specialized-rules/integration-chatbot-rules.mdc
|
||||
- Storage Integrations: @specialized-rules/integration-storage-rules.mdc
|
||||
- Event Integrations: @specialized-rules/integration-event-rules.mdc
|
||||
- **TypeScript/Node.js**: Node.js 20+ + TypeScript 5+ backend standards
|
||||
- **Express/Prisma**: Express.js + Prisma ORM patterns
|
||||
- **WhatsApp Integrations**: Baileys, Meta Business API, and other messaging platforms
|
||||
|
||||
## Technology Stack
|
||||
- **Backend**: Node.js 20+ + TypeScript 5+ + Express.js
|
||||
- **Database**: Prisma ORM (PostgreSQL/MySQL support)
|
||||
- **Cache**: Redis + Node-cache for local fallback
|
||||
- **Queue**: RabbitMQ + Amazon SQS for message processing
|
||||
- **Real-time**: Socket.io for WebSocket connections
|
||||
- **Storage**: AWS S3 + Minio for file storage
|
||||
- **Validation**: class-validator for input validation
|
||||
- **Logging**: Pino for structured logging
|
||||
- **Architecture**: Multi-tenant API with WhatsApp integrations
|
||||
|
||||
## Project-Specific Patterns
|
||||
|
||||
### WhatsApp Integration Architecture
|
||||
- **MANDATORY**: All WhatsApp integrations must follow established patterns
|
||||
- **BAILEYS**: Use `whatsapp.baileys.service.ts` patterns for WhatsApp Web
|
||||
- **META BUSINESS**: Use `whatsapp.business.service.ts` for official API
|
||||
- **CONNECTION MANAGEMENT**: One connection per instance with proper lifecycle
|
||||
- **EVENT HANDLING**: Proper event listeners and error handling
|
||||
|
||||
### Multi-Database Architecture
|
||||
- **CRITICAL**: Support both PostgreSQL and MySQL
|
||||
- **SCHEMAS**: Use appropriate schema files (postgresql-schema.prisma / mysql-schema.prisma)
|
||||
- **MIGRATIONS**: Keep migrations synchronized between databases
|
||||
- **TYPES**: Use database-specific types (@db.JsonB vs @db.Json)
|
||||
- **COMPATIBILITY**: Ensure feature parity between databases
|
||||
|
||||
### API Integration Workflow
|
||||
- **CORE FEATURE**: REST API for WhatsApp communication
|
||||
- **COMPLEXITY**: High - involves webhook processing, message routing, and instance management
|
||||
- **COMPONENTS**: Instance management, message handling, media processing
|
||||
- **INTEGRATIONS**: Baileys, Meta Business API, Chatwoot, Typebot, OpenAI, Dify
|
||||
|
||||
### Multi-Tenant Instance Architecture
|
||||
- **CRITICAL**: All operations must be scoped by instance
|
||||
- **ISOLATION**: Complete data isolation between instances
|
||||
- **SECURITY**: Validate instance ownership before operations
|
||||
- **SCALING**: Support thousands of concurrent instances
|
||||
- **AUTHENTICATION**: API key-based authentication per instance
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
### Implementation Documentation
|
||||
- **MANDATORY**: Document complex integration patterns
|
||||
- **LOCATION**: Use inline comments for business logic
|
||||
- **API DOCS**: Document all public endpoints
|
||||
- **WEBHOOK DOCS**: Document webhook payloads and signatures
|
||||
|
||||
### Change Documentation
|
||||
- **CHANGELOG**: Document breaking changes
|
||||
- **MIGRATION GUIDES**: Document database migrations
|
||||
- **INTEGRATION GUIDES**: Document new integration patterns
|
||||
|
||||
## Environment and Security
|
||||
|
||||
### Environment Variables
|
||||
- **CRITICAL**: Never hardcode sensitive values
|
||||
- **VALIDATION**: Validate required environment variables on startup
|
||||
- **SECURITY**: Use secure defaults and proper encryption
|
||||
- **DOCUMENTATION**: Document all environment variables
|
||||
|
||||
### File Organization - Node.js/TypeScript Structure
|
||||
- **CONTROLLERS**: Organized by feature (`api/controllers/`)
|
||||
- **SERVICES**: Business logic in service classes (`api/services/`)
|
||||
- **INTEGRATIONS**: External integrations (`api/integrations/`)
|
||||
- **DTOS**: Data transfer objects (`api/dto/`)
|
||||
- **TYPES**: TypeScript types (`api/types/`)
|
||||
- **UTILS**: Utility functions (`utils/`)
|
||||
|
||||
## Integration Points
|
||||
|
||||
### WhatsApp Providers
|
||||
- **BAILEYS**: WhatsApp Web integration with QR code
|
||||
- **META BUSINESS**: Official WhatsApp Business API
|
||||
- **CLOUD API**: WhatsApp Cloud API integration
|
||||
- **WEBHOOK PROCESSING**: Proper webhook validation and processing
|
||||
|
||||
### External Integrations
|
||||
- **CHATWOOT**: Customer support platform integration
|
||||
- **TYPEBOT**: Chatbot flow integration
|
||||
- **OPENAI**: AI-powered chat integration
|
||||
- **DIFY**: AI workflow integration
|
||||
- **STORAGE**: S3/Minio for media file storage
|
||||
|
||||
### Event-Driven Communication
|
||||
- **EVENTEMITTER2**: Internal event system
|
||||
- **SOCKET.IO**: Real-time WebSocket communication
|
||||
- **RABBITMQ**: Message queue for async processing
|
||||
- **SQS**: Amazon SQS for cloud-based queuing
|
||||
- **WEBHOOKS**: Outbound webhook system
|
||||
|
||||
## Development Constraints
|
||||
|
||||
### Language Requirements
|
||||
- **USER COMMUNICATION**: Always respond in Portuguese (PT-BR)
|
||||
- **CODE/COMMENTS**: English for code and technical documentation
|
||||
- **API RESPONSES**: English for consistency
|
||||
- **ERROR MESSAGES**: Portuguese for user-facing errors
|
||||
|
||||
### Performance Constraints
|
||||
- **MEMORY**: Efficient memory usage for multiple instances
|
||||
- **DATABASE**: Optimized queries with proper indexing
|
||||
- **CACHE**: Strategic caching for frequently accessed data
|
||||
- **CONNECTIONS**: Proper connection pooling and management
|
||||
|
||||
### Security Constraints
|
||||
- **AUTHENTICATION**: API key validation for all endpoints
|
||||
- **AUTHORIZATION**: Instance-based access control
|
||||
- **INPUT VALIDATION**: Validate all inputs with class-validator
|
||||
- **RATE LIMITING**: Prevent abuse with rate limiting
|
||||
- **WEBHOOK SECURITY**: Validate webhook signatures
|
||||
|
||||
## Quality Standards
|
||||
- **TYPE SAFETY**: Full TypeScript coverage with strict mode
|
||||
- **ERROR HANDLING**: Comprehensive error scenarios with proper logging
|
||||
- **TESTING**: Unit and integration tests for critical paths
|
||||
- **MONITORING**: Proper logging and error tracking
|
||||
- **DOCUMENTATION**: Clear API documentation and code comments
|
||||
- **PERFORMANCE**: Optimized for high-throughput message processing
|
||||
- **SECURITY**: Secure by default with proper validation
|
||||
- **SCALABILITY**: Design for horizontal scaling
|
||||
|
||||
## Evolution API Specific Development Patterns
|
||||
|
||||
### Instance Management
|
||||
- **LIFECYCLE**: Proper instance creation, connection, and cleanup
|
||||
- **STATE MANAGEMENT**: Track connection status and health
|
||||
- **RECOVERY**: Automatic reconnection and error recovery
|
||||
- **MONITORING**: Health checks and status reporting
|
||||
|
||||
### Message Processing
|
||||
- **QUEUE-BASED**: Use queues for message processing
|
||||
- **RETRY LOGIC**: Implement exponential backoff for failures
|
||||
- **MEDIA HANDLING**: Proper media upload and processing
|
||||
- **WEBHOOK DELIVERY**: Reliable webhook delivery with retries
|
||||
|
||||
### Integration Patterns
|
||||
- **SERVICE LAYER**: Business logic in service classes
|
||||
- **DTO VALIDATION**: Input validation with class-validator
|
||||
- **ERROR HANDLING**: Consistent error responses
|
||||
- **LOGGING**: Structured logging with correlation IDs
|
||||
|
||||
### Database Patterns
|
||||
- **PRISMA**: Use Prisma ORM for all database operations
|
||||
- **TRANSACTIONS**: Use transactions for multi-step operations
|
||||
- **MIGRATIONS**: Proper migration management
|
||||
- **INDEXING**: Optimize queries with appropriate indexes
|
||||
342
.cursor/rules/specialized-rules/controller-rules.mdc
Normal file
342
.cursor/rules/specialized-rules/controller-rules.mdc
Normal file
@ -0,0 +1,342 @@
|
||||
---
|
||||
description: Controller patterns for Evolution API
|
||||
globs:
|
||||
- "src/api/controllers/**/*.ts"
|
||||
- "src/api/integrations/**/controllers/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Controller Rules
|
||||
|
||||
## Controller Structure Pattern
|
||||
|
||||
### Standard Controller Class
|
||||
```typescript
|
||||
export class ExampleController {
|
||||
constructor(private readonly exampleService: ExampleService) {}
|
||||
|
||||
public async createExample(instance: InstanceDto, data: ExampleDto) {
|
||||
return this.exampleService.create(instance, data);
|
||||
}
|
||||
|
||||
public async findExample(instance: InstanceDto) {
|
||||
return this.exampleService.find(instance);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Injection Pattern
|
||||
|
||||
### Service Injection
|
||||
```typescript
|
||||
// CORRECT - Evolution API pattern
|
||||
export class ChatController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].getWhatsAppNumbers(data);
|
||||
}
|
||||
}
|
||||
|
||||
// INCORRECT - Don't inject multiple services when waMonitor is sufficient
|
||||
export class ChatController {
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly prismaRepository: PrismaRepository, // ❌ Unnecessary if waMonitor has access
|
||||
private readonly configService: ConfigService, // ❌ Unnecessary if waMonitor has access
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
## Method Signature Pattern
|
||||
|
||||
### Instance Parameter Pattern
|
||||
```typescript
|
||||
// CORRECT - Evolution API pattern (destructuring instanceName)
|
||||
public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data);
|
||||
}
|
||||
|
||||
// CORRECT - Alternative pattern for full instance (when using services)
|
||||
public async createTemplate(instance: InstanceDto, data: TemplateDto) {
|
||||
return this.templateService.create(instance, data);
|
||||
}
|
||||
|
||||
// INCORRECT - Don't use generic method names
|
||||
public async methodName(instance: InstanceDto, data: DataDto) { // ❌ Use specific names
|
||||
return this.service.performAction(instance, data);
|
||||
}
|
||||
```
|
||||
|
||||
## WAMonitor Access Pattern
|
||||
|
||||
### Direct WAMonitor Usage
|
||||
```typescript
|
||||
// CORRECT - Standard pattern in controllers
|
||||
export class CallController {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
public async offerCall({ instanceName }: InstanceDto, data: OfferCallDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].offerCall(data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Controller Registration Pattern
|
||||
|
||||
### Server Module Registration
|
||||
```typescript
|
||||
// In server.module.ts
|
||||
export const templateController = new TemplateController(templateService);
|
||||
export const businessController = new BusinessController(waMonitor);
|
||||
export const chatController = new ChatController(waMonitor);
|
||||
export const callController = new CallController(waMonitor);
|
||||
```
|
||||
|
||||
## Error Handling in Controllers
|
||||
|
||||
### Let Services Handle Errors
|
||||
```typescript
|
||||
// CORRECT - Let service handle errors
|
||||
public async fetchCatalog(instance: InstanceDto, data: getCatalogDto) {
|
||||
return await this.waMonitor.waInstances[instance.instanceName].fetchCatalog(instance.instanceName, data);
|
||||
}
|
||||
|
||||
// INCORRECT - Don't add try-catch in controllers unless specific handling needed
|
||||
public async fetchCatalog(instance: InstanceDto, data: getCatalogDto) {
|
||||
try {
|
||||
return await this.waMonitor.waInstances[instance.instanceName].fetchCatalog(instance.instanceName, data);
|
||||
} catch (error) {
|
||||
throw error; // ❌ Unnecessary try-catch
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Controller Pattern
|
||||
|
||||
### Instance Controller Pattern
|
||||
```typescript
|
||||
export class InstanceController {
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly chatwootService: ChatwootService,
|
||||
private readonly settingsService: SettingsService,
|
||||
private readonly proxyService: ProxyController,
|
||||
private readonly cache: CacheService,
|
||||
private readonly chatwootCache: CacheService,
|
||||
private readonly baileysCache: CacheService,
|
||||
private readonly providerFiles: ProviderFiles,
|
||||
) {}
|
||||
|
||||
private readonly logger = new Logger('InstanceController');
|
||||
|
||||
// Multiple methods handling different aspects
|
||||
public async createInstance(data: InstanceDto) {
|
||||
// Complex instance creation logic
|
||||
}
|
||||
|
||||
public async deleteInstance({ instanceName }: InstanceDto) {
|
||||
// Complex instance deletion logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Channel Controller Pattern
|
||||
|
||||
### Base Channel Controller
|
||||
```typescript
|
||||
export class ChannelController {
|
||||
public prismaRepository: PrismaRepository;
|
||||
public waMonitor: WAMonitoringService;
|
||||
|
||||
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||
this.prisma = prismaRepository;
|
||||
this.monitor = waMonitor;
|
||||
}
|
||||
|
||||
// Getters and setters for dependency access
|
||||
public set prisma(prisma: PrismaRepository) {
|
||||
this.prismaRepository = prisma;
|
||||
}
|
||||
|
||||
public get prisma() {
|
||||
return this.prismaRepository;
|
||||
}
|
||||
|
||||
public set monitor(waMonitor: WAMonitoringService) {
|
||||
this.waMonitor = waMonitor;
|
||||
}
|
||||
|
||||
public get monitor() {
|
||||
return this.waMonitor;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Extended Channel Controller
|
||||
```typescript
|
||||
export class EvolutionController extends ChannelController implements ChannelControllerInterface {
|
||||
private readonly logger = new Logger('EvolutionController');
|
||||
|
||||
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||
super(prismaRepository, waMonitor);
|
||||
}
|
||||
|
||||
integrationEnabled: boolean;
|
||||
|
||||
public async receiveWebhook(data: any) {
|
||||
const numberId = data.numberId;
|
||||
|
||||
if (!numberId) {
|
||||
this.logger.error('WebhookService -> receiveWebhookEvolution -> numberId not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = await this.prismaRepository.instance.findFirst({
|
||||
where: { number: numberId },
|
||||
});
|
||||
|
||||
if (!instance) {
|
||||
this.logger.error('WebhookService -> receiveWebhook -> instance not found');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data);
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Chatbot Controller Pattern
|
||||
|
||||
### Base Chatbot Controller
|
||||
```typescript
|
||||
export abstract class BaseChatbotController<BotType = any, BotData extends BaseChatbotDto = BaseChatbotDto>
|
||||
extends ChatbotController
|
||||
implements ChatbotControllerInterface
|
||||
{
|
||||
public readonly logger: Logger;
|
||||
integrationEnabled: boolean;
|
||||
|
||||
// Abstract methods to be implemented
|
||||
protected abstract readonly integrationName: string;
|
||||
protected abstract processBot(/* parameters */): Promise<void>;
|
||||
protected abstract getFallbackBotId(settings: any): string | undefined;
|
||||
|
||||
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||
super(prismaRepository, waMonitor);
|
||||
}
|
||||
|
||||
// Base implementation methods
|
||||
public async createBot(instance: InstanceDto, data: BotData) {
|
||||
// Common bot creation logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Method Naming Conventions
|
||||
|
||||
### Standard Method Names
|
||||
- `create*()` - Create operations
|
||||
- `find*()` - Find operations
|
||||
- `fetch*()` - Fetch from external APIs
|
||||
- `send*()` - Send operations
|
||||
- `receive*()` - Receive webhook/data
|
||||
- `handle*()` - Handle specific actions
|
||||
- `offer*()` - Offer services (like calls)
|
||||
|
||||
## Return Patterns
|
||||
|
||||
### Direct Return Pattern
|
||||
```typescript
|
||||
// CORRECT - Direct return from service
|
||||
public async createTemplate(instance: InstanceDto, data: TemplateDto) {
|
||||
return this.templateService.create(instance, data);
|
||||
}
|
||||
|
||||
// CORRECT - Direct return from waMonitor
|
||||
public async offerCall({ instanceName }: InstanceDto, data: OfferCallDto) {
|
||||
return await this.waMonitor.waInstances[instanceName].offerCall(data);
|
||||
}
|
||||
```
|
||||
|
||||
## Controller Testing Pattern
|
||||
|
||||
### Unit Test Structure
|
||||
```typescript
|
||||
describe('ExampleController', () => {
|
||||
let controller: ExampleController;
|
||||
let service: jest.Mocked<ExampleService>;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockService = {
|
||||
create: jest.fn(),
|
||||
find: jest.fn(),
|
||||
};
|
||||
|
||||
controller = new ExampleController(mockService as any);
|
||||
service = mockService as any;
|
||||
});
|
||||
|
||||
describe('createExample', () => {
|
||||
it('should call service create method', async () => {
|
||||
const instance = { instanceName: 'test' };
|
||||
const data = { test: 'data' };
|
||||
const expectedResult = { success: true };
|
||||
|
||||
service.create.mockResolvedValue(expectedResult);
|
||||
|
||||
const result = await controller.createExample(instance, data);
|
||||
|
||||
expect(service.create).toHaveBeenCalledWith(instance, data);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Interface Implementation
|
||||
|
||||
### Controller Interface Pattern
|
||||
```typescript
|
||||
export interface ChannelControllerInterface {
|
||||
integrationEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface ChatbotControllerInterface {
|
||||
integrationEnabled: boolean;
|
||||
createBot(instance: InstanceDto, data: any): Promise<any>;
|
||||
findBot(instance: InstanceDto): Promise<any>;
|
||||
// ... other methods
|
||||
}
|
||||
```
|
||||
|
||||
## Controller Organization
|
||||
|
||||
### File Naming Convention
|
||||
- `*.controller.ts` - Main controllers
|
||||
- `*/*.controller.ts` - Integration-specific controllers
|
||||
|
||||
### Method Organization
|
||||
1. Constructor
|
||||
2. Public methods (alphabetical order)
|
||||
3. Private methods (if any)
|
||||
|
||||
### Import Organization
|
||||
```typescript
|
||||
// DTOs first
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { ExampleDto } from '@api/dto/example.dto';
|
||||
|
||||
// Services
|
||||
import { ExampleService } from '@api/services/example.service';
|
||||
|
||||
// Types
|
||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||
```
|
||||
433
.cursor/rules/specialized-rules/dto-rules.mdc
Normal file
433
.cursor/rules/specialized-rules/dto-rules.mdc
Normal file
@ -0,0 +1,433 @@
|
||||
---
|
||||
description: DTO patterns and validation for Evolution API
|
||||
globs:
|
||||
- "src/api/dto/**/*.ts"
|
||||
- "src/api/integrations/**/dto/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API DTO Rules
|
||||
|
||||
## DTO Structure Pattern
|
||||
|
||||
### Basic DTO Class
|
||||
```typescript
|
||||
export class ExampleDto {
|
||||
name: string;
|
||||
category: string;
|
||||
allowCategoryChange: boolean;
|
||||
language: string;
|
||||
components: any;
|
||||
webhookUrl?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Inheritance Pattern
|
||||
|
||||
### DTO Inheritance
|
||||
```typescript
|
||||
// CORRECT - Evolution API pattern
|
||||
export class InstanceDto extends IntegrationDto {
|
||||
instanceName: string;
|
||||
instanceId?: string;
|
||||
qrcode?: boolean;
|
||||
businessId?: string;
|
||||
number?: string;
|
||||
integration?: string;
|
||||
token?: string;
|
||||
status?: string;
|
||||
ownerJid?: string;
|
||||
profileName?: string;
|
||||
profilePicUrl?: string;
|
||||
|
||||
// Settings
|
||||
rejectCall?: boolean;
|
||||
msgCall?: string;
|
||||
groupsIgnore?: boolean;
|
||||
alwaysOnline?: boolean;
|
||||
readMessages?: boolean;
|
||||
readStatus?: boolean;
|
||||
syncFullHistory?: boolean;
|
||||
wavoipToken?: string;
|
||||
|
||||
// Proxy settings
|
||||
proxyHost?: string;
|
||||
proxyPort?: string;
|
||||
proxyProtocol?: string;
|
||||
proxyUsername?: string;
|
||||
proxyPassword?: string;
|
||||
|
||||
// Webhook configuration
|
||||
webhook?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
headers?: JsonValue;
|
||||
url?: string;
|
||||
byEvents?: boolean;
|
||||
base64?: boolean;
|
||||
};
|
||||
|
||||
// Chatwoot integration
|
||||
chatwootAccountId?: string;
|
||||
chatwootConversationPending?: boolean;
|
||||
chatwootAutoCreate?: boolean;
|
||||
chatwootDaysLimitImportMessages?: number;
|
||||
chatwootImportContacts?: boolean;
|
||||
chatwootImportMessages?: boolean;
|
||||
chatwootLogo?: string;
|
||||
chatwootMergeBrazilContacts?: boolean;
|
||||
chatwootNameInbox?: string;
|
||||
chatwootOrganization?: string;
|
||||
chatwootReopenConversation?: boolean;
|
||||
chatwootSignMsg?: boolean;
|
||||
chatwootToken?: string;
|
||||
chatwootUrl?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Base DTO Pattern
|
||||
|
||||
### Base Chatbot DTO
|
||||
```typescript
|
||||
/**
|
||||
* Base DTO for all chatbot integrations
|
||||
* Contains common properties shared by all chatbot types
|
||||
*/
|
||||
export class BaseChatbotDto {
|
||||
enabled?: boolean;
|
||||
description: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: string[];
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base settings DTO for all chatbot integrations
|
||||
*/
|
||||
export class BaseChatbotSettingDto {
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
ignoreJids?: string[];
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
```
|
||||
|
||||
## Message DTO Patterns
|
||||
|
||||
### Send Message DTOs
|
||||
```typescript
|
||||
export class Metadata {
|
||||
number: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export class SendTextDto extends Metadata {
|
||||
text: string;
|
||||
linkPreview?: boolean;
|
||||
mentionsEveryOne?: boolean;
|
||||
mentioned?: string[];
|
||||
}
|
||||
|
||||
export class SendListDto extends Metadata {
|
||||
title: string;
|
||||
description: string;
|
||||
buttonText: string;
|
||||
footerText?: string;
|
||||
|
||||
sections: Section[];
|
||||
}
|
||||
|
||||
export class ContactMessage {
|
||||
fullName: string;
|
||||
wuid: string;
|
||||
phoneNumber: string;
|
||||
organization?: string;
|
||||
email?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export class SendTemplateDto extends Metadata {
|
||||
name: string;
|
||||
language: string;
|
||||
components: any;
|
||||
}
|
||||
```
|
||||
|
||||
## Simple DTO Patterns
|
||||
|
||||
### Basic DTOs
|
||||
```typescript
|
||||
export class NumberDto {
|
||||
number: string;
|
||||
}
|
||||
|
||||
export class LabelDto {
|
||||
id?: string;
|
||||
name: string;
|
||||
color: string;
|
||||
predefinedId?: string;
|
||||
}
|
||||
|
||||
export class HandleLabelDto {
|
||||
number: string;
|
||||
labelId: string;
|
||||
}
|
||||
|
||||
export class ProfileNameDto {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class WhatsAppNumberDto {
|
||||
numbers: string[];
|
||||
}
|
||||
```
|
||||
|
||||
## Complex DTO Patterns
|
||||
|
||||
### Business DTOs
|
||||
```typescript
|
||||
export class getCatalogDto {
|
||||
number?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export class getCollectionsDto {
|
||||
number?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export class NumberBusiness {
|
||||
number: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
email?: string;
|
||||
websites?: string[];
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
address?: string;
|
||||
profilehandle?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Settings DTO Pattern
|
||||
|
||||
### Settings Configuration
|
||||
```typescript
|
||||
export class SettingsDto {
|
||||
rejectCall?: boolean;
|
||||
msgCall?: string;
|
||||
groupsIgnore?: boolean;
|
||||
alwaysOnline?: boolean;
|
||||
readMessages?: boolean;
|
||||
readStatus?: boolean;
|
||||
syncFullHistory?: boolean;
|
||||
wavoipToken?: string;
|
||||
}
|
||||
|
||||
export class ProxyDto {
|
||||
host?: string;
|
||||
port?: string;
|
||||
protocol?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Presence DTO Pattern
|
||||
|
||||
### WhatsApp Presence
|
||||
```typescript
|
||||
export class SetPresenceDto {
|
||||
presence: WAPresence;
|
||||
}
|
||||
|
||||
export class SendPresenceDto {
|
||||
number: string;
|
||||
presence: WAPresence;
|
||||
}
|
||||
```
|
||||
|
||||
## DTO Structure (No Decorators)
|
||||
|
||||
### Simple DTO Classes (Evolution API Pattern)
|
||||
```typescript
|
||||
// CORRECT - Evolution API pattern (no decorators)
|
||||
export class ExampleDto {
|
||||
name: string;
|
||||
description?: string;
|
||||
enabled: boolean;
|
||||
items?: string[];
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
// INCORRECT - Don't use class-validator decorators
|
||||
export class ValidatedDto {
|
||||
@IsString() // ❌ Evolution API doesn't use decorators
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Type Safety Patterns
|
||||
|
||||
### Prisma Type Integration
|
||||
```typescript
|
||||
import { JsonValue } from '@prisma/client/runtime/library';
|
||||
import { WAPresence } from 'baileys';
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
export class TypeSafeDto {
|
||||
presence: WAPresence;
|
||||
triggerType: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
metadata?: JsonValue;
|
||||
}
|
||||
```
|
||||
|
||||
## DTO Documentation
|
||||
|
||||
### JSDoc Comments
|
||||
```typescript
|
||||
/**
|
||||
* DTO for creating WhatsApp templates
|
||||
* Used by Meta Business API integration
|
||||
*/
|
||||
export class TemplateDto {
|
||||
/** Template name - must be unique */
|
||||
name: string;
|
||||
|
||||
/** Template category (MARKETING, UTILITY, AUTHENTICATION) */
|
||||
category: string;
|
||||
|
||||
/** Whether category can be changed after creation */
|
||||
allowCategoryChange: boolean;
|
||||
|
||||
/** Language code (e.g., 'pt_BR', 'en_US') */
|
||||
language: string;
|
||||
|
||||
/** Template components (header, body, footer, buttons) */
|
||||
components: any;
|
||||
|
||||
/** Optional webhook URL for template status updates */
|
||||
webhookUrl?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## DTO Naming Conventions
|
||||
|
||||
### Standard Naming Patterns
|
||||
- `*Dto` - Data transfer objects
|
||||
- `Create*Dto` - Creation DTOs
|
||||
- `Update*Dto` - Update DTOs
|
||||
- `Send*Dto` - Message sending DTOs
|
||||
- `Get*Dto` - Query DTOs
|
||||
- `Handle*Dto` - Action DTOs
|
||||
|
||||
## File Organization
|
||||
|
||||
### DTO File Structure
|
||||
```
|
||||
src/api/dto/
|
||||
├── instance.dto.ts # Main instance DTO
|
||||
├── template.dto.ts # Template management
|
||||
├── sendMessage.dto.ts # Message sending DTOs
|
||||
├── chat.dto.ts # Chat operations
|
||||
├── business.dto.ts # Business API DTOs
|
||||
├── group.dto.ts # Group management
|
||||
├── label.dto.ts # Label management
|
||||
├── proxy.dto.ts # Proxy configuration
|
||||
├── settings.dto.ts # Instance settings
|
||||
└── call.dto.ts # Call operations
|
||||
```
|
||||
|
||||
## Integration DTO Patterns
|
||||
|
||||
### Chatbot Integration DTOs
|
||||
```typescript
|
||||
// Base for all chatbot DTOs
|
||||
export class BaseChatbotDto {
|
||||
enabled?: boolean;
|
||||
description: string;
|
||||
// ... common properties
|
||||
}
|
||||
|
||||
// Specific chatbot DTOs extend base
|
||||
export class TypebotDto extends BaseChatbotDto {
|
||||
url: string;
|
||||
typebot: string;
|
||||
// ... typebot-specific properties
|
||||
}
|
||||
|
||||
export class OpenaiDto extends BaseChatbotDto {
|
||||
apiKey: string;
|
||||
model: string;
|
||||
// ... openai-specific properties
|
||||
}
|
||||
```
|
||||
|
||||
## DTO Testing Pattern
|
||||
|
||||
### DTO Validation Tests
|
||||
```typescript
|
||||
describe('ExampleDto', () => {
|
||||
it('should validate required fields', () => {
|
||||
const dto = new ExampleDto();
|
||||
dto.name = 'test';
|
||||
dto.category = 'MARKETING';
|
||||
dto.allowCategoryChange = true;
|
||||
dto.language = 'pt_BR';
|
||||
dto.components = {};
|
||||
|
||||
expect(dto.name).toBe('test');
|
||||
expect(dto.category).toBe('MARKETING');
|
||||
});
|
||||
|
||||
it('should handle optional fields', () => {
|
||||
const dto = new ExampleDto();
|
||||
dto.name = 'test';
|
||||
dto.category = 'MARKETING';
|
||||
dto.allowCategoryChange = true;
|
||||
dto.language = 'pt_BR';
|
||||
dto.components = {};
|
||||
dto.webhookUrl = 'https://example.com/webhook';
|
||||
|
||||
expect(dto.webhookUrl).toBe('https://example.com/webhook');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## DTO Transformation
|
||||
|
||||
### Request to DTO Mapping (Evolution API Pattern)
|
||||
```typescript
|
||||
// CORRECT - Evolution API uses RouterBroker dataValidate
|
||||
const response = await this.dataValidate<ExampleDto>({
|
||||
request: req,
|
||||
schema: exampleSchema, // JSONSchema7
|
||||
ClassRef: ExampleDto,
|
||||
execute: (instance, data) => controller.method(instance, data),
|
||||
});
|
||||
|
||||
// INCORRECT - Don't use class-validator
|
||||
const dto = plainToClass(ExampleDto, req.body); // ❌ Not used in Evolution API
|
||||
const errors = await validate(dto); // ❌ Not used in Evolution API
|
||||
```
|
||||
416
.cursor/rules/specialized-rules/guard-rules.mdc
Normal file
416
.cursor/rules/specialized-rules/guard-rules.mdc
Normal file
@ -0,0 +1,416 @@
|
||||
---
|
||||
description: Guard patterns for authentication and authorization in Evolution API
|
||||
globs:
|
||||
- "src/api/guards/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Guard Rules
|
||||
|
||||
## Guard Structure Pattern
|
||||
|
||||
### Standard Guard Function
|
||||
```typescript
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { UnauthorizedException, ForbiddenException } from '@exceptions';
|
||||
|
||||
const logger = new Logger('GUARD');
|
||||
|
||||
async function guardFunction(req: Request, _: Response, next: NextFunction) {
|
||||
// Guard logic here
|
||||
|
||||
if (validationFails) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
export const guardName = { guardFunction };
|
||||
```
|
||||
|
||||
## Authentication Guard Pattern
|
||||
|
||||
### API Key Authentication
|
||||
```typescript
|
||||
async function apikey(req: Request, _: Response, next: NextFunction) {
|
||||
const env = configService.get<Auth>('AUTHENTICATION').API_KEY;
|
||||
const key = req.get('apikey');
|
||||
const db = configService.get<Database>('DATABASE');
|
||||
|
||||
if (!key) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
// Global API key check
|
||||
if (env.KEY === key) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Special routes handling
|
||||
if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) {
|
||||
throw new ForbiddenException('Missing global api key', 'The global api key must be set');
|
||||
}
|
||||
|
||||
const param = req.params as unknown as InstanceDto;
|
||||
|
||||
try {
|
||||
if (param?.instanceName) {
|
||||
const instance = await prismaRepository.instance.findUnique({
|
||||
where: { name: param.instanceName },
|
||||
});
|
||||
if (instance.token === key) {
|
||||
return next();
|
||||
}
|
||||
} else {
|
||||
if (req.originalUrl.includes('/instance/fetchInstances') && db.SAVE_DATA.INSTANCE) {
|
||||
const instanceByKey = await prismaRepository.instance.findFirst({
|
||||
where: { token: key },
|
||||
});
|
||||
if (instanceByKey) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
export const authGuard = { apikey };
|
||||
```
|
||||
|
||||
## Instance Validation Guards
|
||||
|
||||
### Instance Exists Guard
|
||||
```typescript
|
||||
async function getInstance(instanceName: string) {
|
||||
try {
|
||||
const cacheConf = configService.get<CacheConf>('CACHE');
|
||||
|
||||
const exists = !!waMonitor.waInstances[instanceName];
|
||||
|
||||
if (cacheConf.REDIS.ENABLED && cacheConf.REDIS.SAVE_INSTANCES) {
|
||||
const keyExists = await cache.has(instanceName);
|
||||
return exists || keyExists;
|
||||
}
|
||||
|
||||
return exists || (await prismaRepository.instance.findMany({ where: { name: instanceName } })).length > 0;
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(error?.toString());
|
||||
}
|
||||
}
|
||||
|
||||
export async function instanceExistsGuard(req: Request, _: Response, next: NextFunction) {
|
||||
if (req.originalUrl.includes('/instance/create')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const param = req.params as unknown as InstanceDto;
|
||||
if (!param?.instanceName) {
|
||||
throw new BadRequestException('"instanceName" not provided.');
|
||||
}
|
||||
|
||||
if (!(await getInstance(param.instanceName))) {
|
||||
throw new NotFoundException(`The "${param.instanceName}" instance does not exist`);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
```
|
||||
|
||||
### Instance Logged Guard
|
||||
```typescript
|
||||
export async function instanceLoggedGuard(req: Request, _: Response, next: NextFunction) {
|
||||
if (req.originalUrl.includes('/instance/create')) {
|
||||
const instance = req.body as InstanceDto;
|
||||
if (await getInstance(instance.instanceName)) {
|
||||
throw new ForbiddenException(`This name "${instance.instanceName}" is already in use.`);
|
||||
}
|
||||
|
||||
if (waMonitor.waInstances[instance.instanceName]) {
|
||||
delete waMonitor.waInstances[instance.instanceName];
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
```
|
||||
|
||||
## Telemetry Guard Pattern
|
||||
|
||||
### Telemetry Collection
|
||||
```typescript
|
||||
class Telemetry {
|
||||
public collectTelemetry(req: Request, res: Response, next: NextFunction): void {
|
||||
// Collect telemetry data
|
||||
const telemetryData = {
|
||||
route: req.originalUrl,
|
||||
method: req.method,
|
||||
timestamp: new Date(),
|
||||
userAgent: req.get('User-Agent'),
|
||||
};
|
||||
|
||||
// Send telemetry asynchronously (don't block request)
|
||||
setImmediate(() => {
|
||||
this.sendTelemetry(telemetryData);
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
private async sendTelemetry(data: any): Promise<void> {
|
||||
try {
|
||||
// Send telemetry data
|
||||
} catch (error) {
|
||||
// Silently fail - don't affect main request
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Telemetry;
|
||||
```
|
||||
|
||||
## Guard Composition Pattern
|
||||
|
||||
### Multiple Guards Usage
|
||||
```typescript
|
||||
// In router setup
|
||||
const guards = [instanceExistsGuard, instanceLoggedGuard, authGuard['apikey']];
|
||||
|
||||
router
|
||||
.use('/instance', new InstanceRouter(configService, ...guards).router)
|
||||
.use('/message', new MessageRouter(...guards).router)
|
||||
.use('/chat', new ChatRouter(...guards).router);
|
||||
```
|
||||
|
||||
## Error Handling in Guards
|
||||
|
||||
### Proper Exception Throwing
|
||||
```typescript
|
||||
// CORRECT - Use proper HTTP exceptions
|
||||
if (!apiKey) {
|
||||
throw new UnauthorizedException('API key required');
|
||||
}
|
||||
|
||||
if (instanceExists) {
|
||||
throw new ForbiddenException('Instance already exists');
|
||||
}
|
||||
|
||||
if (!instanceFound) {
|
||||
throw new NotFoundException('Instance not found');
|
||||
}
|
||||
|
||||
if (validationFails) {
|
||||
throw new BadRequestException('Invalid request parameters');
|
||||
}
|
||||
|
||||
// INCORRECT - Don't use generic Error
|
||||
if (!apiKey) {
|
||||
throw new Error('API key required'); // ❌ Use specific exceptions
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Access in Guards
|
||||
|
||||
### Config Service Usage
|
||||
```typescript
|
||||
async function configAwareGuard(req: Request, _: Response, next: NextFunction) {
|
||||
const authConfig = configService.get<Auth>('AUTHENTICATION');
|
||||
const cacheConfig = configService.get<CacheConf>('CACHE');
|
||||
const dbConfig = configService.get<Database>('DATABASE');
|
||||
|
||||
// Use configuration for guard logic
|
||||
if (authConfig.API_KEY.KEY === providedKey) {
|
||||
return next();
|
||||
}
|
||||
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
```
|
||||
|
||||
## Database Access in Guards
|
||||
|
||||
### Prisma Repository Usage
|
||||
```typescript
|
||||
async function databaseGuard(req: Request, _: Response, next: NextFunction) {
|
||||
try {
|
||||
const param = req.params as unknown as InstanceDto;
|
||||
|
||||
const instance = await prismaRepository.instance.findUnique({
|
||||
where: { name: param.instanceName },
|
||||
});
|
||||
|
||||
if (!instance) {
|
||||
throw new NotFoundException('Instance not found');
|
||||
}
|
||||
|
||||
// Additional validation logic
|
||||
if (instance.status !== 'active') {
|
||||
throw new ForbiddenException('Instance not active');
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
logger.error('Database guard error:', error);
|
||||
throw new InternalServerErrorException('Database access failed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cache Integration in Guards
|
||||
|
||||
### Cache Service Usage
|
||||
```typescript
|
||||
async function cacheAwareGuard(req: Request, _: Response, next: NextFunction) {
|
||||
const cacheConf = configService.get<CacheConf>('CACHE');
|
||||
|
||||
if (cacheConf.REDIS.ENABLED) {
|
||||
const cached = await cache.get(`guard:${req.params.instanceName}`);
|
||||
if (cached) {
|
||||
// Use cached validation result
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
// Perform validation and cache result
|
||||
const isValid = await performValidation(req.params.instanceName);
|
||||
|
||||
if (cacheConf.REDIS.ENABLED) {
|
||||
await cache.set(`guard:${req.params.instanceName}`, isValid, 300); // 5 min TTL
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
return next();
|
||||
}
|
||||
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
```
|
||||
|
||||
## Logging in Guards
|
||||
|
||||
### Structured Logging
|
||||
```typescript
|
||||
const logger = new Logger('GUARD');
|
||||
|
||||
async function loggedGuard(req: Request, _: Response, next: NextFunction) {
|
||||
logger.log(`Guard validation started for ${req.originalUrl}`);
|
||||
|
||||
try {
|
||||
// Guard logic
|
||||
const isValid = await validateRequest(req);
|
||||
|
||||
if (isValid) {
|
||||
logger.log(`Guard validation successful for ${req.params.instanceName}`);
|
||||
return next();
|
||||
}
|
||||
|
||||
logger.warn(`Guard validation failed for ${req.params.instanceName}`);
|
||||
throw new UnauthorizedException();
|
||||
} catch (error) {
|
||||
logger.error(`Guard validation error: ${error.message}`, error.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Guard Testing Pattern
|
||||
|
||||
### Unit Test Structure
|
||||
```typescript
|
||||
describe('authGuard', () => {
|
||||
let req: Partial<Request>;
|
||||
let res: Partial<Response>;
|
||||
let next: NextFunction;
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
get: jest.fn(),
|
||||
params: {},
|
||||
originalUrl: '/test',
|
||||
};
|
||||
res = {};
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
describe('apikey', () => {
|
||||
it('should pass with valid global API key', async () => {
|
||||
(req.get as jest.Mock).mockReturnValue('valid-global-key');
|
||||
|
||||
await authGuard.apikey(req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException with no API key', async () => {
|
||||
(req.get as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
await expect(
|
||||
authGuard.apikey(req as Request, res as Response, next)
|
||||
).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should pass with valid instance token', async () => {
|
||||
(req.get as jest.Mock).mockReturnValue('instance-token');
|
||||
req.params = { instanceName: 'test-instance' };
|
||||
|
||||
// Mock prisma repository
|
||||
jest.spyOn(prismaRepository.instance, 'findUnique').mockResolvedValue({
|
||||
token: 'instance-token',
|
||||
} as any);
|
||||
|
||||
await authGuard.apikey(req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Guard Performance Considerations
|
||||
|
||||
### Efficient Validation
|
||||
```typescript
|
||||
// CORRECT - Efficient guard with early returns
|
||||
async function efficientGuard(req: Request, _: Response, next: NextFunction) {
|
||||
// Quick checks first
|
||||
if (req.originalUrl.includes('/public')) {
|
||||
return next(); // Skip validation for public routes
|
||||
}
|
||||
|
||||
const apiKey = req.get('apikey');
|
||||
if (!apiKey) {
|
||||
throw new UnauthorizedException(); // Fail fast
|
||||
}
|
||||
|
||||
// More expensive checks only if needed
|
||||
if (apiKey === globalKey) {
|
||||
return next(); // Skip database check
|
||||
}
|
||||
|
||||
// Database check only as last resort
|
||||
const isValid = await validateInDatabase(apiKey);
|
||||
if (isValid) {
|
||||
return next();
|
||||
}
|
||||
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
// INCORRECT - Inefficient guard
|
||||
async function inefficientGuard(req: Request, _: Response, next: NextFunction) {
|
||||
// Always do expensive database check first
|
||||
const dbResult = await expensiveDatabaseQuery(); // ❌ Expensive operation first
|
||||
|
||||
const apiKey = req.get('apikey');
|
||||
if (!apiKey && dbResult) { // ❌ Complex logic
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
```
|
||||
552
.cursor/rules/specialized-rules/integration-channel-rules.mdc
Normal file
552
.cursor/rules/specialized-rules/integration-channel-rules.mdc
Normal file
@ -0,0 +1,552 @@
|
||||
---
|
||||
description: Channel integration patterns for Evolution API
|
||||
globs:
|
||||
- "src/api/integrations/channel/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Channel Integration Rules
|
||||
|
||||
## Channel Controller Pattern
|
||||
|
||||
### Base Channel Controller
|
||||
```typescript
|
||||
export interface ChannelControllerInterface {
|
||||
integrationEnabled: boolean;
|
||||
}
|
||||
|
||||
export class ChannelController {
|
||||
public prismaRepository: PrismaRepository;
|
||||
public waMonitor: WAMonitoringService;
|
||||
|
||||
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||
this.prisma = prismaRepository;
|
||||
this.monitor = waMonitor;
|
||||
}
|
||||
|
||||
public set prisma(prisma: PrismaRepository) {
|
||||
this.prismaRepository = prisma;
|
||||
}
|
||||
|
||||
public get prisma() {
|
||||
return this.prismaRepository;
|
||||
}
|
||||
|
||||
public set monitor(waMonitor: WAMonitoringService) {
|
||||
this.waMonitor = waMonitor;
|
||||
}
|
||||
|
||||
public get monitor() {
|
||||
return this.waMonitor;
|
||||
}
|
||||
|
||||
public init(instanceData: InstanceDto, data: ChannelDataType) {
|
||||
if (!instanceData.token && instanceData.integration === Integration.WHATSAPP_BUSINESS) {
|
||||
throw new BadRequestException('token is required');
|
||||
}
|
||||
|
||||
if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
|
||||
return new BusinessStartupService(/* dependencies */);
|
||||
}
|
||||
|
||||
if (instanceData.integration === Integration.EVOLUTION) {
|
||||
return new EvolutionStartupService(/* dependencies */);
|
||||
}
|
||||
|
||||
if (instanceData.integration === Integration.WHATSAPP_BAILEYS) {
|
||||
return new BaileysStartupService(/* dependencies */);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Extended Channel Controller
|
||||
```typescript
|
||||
export class EvolutionController extends ChannelController implements ChannelControllerInterface {
|
||||
private readonly logger = new Logger('EvolutionController');
|
||||
|
||||
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||
super(prismaRepository, waMonitor);
|
||||
}
|
||||
|
||||
integrationEnabled: boolean;
|
||||
|
||||
public async receiveWebhook(data: any) {
|
||||
const numberId = data.numberId;
|
||||
|
||||
if (!numberId) {
|
||||
this.logger.error('WebhookService -> receiveWebhookEvolution -> numberId not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = await this.prismaRepository.instance.findFirst({
|
||||
where: { number: numberId },
|
||||
});
|
||||
|
||||
if (!instance) {
|
||||
this.logger.error('WebhookService -> receiveWebhook -> instance not found');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data);
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Channel Service Pattern
|
||||
|
||||
### Base Channel Service
|
||||
```typescript
|
||||
export class ChannelStartupService {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
public readonly cache: CacheService,
|
||||
public readonly chatwootCache: CacheService,
|
||||
) {}
|
||||
|
||||
public readonly logger = new Logger('ChannelStartupService');
|
||||
|
||||
public client: WASocket;
|
||||
public readonly instance: wa.Instance = {};
|
||||
public readonly localChatwoot: wa.LocalChatwoot = {};
|
||||
public readonly localProxy: wa.LocalProxy = {};
|
||||
public readonly localSettings: wa.LocalSettings = {};
|
||||
public readonly localWebhook: wa.LocalWebHook = {};
|
||||
|
||||
public setInstance(instance: InstanceDto) {
|
||||
this.logger.setInstance(instance.instanceName);
|
||||
|
||||
this.instance.name = instance.instanceName;
|
||||
this.instance.id = instance.instanceId;
|
||||
this.instance.integration = instance.integration;
|
||||
this.instance.number = instance.number;
|
||||
this.instance.token = instance.token;
|
||||
this.instance.businessId = instance.businessId;
|
||||
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
this.chatwootService.eventWhatsapp(
|
||||
Events.STATUS_INSTANCE,
|
||||
{ instanceName: this.instance.name },
|
||||
{
|
||||
instance: this.instance.name,
|
||||
status: 'created',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public set instanceName(name: string) {
|
||||
this.logger.setInstance(name);
|
||||
this.instance.name = name;
|
||||
}
|
||||
|
||||
public get instanceName() {
|
||||
return this.instance.name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Extended Channel Service
|
||||
```typescript
|
||||
export class EvolutionStartupService extends ChannelStartupService {
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
eventEmitter: EventEmitter2,
|
||||
prismaRepository: PrismaRepository,
|
||||
cache: CacheService,
|
||||
chatwootCache: CacheService,
|
||||
) {
|
||||
super(configService, eventEmitter, prismaRepository, cache, chatwootCache);
|
||||
}
|
||||
|
||||
public async sendMessage(data: SendTextDto): Promise<any> {
|
||||
// Evolution-specific message sending logic
|
||||
const response = await this.evolutionApiCall('/send-message', data);
|
||||
return response;
|
||||
}
|
||||
|
||||
public async connectToWhatsapp(data: any): Promise<void> {
|
||||
// Evolution-specific connection logic
|
||||
this.logger.log('Connecting to Evolution API');
|
||||
|
||||
// Set up webhook listeners
|
||||
this.setupWebhookHandlers();
|
||||
|
||||
// Initialize connection
|
||||
await this.initializeConnection(data);
|
||||
}
|
||||
|
||||
private async evolutionApiCall(endpoint: string, data: any): Promise<any> {
|
||||
const config = this.configService.get<Evolution>('EVOLUTION');
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${config.API_URL}${endpoint}`, data, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.instance.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error(`Evolution API call failed: ${error.message}`);
|
||||
throw new InternalServerErrorException('Evolution API call failed');
|
||||
}
|
||||
}
|
||||
|
||||
private setupWebhookHandlers(): void {
|
||||
// Set up webhook event handlers
|
||||
}
|
||||
|
||||
private async initializeConnection(data: any): Promise<void> {
|
||||
// Initialize connection with Evolution API
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Business API Service Pattern
|
||||
|
||||
### Meta Business Service
|
||||
```typescript
|
||||
export class BusinessStartupService extends ChannelStartupService {
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
eventEmitter: EventEmitter2,
|
||||
prismaRepository: PrismaRepository,
|
||||
cache: CacheService,
|
||||
chatwootCache: CacheService,
|
||||
baileysCache: CacheService,
|
||||
providerFiles: ProviderFiles,
|
||||
) {
|
||||
super(configService, eventEmitter, prismaRepository, cache, chatwootCache);
|
||||
}
|
||||
|
||||
public async sendMessage(data: SendTextDto): Promise<any> {
|
||||
const businessConfig = this.configService.get<WaBusiness>('WA_BUSINESS');
|
||||
|
||||
const payload = {
|
||||
messaging_product: 'whatsapp',
|
||||
to: data.number,
|
||||
type: 'text',
|
||||
text: {
|
||||
body: data.text,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${businessConfig.URL}/${businessConfig.VERSION}/${this.instance.businessId}/messages`,
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.instance.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error(`Business API call failed: ${error.message}`);
|
||||
throw new BadRequestException('Failed to send message via Business API');
|
||||
}
|
||||
}
|
||||
|
||||
public async receiveWebhook(data: any): Promise<void> {
|
||||
// Process incoming webhook from Meta Business API
|
||||
const { entry } = data;
|
||||
|
||||
for (const entryItem of entry) {
|
||||
const { changes } = entryItem;
|
||||
|
||||
for (const change of changes) {
|
||||
if (change.field === 'messages') {
|
||||
await this.processMessage(change.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async processMessage(messageData: any): Promise<void> {
|
||||
// Process incoming message from Business API
|
||||
const { messages, contacts } = messageData;
|
||||
|
||||
if (messages) {
|
||||
for (const message of messages) {
|
||||
await this.handleIncomingMessage(message, contacts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleIncomingMessage(message: any, contacts: any[]): Promise<void> {
|
||||
// Handle individual message
|
||||
const contact = contacts?.find(c => c.wa_id === message.from);
|
||||
|
||||
// Emit event for message processing
|
||||
this.eventEmitter.emit(Events.MESSAGES_UPSERT, {
|
||||
instanceName: this.instance.name,
|
||||
message,
|
||||
contact,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Baileys Service Pattern
|
||||
|
||||
### Baileys Integration Service
|
||||
```typescript
|
||||
export class BaileysStartupService extends ChannelStartupService {
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
eventEmitter: EventEmitter2,
|
||||
prismaRepository: PrismaRepository,
|
||||
cache: CacheService,
|
||||
chatwootCache: CacheService,
|
||||
baileysCache: CacheService,
|
||||
providerFiles: ProviderFiles,
|
||||
) {
|
||||
super(configService, eventEmitter, prismaRepository, cache, chatwootCache);
|
||||
}
|
||||
|
||||
public async connectToWhatsapp(): Promise<void> {
|
||||
const authPath = path.join(INSTANCE_DIR, this.instance.name);
|
||||
const { state, saveCreds } = await useMultiFileAuthState(authPath);
|
||||
|
||||
this.client = makeWASocket({
|
||||
auth: state,
|
||||
logger: P({ level: 'error' }),
|
||||
printQRInTerminal: false,
|
||||
browser: ['Evolution API', 'Chrome', '4.0.0'],
|
||||
defaultQueryTimeoutMs: 60000,
|
||||
});
|
||||
|
||||
this.setupEventHandlers();
|
||||
this.client.ev.on('creds.update', saveCreds);
|
||||
}
|
||||
|
||||
private setupEventHandlers(): void {
|
||||
this.client.ev.on('connection.update', (update) => {
|
||||
this.handleConnectionUpdate(update);
|
||||
});
|
||||
|
||||
this.client.ev.on('messages.upsert', ({ messages, type }) => {
|
||||
this.handleIncomingMessages(messages, type);
|
||||
});
|
||||
|
||||
this.client.ev.on('messages.update', (updates) => {
|
||||
this.handleMessageUpdates(updates);
|
||||
});
|
||||
|
||||
this.client.ev.on('contacts.upsert', (contacts) => {
|
||||
this.handleContactsUpdate(contacts);
|
||||
});
|
||||
|
||||
this.client.ev.on('chats.upsert', (chats) => {
|
||||
this.handleChatsUpdate(chats);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleConnectionUpdate(update: ConnectionUpdate): Promise<void> {
|
||||
const { connection, lastDisconnect, qr } = update;
|
||||
|
||||
if (qr) {
|
||||
this.instance.qrcode = {
|
||||
count: this.instance.qrcode?.count ? this.instance.qrcode.count + 1 : 1,
|
||||
base64: qr,
|
||||
};
|
||||
|
||||
this.eventEmitter.emit(Events.QRCODE_UPDATED, {
|
||||
instanceName: this.instance.name,
|
||||
qrcode: this.instance.qrcode,
|
||||
});
|
||||
}
|
||||
|
||||
if (connection === 'close') {
|
||||
const shouldReconnect = (lastDisconnect?.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut;
|
||||
|
||||
if (shouldReconnect) {
|
||||
this.logger.log('Connection closed, reconnecting...');
|
||||
await this.connectToWhatsapp();
|
||||
} else {
|
||||
this.logger.log('Connection closed, logged out');
|
||||
this.eventEmitter.emit(Events.LOGOUT_INSTANCE, {
|
||||
instanceName: this.instance.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (connection === 'open') {
|
||||
this.logger.log('Connection opened successfully');
|
||||
this.instance.wuid = this.client.user?.id;
|
||||
|
||||
this.eventEmitter.emit(Events.CONNECTION_UPDATE, {
|
||||
instanceName: this.instance.name,
|
||||
state: 'open',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async sendMessage(data: SendTextDto): Promise<any> {
|
||||
const jid = createJid(data.number);
|
||||
|
||||
const message = {
|
||||
text: data.text,
|
||||
};
|
||||
|
||||
if (data.linkPreview !== undefined) {
|
||||
message.linkPreview = data.linkPreview;
|
||||
}
|
||||
|
||||
if (data.mentionsEveryOne) {
|
||||
// Handle mentions
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.client.sendMessage(jid, message);
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send message: ${error.message}`);
|
||||
throw new BadRequestException('Failed to send message');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Channel Router Pattern
|
||||
|
||||
### Channel Router Structure
|
||||
```typescript
|
||||
export class ChannelRouter {
|
||||
public readonly router: Router;
|
||||
|
||||
constructor(configService: any, ...guards: any[]) {
|
||||
this.router = Router();
|
||||
|
||||
this.router.use('/', new EvolutionRouter(configService).router);
|
||||
this.router.use('/', new MetaRouter(configService).router);
|
||||
this.router.use('/baileys', new BaileysRouter(...guards).router);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Specific Channel Router
|
||||
```typescript
|
||||
export class EvolutionRouter extends RouterBroker {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('webhook'), async (req, res) => {
|
||||
const response = await evolutionController.receiveWebhook(req.body);
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Types
|
||||
|
||||
### Channel Data Types
|
||||
```typescript
|
||||
type ChannelDataType = {
|
||||
configService: ConfigService;
|
||||
eventEmitter: EventEmitter2;
|
||||
prismaRepository: PrismaRepository;
|
||||
cache: CacheService;
|
||||
chatwootCache: CacheService;
|
||||
baileysCache: CacheService;
|
||||
providerFiles: ProviderFiles;
|
||||
};
|
||||
|
||||
export enum Integration {
|
||||
WHATSAPP_BUSINESS = 'WHATSAPP-BUSINESS',
|
||||
WHATSAPP_BAILEYS = 'WHATSAPP-BAILEYS',
|
||||
EVOLUTION = 'EVOLUTION',
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling in Channels
|
||||
|
||||
### Channel-Specific Error Handling
|
||||
```typescript
|
||||
// CORRECT - Channel-specific error handling
|
||||
public async sendMessage(data: SendTextDto): Promise<any> {
|
||||
try {
|
||||
const response = await this.channelSpecificSend(data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.logger.error(`${this.constructor.name} send failed: ${error.message}`);
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
throw new UnauthorizedException('Invalid token for channel');
|
||||
}
|
||||
|
||||
if (error.response?.status === 429) {
|
||||
throw new BadRequestException('Rate limit exceeded');
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException('Channel communication failed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Channel Testing Pattern
|
||||
|
||||
### Channel Service Testing
|
||||
```typescript
|
||||
describe('EvolutionStartupService', () => {
|
||||
let service: EvolutionStartupService;
|
||||
let configService: jest.Mocked<ConfigService>;
|
||||
let eventEmitter: jest.Mocked<EventEmitter2>;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockConfig = {
|
||||
get: jest.fn().mockReturnValue({
|
||||
API_URL: 'https://api.evolution.com',
|
||||
}),
|
||||
};
|
||||
|
||||
service = new EvolutionStartupService(
|
||||
mockConfig as any,
|
||||
eventEmitter,
|
||||
prismaRepository,
|
||||
cache,
|
||||
chatwootCache,
|
||||
);
|
||||
});
|
||||
|
||||
describe('sendMessage', () => {
|
||||
it('should send message successfully', async () => {
|
||||
const data = { number: '5511999999999', text: 'Test message' };
|
||||
|
||||
// Mock axios response
|
||||
jest.spyOn(axios, 'post').mockResolvedValue({
|
||||
data: { success: true, messageId: '123' },
|
||||
});
|
||||
|
||||
const result = await service.sendMessage(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/send-message'),
|
||||
data,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
'Authorization': expect.stringContaining('Bearer'),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
597
.cursor/rules/specialized-rules/integration-chatbot-rules.mdc
Normal file
597
.cursor/rules/specialized-rules/integration-chatbot-rules.mdc
Normal file
@ -0,0 +1,597 @@
|
||||
---
|
||||
description: Chatbot integration patterns for Evolution API
|
||||
globs:
|
||||
- "src/api/integrations/chatbot/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Chatbot Integration Rules
|
||||
|
||||
## Base Chatbot Pattern
|
||||
|
||||
### Base Chatbot DTO
|
||||
```typescript
|
||||
/**
|
||||
* Base DTO for all chatbot integrations
|
||||
* Contains common properties shared by all chatbot types
|
||||
*/
|
||||
export class BaseChatbotDto {
|
||||
enabled?: boolean;
|
||||
description: string;
|
||||
expire?: number;
|
||||
keywordFinish?: string;
|
||||
delayMessage?: number;
|
||||
unknownMessage?: string;
|
||||
listeningFromMe?: boolean;
|
||||
stopBotFromMe?: boolean;
|
||||
keepOpen?: boolean;
|
||||
debounceTime?: number;
|
||||
triggerType: TriggerType;
|
||||
triggerOperator?: TriggerOperator;
|
||||
triggerValue?: string;
|
||||
ignoreJids?: string[];
|
||||
splitMessages?: boolean;
|
||||
timePerChar?: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Base Chatbot Controller
|
||||
```typescript
|
||||
export abstract class BaseChatbotController<BotType = any, BotData extends BaseChatbotDto = BaseChatbotDto>
|
||||
extends ChatbotController
|
||||
implements ChatbotControllerInterface
|
||||
{
|
||||
public readonly logger: Logger;
|
||||
integrationEnabled: boolean;
|
||||
botRepository: any;
|
||||
settingsRepository: any;
|
||||
sessionRepository: any;
|
||||
userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {};
|
||||
|
||||
// Abstract methods to be implemented by specific chatbots
|
||||
protected abstract readonly integrationName: string;
|
||||
protected abstract processBot(
|
||||
waInstance: any,
|
||||
remoteJid: string,
|
||||
bot: BotType,
|
||||
session: any,
|
||||
settings: ChatbotSettings,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
msg?: any,
|
||||
): Promise<void>;
|
||||
protected abstract getFallbackBotId(settings: any): string | undefined;
|
||||
|
||||
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
|
||||
super(prismaRepository, waMonitor);
|
||||
this.sessionRepository = this.prismaRepository.integrationSession;
|
||||
}
|
||||
|
||||
// Base implementation methods
|
||||
public async createBot(instance: InstanceDto, data: BotData) {
|
||||
if (!data.enabled) {
|
||||
throw new BadRequestException(`${this.integrationName} is disabled`);
|
||||
}
|
||||
|
||||
// Common bot creation logic
|
||||
const bot = await this.botRepository.create({
|
||||
data: {
|
||||
...data,
|
||||
instanceId: instance.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
return bot;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Base Chatbot Service
|
||||
```typescript
|
||||
/**
|
||||
* Base class for all chatbot service implementations
|
||||
* Contains common methods shared across different chatbot integrations
|
||||
*/
|
||||
export abstract class BaseChatbotService<BotType = any, SettingsType = any> {
|
||||
protected readonly logger: Logger;
|
||||
protected readonly waMonitor: WAMonitoringService;
|
||||
protected readonly prismaRepository: PrismaRepository;
|
||||
protected readonly configService?: ConfigService;
|
||||
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
loggerName: string,
|
||||
configService?: ConfigService,
|
||||
) {
|
||||
this.waMonitor = waMonitor;
|
||||
this.prismaRepository = prismaRepository;
|
||||
this.logger = new Logger(loggerName);
|
||||
this.configService = configService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a message contains an image
|
||||
*/
|
||||
protected isImageMessage(content: string): boolean {
|
||||
return content.includes('imageMessage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from message
|
||||
*/
|
||||
protected getMessageContent(msg: any): string {
|
||||
return getConversationMessage(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send typing indicator
|
||||
*/
|
||||
protected async sendTyping(instanceName: string, remoteJid: string): Promise<void> {
|
||||
await this.waMonitor.waInstances[instanceName].sendPresence(remoteJid, 'composing');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Typebot Integration Pattern
|
||||
|
||||
### Typebot Service
|
||||
```typescript
|
||||
export class TypebotService extends BaseChatbotService<TypebotModel, any> {
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
configService: ConfigService,
|
||||
prismaRepository: PrismaRepository,
|
||||
private readonly openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'TypebotService', configService);
|
||||
}
|
||||
|
||||
public async sendTypebotMessage(
|
||||
instanceName: string,
|
||||
remoteJid: string,
|
||||
typebot: TypebotModel,
|
||||
content: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${typebot.url}/api/v1/typebots/${typebot.typebot}/startChat`,
|
||||
{
|
||||
message: content,
|
||||
sessionId: `${instanceName}-${remoteJid}`,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const { messages } = response.data;
|
||||
|
||||
for (const message of messages) {
|
||||
await this.processTypebotMessage(instanceName, remoteJid, message);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Typebot API error: ${error.message}`);
|
||||
throw new InternalServerErrorException('Typebot communication failed');
|
||||
}
|
||||
}
|
||||
|
||||
private async processTypebotMessage(
|
||||
instanceName: string,
|
||||
remoteJid: string,
|
||||
message: any,
|
||||
): Promise<void> {
|
||||
const waInstance = this.waMonitor.waInstances[instanceName];
|
||||
|
||||
if (message.type === 'text') {
|
||||
await waInstance.sendMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
text: message.content.richText[0].children[0].text,
|
||||
});
|
||||
}
|
||||
|
||||
if (message.type === 'image') {
|
||||
await waInstance.sendMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
mediaMessage: {
|
||||
mediatype: 'image',
|
||||
media: message.content.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## OpenAI Integration Pattern
|
||||
|
||||
### OpenAI Service
|
||||
```typescript
|
||||
export class OpenaiService extends BaseChatbotService<OpenaiModel, any> {
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'OpenaiService', configService);
|
||||
}
|
||||
|
||||
public async sendOpenaiMessage(
|
||||
instanceName: string,
|
||||
remoteJid: string,
|
||||
openai: OpenaiModel,
|
||||
content: string,
|
||||
pushName?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const openaiConfig = this.configService.get<Openai>('OPENAI');
|
||||
|
||||
const response = await axios.post(
|
||||
'https://api.openai.com/v1/chat/completions',
|
||||
{
|
||||
model: openai.model || 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: openai.systemMessage || 'You are a helpful assistant.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: content,
|
||||
},
|
||||
],
|
||||
max_tokens: openai.maxTokens || 1000,
|
||||
temperature: openai.temperature || 0.7,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${openai.apiKey || openaiConfig.API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const aiResponse = response.data.choices[0].message.content;
|
||||
|
||||
await this.waMonitor.waInstances[instanceName].sendMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
text: aiResponse,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`OpenAI API error: ${error.message}`);
|
||||
|
||||
// Send fallback message
|
||||
await this.waMonitor.waInstances[instanceName].sendMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
text: openai.unknownMessage || 'Desculpe, não consegui processar sua mensagem.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Chatwoot Integration Pattern
|
||||
|
||||
### Chatwoot Service
|
||||
```typescript
|
||||
export class ChatwootService extends BaseChatbotService<any, any> {
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
configService: ConfigService,
|
||||
prismaRepository: PrismaRepository,
|
||||
private readonly chatwootCache: CacheService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'ChatwootService', configService);
|
||||
}
|
||||
|
||||
public async eventWhatsapp(
|
||||
event: Events,
|
||||
instanceName: { instanceName: string },
|
||||
data: any,
|
||||
): Promise<void> {
|
||||
const chatwootConfig = this.configService.get<Chatwoot>('CHATWOOT');
|
||||
|
||||
if (!chatwootConfig.ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const instance = await this.prismaRepository.instance.findUnique({
|
||||
where: { name: instanceName.instanceName },
|
||||
});
|
||||
|
||||
if (!instance?.chatwootAccountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const webhook = {
|
||||
event,
|
||||
instance: instanceName.instanceName,
|
||||
data,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await axios.post(
|
||||
`${instance.chatwootUrl}/api/v1/accounts/${instance.chatwootAccountId}/webhooks`,
|
||||
webhook,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${instance.chatwootToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Chatwoot webhook error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async createConversation(
|
||||
instanceName: string,
|
||||
contact: any,
|
||||
message: any,
|
||||
): Promise<void> {
|
||||
// Create conversation in Chatwoot
|
||||
const instance = await this.prismaRepository.instance.findUnique({
|
||||
where: { name: instanceName },
|
||||
});
|
||||
|
||||
if (!instance?.chatwootAccountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const conversation = await axios.post(
|
||||
`${instance.chatwootUrl}/api/v1/accounts/${instance.chatwootAccountId}/conversations`,
|
||||
{
|
||||
source_id: contact.id,
|
||||
inbox_id: instance.chatwootInboxId,
|
||||
contact_id: contact.chatwootContactId,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${instance.chatwootToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Cache conversation
|
||||
await this.chatwootCache.set(
|
||||
`conversation:${instanceName}:${contact.id}`,
|
||||
conversation.data,
|
||||
3600
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(`Chatwoot conversation creation error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dify Integration Pattern
|
||||
|
||||
### Dify Service
|
||||
```typescript
|
||||
export class DifyService extends BaseChatbotService<DifyModel, any> {
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
private readonly openaiService: OpenaiService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'DifyService', configService);
|
||||
}
|
||||
|
||||
public async sendDifyMessage(
|
||||
instanceName: string,
|
||||
remoteJid: string,
|
||||
dify: DifyModel,
|
||||
content: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${dify.apiUrl}/v1/chat-messages`,
|
||||
{
|
||||
inputs: {},
|
||||
query: content,
|
||||
user: remoteJid,
|
||||
conversation_id: `${instanceName}-${remoteJid}`,
|
||||
response_mode: 'blocking',
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${dify.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const aiResponse = response.data.answer;
|
||||
|
||||
await this.waMonitor.waInstances[instanceName].sendMessage({
|
||||
number: remoteJid.split('@')[0],
|
||||
text: aiResponse,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Dify API error: ${error.message}`);
|
||||
|
||||
// Fallback to OpenAI if configured
|
||||
if (dify.fallbackOpenai && this.openaiService) {
|
||||
await this.openaiService.sendOpenaiMessage(instanceName, remoteJid, dify.openaiBot, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Chatbot Router Pattern
|
||||
|
||||
### Chatbot Router Structure (Evolution API Real Pattern)
|
||||
```typescript
|
||||
export class ChatbotRouter {
|
||||
public readonly router: Router;
|
||||
|
||||
constructor(...guards: any[]) {
|
||||
this.router = Router();
|
||||
|
||||
// Real Evolution API chatbot integrations
|
||||
this.router.use('/evolutionBot', new EvolutionBotRouter(...guards).router);
|
||||
this.router.use('/chatwoot', new ChatwootRouter(...guards).router);
|
||||
this.router.use('/typebot', new TypebotRouter(...guards).router);
|
||||
this.router.use('/openai', new OpenaiRouter(...guards).router);
|
||||
this.router.use('/dify', new DifyRouter(...guards).router);
|
||||
this.router.use('/flowise', new FlowiseRouter(...guards).router);
|
||||
this.router.use('/n8n', new N8nRouter(...guards).router);
|
||||
this.router.use('/evoai', new EvoaiRouter(...guards).router);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Chatbot Validation Patterns
|
||||
|
||||
### Chatbot Schema Validation (Evolution API Pattern)
|
||||
```typescript
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...fields: string[]) => {
|
||||
const properties = {};
|
||||
fields.forEach((field) => {
|
||||
properties[field] = {
|
||||
if: { properties: { [field]: { type: 'string' } } },
|
||||
then: { properties: { [field]: { minLength: 1 } } },
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
allOf: Object.values(properties),
|
||||
};
|
||||
};
|
||||
|
||||
export const evolutionBotSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
description: { type: 'string' },
|
||||
apiUrl: { type: 'string' },
|
||||
apiKey: { type: 'string' },
|
||||
triggerType: { type: 'string', enum: ['all', 'keyword', 'none', 'advanced'] },
|
||||
triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] },
|
||||
triggerValue: { type: 'string' },
|
||||
expire: { type: 'integer' },
|
||||
keywordFinish: { type: 'string' },
|
||||
delayMessage: { type: 'integer' },
|
||||
unknownMessage: { type: 'string' },
|
||||
listeningFromMe: { type: 'boolean' },
|
||||
stopBotFromMe: { type: 'boolean' },
|
||||
keepOpen: { type: 'boolean' },
|
||||
debounceTime: { type: 'integer' },
|
||||
ignoreJids: { type: 'array', items: { type: 'string' } },
|
||||
splitMessages: { type: 'boolean' },
|
||||
timePerChar: { type: 'integer' },
|
||||
},
|
||||
required: ['enabled', 'apiUrl', 'triggerType'],
|
||||
...isNotEmpty('enabled', 'apiUrl', 'triggerType'),
|
||||
};
|
||||
|
||||
function validateKeywordTrigger(
|
||||
content: string,
|
||||
operator: TriggerOperator,
|
||||
value: string,
|
||||
): boolean {
|
||||
const normalizedContent = content.toLowerCase().trim();
|
||||
const normalizedValue = value.toLowerCase().trim();
|
||||
|
||||
switch (operator) {
|
||||
case TriggerOperator.EQUALS:
|
||||
return normalizedContent === normalizedValue;
|
||||
case TriggerOperator.CONTAINS:
|
||||
return normalizedContent.includes(normalizedValue);
|
||||
case TriggerOperator.STARTS_WITH:
|
||||
return normalizedContent.startsWith(normalizedValue);
|
||||
case TriggerOperator.ENDS_WITH:
|
||||
return normalizedContent.endsWith(normalizedValue);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Session Management Pattern
|
||||
|
||||
### Chatbot Session Handling
|
||||
```typescript
|
||||
export class ChatbotSessionManager {
|
||||
constructor(
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
private readonly cache: CacheService,
|
||||
) {}
|
||||
|
||||
public async getSession(
|
||||
instanceName: string,
|
||||
remoteJid: string,
|
||||
botId: string,
|
||||
): Promise<IntegrationSession | null> {
|
||||
const cacheKey = `session:${instanceName}:${remoteJid}:${botId}`;
|
||||
|
||||
// Try cache first
|
||||
let session = await this.cache.get(cacheKey);
|
||||
if (session) {
|
||||
return session;
|
||||
}
|
||||
|
||||
// Query database
|
||||
session = await this.prismaRepository.integrationSession.findFirst({
|
||||
where: {
|
||||
instanceId: instanceName,
|
||||
remoteJid,
|
||||
botId,
|
||||
status: 'opened',
|
||||
},
|
||||
});
|
||||
|
||||
// Cache result
|
||||
if (session) {
|
||||
await this.cache.set(cacheKey, session, 300); // 5 min TTL
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
public async createSession(
|
||||
instanceName: string,
|
||||
remoteJid: string,
|
||||
botId: string,
|
||||
): Promise<IntegrationSession> {
|
||||
const session = await this.prismaRepository.integrationSession.create({
|
||||
data: {
|
||||
instanceId: instanceName,
|
||||
remoteJid,
|
||||
botId,
|
||||
status: 'opened',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Cache new session
|
||||
const cacheKey = `session:${instanceName}:${remoteJid}:${botId}`;
|
||||
await this.cache.set(cacheKey, session, 300);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
public async closeSession(sessionId: string): Promise<void> {
|
||||
await this.prismaRepository.integrationSession.update({
|
||||
where: { id: sessionId },
|
||||
data: { status: 'closed', updatedAt: new Date() },
|
||||
});
|
||||
|
||||
// Invalidate cache
|
||||
// Note: In a real implementation, you'd need to track cache keys by session ID
|
||||
}
|
||||
}
|
||||
```
|
||||
851
.cursor/rules/specialized-rules/integration-event-rules.mdc
Normal file
851
.cursor/rules/specialized-rules/integration-event-rules.mdc
Normal file
@ -0,0 +1,851 @@
|
||||
---
|
||||
description: Event integration patterns for Evolution API
|
||||
globs:
|
||||
- "src/api/integrations/event/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Event Integration Rules
|
||||
|
||||
## Event Manager Pattern
|
||||
|
||||
### Event Manager Structure
|
||||
```typescript
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { ConfigService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { Server } from 'http';
|
||||
|
||||
export class EventManager {
|
||||
private prismaRepository: PrismaRepository;
|
||||
private configService: ConfigService;
|
||||
private logger = new Logger('EventManager');
|
||||
|
||||
// Event integrations
|
||||
private webhook: WebhookController;
|
||||
private websocket: WebsocketController;
|
||||
private rabbitmq: RabbitmqController;
|
||||
private nats: NatsController;
|
||||
private sqs: SqsController;
|
||||
private pusher: PusherController;
|
||||
|
||||
constructor(
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
server?: Server,
|
||||
) {
|
||||
this.prismaRepository = prismaRepository;
|
||||
this.configService = configService;
|
||||
|
||||
// Initialize event controllers
|
||||
this.webhook = new WebhookController(prismaRepository, configService);
|
||||
this.websocket = new WebsocketController(prismaRepository, configService, server);
|
||||
this.rabbitmq = new RabbitmqController(prismaRepository, configService);
|
||||
this.nats = new NatsController(prismaRepository, configService);
|
||||
this.sqs = new SqsController(prismaRepository, configService);
|
||||
this.pusher = new PusherController(prismaRepository, configService);
|
||||
}
|
||||
|
||||
public async emit(eventData: {
|
||||
instanceName: string;
|
||||
origin: string;
|
||||
event: string;
|
||||
data: Object;
|
||||
serverUrl: string;
|
||||
dateTime: string;
|
||||
sender: string;
|
||||
apiKey?: string;
|
||||
local?: boolean;
|
||||
integration?: string[];
|
||||
}): Promise<void> {
|
||||
this.logger.log(`Emitting event ${eventData.event} for instance ${eventData.instanceName}`);
|
||||
|
||||
// Emit to all configured integrations
|
||||
await Promise.allSettled([
|
||||
this.webhook.emit(eventData),
|
||||
this.websocket.emit(eventData),
|
||||
this.rabbitmq.emit(eventData),
|
||||
this.nats.emit(eventData),
|
||||
this.sqs.emit(eventData),
|
||||
this.pusher.emit(eventData),
|
||||
]);
|
||||
}
|
||||
|
||||
public async setInstance(instanceName: string, data: any): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
if (data.websocket) {
|
||||
promises.push(
|
||||
this.websocket.set(instanceName, {
|
||||
websocket: {
|
||||
enabled: true,
|
||||
events: data.websocket?.events,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (data.rabbitmq) {
|
||||
promises.push(
|
||||
this.rabbitmq.set(instanceName, {
|
||||
rabbitmq: {
|
||||
enabled: true,
|
||||
events: data.rabbitmq?.events,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (data.webhook) {
|
||||
promises.push(
|
||||
this.webhook.set(instanceName, {
|
||||
webhook: {
|
||||
enabled: true,
|
||||
events: data.webhook?.events,
|
||||
url: data.webhook?.url,
|
||||
headers: data.webhook?.headers,
|
||||
base64: data.webhook?.base64,
|
||||
byEvents: data.webhook?.byEvents,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Set other integrations...
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Base Event Controller Pattern
|
||||
|
||||
### Abstract Event Controller
|
||||
```typescript
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { ConfigService } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
|
||||
export type EmitData = {
|
||||
instanceName: string;
|
||||
origin: string;
|
||||
event: string;
|
||||
data: Object;
|
||||
serverUrl: string;
|
||||
dateTime: string;
|
||||
sender: string;
|
||||
apiKey?: string;
|
||||
local?: boolean;
|
||||
integration?: string[];
|
||||
};
|
||||
|
||||
export interface EventControllerInterface {
|
||||
integrationEnabled: boolean;
|
||||
emit(data: EmitData): Promise<void>;
|
||||
set(instanceName: string, data: any): Promise<any>;
|
||||
}
|
||||
|
||||
export abstract class EventController implements EventControllerInterface {
|
||||
protected readonly logger: Logger;
|
||||
protected readonly prismaRepository: PrismaRepository;
|
||||
protected readonly configService: ConfigService;
|
||||
|
||||
public integrationEnabled: boolean = false;
|
||||
|
||||
// Available events for all integrations
|
||||
public static readonly events = [
|
||||
'APPLICATION_STARTUP',
|
||||
'INSTANCE_CREATE',
|
||||
'INSTANCE_DELETE',
|
||||
'QRCODE_UPDATED',
|
||||
'CONNECTION_UPDATE',
|
||||
'STATUS_INSTANCE',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_EDITED',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'SEND_MESSAGE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'PRESENCE_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUPS_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CALL',
|
||||
'TYPEBOT_START',
|
||||
'TYPEBOT_CHANGE_STATUS',
|
||||
'LABELS_EDIT',
|
||||
'LABELS_ASSOCIATION',
|
||||
'CREDS_UPDATE',
|
||||
'MESSAGING_HISTORY_SET',
|
||||
'REMOVE_INSTANCE',
|
||||
'LOGOUT_INSTANCE',
|
||||
];
|
||||
|
||||
constructor(
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
loggerName: string,
|
||||
) {
|
||||
this.prismaRepository = prismaRepository;
|
||||
this.configService = configService;
|
||||
this.logger = new Logger(loggerName);
|
||||
}
|
||||
|
||||
// Abstract methods to be implemented by specific integrations
|
||||
public abstract emit(data: EmitData): Promise<void>;
|
||||
public abstract set(instanceName: string, data: any): Promise<any>;
|
||||
|
||||
// Helper method to check if event should be processed
|
||||
protected shouldProcessEvent(eventName: string, configuredEvents?: string[]): boolean {
|
||||
if (!configuredEvents || configuredEvents.length === 0) {
|
||||
return true; // Process all events if none specified
|
||||
}
|
||||
return configuredEvents.includes(eventName);
|
||||
}
|
||||
|
||||
// Helper method to get instance configuration
|
||||
protected async getInstanceConfig(instanceName: string): Promise<any> {
|
||||
try {
|
||||
const instance = await this.prismaRepository.instance.findUnique({
|
||||
where: { name: instanceName },
|
||||
});
|
||||
return instance;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get instance config for ${instanceName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Webhook Integration Pattern
|
||||
|
||||
### Webhook Controller Implementation
|
||||
```typescript
|
||||
export class WebhookController extends EventController {
|
||||
constructor(
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(prismaRepository, configService, 'WebhookController');
|
||||
}
|
||||
|
||||
public async emit(data: EmitData): Promise<void> {
|
||||
try {
|
||||
const instance = await this.getInstanceConfig(data.instanceName);
|
||||
if (!instance?.webhook?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const webhookConfig = instance.webhook;
|
||||
|
||||
if (!this.shouldProcessEvent(data.event, webhookConfig.events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
event: data.event,
|
||||
instance: data.instanceName,
|
||||
data: data.data,
|
||||
timestamp: data.dateTime,
|
||||
sender: data.sender,
|
||||
server: {
|
||||
version: process.env.npm_package_version,
|
||||
url: data.serverUrl,
|
||||
},
|
||||
};
|
||||
|
||||
// Encode data as base64 if configured
|
||||
if (webhookConfig.base64) {
|
||||
payload.data = Buffer.from(JSON.stringify(payload.data)).toString('base64');
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Evolution-API-Webhook',
|
||||
...webhookConfig.headers,
|
||||
};
|
||||
|
||||
if (webhookConfig.byEvents) {
|
||||
// Send to event-specific endpoint
|
||||
const eventUrl = `${webhookConfig.url}/${data.event.toLowerCase()}`;
|
||||
await this.sendWebhook(eventUrl, payload, headers);
|
||||
} else {
|
||||
// Send to main webhook URL
|
||||
await this.sendWebhook(webhookConfig.url, payload, headers);
|
||||
}
|
||||
|
||||
this.logger.log(`Webhook sent for event ${data.event} to instance ${data.instanceName}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Webhook emission failed for ${data.instanceName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public async set(instanceName: string, data: any): Promise<any> {
|
||||
try {
|
||||
const webhookData = data.webhook;
|
||||
|
||||
await this.prismaRepository.instance.update({
|
||||
where: { name: instanceName },
|
||||
data: {
|
||||
webhook: webhookData,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`Webhook configuration set for instance ${instanceName}`);
|
||||
return { webhook: webhookData };
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set webhook config for ${instanceName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async sendWebhook(url: string, payload: any, headers: any): Promise<void> {
|
||||
try {
|
||||
const response = await axios.post(url, payload, {
|
||||
headers,
|
||||
timeout: 30000,
|
||||
maxRedirects: 3,
|
||||
});
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
this.logger.log(`Webhook delivered successfully to ${url}`);
|
||||
} else {
|
||||
this.logger.warn(`Webhook returned status ${response.status} for ${url}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Webhook delivery failed to ${url}:`, error.message);
|
||||
|
||||
// Implement retry logic here if needed
|
||||
if (error.response?.status >= 500) {
|
||||
// Server error - might be worth retrying
|
||||
this.logger.log(`Server error detected, webhook might be retried later`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket Integration Pattern
|
||||
|
||||
### WebSocket Controller Implementation
|
||||
```typescript
|
||||
import { Server as SocketIOServer } from 'socket.io';
|
||||
import { Server } from 'http';
|
||||
|
||||
export class WebsocketController extends EventController {
|
||||
private io: SocketIOServer;
|
||||
|
||||
constructor(
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
server?: Server,
|
||||
) {
|
||||
super(prismaRepository, configService, 'WebsocketController');
|
||||
|
||||
if (server) {
|
||||
this.io = new SocketIOServer(server, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"],
|
||||
},
|
||||
});
|
||||
|
||||
this.setupSocketHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
private setupSocketHandlers(): void {
|
||||
this.io.on('connection', (socket) => {
|
||||
this.logger.log(`WebSocket client connected: ${socket.id}`);
|
||||
|
||||
socket.on('join-instance', (instanceName: string) => {
|
||||
socket.join(`instance:${instanceName}`);
|
||||
this.logger.log(`Client ${socket.id} joined instance ${instanceName}`);
|
||||
});
|
||||
|
||||
socket.on('leave-instance', (instanceName: string) => {
|
||||
socket.leave(`instance:${instanceName}`);
|
||||
this.logger.log(`Client ${socket.id} left instance ${instanceName}`);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
this.logger.log(`WebSocket client disconnected: ${socket.id}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async emit(data: EmitData): Promise<void> {
|
||||
if (!this.io) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const instance = await this.getInstanceConfig(data.instanceName);
|
||||
if (!instance?.websocket?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const websocketConfig = instance.websocket;
|
||||
|
||||
if (!this.shouldProcessEvent(data.event, websocketConfig.events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
event: data.event,
|
||||
instance: data.instanceName,
|
||||
data: data.data,
|
||||
timestamp: data.dateTime,
|
||||
sender: data.sender,
|
||||
};
|
||||
|
||||
// Emit to specific instance room
|
||||
this.io.to(`instance:${data.instanceName}`).emit('evolution-event', payload);
|
||||
|
||||
// Also emit to global room for monitoring
|
||||
this.io.emit('global-event', payload);
|
||||
|
||||
this.logger.log(`WebSocket event ${data.event} emitted for instance ${data.instanceName}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`WebSocket emission failed for ${data.instanceName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public async set(instanceName: string, data: any): Promise<any> {
|
||||
try {
|
||||
const websocketData = data.websocket;
|
||||
|
||||
await this.prismaRepository.instance.update({
|
||||
where: { name: instanceName },
|
||||
data: {
|
||||
websocket: websocketData,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`WebSocket configuration set for instance ${instanceName}`);
|
||||
return { websocket: websocketData };
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set WebSocket config for ${instanceName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Queue Integration Patterns
|
||||
|
||||
### RabbitMQ Controller Implementation
|
||||
```typescript
|
||||
import amqp from 'amqplib';
|
||||
|
||||
export class RabbitmqController extends EventController {
|
||||
private connection: amqp.Connection | null = null;
|
||||
private channel: amqp.Channel | null = null;
|
||||
|
||||
constructor(
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(prismaRepository, configService, 'RabbitmqController');
|
||||
this.initializeConnection();
|
||||
}
|
||||
|
||||
private async initializeConnection(): Promise<void> {
|
||||
try {
|
||||
const rabbitmqConfig = this.configService.get('RABBITMQ');
|
||||
if (!rabbitmqConfig?.ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.connection = await amqp.connect(rabbitmqConfig.URI);
|
||||
this.channel = await this.connection.createChannel();
|
||||
|
||||
// Declare exchange for Evolution API events
|
||||
await this.channel.assertExchange('evolution-events', 'topic', { durable: true });
|
||||
|
||||
this.logger.log('RabbitMQ connection established');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize RabbitMQ connection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public async emit(data: EmitData): Promise<void> {
|
||||
if (!this.channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const instance = await this.getInstanceConfig(data.instanceName);
|
||||
if (!instance?.rabbitmq?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rabbitmqConfig = instance.rabbitmq;
|
||||
|
||||
if (!this.shouldProcessEvent(data.event, rabbitmqConfig.events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
event: data.event,
|
||||
instance: data.instanceName,
|
||||
data: data.data,
|
||||
timestamp: data.dateTime,
|
||||
sender: data.sender,
|
||||
};
|
||||
|
||||
const routingKey = `evolution.${data.instanceName}.${data.event.toLowerCase()}`;
|
||||
|
||||
await this.channel.publish(
|
||||
'evolution-events',
|
||||
routingKey,
|
||||
Buffer.from(JSON.stringify(payload)),
|
||||
{
|
||||
persistent: true,
|
||||
timestamp: Date.now(),
|
||||
messageId: `${data.instanceName}-${Date.now()}`,
|
||||
}
|
||||
);
|
||||
|
||||
this.logger.log(`RabbitMQ message published for event ${data.event} to instance ${data.instanceName}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`RabbitMQ emission failed for ${data.instanceName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public async set(instanceName: string, data: any): Promise<any> {
|
||||
try {
|
||||
const rabbitmqData = data.rabbitmq;
|
||||
|
||||
await this.prismaRepository.instance.update({
|
||||
where: { name: instanceName },
|
||||
data: {
|
||||
rabbitmq: rabbitmqData,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`RabbitMQ configuration set for instance ${instanceName}`);
|
||||
return { rabbitmq: rabbitmqData };
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set RabbitMQ config for ${instanceName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SQS Controller Implementation
|
||||
```typescript
|
||||
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
|
||||
|
||||
export class SqsController extends EventController {
|
||||
private sqsClient: SQSClient | null = null;
|
||||
|
||||
constructor(
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(prismaRepository, configService, 'SqsController');
|
||||
this.initializeSQSClient();
|
||||
}
|
||||
|
||||
private initializeSQSClient(): void {
|
||||
try {
|
||||
const sqsConfig = this.configService.get('SQS');
|
||||
if (!sqsConfig?.ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sqsClient = new SQSClient({
|
||||
region: sqsConfig.REGION,
|
||||
credentials: {
|
||||
accessKeyId: sqsConfig.ACCESS_KEY_ID,
|
||||
secretAccessKey: sqsConfig.SECRET_ACCESS_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log('SQS client initialized');
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize SQS client:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public async emit(data: EmitData): Promise<void> {
|
||||
if (!this.sqsClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const instance = await this.getInstanceConfig(data.instanceName);
|
||||
if (!instance?.sqs?.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sqsConfig = instance.sqs;
|
||||
|
||||
if (!this.shouldProcessEvent(data.event, sqsConfig.events)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
event: data.event,
|
||||
instance: data.instanceName,
|
||||
data: data.data,
|
||||
timestamp: data.dateTime,
|
||||
sender: data.sender,
|
||||
};
|
||||
|
||||
const command = new SendMessageCommand({
|
||||
QueueUrl: sqsConfig.queueUrl,
|
||||
MessageBody: JSON.stringify(payload),
|
||||
MessageAttributes: {
|
||||
event: {
|
||||
DataType: 'String',
|
||||
StringValue: data.event,
|
||||
},
|
||||
instance: {
|
||||
DataType: 'String',
|
||||
StringValue: data.instanceName,
|
||||
},
|
||||
},
|
||||
MessageGroupId: data.instanceName, // For FIFO queues
|
||||
MessageDeduplicationId: `${data.instanceName}-${Date.now()}`, // For FIFO queues
|
||||
});
|
||||
|
||||
await this.sqsClient.send(command);
|
||||
|
||||
this.logger.log(`SQS message sent for event ${data.event} to instance ${data.instanceName}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`SQS emission failed for ${data.instanceName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public async set(instanceName: string, data: any): Promise<any> {
|
||||
try {
|
||||
const sqsData = data.sqs;
|
||||
|
||||
await this.prismaRepository.instance.update({
|
||||
where: { name: instanceName },
|
||||
data: {
|
||||
sqs: sqsData,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`SQS configuration set for instance ${instanceName}`);
|
||||
return { sqs: sqsData };
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to set SQS config for ${instanceName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event DTO Pattern
|
||||
|
||||
### Event Configuration DTO
|
||||
```typescript
|
||||
import { JsonValue } from '@prisma/client/runtime/library';
|
||||
|
||||
export class EventDto {
|
||||
webhook?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
url?: string;
|
||||
headers?: JsonValue;
|
||||
byEvents?: boolean;
|
||||
base64?: boolean;
|
||||
};
|
||||
|
||||
websocket?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
};
|
||||
|
||||
sqs?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
queueUrl?: string;
|
||||
};
|
||||
|
||||
rabbitmq?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
exchange?: string;
|
||||
};
|
||||
|
||||
nats?: {
|
||||
enabled?: boolean;
|
||||
events?: string[];
|
||||
subject?: string;
|
||||
};
|
||||
|
||||
pusher?: {
|
||||
enabled?: boolean;
|
||||
appId?: string;
|
||||
key?: string;
|
||||
secret?: string;
|
||||
cluster?: string;
|
||||
useTLS?: boolean;
|
||||
events?: string[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Event Router Pattern
|
||||
|
||||
### Event Router Structure
|
||||
```typescript
|
||||
import { NatsRouter } from '@api/integrations/event/nats/nats.router';
|
||||
import { PusherRouter } from '@api/integrations/event/pusher/pusher.router';
|
||||
import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router';
|
||||
import { SqsRouter } from '@api/integrations/event/sqs/sqs.router';
|
||||
import { WebhookRouter } from '@api/integrations/event/webhook/webhook.router';
|
||||
import { WebsocketRouter } from '@api/integrations/event/websocket/websocket.router';
|
||||
import { Router } from 'express';
|
||||
|
||||
export class EventRouter {
|
||||
public readonly router: Router;
|
||||
|
||||
constructor(configService: any, ...guards: any[]) {
|
||||
this.router = Router();
|
||||
|
||||
this.router.use('/webhook', new WebhookRouter(configService, ...guards).router);
|
||||
this.router.use('/websocket', new WebsocketRouter(...guards).router);
|
||||
this.router.use('/rabbitmq', new RabbitmqRouter(...guards).router);
|
||||
this.router.use('/nats', new NatsRouter(...guards).router);
|
||||
this.router.use('/pusher', new PusherRouter(...guards).router);
|
||||
this.router.use('/sqs', new SqsRouter(...guards).router);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Validation Schema
|
||||
|
||||
### Event Configuration Validation
|
||||
```typescript
|
||||
import Joi from 'joi';
|
||||
import { EventController } from '@api/integrations/event/event.controller';
|
||||
|
||||
const eventListSchema = Joi.array().items(
|
||||
Joi.string().valid(...EventController.events)
|
||||
).optional();
|
||||
|
||||
export const webhookSchema = Joi.object({
|
||||
enabled: Joi.boolean().required(),
|
||||
url: Joi.string().when('enabled', {
|
||||
is: true,
|
||||
then: Joi.required().uri({ scheme: ['http', 'https'] }),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
events: eventListSchema,
|
||||
headers: Joi.object().pattern(Joi.string(), Joi.string()).optional(),
|
||||
byEvents: Joi.boolean().optional().default(false),
|
||||
base64: Joi.boolean().optional().default(false),
|
||||
}).required();
|
||||
|
||||
export const websocketSchema = Joi.object({
|
||||
enabled: Joi.boolean().required(),
|
||||
events: eventListSchema,
|
||||
}).required();
|
||||
|
||||
export const rabbitmqSchema = Joi.object({
|
||||
enabled: Joi.boolean().required(),
|
||||
events: eventListSchema,
|
||||
exchange: Joi.string().optional().default('evolution-events'),
|
||||
}).required();
|
||||
|
||||
export const sqsSchema = Joi.object({
|
||||
enabled: Joi.boolean().required(),
|
||||
events: eventListSchema,
|
||||
queueUrl: Joi.string().when('enabled', {
|
||||
is: true,
|
||||
then: Joi.required().uri(),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
}).required();
|
||||
|
||||
export const eventSchema = Joi.object({
|
||||
webhook: webhookSchema.optional(),
|
||||
websocket: websocketSchema.optional(),
|
||||
rabbitmq: rabbitmqSchema.optional(),
|
||||
sqs: sqsSchema.optional(),
|
||||
nats: Joi.object({
|
||||
enabled: Joi.boolean().required(),
|
||||
events: eventListSchema,
|
||||
subject: Joi.string().optional().default('evolution.events'),
|
||||
}).optional(),
|
||||
pusher: Joi.object({
|
||||
enabled: Joi.boolean().required(),
|
||||
appId: Joi.string().when('enabled', { is: true, then: Joi.required() }),
|
||||
key: Joi.string().when('enabled', { is: true, then: Joi.required() }),
|
||||
secret: Joi.string().when('enabled', { is: true, then: Joi.required() }),
|
||||
cluster: Joi.string().when('enabled', { is: true, then: Joi.required() }),
|
||||
useTLS: Joi.boolean().optional().default(true),
|
||||
events: eventListSchema,
|
||||
}).optional(),
|
||||
}).min(1).required();
|
||||
```
|
||||
|
||||
## Event Testing Pattern
|
||||
|
||||
### Event Controller Testing
|
||||
```typescript
|
||||
describe('WebhookController', () => {
|
||||
let controller: WebhookController;
|
||||
let prismaRepository: jest.Mocked<PrismaRepository>;
|
||||
let configService: jest.Mocked<ConfigService>;
|
||||
|
||||
beforeEach(() => {
|
||||
controller = new WebhookController(prismaRepository, configService);
|
||||
});
|
||||
|
||||
describe('emit', () => {
|
||||
it('should send webhook when enabled', async () => {
|
||||
const mockInstance = {
|
||||
webhook: {
|
||||
enabled: true,
|
||||
url: 'https://example.com/webhook',
|
||||
events: ['MESSAGES_UPSERT'],
|
||||
},
|
||||
};
|
||||
|
||||
prismaRepository.instance.findUnique.mockResolvedValue(mockInstance);
|
||||
jest.spyOn(axios, 'post').mockResolvedValue({ status: 200 });
|
||||
|
||||
const eventData = {
|
||||
instanceName: 'test-instance',
|
||||
event: 'MESSAGES_UPSERT',
|
||||
data: { message: 'test' },
|
||||
origin: 'test',
|
||||
serverUrl: 'http://localhost',
|
||||
dateTime: new Date().toISOString(),
|
||||
sender: 'test',
|
||||
};
|
||||
|
||||
await controller.emit(eventData);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'https://example.com/webhook',
|
||||
expect.objectContaining({
|
||||
event: 'MESSAGES_UPSERT',
|
||||
instance: 'test-instance',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
608
.cursor/rules/specialized-rules/integration-storage-rules.mdc
Normal file
608
.cursor/rules/specialized-rules/integration-storage-rules.mdc
Normal file
@ -0,0 +1,608 @@
|
||||
---
|
||||
description: Storage integration patterns for Evolution API
|
||||
globs:
|
||||
- "src/api/integrations/storage/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Storage Integration Rules
|
||||
|
||||
## Storage Service Pattern
|
||||
|
||||
### Base Storage Service Structure
|
||||
```typescript
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
|
||||
export class StorageService {
|
||||
constructor(private readonly prismaRepository: PrismaRepository) {}
|
||||
|
||||
private readonly logger = new Logger('StorageService');
|
||||
|
||||
public async getMedia(instance: InstanceDto, query?: MediaDto) {
|
||||
try {
|
||||
const where: any = {
|
||||
instanceId: instance.instanceId,
|
||||
...query,
|
||||
};
|
||||
|
||||
const media = await this.prismaRepository.media.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
fileName: true,
|
||||
type: true,
|
||||
mimetype: true,
|
||||
createdAt: true,
|
||||
Message: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!media || media.length === 0) {
|
||||
throw 'Media not found';
|
||||
}
|
||||
|
||||
return media;
|
||||
} catch (error) {
|
||||
throw new BadRequestException(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async getMediaUrl(instance: InstanceDto, data: MediaDto) {
|
||||
const media = (await this.getMedia(instance, { id: data.id }))[0];
|
||||
const mediaUrl = await this.generateUrl(media.fileName, data.expiry);
|
||||
return {
|
||||
mediaUrl,
|
||||
...media,
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract generateUrl(fileName: string, expiry?: number): Promise<string>;
|
||||
}
|
||||
```
|
||||
|
||||
## S3/MinIO Integration Pattern
|
||||
|
||||
### MinIO Client Setup
|
||||
```typescript
|
||||
import { ConfigService, S3 } from '@config/env.config';
|
||||
import { Logger } from '@config/logger.config';
|
||||
import { BadRequestException } from '@exceptions';
|
||||
import * as MinIo from 'minio';
|
||||
import { join } from 'path';
|
||||
import { Readable, Transform } from 'stream';
|
||||
|
||||
const logger = new Logger('S3 Service');
|
||||
const BUCKET = new ConfigService().get<S3>('S3');
|
||||
|
||||
interface Metadata extends MinIo.ItemBucketMetadata {
|
||||
instanceId: string;
|
||||
messageId?: string;
|
||||
}
|
||||
|
||||
const minioClient = (() => {
|
||||
if (BUCKET?.ENABLE) {
|
||||
return new MinIo.Client({
|
||||
endPoint: BUCKET.ENDPOINT,
|
||||
port: BUCKET.PORT,
|
||||
useSSL: BUCKET.USE_SSL,
|
||||
accessKey: BUCKET.ACCESS_KEY,
|
||||
secretKey: BUCKET.SECRET_KEY,
|
||||
region: BUCKET.REGION,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
const bucketName = process.env.S3_BUCKET;
|
||||
```
|
||||
|
||||
### Bucket Management Functions
|
||||
```typescript
|
||||
const bucketExists = async (): Promise<boolean> => {
|
||||
if (minioClient) {
|
||||
try {
|
||||
const list = await minioClient.listBuckets();
|
||||
return !!list.find((bucket) => bucket.name === bucketName);
|
||||
} catch (error) {
|
||||
logger.error('Error checking bucket existence:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const setBucketPolicy = async (): Promise<void> => {
|
||||
if (minioClient && bucketName) {
|
||||
try {
|
||||
const policy = {
|
||||
Version: '2012-10-17',
|
||||
Statement: [
|
||||
{
|
||||
Effect: 'Allow',
|
||||
Principal: { AWS: ['*'] },
|
||||
Action: ['s3:GetObject'],
|
||||
Resource: [`arn:aws:s3:::${bucketName}/*`],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await minioClient.setBucketPolicy(bucketName, JSON.stringify(policy));
|
||||
logger.log('Bucket policy set successfully');
|
||||
} catch (error) {
|
||||
logger.error('Error setting bucket policy:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createBucket = async (): Promise<void> => {
|
||||
if (minioClient && bucketName) {
|
||||
try {
|
||||
const exists = await bucketExists();
|
||||
if (!exists) {
|
||||
await minioClient.makeBucket(bucketName, BUCKET.REGION || 'us-east-1');
|
||||
await setBucketPolicy();
|
||||
logger.log(`Bucket ${bucketName} created successfully`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error creating bucket:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### File Upload Functions
|
||||
```typescript
|
||||
export const uploadFile = async (
|
||||
fileName: string,
|
||||
buffer: Buffer,
|
||||
mimetype: string,
|
||||
metadata?: Metadata,
|
||||
): Promise<string> => {
|
||||
if (!minioClient || !bucketName) {
|
||||
throw new BadRequestException('S3 storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
await createBucket();
|
||||
|
||||
const uploadMetadata = {
|
||||
'Content-Type': mimetype,
|
||||
...metadata,
|
||||
};
|
||||
|
||||
await minioClient.putObject(bucketName, fileName, buffer, buffer.length, uploadMetadata);
|
||||
|
||||
logger.log(`File ${fileName} uploaded successfully`);
|
||||
return fileName;
|
||||
} catch (error) {
|
||||
logger.error(`Error uploading file ${fileName}:`, error);
|
||||
throw new BadRequestException(`Failed to upload file: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const uploadStream = async (
|
||||
fileName: string,
|
||||
stream: Readable,
|
||||
size: number,
|
||||
mimetype: string,
|
||||
metadata?: Metadata,
|
||||
): Promise<string> => {
|
||||
if (!minioClient || !bucketName) {
|
||||
throw new BadRequestException('S3 storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
await createBucket();
|
||||
|
||||
const uploadMetadata = {
|
||||
'Content-Type': mimetype,
|
||||
...metadata,
|
||||
};
|
||||
|
||||
await minioClient.putObject(bucketName, fileName, stream, size, uploadMetadata);
|
||||
|
||||
logger.log(`Stream ${fileName} uploaded successfully`);
|
||||
return fileName;
|
||||
} catch (error) {
|
||||
logger.error(`Error uploading stream ${fileName}:`, error);
|
||||
throw new BadRequestException(`Failed to upload stream: ${error.message}`);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### File Download Functions
|
||||
```typescript
|
||||
export const getObject = async (fileName: string): Promise<Buffer> => {
|
||||
if (!minioClient || !bucketName) {
|
||||
throw new BadRequestException('S3 storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await minioClient.getObject(bucketName, fileName);
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', (chunk) => chunks.push(chunk));
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
stream.on('error', reject);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error getting object ${fileName}:`, error);
|
||||
throw new BadRequestException(`Failed to get object: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getObjectUrl = async (fileName: string, expiry: number = 3600): Promise<string> => {
|
||||
if (!minioClient || !bucketName) {
|
||||
throw new BadRequestException('S3 storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const url = await minioClient.presignedGetObject(bucketName, fileName, expiry);
|
||||
logger.log(`Generated URL for ${fileName} with expiry ${expiry}s`);
|
||||
return url;
|
||||
} catch (error) {
|
||||
logger.error(`Error generating URL for ${fileName}:`, error);
|
||||
throw new BadRequestException(`Failed to generate URL: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getObjectStream = async (fileName: string): Promise<Readable> => {
|
||||
if (!minioClient || !bucketName) {
|
||||
throw new BadRequestException('S3 storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = await minioClient.getObject(bucketName, fileName);
|
||||
return stream;
|
||||
} catch (error) {
|
||||
logger.error(`Error getting object stream ${fileName}:`, error);
|
||||
throw new BadRequestException(`Failed to get object stream: ${error.message}`);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### File Management Functions
|
||||
```typescript
|
||||
export const deleteObject = async (fileName: string): Promise<void> => {
|
||||
if (!minioClient || !bucketName) {
|
||||
throw new BadRequestException('S3 storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
await minioClient.removeObject(bucketName, fileName);
|
||||
logger.log(`File ${fileName} deleted successfully`);
|
||||
} catch (error) {
|
||||
logger.error(`Error deleting file ${fileName}:`, error);
|
||||
throw new BadRequestException(`Failed to delete file: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const listObjects = async (prefix?: string): Promise<MinIo.BucketItem[]> => {
|
||||
if (!minioClient || !bucketName) {
|
||||
throw new BadRequestException('S3 storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const objects: MinIo.BucketItem[] = [];
|
||||
const stream = minioClient.listObjects(bucketName, prefix, true);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.on('data', (obj) => objects.push(obj));
|
||||
stream.on('end', () => resolve(objects));
|
||||
stream.on('error', reject);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error listing objects:', error);
|
||||
throw new BadRequestException(`Failed to list objects: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const objectExists = async (fileName: string): Promise<boolean> => {
|
||||
if (!minioClient || !bucketName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await minioClient.statObject(bucketName, fileName);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Storage Controller Pattern
|
||||
|
||||
### S3 Controller Implementation
|
||||
```typescript
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import { MediaDto } from '@api/integrations/storage/s3/dto/media.dto';
|
||||
import { S3Service } from '@api/integrations/storage/s3/services/s3.service';
|
||||
|
||||
export class S3Controller {
|
||||
constructor(private readonly s3Service: S3Service) {}
|
||||
|
||||
public async getMedia(instance: InstanceDto, data: MediaDto) {
|
||||
return this.s3Service.getMedia(instance, data);
|
||||
}
|
||||
|
||||
public async getMediaUrl(instance: InstanceDto, data: MediaDto) {
|
||||
return this.s3Service.getMediaUrl(instance, data);
|
||||
}
|
||||
|
||||
public async uploadMedia(instance: InstanceDto, data: UploadMediaDto) {
|
||||
return this.s3Service.uploadMedia(instance, data);
|
||||
}
|
||||
|
||||
public async deleteMedia(instance: InstanceDto, data: MediaDto) {
|
||||
return this.s3Service.deleteMedia(instance, data);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Storage Router Pattern
|
||||
|
||||
### Storage Router Structure
|
||||
```typescript
|
||||
import { S3Router } from '@api/integrations/storage/s3/routes/s3.router';
|
||||
import { Router } from 'express';
|
||||
|
||||
export class StorageRouter {
|
||||
public readonly router: Router;
|
||||
|
||||
constructor(...guards: any[]) {
|
||||
this.router = Router();
|
||||
|
||||
this.router.use('/s3', new S3Router(...guards).router);
|
||||
// Add other storage providers here
|
||||
// this.router.use('/gcs', new GCSRouter(...guards).router);
|
||||
// this.router.use('/azure', new AzureRouter(...guards).router);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### S3 Specific Router
|
||||
```typescript
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { MediaDto } from '@api/integrations/storage/s3/dto/media.dto';
|
||||
import { s3Schema, s3UrlSchema } from '@api/integrations/storage/s3/validate/s3.schema';
|
||||
import { HttpStatus } from '@api/routes/index.router';
|
||||
import { s3Controller } from '@api/server.module';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
|
||||
export class S3Router extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.post(this.routerPath('getMedia'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<MediaDto>({
|
||||
request: req,
|
||||
schema: s3Schema,
|
||||
ClassRef: MediaDto,
|
||||
execute: (instance, data) => s3Controller.getMedia(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('getMediaUrl'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<MediaDto>({
|
||||
request: req,
|
||||
schema: s3UrlSchema,
|
||||
ClassRef: MediaDto,
|
||||
execute: (instance, data) => s3Controller.getMediaUrl(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('uploadMedia'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<UploadMediaDto>({
|
||||
request: req,
|
||||
schema: uploadSchema,
|
||||
ClassRef: UploadMediaDto,
|
||||
execute: (instance, data) => s3Controller.uploadMedia(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
.delete(this.routerPath('deleteMedia'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<MediaDto>({
|
||||
request: req,
|
||||
schema: s3Schema,
|
||||
ClassRef: MediaDto,
|
||||
execute: (instance, data) => s3Controller.deleteMedia(instance, data),
|
||||
});
|
||||
|
||||
res.status(HttpStatus.OK).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
```
|
||||
|
||||
## Storage DTO Pattern
|
||||
|
||||
### Media DTO
|
||||
```typescript
|
||||
export class MediaDto {
|
||||
id?: string;
|
||||
fileName?: string;
|
||||
type?: string;
|
||||
mimetype?: string;
|
||||
expiry?: number;
|
||||
}
|
||||
|
||||
export class UploadMediaDto {
|
||||
fileName: string;
|
||||
mimetype: string;
|
||||
buffer?: Buffer;
|
||||
base64?: string;
|
||||
url?: string;
|
||||
metadata?: {
|
||||
instanceId: string;
|
||||
messageId?: string;
|
||||
contactId?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Storage Validation Schema
|
||||
|
||||
### S3 Validation Schemas
|
||||
```typescript
|
||||
import Joi from 'joi';
|
||||
|
||||
export const s3Schema = Joi.object({
|
||||
id: Joi.string().optional(),
|
||||
fileName: Joi.string().optional(),
|
||||
type: Joi.string().optional().valid('image', 'video', 'audio', 'document'),
|
||||
mimetype: Joi.string().optional(),
|
||||
expiry: Joi.number().optional().min(60).max(604800).default(3600), // 1 min to 7 days
|
||||
}).min(1).required();
|
||||
|
||||
export const s3UrlSchema = Joi.object({
|
||||
id: Joi.string().required(),
|
||||
expiry: Joi.number().optional().min(60).max(604800).default(3600),
|
||||
}).required();
|
||||
|
||||
export const uploadSchema = Joi.object({
|
||||
fileName: Joi.string().required().max(255),
|
||||
mimetype: Joi.string().required(),
|
||||
buffer: Joi.binary().optional(),
|
||||
base64: Joi.string().base64().optional(),
|
||||
url: Joi.string().uri().optional(),
|
||||
metadata: Joi.object({
|
||||
instanceId: Joi.string().required(),
|
||||
messageId: Joi.string().optional(),
|
||||
contactId: Joi.string().optional(),
|
||||
}).optional(),
|
||||
}).xor('buffer', 'base64', 'url').required(); // Exactly one of these must be present
|
||||
```
|
||||
|
||||
## Error Handling in Storage
|
||||
|
||||
### Storage-Specific Error Handling
|
||||
```typescript
|
||||
// CORRECT - Storage-specific error handling
|
||||
public async uploadFile(fileName: string, buffer: Buffer): Promise<string> {
|
||||
try {
|
||||
const result = await this.storageClient.upload(fileName, buffer);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(`Storage upload failed: ${error.message}`);
|
||||
|
||||
if (error.code === 'NoSuchBucket') {
|
||||
throw new BadRequestException('Storage bucket not found');
|
||||
}
|
||||
|
||||
if (error.code === 'AccessDenied') {
|
||||
throw new UnauthorizedException('Storage access denied');
|
||||
}
|
||||
|
||||
if (error.code === 'EntityTooLarge') {
|
||||
throw new BadRequestException('File too large');
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException('Storage operation failed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Storage Configuration Pattern
|
||||
|
||||
### Environment Configuration
|
||||
```typescript
|
||||
export interface S3Config {
|
||||
ENABLE: boolean;
|
||||
ENDPOINT: string;
|
||||
PORT: number;
|
||||
USE_SSL: boolean;
|
||||
ACCESS_KEY: string;
|
||||
SECRET_KEY: string;
|
||||
REGION: string;
|
||||
BUCKET: string;
|
||||
}
|
||||
|
||||
// Usage in service
|
||||
const s3Config = this.configService.get<S3Config>('S3');
|
||||
if (!s3Config.ENABLE) {
|
||||
throw new BadRequestException('S3 storage is disabled');
|
||||
}
|
||||
```
|
||||
|
||||
## Storage Testing Pattern
|
||||
|
||||
### Storage Service Testing
|
||||
```typescript
|
||||
describe('S3Service', () => {
|
||||
let service: S3Service;
|
||||
let prismaRepository: jest.Mocked<PrismaRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new S3Service(prismaRepository);
|
||||
});
|
||||
|
||||
describe('getMedia', () => {
|
||||
it('should return media list', async () => {
|
||||
const instance = { instanceId: 'test-instance' };
|
||||
const mockMedia = [
|
||||
{ id: '1', fileName: 'test.jpg', type: 'image', mimetype: 'image/jpeg' },
|
||||
];
|
||||
|
||||
prismaRepository.media.findMany.mockResolvedValue(mockMedia);
|
||||
|
||||
const result = await service.getMedia(instance);
|
||||
|
||||
expect(result).toEqual(mockMedia);
|
||||
expect(prismaRepository.media.findMany).toHaveBeenCalledWith({
|
||||
where: { instanceId: 'test-instance' },
|
||||
select: expect.objectContaining({
|
||||
id: true,
|
||||
fileName: true,
|
||||
type: true,
|
||||
mimetype: true,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when no media found', async () => {
|
||||
const instance = { instanceId: 'test-instance' };
|
||||
prismaRepository.media.findMany.mockResolvedValue([]);
|
||||
|
||||
await expect(service.getMedia(instance)).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Storage Performance Considerations
|
||||
|
||||
### Efficient File Handling
|
||||
```typescript
|
||||
// CORRECT - Stream-based upload for large files
|
||||
public async uploadLargeFile(fileName: string, stream: Readable, size: number): Promise<string> {
|
||||
const uploadStream = new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
// Optional: Add compression, encryption, etc.
|
||||
callback(null, chunk);
|
||||
},
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
stream
|
||||
.pipe(uploadStream)
|
||||
.on('error', reject)
|
||||
.on('finish', () => resolve(fileName));
|
||||
});
|
||||
}
|
||||
|
||||
// INCORRECT - Loading entire file into memory
|
||||
public async uploadLargeFile(fileName: string, filePath: string): Promise<string> {
|
||||
const buffer = fs.readFileSync(filePath); // ❌ Memory intensive for large files
|
||||
return await this.uploadFile(fileName, buffer);
|
||||
}
|
||||
```
|
||||
416
.cursor/rules/specialized-rules/route-rules.mdc
Normal file
416
.cursor/rules/specialized-rules/route-rules.mdc
Normal file
@ -0,0 +1,416 @@
|
||||
---
|
||||
description: Router patterns for Evolution API
|
||||
globs:
|
||||
- "src/api/routes/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Route Rules
|
||||
|
||||
## Router Base Pattern
|
||||
|
||||
### RouterBroker Extension
|
||||
```typescript
|
||||
import { RouterBroker } from '@api/abstract/abstract.router';
|
||||
import { RequestHandler, Router } from 'express';
|
||||
import { HttpStatus } from './index.router';
|
||||
|
||||
export class ExampleRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.get(this.routerPath('findExample'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<ExampleDto>({
|
||||
request: req,
|
||||
schema: null,
|
||||
ClassRef: ExampleDto,
|
||||
execute: (instance) => exampleController.find(instance),
|
||||
});
|
||||
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
.post(this.routerPath('createExample'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<ExampleDto>({
|
||||
request: req,
|
||||
schema: exampleSchema,
|
||||
ClassRef: ExampleDto,
|
||||
execute: (instance, data) => exampleController.create(instance, data),
|
||||
});
|
||||
|
||||
return res.status(HttpStatus.CREATED).json(response);
|
||||
});
|
||||
}
|
||||
|
||||
public readonly router: Router = Router();
|
||||
}
|
||||
```
|
||||
|
||||
## Main Router Pattern
|
||||
|
||||
### Index Router Structure
|
||||
```typescript
|
||||
import { Router } from 'express';
|
||||
import { authGuard } from '@api/guards/auth.guard';
|
||||
import { instanceExistsGuard, instanceLoggedGuard } from '@api/guards/instance.guard';
|
||||
import Telemetry from '@api/guards/telemetry.guard';
|
||||
|
||||
enum HttpStatus {
|
||||
OK = 200,
|
||||
CREATED = 201,
|
||||
NOT_FOUND = 404,
|
||||
FORBIDDEN = 403,
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED = 401,
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
}
|
||||
|
||||
const router: Router = Router();
|
||||
const guards = [instanceExistsGuard, instanceLoggedGuard, authGuard['apikey']];
|
||||
const telemetry = new Telemetry();
|
||||
|
||||
router
|
||||
.use((req, res, next) => telemetry.collectTelemetry(req, res, next))
|
||||
.get('/', async (req, res) => {
|
||||
res.status(HttpStatus.OK).json({
|
||||
status: HttpStatus.OK,
|
||||
message: 'Welcome to the Evolution API, it is working!',
|
||||
version: packageJson.version,
|
||||
clientName: process.env.DATABASE_CONNECTION_CLIENT_NAME,
|
||||
manager: !serverConfig.DISABLE_MANAGER ? `${req.protocol}://${req.get('host')}/manager` : undefined,
|
||||
documentation: `https://doc.evolution-api.com`,
|
||||
whatsappWebVersion: (await fetchLatestWaWebVersion({})).version.join('.'),
|
||||
});
|
||||
})
|
||||
.use('/instance', new InstanceRouter(configService, ...guards).router)
|
||||
.use('/message', new MessageRouter(...guards).router)
|
||||
.use('/chat', new ChatRouter(...guards).router)
|
||||
.use('/business', new BusinessRouter(...guards).router);
|
||||
|
||||
export { HttpStatus, router };
|
||||
```
|
||||
|
||||
## Data Validation Pattern
|
||||
|
||||
### RouterBroker dataValidate Usage
|
||||
```typescript
|
||||
// CORRECT - Standard validation pattern
|
||||
.post(this.routerPath('createTemplate'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<TemplateDto>({
|
||||
request: req,
|
||||
schema: templateSchema,
|
||||
ClassRef: TemplateDto,
|
||||
execute: (instance, data) => templateController.create(instance, data),
|
||||
});
|
||||
|
||||
return res.status(HttpStatus.CREATED).json(response);
|
||||
})
|
||||
|
||||
// CORRECT - No schema validation (for simple DTOs)
|
||||
.get(this.routerPath('findTemplate'), ...guards, async (req, res) => {
|
||||
const response = await this.dataValidate<InstanceDto>({
|
||||
request: req,
|
||||
schema: null,
|
||||
ClassRef: InstanceDto,
|
||||
execute: (instance) => templateController.find(instance),
|
||||
});
|
||||
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
```
|
||||
|
||||
## Error Handling in Routes
|
||||
|
||||
### Try-Catch Pattern
|
||||
```typescript
|
||||
// CORRECT - Error handling with utility function
|
||||
.post(this.routerPath('getCatalog'), ...guards, async (req, res) => {
|
||||
try {
|
||||
const response = await this.dataValidate<NumberDto>({
|
||||
request: req,
|
||||
schema: catalogSchema,
|
||||
ClassRef: NumberDto,
|
||||
execute: (instance, data) => businessController.fetchCatalog(instance, data),
|
||||
});
|
||||
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
} catch (error) {
|
||||
// Log error for debugging
|
||||
console.error('Business catalog error:', error);
|
||||
|
||||
// Use utility function to create standardized error response
|
||||
const errorResponse = createMetaErrorResponse(error, 'business_catalog');
|
||||
return res.status(errorResponse.status).json(errorResponse);
|
||||
}
|
||||
})
|
||||
|
||||
// INCORRECT - Let RouterBroker handle errors (when possible)
|
||||
.post(this.routerPath('simpleOperation'), ...guards, async (req, res) => {
|
||||
try {
|
||||
const response = await this.dataValidate<SimpleDto>({
|
||||
request: req,
|
||||
schema: simpleSchema,
|
||||
ClassRef: SimpleDto,
|
||||
execute: (instance, data) => controller.simpleOperation(instance, data),
|
||||
});
|
||||
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
} catch (error) {
|
||||
throw error; // ❌ Unnecessary - RouterBroker handles this
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Route Path Pattern
|
||||
|
||||
### routerPath Usage
|
||||
```typescript
|
||||
// CORRECT - Use routerPath for consistent naming
|
||||
.get(this.routerPath('findLabels'), ...guards, async (req, res) => {
|
||||
// Implementation
|
||||
})
|
||||
.post(this.routerPath('handleLabel'), ...guards, async (req, res) => {
|
||||
// Implementation
|
||||
})
|
||||
|
||||
// INCORRECT - Hardcoded paths
|
||||
.get('/labels', ...guards, async (req, res) => { // ❌ Use routerPath
|
||||
// Implementation
|
||||
})
|
||||
```
|
||||
|
||||
## Guard Application Pattern
|
||||
|
||||
### Guards Usage
|
||||
```typescript
|
||||
// CORRECT - Apply guards to protected routes
|
||||
export class ProtectedRouter extends RouterBroker {
|
||||
constructor(...guards: RequestHandler[]) {
|
||||
super();
|
||||
this.router
|
||||
.get(this.routerPath('protectedAction'), ...guards, async (req, res) => {
|
||||
// Protected action
|
||||
})
|
||||
.post(this.routerPath('anotherAction'), ...guards, async (req, res) => {
|
||||
// Another protected action
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// CORRECT - No guards for public routes
|
||||
export class PublicRouter extends RouterBroker {
|
||||
constructor() {
|
||||
super();
|
||||
this.router
|
||||
.get('/health', async (req, res) => {
|
||||
res.status(HttpStatus.OK).json({ status: 'healthy' });
|
||||
})
|
||||
.get('/version', async (req, res) => {
|
||||
res.status(HttpStatus.OK).json({ version: packageJson.version });
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Static File Serving Pattern
|
||||
|
||||
### Static Assets Route
|
||||
```typescript
|
||||
// CORRECT - Secure static file serving
|
||||
router.get('/assets/*', (req, res) => {
|
||||
const fileName = req.params[0];
|
||||
|
||||
// Security: Reject paths containing traversal patterns
|
||||
if (!fileName || fileName.includes('..') || fileName.includes('\\') || path.isAbsolute(fileName)) {
|
||||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
|
||||
const basePath = path.join(process.cwd(), 'manager', 'dist');
|
||||
const assetsPath = path.join(basePath, 'assets');
|
||||
const filePath = path.join(assetsPath, fileName);
|
||||
|
||||
// Security: Ensure the resolved path is within the assets directory
|
||||
const resolvedPath = path.resolve(filePath);
|
||||
const resolvedAssetsPath = path.resolve(assetsPath);
|
||||
|
||||
if (!resolvedPath.startsWith(resolvedAssetsPath + path.sep) && resolvedPath !== resolvedAssetsPath) {
|
||||
return res.status(403).send('Forbidden');
|
||||
}
|
||||
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
res.set('Content-Type', mimeTypes.lookup(resolvedPath) || 'text/css');
|
||||
res.send(fs.readFileSync(resolvedPath));
|
||||
} else {
|
||||
res.status(404).send('File not found');
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Special Route Patterns
|
||||
|
||||
### Manager Route Pattern
|
||||
```typescript
|
||||
export class ViewsRouter extends RouterBroker {
|
||||
public readonly router: Router;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.router = Router();
|
||||
|
||||
const basePath = path.join(process.cwd(), 'manager', 'dist');
|
||||
const indexPath = path.join(basePath, 'index.html');
|
||||
|
||||
this.router.use(express.static(basePath));
|
||||
|
||||
this.router.get('*', (req, res) => {
|
||||
res.sendFile(indexPath);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook Route Pattern
|
||||
```typescript
|
||||
// CORRECT - Webhook without guards
|
||||
.post('/webhook/evolution', async (req, res) => {
|
||||
const response = await evolutionController.receiveWebhook(req.body);
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
|
||||
// CORRECT - Webhook with signature validation
|
||||
.post('/webhook/meta', validateWebhookSignature, async (req, res) => {
|
||||
const response = await metaController.receiveWebhook(req.body);
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
})
|
||||
```
|
||||
|
||||
## Response Pattern
|
||||
|
||||
### Standard Response Format
|
||||
```typescript
|
||||
// CORRECT - Standard success response
|
||||
return res.status(HttpStatus.OK).json(response);
|
||||
|
||||
// CORRECT - Created response
|
||||
return res.status(HttpStatus.CREATED).json(response);
|
||||
|
||||
// CORRECT - Custom response with additional data
|
||||
return res.status(HttpStatus.OK).json({
|
||||
...response,
|
||||
timestamp: new Date().toISOString(),
|
||||
instanceName: req.params.instanceName,
|
||||
});
|
||||
```
|
||||
|
||||
## Route Organization
|
||||
|
||||
### File Structure
|
||||
```
|
||||
src/api/routes/
|
||||
├── index.router.ts # Main router with all route registrations
|
||||
├── instance.router.ts # Instance management routes
|
||||
├── sendMessage.router.ts # Message sending routes
|
||||
├── chat.router.ts # Chat operations routes
|
||||
├── business.router.ts # Business API routes
|
||||
├── group.router.ts # Group management routes
|
||||
├── label.router.ts # Label management routes
|
||||
├── proxy.router.ts # Proxy configuration routes
|
||||
├── settings.router.ts # Instance settings routes
|
||||
├── template.router.ts # Template management routes
|
||||
├── call.router.ts # Call operations routes
|
||||
└── view.router.ts # Frontend views routes
|
||||
```
|
||||
|
||||
## Route Testing Pattern
|
||||
|
||||
### Router Testing
|
||||
```typescript
|
||||
describe('ExampleRouter', () => {
|
||||
let app: express.Application;
|
||||
let router: ExampleRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
app = express();
|
||||
router = new ExampleRouter();
|
||||
app.use('/api', router.router);
|
||||
app.use(express.json());
|
||||
});
|
||||
|
||||
describe('GET /findExample', () => {
|
||||
it('should return example data', async () => {
|
||||
const response = await request(app)
|
||||
.get('/api/findExample/test-instance')
|
||||
.set('apikey', 'test-key')
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toBeDefined();
|
||||
expect(response.body.instanceName).toBe('test-instance');
|
||||
});
|
||||
|
||||
it('should return 401 without API key', async () => {
|
||||
await request(app)
|
||||
.get('/api/findExample/test-instance')
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /createExample', () => {
|
||||
it('should create example successfully', async () => {
|
||||
const data = {
|
||||
name: 'Test Example',
|
||||
description: 'Test Description',
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/createExample/test-instance')
|
||||
.set('apikey', 'test-key')
|
||||
.send(data)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.name).toBe(data.name);
|
||||
});
|
||||
|
||||
it('should validate required fields', async () => {
|
||||
const data = {
|
||||
description: 'Test Description',
|
||||
// Missing required 'name' field
|
||||
};
|
||||
|
||||
await request(app)
|
||||
.post('/api/createExample/test-instance')
|
||||
.set('apikey', 'test-key')
|
||||
.send(data)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Route Documentation
|
||||
|
||||
### JSDoc for Routes
|
||||
```typescript
|
||||
/**
|
||||
* @route GET /api/template/findTemplate/:instanceName
|
||||
* @description Find template for instance
|
||||
* @param {string} instanceName - Instance name
|
||||
* @returns {TemplateDto} Template data
|
||||
* @throws {404} Template not found
|
||||
* @throws {401} Unauthorized
|
||||
*/
|
||||
.get(this.routerPath('findTemplate'), ...guards, async (req, res) => {
|
||||
// Implementation
|
||||
})
|
||||
|
||||
/**
|
||||
* @route POST /api/template/createTemplate/:instanceName
|
||||
* @description Create new template
|
||||
* @param {string} instanceName - Instance name
|
||||
* @body {TemplateDto} Template data
|
||||
* @returns {TemplateDto} Created template
|
||||
* @throws {400} Validation error
|
||||
* @throws {401} Unauthorized
|
||||
*/
|
||||
.post(this.routerPath('createTemplate'), ...guards, async (req, res) => {
|
||||
// Implementation
|
||||
})
|
||||
```
|
||||
294
.cursor/rules/specialized-rules/service-rules.mdc
Normal file
294
.cursor/rules/specialized-rules/service-rules.mdc
Normal file
@ -0,0 +1,294 @@
|
||||
---
|
||||
description: Service layer patterns for Evolution API
|
||||
globs:
|
||||
- "src/api/services/**/*.ts"
|
||||
- "src/api/integrations/**/services/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Service Rules
|
||||
|
||||
## Service Structure Pattern
|
||||
|
||||
### Standard Service Class
|
||||
```typescript
|
||||
export class ExampleService {
|
||||
constructor(private readonly waMonitor: WAMonitoringService) {}
|
||||
|
||||
private readonly logger = new Logger('ExampleService');
|
||||
|
||||
public async create(instance: InstanceDto, data: ExampleDto) {
|
||||
await this.waMonitor.waInstances[instance.instanceName].setData(data);
|
||||
return { example: { ...instance, data } };
|
||||
}
|
||||
|
||||
public async find(instance: InstanceDto): Promise<ExampleDto> {
|
||||
try {
|
||||
const result = await this.waMonitor.waInstances[instance.instanceName].findData();
|
||||
|
||||
if (Object.keys(result).length === 0) {
|
||||
throw new Error('Data not found');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return null; // Evolution pattern - return null on error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Injection Pattern
|
||||
|
||||
### Constructor Pattern
|
||||
```typescript
|
||||
// CORRECT - Evolution API pattern
|
||||
constructor(
|
||||
private readonly waMonitor: WAMonitoringService,
|
||||
private readonly prismaRepository: PrismaRepository,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
// INCORRECT - Don't use
|
||||
constructor(waMonitor, prismaRepository, configService) {} // ❌ No types
|
||||
```
|
||||
|
||||
## Logger Pattern
|
||||
|
||||
### Standard Logger Usage
|
||||
```typescript
|
||||
// CORRECT - Evolution API pattern
|
||||
private readonly logger = new Logger('ServiceName');
|
||||
|
||||
// Usage
|
||||
this.logger.log('Operation started');
|
||||
this.logger.error('Operation failed', error);
|
||||
|
||||
// INCORRECT
|
||||
console.log('Operation started'); // ❌ Use Logger
|
||||
```
|
||||
|
||||
## WAMonitor Integration Pattern
|
||||
|
||||
### Instance Access Pattern
|
||||
```typescript
|
||||
// CORRECT - Standard pattern
|
||||
public async operation(instance: InstanceDto, data: DataDto) {
|
||||
await this.waMonitor.waInstances[instance.instanceName].performAction(data);
|
||||
return { result: { ...instance, data } };
|
||||
}
|
||||
|
||||
// Instance validation
|
||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||
if (!waInstance) {
|
||||
throw new NotFoundException('Instance not found');
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Pattern
|
||||
|
||||
### Try-Catch Pattern
|
||||
```typescript
|
||||
// CORRECT - Evolution API pattern
|
||||
public async find(instance: InstanceDto): Promise<DataDto> {
|
||||
try {
|
||||
const result = await this.waMonitor.waInstances[instance.instanceName].findData();
|
||||
|
||||
if (Object.keys(result).length === 0) {
|
||||
throw new Error('Data not found');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error('Find operation failed', error);
|
||||
return null; // Return null on error (Evolution pattern)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cache Integration Pattern
|
||||
|
||||
### Cache Service Usage
|
||||
```typescript
|
||||
export class CacheAwareService {
|
||||
constructor(
|
||||
private readonly cache: CacheService,
|
||||
private readonly chatwootCache: CacheService,
|
||||
private readonly baileysCache: CacheService,
|
||||
) {}
|
||||
|
||||
public async getCachedData(key: string): Promise<any> {
|
||||
const cached = await this.cache.get(key);
|
||||
if (cached) return cached;
|
||||
|
||||
const data = await this.fetchFromSource(key);
|
||||
await this.cache.set(key, data, 300); // 5 min TTL
|
||||
return data;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Service Patterns
|
||||
|
||||
### Chatbot Service Base Pattern
|
||||
```typescript
|
||||
export class ChatbotService extends BaseChatbotService<BotType, SettingsType> {
|
||||
constructor(
|
||||
waMonitor: WAMonitoringService,
|
||||
prismaRepository: PrismaRepository,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(waMonitor, prismaRepository, 'ChatbotService', configService);
|
||||
}
|
||||
|
||||
protected async processBot(
|
||||
waInstance: any,
|
||||
remoteJid: string,
|
||||
bot: BotType,
|
||||
session: any,
|
||||
settings: any,
|
||||
content: string,
|
||||
): Promise<void> {
|
||||
// Implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Channel Service Pattern
|
||||
```typescript
|
||||
export class ChannelService extends ChannelStartupService {
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
eventEmitter: EventEmitter2,
|
||||
prismaRepository: PrismaRepository,
|
||||
cache: CacheService,
|
||||
chatwootCache: CacheService,
|
||||
baileysCache: CacheService,
|
||||
) {
|
||||
super(configService, eventEmitter, prismaRepository, cache, chatwootCache, baileysCache);
|
||||
}
|
||||
|
||||
public readonly logger = new Logger('ChannelService');
|
||||
public client: WASocket;
|
||||
public readonly instance: wa.Instance = {};
|
||||
}
|
||||
```
|
||||
|
||||
## Service Initialization Pattern
|
||||
|
||||
### Service Registration
|
||||
```typescript
|
||||
// In server.module.ts pattern
|
||||
export const templateService = new TemplateService(
|
||||
waMonitor,
|
||||
prismaRepository,
|
||||
configService,
|
||||
);
|
||||
|
||||
export const settingsService = new SettingsService(waMonitor);
|
||||
```
|
||||
|
||||
## Async Operation Patterns
|
||||
|
||||
### Promise Handling
|
||||
```typescript
|
||||
// CORRECT - Evolution API pattern
|
||||
public async sendMessage(instance: InstanceDto, data: MessageDto) {
|
||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||
return await waInstance.sendMessage(data);
|
||||
}
|
||||
|
||||
// INCORRECT - Don't use .then()
|
||||
public sendMessage(instance: InstanceDto, data: MessageDto) {
|
||||
return this.waMonitor.waInstances[instance.instanceName]
|
||||
.sendMessage(data)
|
||||
.then(result => result); // ❌ Use async/await
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Access Pattern
|
||||
|
||||
### Config Service Usage
|
||||
```typescript
|
||||
// CORRECT - Evolution API pattern
|
||||
const serverConfig = this.configService.get<HttpServer>('SERVER');
|
||||
const authConfig = this.configService.get<Auth>('AUTHENTICATION');
|
||||
const dbConfig = this.configService.get<Database>('DATABASE');
|
||||
|
||||
// Type-safe configuration access
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||
// Chatwoot logic
|
||||
}
|
||||
```
|
||||
|
||||
## Event Emission Pattern
|
||||
|
||||
### EventEmitter2 Usage
|
||||
```typescript
|
||||
// CORRECT - Evolution API pattern
|
||||
this.eventEmitter.emit(Events.INSTANCE_CREATE, {
|
||||
instanceName: instance.name,
|
||||
status: 'created',
|
||||
});
|
||||
|
||||
// Chatwoot event pattern
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
this.chatwootService.eventWhatsapp(
|
||||
Events.STATUS_INSTANCE,
|
||||
{ instanceName: this.instance.name },
|
||||
{
|
||||
instance: this.instance.name,
|
||||
status: 'created',
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Service Method Naming
|
||||
|
||||
### Standard Method Names
|
||||
- `create()` - Create new resource
|
||||
- `find()` - Find single resource
|
||||
- `findAll()` - Find multiple resources
|
||||
- `update()` - Update resource
|
||||
- `delete()` - Delete resource
|
||||
- `fetch*()` - Fetch from external API
|
||||
- `send*()` - Send data/messages
|
||||
- `process*()` - Process data
|
||||
|
||||
## Service Testing Pattern
|
||||
|
||||
### Unit Test Structure
|
||||
```typescript
|
||||
describe('ExampleService', () => {
|
||||
let service: ExampleService;
|
||||
let waMonitor: jest.Mocked<WAMonitoringService>;
|
||||
let prismaRepository: jest.Mocked<PrismaRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockWaMonitor = {
|
||||
waInstances: {
|
||||
'test-instance': {
|
||||
performAction: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
service = new ExampleService(
|
||||
mockWaMonitor as any,
|
||||
prismaRepository,
|
||||
configService,
|
||||
);
|
||||
});
|
||||
|
||||
it('should perform action successfully', async () => {
|
||||
const instance = { instanceName: 'test-instance' };
|
||||
const data = { test: 'data' };
|
||||
|
||||
const result = await service.create(instance, data);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(waMonitor.waInstances['test-instance'].performAction).toHaveBeenCalledWith(data);
|
||||
});
|
||||
});
|
||||
```
|
||||
490
.cursor/rules/specialized-rules/type-rules.mdc
Normal file
490
.cursor/rules/specialized-rules/type-rules.mdc
Normal file
@ -0,0 +1,490 @@
|
||||
---
|
||||
description: Type definitions and interfaces for Evolution API
|
||||
globs:
|
||||
- "src/api/types/**/*.ts"
|
||||
- "src/@types/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Type Rules
|
||||
|
||||
## Namespace Pattern
|
||||
|
||||
### WhatsApp Types Namespace
|
||||
```typescript
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
import { JsonValue } from '@prisma/client/runtime/library';
|
||||
import { AuthenticationState, WAConnectionState } from 'baileys';
|
||||
|
||||
export declare namespace wa {
|
||||
export type QrCode = {
|
||||
count?: number;
|
||||
pairingCode?: string;
|
||||
base64?: string;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
export type Instance = {
|
||||
id?: string;
|
||||
qrcode?: QrCode;
|
||||
pairingCode?: string;
|
||||
authState?: { state: AuthenticationState; saveCreds: () => void };
|
||||
name?: string;
|
||||
wuid?: string;
|
||||
profileName?: string;
|
||||
profilePictureUrl?: string;
|
||||
token?: string;
|
||||
number?: string;
|
||||
integration?: string;
|
||||
businessId?: string;
|
||||
};
|
||||
|
||||
export type LocalChatwoot = {
|
||||
enabled?: boolean;
|
||||
accountId?: string;
|
||||
token?: string;
|
||||
url?: string;
|
||||
nameInbox?: string;
|
||||
mergeBrazilContacts?: boolean;
|
||||
importContacts?: boolean;
|
||||
importMessages?: boolean;
|
||||
daysLimitImportMessages?: number;
|
||||
organization?: string;
|
||||
logo?: string;
|
||||
};
|
||||
|
||||
export type LocalProxy = {
|
||||
enabled?: boolean;
|
||||
host?: string;
|
||||
port?: string;
|
||||
protocol?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export type LocalSettings = {
|
||||
rejectCall?: boolean;
|
||||
msgCall?: string;
|
||||
groupsIgnore?: boolean;
|
||||
alwaysOnline?: boolean;
|
||||
readMessages?: boolean;
|
||||
readStatus?: boolean;
|
||||
syncFullHistory?: boolean;
|
||||
};
|
||||
|
||||
export type LocalWebHook = {
|
||||
enabled?: boolean;
|
||||
url?: string;
|
||||
events?: string[];
|
||||
headers?: JsonValue;
|
||||
byEvents?: boolean;
|
||||
base64?: boolean;
|
||||
};
|
||||
|
||||
export type StatusMessage = 'ERROR' | 'PENDING' | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' | 'DELETED' | 'PLAYED';
|
||||
}
|
||||
```
|
||||
|
||||
## Enum Definitions
|
||||
|
||||
### Events Enum
|
||||
```typescript
|
||||
export enum Events {
|
||||
APPLICATION_STARTUP = 'application.startup',
|
||||
INSTANCE_CREATE = 'instance.create',
|
||||
INSTANCE_DELETE = 'instance.delete',
|
||||
QRCODE_UPDATED = 'qrcode.updated',
|
||||
CONNECTION_UPDATE = 'connection.update',
|
||||
STATUS_INSTANCE = 'status.instance',
|
||||
MESSAGES_SET = 'messages.set',
|
||||
MESSAGES_UPSERT = 'messages.upsert',
|
||||
MESSAGES_EDITED = 'messages.edited',
|
||||
MESSAGES_UPDATE = 'messages.update',
|
||||
MESSAGES_DELETE = 'messages.delete',
|
||||
SEND_MESSAGE = 'send.message',
|
||||
SEND_MESSAGE_UPDATE = 'send.message.update',
|
||||
CONTACTS_SET = 'contacts.set',
|
||||
CONTACTS_UPSERT = 'contacts.upsert',
|
||||
CONTACTS_UPDATE = 'contacts.update',
|
||||
PRESENCE_UPDATE = 'presence.update',
|
||||
CHATS_SET = 'chats.set',
|
||||
CHATS_UPDATE = 'chats.update',
|
||||
CHATS_UPSERT = 'chats.upsert',
|
||||
CHATS_DELETE = 'chats.delete',
|
||||
GROUPS_UPSERT = 'groups.upsert',
|
||||
GROUPS_UPDATE = 'groups.update',
|
||||
GROUP_PARTICIPANTS_UPDATE = 'group-participants.update',
|
||||
CALL = 'call',
|
||||
TYPEBOT_START = 'typebot.start',
|
||||
TYPEBOT_CHANGE_STATUS = 'typebot.change-status',
|
||||
LABELS_EDIT = 'labels.edit',
|
||||
LABELS_ASSOCIATION = 'labels.association',
|
||||
CREDS_UPDATE = 'creds.update',
|
||||
MESSAGING_HISTORY_SET = 'messaging-history.set',
|
||||
REMOVE_INSTANCE = 'remove.instance',
|
||||
LOGOUT_INSTANCE = 'logout.instance',
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Types
|
||||
```typescript
|
||||
export const Integration = {
|
||||
WHATSAPP_BUSINESS: 'WHATSAPP-BUSINESS',
|
||||
WHATSAPP_BAILEYS: 'WHATSAPP-BAILEYS',
|
||||
EVOLUTION: 'EVOLUTION',
|
||||
} as const;
|
||||
|
||||
export type IntegrationType = typeof Integration[keyof typeof Integration];
|
||||
```
|
||||
|
||||
## Constant Arrays
|
||||
|
||||
### Message Type Constants
|
||||
```typescript
|
||||
export const TypeMediaMessage = [
|
||||
'imageMessage',
|
||||
'documentMessage',
|
||||
'audioMessage',
|
||||
'videoMessage',
|
||||
'stickerMessage',
|
||||
'ptvMessage', // Evolution API includes this
|
||||
];
|
||||
|
||||
export const MessageSubtype = [
|
||||
'ephemeralMessage',
|
||||
'documentWithCaptionMessage',
|
||||
'viewOnceMessage',
|
||||
'viewOnceMessageV2',
|
||||
];
|
||||
|
||||
export type MediaMessageType = typeof TypeMediaMessage[number];
|
||||
export type MessageSubtypeType = typeof MessageSubtype[number];
|
||||
```
|
||||
|
||||
## Interface Definitions
|
||||
|
||||
### Service Interfaces
|
||||
```typescript
|
||||
export interface ServiceInterface {
|
||||
create(instance: InstanceDto, data: any): Promise<any>;
|
||||
find(instance: InstanceDto): Promise<any>;
|
||||
update?(instance: InstanceDto, data: any): Promise<any>;
|
||||
delete?(instance: InstanceDto): Promise<any>;
|
||||
}
|
||||
|
||||
export interface ChannelServiceInterface extends ServiceInterface {
|
||||
sendMessage(data: SendMessageDto): Promise<any>;
|
||||
connectToWhatsapp(data?: any): Promise<void>;
|
||||
receiveWebhook?(data: any): Promise<void>;
|
||||
}
|
||||
|
||||
export interface ChatbotServiceInterface extends ServiceInterface {
|
||||
processMessage(
|
||||
instanceName: string,
|
||||
remoteJid: string,
|
||||
message: any,
|
||||
pushName?: string,
|
||||
): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Types
|
||||
|
||||
### Environment Configuration Types
|
||||
```typescript
|
||||
export interface DatabaseConfig {
|
||||
CONNECTION: {
|
||||
URI: string;
|
||||
DB_PREFIX_NAME: string;
|
||||
CLIENT_NAME?: string;
|
||||
};
|
||||
ENABLED: boolean;
|
||||
SAVE_DATA: {
|
||||
INSTANCE: boolean;
|
||||
NEW_MESSAGE: boolean;
|
||||
MESSAGE_UPDATE: boolean;
|
||||
CONTACTS: boolean;
|
||||
CHATS: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AuthConfig {
|
||||
TYPE: 'apikey' | 'jwt';
|
||||
API_KEY: {
|
||||
KEY: string;
|
||||
};
|
||||
JWT?: {
|
||||
EXPIRIN_IN: number;
|
||||
SECRET: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CacheConfig {
|
||||
REDIS: {
|
||||
ENABLED: boolean;
|
||||
URI: string;
|
||||
PREFIX_KEY: string;
|
||||
SAVE_INSTANCES: boolean;
|
||||
};
|
||||
LOCAL: {
|
||||
ENABLED: boolean;
|
||||
TTL: number;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Message Types
|
||||
|
||||
### Message Structure Types
|
||||
```typescript
|
||||
export interface MessageContent {
|
||||
text?: string;
|
||||
caption?: string;
|
||||
media?: Buffer | string;
|
||||
mediatype?: 'image' | 'video' | 'audio' | 'document' | 'sticker';
|
||||
fileName?: string;
|
||||
mimetype?: string;
|
||||
}
|
||||
|
||||
export interface MessageOptions {
|
||||
delay?: number;
|
||||
presence?: 'unavailable' | 'available' | 'composing' | 'recording' | 'paused';
|
||||
linkPreview?: boolean;
|
||||
mentionsEveryOne?: boolean;
|
||||
mentioned?: string[];
|
||||
quoted?: {
|
||||
key: {
|
||||
remoteJid: string;
|
||||
fromMe: boolean;
|
||||
id: string;
|
||||
};
|
||||
message: any;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SendMessageRequest {
|
||||
number: string;
|
||||
content: MessageContent;
|
||||
options?: MessageOptions;
|
||||
}
|
||||
```
|
||||
|
||||
## Webhook Types
|
||||
|
||||
### Webhook Payload Types
|
||||
```typescript
|
||||
export interface WebhookPayload {
|
||||
event: Events;
|
||||
instance: string;
|
||||
data: any;
|
||||
timestamp: string;
|
||||
server?: {
|
||||
version: string;
|
||||
host: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WebhookConfig {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
events: Events[];
|
||||
headers?: Record<string, string>;
|
||||
byEvents?: boolean;
|
||||
base64?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Error Types
|
||||
|
||||
### Custom Error Types
|
||||
```typescript
|
||||
export interface ApiError {
|
||||
status: number;
|
||||
message: string;
|
||||
error?: string;
|
||||
details?: any;
|
||||
timestamp: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ValidationError extends ApiError {
|
||||
status: 400;
|
||||
validationErrors: Array<{
|
||||
field: string;
|
||||
message: string;
|
||||
value?: any;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AuthenticationError extends ApiError {
|
||||
status: 401;
|
||||
message: 'Unauthorized' | 'Invalid API Key' | 'Token Expired';
|
||||
}
|
||||
```
|
||||
|
||||
## Utility Types
|
||||
|
||||
### Generic Utility Types
|
||||
```typescript
|
||||
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
||||
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||
};
|
||||
|
||||
export type NonEmptyArray<T> = [T, ...T[]];
|
||||
|
||||
export type StringKeys<T> = {
|
||||
[K in keyof T]: T[K] extends string ? K : never;
|
||||
}[keyof T];
|
||||
```
|
||||
|
||||
## Response Types
|
||||
|
||||
### API Response Types
|
||||
```typescript
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InstanceResponse extends ApiResponse {
|
||||
instance: {
|
||||
instanceName: string;
|
||||
status: 'connecting' | 'open' | 'close' | 'qr';
|
||||
qrcode?: string;
|
||||
profileName?: string;
|
||||
profilePicUrl?: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Integration-Specific Types
|
||||
|
||||
### Baileys Types Extension
|
||||
```typescript
|
||||
import { WASocket, ConnectionState, DisconnectReason } from 'baileys';
|
||||
|
||||
export interface BaileysInstance {
|
||||
client: WASocket;
|
||||
state: ConnectionState;
|
||||
qrRetry: number;
|
||||
authPath: string;
|
||||
}
|
||||
|
||||
export interface BaileysConfig {
|
||||
qrTimeout: number;
|
||||
maxQrRetries: number;
|
||||
authTimeout: number;
|
||||
reconnectInterval: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Business API Types
|
||||
```typescript
|
||||
export interface BusinessApiConfig {
|
||||
version: string;
|
||||
baseUrl: string;
|
||||
timeout: number;
|
||||
retries: number;
|
||||
}
|
||||
|
||||
export interface BusinessApiMessage {
|
||||
messaging_product: 'whatsapp';
|
||||
to: string;
|
||||
type: 'text' | 'image' | 'document' | 'audio' | 'video' | 'template';
|
||||
text?: {
|
||||
body: string;
|
||||
preview_url?: boolean;
|
||||
};
|
||||
image?: {
|
||||
link?: string;
|
||||
id?: string;
|
||||
caption?: string;
|
||||
};
|
||||
template?: {
|
||||
name: string;
|
||||
language: {
|
||||
code: string;
|
||||
};
|
||||
components?: any[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Type Guards
|
||||
|
||||
### Type Guard Functions
|
||||
```typescript
|
||||
export function isMediaMessage(message: any): message is MediaMessage {
|
||||
return message && TypeMediaMessage.some(type => message[type]);
|
||||
}
|
||||
|
||||
export function isTextMessage(message: any): message is TextMessage {
|
||||
return message && message.conversation;
|
||||
}
|
||||
|
||||
export function isValidIntegration(integration: string): integration is IntegrationType {
|
||||
return Object.values(Integration).includes(integration as IntegrationType);
|
||||
}
|
||||
|
||||
export function isValidEvent(event: string): event is Events {
|
||||
return Object.values(Events).includes(event as Events);
|
||||
}
|
||||
```
|
||||
|
||||
## Module Augmentation
|
||||
|
||||
### Express Request Extension
|
||||
```typescript
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
instanceName?: string;
|
||||
instanceData?: InstanceDto;
|
||||
user?: {
|
||||
id: string;
|
||||
apiKey: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Type Documentation
|
||||
|
||||
### JSDoc Type Documentation
|
||||
```typescript
|
||||
/**
|
||||
* WhatsApp instance configuration
|
||||
* @interface InstanceConfig
|
||||
* @property {string} name - Unique instance name
|
||||
* @property {IntegrationType} integration - Integration type
|
||||
* @property {string} [token] - API token for business integrations
|
||||
* @property {WebhookConfig} [webhook] - Webhook configuration
|
||||
* @property {ProxyConfig} [proxy] - Proxy configuration
|
||||
*/
|
||||
export interface InstanceConfig {
|
||||
name: string;
|
||||
integration: IntegrationType;
|
||||
token?: string;
|
||||
webhook?: WebhookConfig;
|
||||
proxy?: ProxyConfig;
|
||||
}
|
||||
```
|
||||
653
.cursor/rules/specialized-rules/util-rules.mdc
Normal file
653
.cursor/rules/specialized-rules/util-rules.mdc
Normal file
@ -0,0 +1,653 @@
|
||||
---
|
||||
description: Utility functions and helpers for Evolution API
|
||||
globs:
|
||||
- "src/utils/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Utility Rules
|
||||
|
||||
## Utility Function Structure
|
||||
|
||||
### Standard Utility Pattern
|
||||
```typescript
|
||||
import { Logger } from '@config/logger.config';
|
||||
|
||||
const logger = new Logger('UtilityName');
|
||||
|
||||
export function utilityFunction(param: ParamType): ReturnType {
|
||||
try {
|
||||
// Utility logic
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(`Utility function failed: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default utilityFunction;
|
||||
```
|
||||
|
||||
## Authentication Utilities
|
||||
|
||||
### Multi-File Auth State Pattern
|
||||
```typescript
|
||||
import { AuthenticationState } from 'baileys';
|
||||
import { CacheService } from '@api/services/cache.service';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
export default async function useMultiFileAuthStatePrisma(
|
||||
sessionId: string,
|
||||
cache: CacheService,
|
||||
): Promise<{
|
||||
state: AuthenticationState;
|
||||
saveCreds: () => Promise<void>;
|
||||
}> {
|
||||
const localFolder = path.join(INSTANCE_DIR, sessionId);
|
||||
const localFile = (key: string) => path.join(localFolder, fixFileName(key) + '.json');
|
||||
await fs.mkdir(localFolder, { recursive: true });
|
||||
|
||||
async function writeData(data: any, key: string): Promise<any> {
|
||||
const dataString = JSON.stringify(data, BufferJSON.replacer);
|
||||
|
||||
if (key !== 'creds') {
|
||||
if (process.env.CACHE_REDIS_ENABLED === 'true') {
|
||||
return await cache.hSet(sessionId, key, data);
|
||||
} else {
|
||||
await fs.writeFile(localFile(key), dataString);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await saveKey(sessionId, dataString);
|
||||
return;
|
||||
}
|
||||
|
||||
async function readData(key: string): Promise<any> {
|
||||
try {
|
||||
let rawData;
|
||||
|
||||
if (key !== 'creds') {
|
||||
if (process.env.CACHE_REDIS_ENABLED === 'true') {
|
||||
return await cache.hGet(sessionId, key);
|
||||
} else {
|
||||
if (!(await fileExists(localFile(key)))) return null;
|
||||
rawData = await fs.readFile(localFile(key), { encoding: 'utf-8' });
|
||||
return JSON.parse(rawData, BufferJSON.reviver);
|
||||
}
|
||||
} else {
|
||||
rawData = await getAuthKey(sessionId);
|
||||
}
|
||||
|
||||
const parsedData = JSON.parse(rawData, BufferJSON.reviver);
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeData(key: string): Promise<any> {
|
||||
try {
|
||||
if (key !== 'creds') {
|
||||
if (process.env.CACHE_REDIS_ENABLED === 'true') {
|
||||
return await cache.hDelete(sessionId, key);
|
||||
} else {
|
||||
await fs.unlink(localFile(key));
|
||||
}
|
||||
} else {
|
||||
await deleteAuthKey(sessionId);
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let creds = await readData('creds');
|
||||
if (!creds) {
|
||||
creds = initAuthCreds();
|
||||
await writeData(creds, 'creds');
|
||||
}
|
||||
|
||||
return {
|
||||
state: {
|
||||
creds,
|
||||
keys: {
|
||||
get: async (type, ids) => {
|
||||
const data = {};
|
||||
await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
let value = await readData(`${type}-${id}`);
|
||||
if (type === 'app-state-sync-key' && value) {
|
||||
value = proto.Message.AppStateSyncKeyData.fromObject(value);
|
||||
}
|
||||
data[id] = value;
|
||||
})
|
||||
);
|
||||
return data;
|
||||
},
|
||||
set: async (data) => {
|
||||
const tasks = [];
|
||||
for (const category in data) {
|
||||
for (const id in data[category]) {
|
||||
const value = data[category][id];
|
||||
const key = `${category}-${id}`;
|
||||
tasks.push(value ? writeData(value, key) : removeData(key));
|
||||
}
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
},
|
||||
},
|
||||
},
|
||||
saveCreds: () => writeData(creds, 'creds'),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Message Processing Utilities
|
||||
|
||||
### Message Content Extraction
|
||||
```typescript
|
||||
export const getConversationMessage = (msg: any): string => {
|
||||
const types = getTypeMessage(msg);
|
||||
const messageContent = getMessageContent(types);
|
||||
return messageContent;
|
||||
};
|
||||
|
||||
const getTypeMessage = (msg: any): any => {
|
||||
return Object.keys(msg?.message || msg || {})[0];
|
||||
};
|
||||
|
||||
const getMessageContent = (type: string, msg?: any): string => {
|
||||
const typeKey = type?.replace('Message', '');
|
||||
|
||||
const types = {
|
||||
conversation: msg?.message?.conversation,
|
||||
extendedTextMessage: msg?.message?.extendedTextMessage?.text,
|
||||
imageMessage: msg?.message?.imageMessage?.caption || 'Image',
|
||||
videoMessage: msg?.message?.videoMessage?.caption || 'Video',
|
||||
audioMessage: 'Audio',
|
||||
documentMessage: msg?.message?.documentMessage?.caption || 'Document',
|
||||
stickerMessage: 'Sticker',
|
||||
contactMessage: 'Contact',
|
||||
locationMessage: 'Location',
|
||||
liveLocationMessage: 'Live Location',
|
||||
viewOnceMessage: 'View Once',
|
||||
reactionMessage: 'Reaction',
|
||||
pollCreationMessage: 'Poll',
|
||||
pollUpdateMessage: 'Poll Update',
|
||||
};
|
||||
|
||||
let result = types[typeKey] || types[type] || 'Unknown';
|
||||
|
||||
if (!result || result === 'Unknown') {
|
||||
result = JSON.stringify(msg);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
```
|
||||
|
||||
### JID Creation Utility
|
||||
```typescript
|
||||
export const createJid = (number: string): string => {
|
||||
if (number.includes('@')) {
|
||||
return number;
|
||||
}
|
||||
|
||||
// Remove any non-numeric characters except +
|
||||
let cleanNumber = number.replace(/[^\d+]/g, '');
|
||||
|
||||
// Remove + if present
|
||||
if (cleanNumber.startsWith('+')) {
|
||||
cleanNumber = cleanNumber.substring(1);
|
||||
}
|
||||
|
||||
// Add country code if missing (assuming Brazil as default)
|
||||
if (cleanNumber.length === 11 && cleanNumber.startsWith('11')) {
|
||||
cleanNumber = '55' + cleanNumber;
|
||||
} else if (cleanNumber.length === 10) {
|
||||
cleanNumber = '5511' + cleanNumber;
|
||||
}
|
||||
|
||||
// Determine if it's a group or individual
|
||||
const isGroup = cleanNumber.includes('-');
|
||||
const domain = isGroup ? 'g.us' : 's.whatsapp.net';
|
||||
|
||||
return `${cleanNumber}@${domain}`;
|
||||
};
|
||||
```
|
||||
|
||||
## Cache Utilities
|
||||
|
||||
### WhatsApp Number Cache
|
||||
```typescript
|
||||
interface ISaveOnWhatsappCacheParams {
|
||||
remoteJid: string;
|
||||
lid?: string;
|
||||
}
|
||||
|
||||
function getAvailableNumbers(remoteJid: string): string[] {
|
||||
const numbersAvailable: string[] = [];
|
||||
|
||||
if (remoteJid.startsWith('+')) {
|
||||
remoteJid = remoteJid.slice(1);
|
||||
}
|
||||
|
||||
const [number, domain] = remoteJid.split('@');
|
||||
|
||||
// Brazilian numbers
|
||||
if (remoteJid.startsWith('55')) {
|
||||
const numberWithDigit =
|
||||
number.slice(4, 5) === '9' && number.length === 13 ? number : `${number.slice(0, 4)}9${number.slice(4)}`;
|
||||
const numberWithoutDigit = number.length === 12 ? number : number.slice(0, 4) + number.slice(5);
|
||||
|
||||
numbersAvailable.push(numberWithDigit);
|
||||
numbersAvailable.push(numberWithoutDigit);
|
||||
}
|
||||
// Mexican/Argentina numbers
|
||||
else if (number.startsWith('52') || number.startsWith('54')) {
|
||||
let prefix = '';
|
||||
if (number.startsWith('52')) {
|
||||
prefix = '1';
|
||||
}
|
||||
if (number.startsWith('54')) {
|
||||
prefix = '9';
|
||||
}
|
||||
|
||||
const numberWithDigit =
|
||||
number.slice(2, 3) === prefix && number.length === 13
|
||||
? number
|
||||
: `${number.slice(0, 2)}${prefix}${number.slice(2)}`;
|
||||
const numberWithoutDigit = number.length === 12 ? number : number.slice(0, 2) + number.slice(3);
|
||||
|
||||
numbersAvailable.push(numberWithDigit);
|
||||
numbersAvailable.push(numberWithoutDigit);
|
||||
}
|
||||
// Other countries
|
||||
else {
|
||||
numbersAvailable.push(remoteJid);
|
||||
}
|
||||
|
||||
return numbersAvailable.map((number) => `${number}@${domain}`);
|
||||
}
|
||||
|
||||
export async function saveOnWhatsappCache(params: ISaveOnWhatsappCacheParams): Promise<void> {
|
||||
const { remoteJid, lid } = params;
|
||||
const db = configService.get<Database>('DATABASE');
|
||||
|
||||
if (!db.SAVE_DATA.CONTACTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const numbersAvailable = getAvailableNumbers(remoteJid);
|
||||
|
||||
const existingContact = await prismaRepository.contact.findFirst({
|
||||
where: {
|
||||
OR: numbersAvailable.map(number => ({ id: number })),
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingContact) {
|
||||
await prismaRepository.contact.create({
|
||||
data: {
|
||||
id: remoteJid,
|
||||
pushName: '',
|
||||
profilePicUrl: '',
|
||||
isOnWhatsapp: true,
|
||||
lid: lid || null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prismaRepository.contact.update({
|
||||
where: { id: existingContact.id },
|
||||
data: {
|
||||
isOnWhatsapp: true,
|
||||
lid: lid || existingContact.lid,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving WhatsApp cache:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Search Utilities
|
||||
|
||||
### Advanced Search Operators
|
||||
```typescript
|
||||
function normalizeString(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
|
||||
export function advancedOperatorsSearch(data: string, query: string): boolean {
|
||||
const normalizedData = normalizeString(data);
|
||||
const normalizedQuery = normalizeString(query);
|
||||
|
||||
// Exact phrase search with quotes
|
||||
if (normalizedQuery.startsWith('"') && normalizedQuery.endsWith('"')) {
|
||||
const phrase = normalizedQuery.slice(1, -1);
|
||||
return normalizedData.includes(phrase);
|
||||
}
|
||||
|
||||
// OR operator
|
||||
if (normalizedQuery.includes(' OR ')) {
|
||||
const terms = normalizedQuery.split(' OR ');
|
||||
return terms.some(term => normalizedData.includes(term.trim()));
|
||||
}
|
||||
|
||||
// AND operator (default behavior)
|
||||
if (normalizedQuery.includes(' AND ')) {
|
||||
const terms = normalizedQuery.split(' AND ');
|
||||
return terms.every(term => normalizedData.includes(term.trim()));
|
||||
}
|
||||
|
||||
// NOT operator
|
||||
if (normalizedQuery.startsWith('NOT ')) {
|
||||
const term = normalizedQuery.slice(4);
|
||||
return !normalizedData.includes(term);
|
||||
}
|
||||
|
||||
// Wildcard search
|
||||
if (normalizedQuery.includes('*')) {
|
||||
const regex = new RegExp(normalizedQuery.replace(/\*/g, '.*'), 'i');
|
||||
return regex.test(normalizedData);
|
||||
}
|
||||
|
||||
// Default: simple contains search
|
||||
return normalizedData.includes(normalizedQuery);
|
||||
}
|
||||
```
|
||||
|
||||
## Proxy Utilities
|
||||
|
||||
### Proxy Agent Creation
|
||||
```typescript
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
|
||||
type Proxy = {
|
||||
host: string;
|
||||
port: string;
|
||||
protocol: 'http' | 'https' | 'socks4' | 'socks5';
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
function selectProxyAgent(proxyUrl: string): HttpsProxyAgent<string> | SocksProxyAgent {
|
||||
const url = new URL(proxyUrl);
|
||||
|
||||
if (url.protocol === 'socks4:' || url.protocol === 'socks5:') {
|
||||
return new SocksProxyAgent(proxyUrl);
|
||||
} else {
|
||||
return new HttpsProxyAgent(proxyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
export function makeProxyAgent(proxy: Proxy): HttpsProxyAgent<string> | SocksProxyAgent | null {
|
||||
if (!proxy.host || !proxy.port) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let proxyUrl = `${proxy.protocol}://`;
|
||||
|
||||
if (proxy.username && proxy.password) {
|
||||
proxyUrl += `${proxy.username}:${proxy.password}@`;
|
||||
}
|
||||
|
||||
proxyUrl += `${proxy.host}:${proxy.port}`;
|
||||
|
||||
try {
|
||||
return selectProxyAgent(proxyUrl);
|
||||
} catch (error) {
|
||||
console.error('Failed to create proxy agent:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Telemetry Utilities
|
||||
|
||||
### Telemetry Data Collection
|
||||
```typescript
|
||||
export interface TelemetryData {
|
||||
route: string;
|
||||
apiVersion: string;
|
||||
timestamp: Date;
|
||||
method?: string;
|
||||
statusCode?: number;
|
||||
responseTime?: number;
|
||||
userAgent?: string;
|
||||
instanceName?: string;
|
||||
}
|
||||
|
||||
export const sendTelemetry = async (route: string): Promise<void> => {
|
||||
try {
|
||||
const telemetryData: TelemetryData = {
|
||||
route,
|
||||
apiVersion: packageJson.version,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
// Only send telemetry if enabled
|
||||
if (process.env.DISABLE_TELEMETRY === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send to telemetry service (implement as needed)
|
||||
await axios.post('https://telemetry.evolution-api.com/collect', telemetryData, {
|
||||
timeout: 5000,
|
||||
});
|
||||
} catch (error) {
|
||||
// Silently fail - don't affect main application
|
||||
console.debug('Telemetry failed:', error.message);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Internationalization Utilities
|
||||
|
||||
### i18n Setup
|
||||
```typescript
|
||||
import { ConfigService, Language } from '@config/env.config';
|
||||
import fs from 'fs';
|
||||
import i18next from 'i18next';
|
||||
import path from 'path';
|
||||
|
||||
const __dirname = path.resolve(process.cwd(), 'src', 'utils');
|
||||
const languages = ['en', 'pt-BR', 'es'];
|
||||
const translationsPath = path.join(__dirname, 'translations');
|
||||
const configService: ConfigService = new ConfigService();
|
||||
|
||||
const resources: any = {};
|
||||
|
||||
languages.forEach((language) => {
|
||||
const languagePath = path.join(translationsPath, `${language}.json`);
|
||||
if (fs.existsSync(languagePath)) {
|
||||
const translationContent = fs.readFileSync(languagePath, 'utf8');
|
||||
resources[language] = {
|
||||
translation: JSON.parse(translationContent),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
i18next.init({
|
||||
resources,
|
||||
fallbackLng: 'en',
|
||||
lng: configService.get<Language>('LANGUAGE') || 'pt-BR',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export const t = i18next.t.bind(i18next);
|
||||
export default i18next;
|
||||
```
|
||||
|
||||
## Bot Trigger Utilities
|
||||
|
||||
### Bot Trigger Matching
|
||||
```typescript
|
||||
import { TriggerOperator, TriggerType } from '@prisma/client';
|
||||
|
||||
export function findBotByTrigger(
|
||||
bots: any[],
|
||||
content: string,
|
||||
remoteJid: string,
|
||||
): any | null {
|
||||
for (const bot of bots) {
|
||||
if (!bot.enabled) continue;
|
||||
|
||||
// Check ignore list
|
||||
if (bot.ignoreJids && bot.ignoreJids.includes(remoteJid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check trigger
|
||||
if (matchesTrigger(content, bot.triggerType, bot.triggerOperator, bot.triggerValue)) {
|
||||
return bot;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function matchesTrigger(
|
||||
content: string,
|
||||
triggerType: TriggerType,
|
||||
triggerOperator: TriggerOperator,
|
||||
triggerValue: string,
|
||||
): boolean {
|
||||
const normalizedContent = content.toLowerCase().trim();
|
||||
const normalizedValue = triggerValue.toLowerCase().trim();
|
||||
|
||||
switch (triggerType) {
|
||||
case TriggerType.ALL:
|
||||
return true;
|
||||
|
||||
case TriggerType.KEYWORD:
|
||||
return matchesKeyword(normalizedContent, triggerOperator, normalizedValue);
|
||||
|
||||
case TriggerType.REGEX:
|
||||
try {
|
||||
const regex = new RegExp(triggerValue, 'i');
|
||||
return regex.test(content);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function matchesKeyword(
|
||||
content: string,
|
||||
operator: TriggerOperator,
|
||||
value: string,
|
||||
): boolean {
|
||||
switch (operator) {
|
||||
case TriggerOperator.EQUALS:
|
||||
return content === value;
|
||||
case TriggerOperator.CONTAINS:
|
||||
return content.includes(value);
|
||||
case TriggerOperator.STARTS_WITH:
|
||||
return content.startsWith(value);
|
||||
case TriggerOperator.ENDS_WITH:
|
||||
return content.endsWith(value);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Server Utilities
|
||||
|
||||
### Server Status Check
|
||||
```typescript
|
||||
export class ServerUP {
|
||||
private static instance: ServerUP;
|
||||
private isServerUp: boolean = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): ServerUP {
|
||||
if (!ServerUP.instance) {
|
||||
ServerUP.instance = new ServerUP();
|
||||
}
|
||||
return ServerUP.instance;
|
||||
}
|
||||
|
||||
public setServerStatus(status: boolean): void {
|
||||
this.isServerUp = status;
|
||||
}
|
||||
|
||||
public getServerStatus(): boolean {
|
||||
return this.isServerUp;
|
||||
}
|
||||
|
||||
public async waitForServer(timeout: number = 30000): Promise<boolean> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (!this.isServerUp && (Date.now() - startTime) < timeout) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
return this.isServerUp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Response Utilities
|
||||
|
||||
### Standardized Error Responses
|
||||
```typescript
|
||||
export function createMetaErrorResponse(error: any, context: string) {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
if (error.response?.data) {
|
||||
return {
|
||||
status: error.response.status || 500,
|
||||
error: {
|
||||
message: error.response.data.error?.message || 'External API error',
|
||||
type: error.response.data.error?.type || 'api_error',
|
||||
code: error.response.data.error?.code || 'unknown_error',
|
||||
context,
|
||||
timestamp,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 500,
|
||||
error: {
|
||||
message: error.message || 'Internal server error',
|
||||
type: 'internal_error',
|
||||
code: 'server_error',
|
||||
context,
|
||||
timestamp,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createValidationErrorResponse(errors: any[], context: string) {
|
||||
return {
|
||||
status: 400,
|
||||
error: {
|
||||
message: 'Validation failed',
|
||||
type: 'validation_error',
|
||||
code: 'invalid_input',
|
||||
context,
|
||||
details: errors,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
498
.cursor/rules/specialized-rules/validate-rules.mdc
Normal file
498
.cursor/rules/specialized-rules/validate-rules.mdc
Normal file
@ -0,0 +1,498 @@
|
||||
---
|
||||
description: Validation schemas and patterns for Evolution API
|
||||
globs:
|
||||
- "src/validate/**/*.ts"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Evolution API Validation Rules
|
||||
|
||||
## Validation Schema Structure
|
||||
|
||||
### JSONSchema7 Pattern (Evolution API Standard)
|
||||
```typescript
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const isNotEmpty = (...fields: string[]) => {
|
||||
const properties = {};
|
||||
fields.forEach((field) => {
|
||||
properties[field] = {
|
||||
if: { properties: { [field]: { type: 'string' } } },
|
||||
then: { properties: { [field]: { minLength: 1 } } },
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
allOf: Object.values(properties),
|
||||
};
|
||||
};
|
||||
|
||||
export const exampleSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
enabled: { type: 'boolean' },
|
||||
settings: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timeout: { type: 'number', minimum: 1000, maximum: 60000 },
|
||||
retries: { type: 'number', minimum: 0, maximum: 5 },
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
required: ['name', 'enabled'],
|
||||
...isNotEmpty('name'),
|
||||
};
|
||||
```
|
||||
|
||||
## Message Validation Schemas
|
||||
|
||||
### Send Message Validation
|
||||
```typescript
|
||||
const numberDefinition = {
|
||||
type: 'string',
|
||||
pattern: '^\\d+[\\.@\\w-]+',
|
||||
description: 'Invalid number',
|
||||
};
|
||||
|
||||
export const sendTextSchema: JSONSchema7 = {
|
||||
$id: v4(),
|
||||
type: 'object',
|
||||
properties: {
|
||||
number: numberDefinition,
|
||||
text: { type: 'string', minLength: 1, maxLength: 4096 },
|
||||
delay: { type: 'number', minimum: 0, maximum: 60000 },
|
||||
linkPreview: { type: 'boolean' },
|
||||
mentionsEveryOne: { type: 'boolean' },
|
||||
mentioned: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
required: ['number', 'text'],
|
||||
...isNotEmpty('number', 'text'),
|
||||
};
|
||||
|
||||
export const sendMediaSchema = Joi.object({
|
||||
number: Joi.string().required().pattern(/^\d+$/),
|
||||
mediatype: Joi.string().required().valid('image', 'video', 'audio', 'document'),
|
||||
media: Joi.alternatives().try(
|
||||
Joi.string().uri(),
|
||||
Joi.string().base64(),
|
||||
).required(),
|
||||
caption: Joi.string().optional().max(1024),
|
||||
fileName: Joi.string().optional().max(255),
|
||||
delay: Joi.number().optional().min(0).max(60000),
|
||||
}).required();
|
||||
|
||||
export const sendButtonsSchema = Joi.object({
|
||||
number: Joi.string().required().pattern(/^\d+$/),
|
||||
title: Joi.string().required().max(1024),
|
||||
description: Joi.string().optional().max(1024),
|
||||
footer: Joi.string().optional().max(60),
|
||||
buttons: Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required().valid('replyButton', 'urlButton', 'callButton'),
|
||||
displayText: Joi.string().required().max(20),
|
||||
id: Joi.string().when('type', {
|
||||
is: 'replyButton',
|
||||
then: Joi.required().max(256),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
url: Joi.string().when('type', {
|
||||
is: 'urlButton',
|
||||
then: Joi.required().uri(),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
phoneNumber: Joi.string().when('type', {
|
||||
is: 'callButton',
|
||||
then: Joi.required().pattern(/^\+?\d+$/),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
})
|
||||
).min(1).max(3).required(),
|
||||
}).required();
|
||||
```
|
||||
|
||||
## Instance Validation Schemas
|
||||
|
||||
### Instance Creation Validation
|
||||
```typescript
|
||||
export const instanceSchema = Joi.object({
|
||||
instanceName: Joi.string().required().min(1).max(100).pattern(/^[a-zA-Z0-9_-]+$/),
|
||||
integration: Joi.string().required().valid('WHATSAPP-BAILEYS', 'WHATSAPP-BUSINESS', 'EVOLUTION'),
|
||||
token: Joi.string().when('integration', {
|
||||
is: Joi.valid('WHATSAPP-BUSINESS', 'EVOLUTION'),
|
||||
then: Joi.required().min(10),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
qrcode: Joi.boolean().optional().default(false),
|
||||
number: Joi.string().optional().pattern(/^\d+$/),
|
||||
businessId: Joi.string().when('integration', {
|
||||
is: 'WHATSAPP-BUSINESS',
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
}).required();
|
||||
|
||||
export const settingsSchema = Joi.object({
|
||||
rejectCall: Joi.boolean().optional(),
|
||||
msgCall: Joi.string().optional().max(500),
|
||||
groupsIgnore: Joi.boolean().optional(),
|
||||
alwaysOnline: Joi.boolean().optional(),
|
||||
readMessages: Joi.boolean().optional(),
|
||||
readStatus: Joi.boolean().optional(),
|
||||
syncFullHistory: Joi.boolean().optional(),
|
||||
wavoipToken: Joi.string().optional(),
|
||||
}).optional();
|
||||
|
||||
export const proxySchema = Joi.object({
|
||||
host: Joi.string().required().hostname(),
|
||||
port: Joi.string().required().pattern(/^\d+$/).custom((value) => {
|
||||
const port = parseInt(value);
|
||||
if (port < 1 || port > 65535) {
|
||||
throw new Error('Port must be between 1 and 65535');
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
protocol: Joi.string().required().valid('http', 'https', 'socks4', 'socks5'),
|
||||
username: Joi.string().optional(),
|
||||
password: Joi.string().optional(),
|
||||
}).optional();
|
||||
```
|
||||
|
||||
## Webhook Validation Schemas
|
||||
|
||||
### Webhook Configuration Validation
|
||||
```typescript
|
||||
export const webhookSchema = Joi.object({
|
||||
enabled: Joi.boolean().required(),
|
||||
url: Joi.string().when('enabled', {
|
||||
is: true,
|
||||
then: Joi.required().uri({ scheme: ['http', 'https'] }),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
events: Joi.array().items(
|
||||
Joi.string().valid(
|
||||
'APPLICATION_STARTUP',
|
||||
'INSTANCE_CREATE',
|
||||
'INSTANCE_DELETE',
|
||||
'QRCODE_UPDATED',
|
||||
'CONNECTION_UPDATE',
|
||||
'STATUS_INSTANCE',
|
||||
'MESSAGES_SET',
|
||||
'MESSAGES_UPSERT',
|
||||
'MESSAGES_UPDATE',
|
||||
'MESSAGES_DELETE',
|
||||
'CONTACTS_SET',
|
||||
'CONTACTS_UPSERT',
|
||||
'CONTACTS_UPDATE',
|
||||
'CHATS_SET',
|
||||
'CHATS_UPDATE',
|
||||
'CHATS_UPSERT',
|
||||
'CHATS_DELETE',
|
||||
'GROUPS_UPSERT',
|
||||
'GROUPS_UPDATE',
|
||||
'GROUP_PARTICIPANTS_UPDATE',
|
||||
'CALL'
|
||||
)
|
||||
).min(1).when('enabled', {
|
||||
is: true,
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
headers: Joi.object().pattern(
|
||||
Joi.string(),
|
||||
Joi.string()
|
||||
).optional(),
|
||||
byEvents: Joi.boolean().optional().default(false),
|
||||
base64: Joi.boolean().optional().default(false),
|
||||
}).required();
|
||||
```
|
||||
|
||||
## Chatbot Validation Schemas
|
||||
|
||||
### Base Chatbot Validation
|
||||
```typescript
|
||||
export const baseChatbotSchema = Joi.object({
|
||||
enabled: Joi.boolean().required(),
|
||||
description: Joi.string().required().min(1).max(500),
|
||||
expire: Joi.number().optional().min(0).max(86400), // 24 hours in seconds
|
||||
keywordFinish: Joi.string().optional().max(100),
|
||||
delayMessage: Joi.number().optional().min(0).max(10000),
|
||||
unknownMessage: Joi.string().optional().max(1000),
|
||||
listeningFromMe: Joi.boolean().optional().default(false),
|
||||
stopBotFromMe: Joi.boolean().optional().default(false),
|
||||
keepOpen: Joi.boolean().optional().default(false),
|
||||
debounceTime: Joi.number().optional().min(0).max(60000),
|
||||
triggerType: Joi.string().required().valid('ALL', 'KEYWORD', 'REGEX'),
|
||||
triggerOperator: Joi.string().when('triggerType', {
|
||||
is: 'KEYWORD',
|
||||
then: Joi.required().valid('EQUALS', 'CONTAINS', 'STARTS_WITH', 'ENDS_WITH'),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
triggerValue: Joi.string().when('triggerType', {
|
||||
is: Joi.valid('KEYWORD', 'REGEX'),
|
||||
then: Joi.required().min(1).max(500),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
ignoreJids: Joi.array().items(Joi.string()).optional(),
|
||||
splitMessages: Joi.boolean().optional().default(false),
|
||||
timePerChar: Joi.number().optional().min(10).max(1000).default(100),
|
||||
}).required();
|
||||
|
||||
export const typebotSchema = baseChatbotSchema.keys({
|
||||
url: Joi.string().required().uri({ scheme: ['http', 'https'] }),
|
||||
typebot: Joi.string().required().min(1).max(100),
|
||||
apiVersion: Joi.string().optional().valid('v1', 'v2').default('v1'),
|
||||
}).required();
|
||||
|
||||
export const openaiSchema = baseChatbotSchema.keys({
|
||||
apiKey: Joi.string().required().min(10),
|
||||
model: Joi.string().optional().valid(
|
||||
'gpt-3.5-turbo',
|
||||
'gpt-3.5-turbo-16k',
|
||||
'gpt-4',
|
||||
'gpt-4-32k',
|
||||
'gpt-4-turbo-preview'
|
||||
).default('gpt-3.5-turbo'),
|
||||
systemMessage: Joi.string().optional().max(2000),
|
||||
maxTokens: Joi.number().optional().min(1).max(4000).default(1000),
|
||||
temperature: Joi.number().optional().min(0).max(2).default(0.7),
|
||||
}).required();
|
||||
```
|
||||
|
||||
## Business API Validation Schemas
|
||||
|
||||
### Template Validation
|
||||
```typescript
|
||||
export const templateSchema = Joi.object({
|
||||
name: Joi.string().required().min(1).max(512).pattern(/^[a-z0-9_]+$/),
|
||||
category: Joi.string().required().valid('MARKETING', 'UTILITY', 'AUTHENTICATION'),
|
||||
allowCategoryChange: Joi.boolean().required(),
|
||||
language: Joi.string().required().pattern(/^[a-z]{2}_[A-Z]{2}$/), // e.g., pt_BR, en_US
|
||||
components: Joi.array().items(
|
||||
Joi.object({
|
||||
type: Joi.string().required().valid('HEADER', 'BODY', 'FOOTER', 'BUTTONS'),
|
||||
format: Joi.string().when('type', {
|
||||
is: 'HEADER',
|
||||
then: Joi.valid('TEXT', 'IMAGE', 'VIDEO', 'DOCUMENT'),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
text: Joi.string().when('type', {
|
||||
is: Joi.valid('HEADER', 'BODY', 'FOOTER'),
|
||||
then: Joi.required().min(1).max(1024),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
buttons: Joi.array().when('type', {
|
||||
is: 'BUTTONS',
|
||||
then: Joi.items(
|
||||
Joi.object({
|
||||
type: Joi.string().required().valid('QUICK_REPLY', 'URL', 'PHONE_NUMBER'),
|
||||
text: Joi.string().required().min(1).max(25),
|
||||
url: Joi.string().when('type', {
|
||||
is: 'URL',
|
||||
then: Joi.required().uri(),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
phone_number: Joi.string().when('type', {
|
||||
is: 'PHONE_NUMBER',
|
||||
then: Joi.required().pattern(/^\+?\d+$/),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
})
|
||||
).min(1).max(10),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
})
|
||||
).min(1).required(),
|
||||
webhookUrl: Joi.string().optional().uri({ scheme: ['http', 'https'] }),
|
||||
}).required();
|
||||
|
||||
export const catalogSchema = Joi.object({
|
||||
number: Joi.string().optional().pattern(/^\d+$/),
|
||||
limit: Joi.number().optional().min(1).max(1000).default(10),
|
||||
cursor: Joi.string().optional(),
|
||||
}).optional();
|
||||
```
|
||||
|
||||
## Group Validation Schemas
|
||||
|
||||
### Group Management Validation
|
||||
```typescript
|
||||
export const createGroupSchema = Joi.object({
|
||||
subject: Joi.string().required().min(1).max(100),
|
||||
description: Joi.string().optional().max(500),
|
||||
participants: Joi.array().items(
|
||||
Joi.string().pattern(/^\d+$/)
|
||||
).min(1).max(256).required(),
|
||||
promoteParticipants: Joi.boolean().optional().default(false),
|
||||
}).required();
|
||||
|
||||
export const updateGroupSchema = Joi.object({
|
||||
subject: Joi.string().optional().min(1).max(100),
|
||||
description: Joi.string().optional().max(500),
|
||||
}).min(1).required();
|
||||
|
||||
export const groupParticipantsSchema = Joi.object({
|
||||
participants: Joi.array().items(
|
||||
Joi.string().pattern(/^\d+$/)
|
||||
).min(1).max(50).required(),
|
||||
action: Joi.string().required().valid('add', 'remove', 'promote', 'demote'),
|
||||
}).required();
|
||||
```
|
||||
|
||||
## Label Validation Schemas
|
||||
|
||||
### Label Management Validation
|
||||
```typescript
|
||||
export const labelSchema = Joi.object({
|
||||
name: Joi.string().required().min(1).max(100),
|
||||
color: Joi.string().required().pattern(/^#[0-9A-Fa-f]{6}$/), // Hex color
|
||||
predefinedId: Joi.string().optional(),
|
||||
}).required();
|
||||
|
||||
export const handleLabelSchema = Joi.object({
|
||||
number: Joi.string().required().pattern(/^\d+$/),
|
||||
labelId: Joi.string().required(),
|
||||
action: Joi.string().required().valid('add', 'remove'),
|
||||
}).required();
|
||||
```
|
||||
|
||||
## Custom Validation Functions
|
||||
|
||||
### Phone Number Validation
|
||||
```typescript
|
||||
export function validatePhoneNumber(number: string): boolean {
|
||||
// Remove any non-digit characters
|
||||
const cleaned = number.replace(/\D/g, '');
|
||||
|
||||
// Check minimum and maximum length
|
||||
if (cleaned.length < 10 || cleaned.length > 15) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for valid country codes (basic validation)
|
||||
const validCountryCodes = ['1', '7', '20', '27', '30', '31', '32', '33', '34', '36', '39', '40', '41', '43', '44', '45', '46', '47', '48', '49', '51', '52', '53', '54', '55', '56', '57', '58', '60', '61', '62', '63', '64', '65', '66', '81', '82', '84', '86', '90', '91', '92', '93', '94', '95', '98'];
|
||||
|
||||
// Check if starts with valid country code
|
||||
const startsWithValidCode = validCountryCodes.some(code => cleaned.startsWith(code));
|
||||
|
||||
return startsWithValidCode;
|
||||
}
|
||||
|
||||
export const phoneNumberValidator = Joi.string().custom((value, helpers) => {
|
||||
if (!validatePhoneNumber(value)) {
|
||||
return helpers.error('any.invalid');
|
||||
}
|
||||
return value;
|
||||
}, 'Phone number validation');
|
||||
```
|
||||
|
||||
### Base64 Validation
|
||||
```typescript
|
||||
export function validateBase64(base64: string): boolean {
|
||||
try {
|
||||
// Check if it's a valid base64 string
|
||||
const decoded = Buffer.from(base64, 'base64').toString('base64');
|
||||
return decoded === base64;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const base64Validator = Joi.string().custom((value, helpers) => {
|
||||
if (!validateBase64(value)) {
|
||||
return helpers.error('any.invalid');
|
||||
}
|
||||
return value;
|
||||
}, 'Base64 validation');
|
||||
```
|
||||
|
||||
### URL Validation with Protocol Check
|
||||
```typescript
|
||||
export function validateWebhookUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return ['http:', 'https:'].includes(parsed.protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const webhookUrlValidator = Joi.string().custom((value, helpers) => {
|
||||
if (!validateWebhookUrl(value)) {
|
||||
return helpers.error('any.invalid');
|
||||
}
|
||||
return value;
|
||||
}, 'Webhook URL validation');
|
||||
```
|
||||
|
||||
## Validation Error Handling
|
||||
|
||||
### Error Message Customization
|
||||
```typescript
|
||||
export const validationMessages = {
|
||||
'any.required': 'O campo {#label} é obrigatório',
|
||||
'string.empty': 'O campo {#label} não pode estar vazio',
|
||||
'string.min': 'O campo {#label} deve ter pelo menos {#limit} caracteres',
|
||||
'string.max': 'O campo {#label} deve ter no máximo {#limit} caracteres',
|
||||
'string.pattern.base': 'O campo {#label} possui formato inválido',
|
||||
'number.min': 'O campo {#label} deve ser maior ou igual a {#limit}',
|
||||
'number.max': 'O campo {#label} deve ser menor ou igual a {#limit}',
|
||||
'array.min': 'O campo {#label} deve ter pelo menos {#limit} itens',
|
||||
'array.max': 'O campo {#label} deve ter no máximo {#limit} itens',
|
||||
'any.only': 'O campo {#label} deve ser um dos valores: {#valids}',
|
||||
};
|
||||
|
||||
export function formatValidationError(error: Joi.ValidationError): any {
|
||||
return {
|
||||
message: 'Dados de entrada inválidos',
|
||||
details: error.details.map(detail => ({
|
||||
field: detail.path.join('.'),
|
||||
message: detail.message,
|
||||
value: detail.context?.value,
|
||||
})),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Schema Composition
|
||||
|
||||
### Reusable Schema Components
|
||||
```typescript
|
||||
export const commonFields = {
|
||||
instanceName: Joi.string().required().min(1).max(100).pattern(/^[a-zA-Z0-9_-]+$/),
|
||||
number: phoneNumberValidator.required(),
|
||||
delay: Joi.number().optional().min(0).max(60000),
|
||||
enabled: Joi.boolean().optional().default(true),
|
||||
};
|
||||
|
||||
export const mediaFields = {
|
||||
mediatype: Joi.string().required().valid('image', 'video', 'audio', 'document'),
|
||||
media: Joi.alternatives().try(
|
||||
Joi.string().uri(),
|
||||
base64Validator,
|
||||
).required(),
|
||||
caption: Joi.string().optional().max(1024),
|
||||
fileName: Joi.string().optional().max(255),
|
||||
};
|
||||
|
||||
// Compose schemas using common fields
|
||||
export const quickMessageSchema = Joi.object({
|
||||
...commonFields,
|
||||
text: Joi.string().required().min(1).max(4096),
|
||||
}).required();
|
||||
|
||||
export const quickMediaSchema = Joi.object({
|
||||
...commonFields,
|
||||
...mediaFields,
|
||||
}).required();
|
||||
```
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -4,12 +4,8 @@ Baileys
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
.cursor*
|
||||
|
||||
/Docker/.env
|
||||
|
||||
.vscode
|
||||
|
||||
# Logs
|
||||
logs/**.json
|
||||
*.log
|
||||
|
||||
41
AGENTS.md
Normal file
41
AGENTS.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- `src/` – TypeScript source. Key areas: `api/controllers`, `api/routes`, `api/services`, `api/integrations/{channel,chatbot,event,storage}`, `config`, `utils`, `exceptions`.
|
||||
- `prisma/` – Prisma schema and migrations. Provider folders: `postgresql-migrations/`, `mysql-migrations/`. Use `DATABASE_PROVIDER` to target the provider.
|
||||
- `dist/` – Build output; do not edit.
|
||||
- `public/` – Static assets.
|
||||
- `Docker*`, `docker-compose*.yaml` – Local stack and deployment helpers.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `npm run build` – Type-check (tsc) and bundle with `tsup` to `dist/`.
|
||||
- `npm run start` – Run dev server via `tsx src/main.ts`.
|
||||
- `npm run dev:server` – Watch mode for local development.
|
||||
- `npm run start:prod` – Run compiled app from `dist/`.
|
||||
- `npm run lint` / `npm run lint:check` – Auto-fix and check linting.
|
||||
- Database (choose provider): `export DATABASE_PROVIDER=postgresql` (or `mysql`), then:
|
||||
- `npm run db:generate` – Generate Prisma client.
|
||||
- `npm run db:migrate:dev` – Apply dev migrations and sync provider folder.
|
||||
- `npm run db:deploy` – Apply migrations in non-dev environments.
|
||||
- `npm run db:studio` – Open Prisma Studio.
|
||||
- Docker: `docker-compose up -d` to start local services.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- TypeScript, 2-space indent, single quotes, trailing commas, 120-char max (Prettier).
|
||||
- Enforced by ESLint + Prettier; import order via `simple-import-sort`.
|
||||
- File names follow `feature.kind.ts` (e.g., `chat.router.ts`, `whatsapp.baileys.service.ts`).
|
||||
- Classes: PascalCase; functions/variables: camelCase; constants: UPPER_SNAKE_CASE.
|
||||
|
||||
## Testing Guidelines
|
||||
- No formal suite yet. Place tests under `test/` as `*.test.ts`.
|
||||
- Run `npm test` (watches `test/all.test.ts` if present). Prefer fast, isolated unit tests.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Conventional Commits enforced by commitlint. Use `npm run commit` (Commitizen).
|
||||
- Examples: `feat(api): add message status`, `fix(route): handle 404 on send`.
|
||||
- PRs: include clear description, linked issues, migration impact (provider), local run steps, and screenshots/logs where relevant.
|
||||
|
||||
## Security & Configuration
|
||||
- Copy `.env.example` to `.env`; never commit secrets.
|
||||
- Set `DATABASE_PROVIDER` before DB commands; see `SECURITY.md` for reporting vulnerabilities.
|
||||
|
||||
204
CLAUDE.md
204
CLAUDE.md
@ -2,77 +2,163 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Commands
|
||||
## Project Overview
|
||||
|
||||
### Core Commands
|
||||
- **Run development server**: `npm run dev:server` - Starts the server with hot reload using tsx watch
|
||||
- **Build project**: `npm run build` - Runs TypeScript check and builds with tsup
|
||||
- **Start production**: `npm run start:prod` - Runs the compiled application from dist/
|
||||
- **Lint code**: `npm run lint` - Runs ESLint with auto-fix on TypeScript files
|
||||
- **Check lint**: `npm run lint:check` - Runs ESLint without auto-fix
|
||||
Evolution API is a REST API for WhatsApp communication that supports both Baileys (WhatsApp Web) and official WhatsApp Business API. It's built with TypeScript/Node.js and provides extensive integrations with various platforms.
|
||||
|
||||
### Database Commands
|
||||
The project uses Prisma with support for multiple database providers (PostgreSQL, MySQL, psql_bouncer). Commands automatically use the DATABASE_PROVIDER from .env:
|
||||
## Common Development Commands
|
||||
|
||||
- **Generate Prisma client**: `npm run db:generate`
|
||||
- **Deploy migrations**: `npm run db:deploy` (Unix/Mac) or `npm run db:deploy:win` (Windows)
|
||||
- **Open Prisma Studio**: `npm run db:studio`
|
||||
- **Create new migration**: `npm run db:migrate:dev` (Unix/Mac) or `npm run db:migrate:dev:win` (Windows)
|
||||
### Build and Run
|
||||
```bash
|
||||
# Development
|
||||
npm run dev:server # Run in development with hot reload (tsx watch)
|
||||
|
||||
# Production
|
||||
npm run build # TypeScript check + tsup build
|
||||
npm run start:prod # Run production build
|
||||
|
||||
# Direct execution
|
||||
npm start # Run with tsx
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
npm run lint # ESLint with auto-fix
|
||||
npm run lint:check # ESLint check only
|
||||
npm run commit # Interactive commit with commitizen
|
||||
```
|
||||
|
||||
### Database Management
|
||||
```bash
|
||||
# Generate Prisma client (automatically uses DATABASE_PROVIDER env)
|
||||
npm run db:generate
|
||||
|
||||
# Deploy migrations
|
||||
npm run db:deploy # Unix/Mac
|
||||
npm run db:deploy:win # Windows
|
||||
|
||||
# Open Prisma Studio
|
||||
npm run db:studio
|
||||
|
||||
# Development migrations
|
||||
npm run db:migrate:dev # Unix/Mac
|
||||
npm run db:migrate:dev:win # Windows
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
npm test # Run tests with watch mode
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
Evolution API is a WhatsApp integration platform built with TypeScript and Express, supporting both Baileys (WhatsApp Web) and WhatsApp Cloud API connections.
|
||||
### Core Structure
|
||||
- **Multi-provider database support**: PostgreSQL and MySQL via Prisma ORM with provider-specific schemas
|
||||
- **Connection management**: Each WhatsApp instance maintains its own connection state and session
|
||||
- **Event-driven architecture**: Uses EventEmitter2 for internal events and supports multiple external event systems
|
||||
|
||||
### Core Components
|
||||
### Directory Layout
|
||||
```
|
||||
src/
|
||||
├── api/
|
||||
│ ├── controllers/ # HTTP route handlers
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── repository/ # Data access layer (Prisma)
|
||||
│ ├── dto/ # Data validation schemas
|
||||
│ ├── guards/ # Authentication/authorization
|
||||
│ ├── integrations/ # External service integrations
|
||||
│ └── routes/ # Express route definitions
|
||||
├── config/ # Environment and app configuration
|
||||
├── cache/ # Redis and local cache implementations
|
||||
├── exceptions/ # Custom exception classes
|
||||
├── utils/ # Shared utilities
|
||||
└── validate/ # Validation schemas
|
||||
```
|
||||
|
||||
**API Layer** (`src/api/`)
|
||||
- **Controllers**: Handle HTTP requests for different resources (instance, chat, group, sendMessage, etc.)
|
||||
- **Services**: Business logic layer containing auth, cache, channel, monitor, proxy services
|
||||
- **Routes**: RESTful API endpoints with authentication guards
|
||||
- **DTOs**: Data transfer objects for request/response validation using class-validator
|
||||
- **Repository**: Database access layer using Prisma ORM
|
||||
### Key Services Integration Points
|
||||
|
||||
**Integrations** (`src/api/integrations/`)
|
||||
- **Chatbot**: Supports multiple chatbot platforms (Typebot, Chatwoot, Dify, OpenAI, Flowise, N8N)
|
||||
- **Event**: WebSocket, RabbitMQ, Amazon SQS event systems
|
||||
- **Storage**: S3/Minio file storage integration
|
||||
- **Channel**: Multi-channel messaging support
|
||||
**WhatsApp Service** (`src/api/integrations/channel/whatsapp/`):
|
||||
- Manages Baileys connections and Meta Business API
|
||||
- Handles message sending, receiving, and status updates
|
||||
- Connection lifecycle management per instance
|
||||
|
||||
**Configuration** (`src/config/`)
|
||||
- Environment configuration management
|
||||
- Database provider switching (PostgreSQL/MySQL/PgBouncer)
|
||||
- Multi-tenant support via DATABASE_CONNECTION_CLIENT_NAME
|
||||
**Integration Services** (`src/api/integrations/`):
|
||||
- Chatwoot: Customer service platform integration
|
||||
- Typebot: Conversational bot builder
|
||||
- OpenAI: AI capabilities including audio transcription
|
||||
- Dify: AI agent platform
|
||||
- RabbitMQ/SQS: Message queue integrations
|
||||
- S3/Minio: Media storage
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
1. **Multi-Provider Database**: Uses `runWithProvider.js` to dynamically select database provider and migrations
|
||||
2. **Module System**: Path aliases configured in tsconfig.json (@api, @cache, @config, @utils, @validate)
|
||||
3. **Event-Driven**: EventEmitter2 for internal events, supports multiple external event systems
|
||||
4. **Instance Management**: Each WhatsApp connection is managed as an instance with memory lifecycle (DEL_INSTANCE config)
|
||||
|
||||
### Database Schema
|
||||
- Supports multiple providers with provider-specific schemas in `prisma/`
|
||||
- Separate migration folders for each provider (postgresql-migrations, mysql-migrations)
|
||||
- psql_bouncer uses PostgreSQL migrations but with connection pooling
|
||||
### Database Schema Management
|
||||
- Separate schema files: `postgresql-schema.prisma` and `mysql-schema.prisma`
|
||||
- Environment variable `DATABASE_PROVIDER` determines active database
|
||||
- Migration folders are provider-specific and auto-selected during deployment
|
||||
|
||||
### Authentication & Security
|
||||
- JWT-based authentication
|
||||
- API key support
|
||||
- Instance-specific authentication
|
||||
- Configurable CORS settings
|
||||
- API key-based authentication via `apikey` header
|
||||
- Instance-specific authentication for WhatsApp connections
|
||||
- Guards system for route protection
|
||||
- Input validation using `class-validator`
|
||||
|
||||
### Messaging Features
|
||||
- WhatsApp Web (Baileys library) and WhatsApp Cloud API support
|
||||
- Message queue support (RabbitMQ, SQS)
|
||||
- Real-time updates via WebSocket
|
||||
- Media file handling with S3/Minio storage
|
||||
- Multiple chatbot integrations with trigger management
|
||||
## Important Implementation Details
|
||||
|
||||
### Environment Variables
|
||||
Critical configuration in `.env`:
|
||||
- SERVER_TYPE, SERVER_PORT, SERVER_URL
|
||||
- DATABASE_PROVIDER and DATABASE_CONNECTION_URI
|
||||
- Log levels and Baileys-specific logging
|
||||
- Instance lifecycle management (DEL_INSTANCE)
|
||||
- Feature toggles for data persistence (DATABASE_SAVE_*)
|
||||
### WhatsApp Instance Management
|
||||
- Each WhatsApp connection is an "instance" with unique name
|
||||
- Instance data stored in database with connection state
|
||||
- Session persistence in database or file system (configurable)
|
||||
- Automatic reconnection handling with exponential backoff
|
||||
|
||||
### Message Queue Architecture
|
||||
- Supports RabbitMQ, Amazon SQS, and WebSocket for events
|
||||
- Event types: message.received, message.sent, connection.update, etc.
|
||||
- Configurable per instance which events to send
|
||||
|
||||
### Media Handling
|
||||
- Local storage or S3/Minio for media files
|
||||
- Automatic media download from WhatsApp
|
||||
- Media URL generation for external access
|
||||
- Support for audio transcription via OpenAI
|
||||
|
||||
### Multi-tenancy Support
|
||||
- Instance isolation at database level
|
||||
- Separate webhook configurations per instance
|
||||
- Independent integration settings per instance
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
Key environment variables are defined in `.env.example`. The system uses a strongly-typed configuration system via `src/config/env.config.ts`.
|
||||
|
||||
Critical configurations:
|
||||
- `DATABASE_PROVIDER`: postgresql or mysql
|
||||
- `DATABASE_CONNECTION_URI`: Database connection string
|
||||
- `AUTHENTICATION_API_KEY`: Global API authentication
|
||||
- `REDIS_ENABLED`: Enable Redis cache
|
||||
- `RABBITMQ_ENABLED`/`SQS_ENABLED`: Message queue options
|
||||
|
||||
## Development Guidelines from Cursor Instructions
|
||||
|
||||
The project includes specific development instructions in `.cursor/instructions`:
|
||||
- Always respond in Portuguese Brazilian
|
||||
- Follow established architecture patterns
|
||||
- Robust error handling with retry logic
|
||||
- Multi-database compatibility requirements
|
||||
- Security validations and rate limiting
|
||||
- Performance optimizations with caching
|
||||
- Minimum 70% test coverage target
|
||||
|
||||
## Testing Approach
|
||||
|
||||
Tests are located alongside source files or in dedicated test directories. The project uses:
|
||||
- Unit tests for services
|
||||
- Integration tests for critical APIs
|
||||
- Mock external dependencies
|
||||
- Test command runs with watch mode for development
|
||||
|
||||
## Deployment Considerations
|
||||
|
||||
- Docker support with `Dockerfile` and `docker-compose.yaml`
|
||||
- Graceful shutdown handling for connections
|
||||
- Health check endpoints for monitoring
|
||||
- Sentry integration for error tracking
|
||||
- Telemetry for usage analytics (non-sensitive data only)
|
||||
Loading…
Reference in New Issue
Block a user