mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-09 09:59:40 -06:00
- 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
498 lines
15 KiB
Plaintext
498 lines
15 KiB
Plaintext
---
|
|
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();
|
|
``` |