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:
Davidson Gomes 2025-09-17 15:43:32 -03:00
parent 805f40c841
commit 7088ad05d2
19 changed files with 6939 additions and 63 deletions

106
.cursor/rules/README.md Normal file
View 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.

View 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
View 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"
]
}
}
}

View 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

View 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';
```

View 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
```

View 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();
}
```

View 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'),
}),
})
);
});
});
});
```

View 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
}
}
```

View 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',
}),
})
);
});
});
});
```

View 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);
}
```

View 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
})
```

View 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);
});
});
```

View 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;
}
```

View 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(),
},
};
}
```

View 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
View File

@ -4,12 +4,8 @@ Baileys
/dist
/node_modules
.cursor*
/Docker/.env
.vscode
# Logs
logs/**.json
*.log

41
AGENTS.md Normal file
View 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
View File

@ -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)