mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-11 02:49:36 -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
552 lines
15 KiB
Plaintext
552 lines
15 KiB
Plaintext
---
|
|
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'),
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|
|
``` |