Skip to content

Commit 22fb1a3

Browse files
committed
feat: implement PAPI license management for interactive messages
- Added a LicenseService to manage PAPI license initialization and validation for interactive messages (buttons, lists, carousel). - Updated sendMessage controller to check license status before sending interactive messages, throwing ForbiddenException if the license is invalid. - Enhanced README with details on interactive messages and license requirements. - Updated .gitignore to exclude machine ID file related to PAPI license.
1 parent f2e2a59 commit 22fb1a3

File tree

6 files changed

+219
-1
lines changed

6 files changed

+219
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@ lerna-debug.log*
4646
.tool-versions
4747

4848
/prisma/migrations/*
49+
50+
# PAPI License machine ID (should not be committed)
51+
.machine-id

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,34 @@ Evolution API supports multiple types of connections to WhatsApp, enabling flexi
4242
- The Cloud API supports features such as end-to-end encryption, advanced analytics, and more comprehensive customer service tools.
4343
- To use this API, you must comply with Meta's policies and potentially pay for usage based on message volume and other factors.
4444

45+
## Interactive Messages
46+
47+
Evolution API uses [PAPI (Pastorini API)](https://github.com/mktpastorini/papi) as a Git submodule to provide robust support for interactive WhatsApp messages. PAPI is integrated directly into the codebase and powers the following features:
48+
49+
- **Interactive Buttons**: Send reply buttons, URL buttons, call buttons, and copy code buttons
50+
- **List Messages**: Create interactive menus with sections and options
51+
- **Carousel Messages**: Send carousel cards with images, videos, and buttons
52+
53+
These interactive message types are fully integrated and work seamlessly with Evolution API's instance management system. The PAPI library is included as a required submodule and is automatically updated when you pull the repository with `--recursive` or update submodules.
54+
55+
**Note**: When cloning the repository, make sure to use `git clone --recursive` or run `git submodule update --init --recursive` after cloning to ensure the PAPI submodule is properly initialized.
56+
57+
### License Requirement
58+
59+
Interactive messages (buttons, lists, and carousel) require a valid PAPI license to function. To obtain a license key:
60+
61+
1. **Register for a license**: Visit [https://padmin.intrategica.com.br/register.html](https://padmin.intrategica.com.br/register.html) to create an account and request a license key
62+
2. **Support the project**: Consider contributing to the [PAPI crowdfunding campaign](https://papi.mundoautomatik.com/) to help fund the development of new features
63+
64+
Once you have your license key, configure it in your `.env` file:
65+
66+
```env
67+
PAPI_LICENSE_KEY=your_license_key_here
68+
PAPI_LICENSE_ADMIN_URL=https://padmin.intrategica.com.br/
69+
```
70+
71+
If no license is configured, interactive messages will be disabled. The license system verifies your license status periodically and blocks interactive messages if the license is invalid, expired, or blocked.
72+
4573
## Integrations
4674

4775
Evolution API supports various integrations to enhance its functionality. Below is a list of available integrations and their uses:

src/api/controllers/sendMessage.controller.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
SendTemplateDto,
1616
SendTextDto,
1717
} from '@api/dto/sendMessage.dto';
18+
import { licenseService } from '@api/services/license.service';
1819
import { WAMonitoringService } from '@api/services/monitor.service';
19-
import { BadRequestException } from '@exceptions';
20+
import { BadRequestException, ForbiddenException } from '@exceptions';
2021
import { isBase64, isURL } from 'class-validator';
2122
import emojiRegex from 'emoji-regex';
2223

@@ -76,10 +77,22 @@ export class SendMessageController {
7677
}
7778

7879
public async sendButtons({ instanceName }: InstanceDto, data: SendButtonsDto) {
80+
if (!(await licenseService.isAllowed())) {
81+
const status = await licenseService.getStatus();
82+
throw new ForbiddenException(
83+
`Interactive messages (buttons) are blocked. License status: ${status.status}. ${status.message}`,
84+
);
85+
}
7986
return await this.waMonitor.waInstances[instanceName].buttonMessage(data);
8087
}
8188

8289
public async sendCarousel({ instanceName }: InstanceDto, data: SendCarouselDto) {
90+
if (!(await licenseService.isAllowed())) {
91+
const status = await licenseService.getStatus();
92+
throw new ForbiddenException(
93+
`Interactive messages (carousel) are blocked. License status: ${status.status}. ${status.message}`,
94+
);
95+
}
8396
return await this.waMonitor.waInstances[instanceName].carouselMessage(data);
8497
}
8598

@@ -88,6 +101,13 @@ export class SendMessageController {
88101
}
89102

90103
public async sendList({ instanceName }: InstanceDto, data: SendListDto) {
104+
const instancesCount = Object.keys(this.waMonitor.waInstances).length;
105+
if (!(await licenseService.isAllowed(instancesCount))) {
106+
const status = await licenseService.getStatus();
107+
throw new ForbiddenException(
108+
`Interactive messages (list) are blocked. License status: ${status.status}. ${status.message}`,
109+
);
110+
}
91111
return await this.waMonitor.waInstances[instanceName].listMessage(data);
92112
}
93113

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { configService, PapiLicense } from '@config/env.config';
2+
import { Logger } from '@config/logger.config';
3+
4+
import licenseManager from '../../../papi/lib/License/licenseManager.js';
5+
6+
export class LicenseService {
7+
private logger = new Logger('LICENSE');
8+
private initialized = false;
9+
private initializing = false;
10+
11+
/**
12+
* Initialize license manager with environment variables (lazy initialization)
13+
* Only initializes when interactive messages are actually used
14+
*/
15+
private async ensureInitialized(): Promise<void> {
16+
// Se já está inicializado, retorna
17+
if (this.initialized) {
18+
return;
19+
}
20+
21+
// Se já está inicializando, aguarda
22+
if (this.initializing) {
23+
// Aguarda até 5 segundos para a inicialização completar
24+
let attempts = 0;
25+
while (this.initializing && attempts < 50) {
26+
await new Promise((resolve) => setTimeout(resolve, 100));
27+
attempts++;
28+
}
29+
return;
30+
}
31+
32+
this.initializing = true;
33+
34+
try {
35+
const licenseConfig = configService.get<PapiLicense>('PAPI_LICENSE');
36+
37+
// Se não há configuração de licença, bloqueia mensagens interativas
38+
if (!licenseConfig.KEY || !licenseConfig.ADMIN_URL) {
39+
this.logger.warn(
40+
'⚠️ PAPI License not configured - Interactive messages (buttons, lists, carousel) will be BLOCKED',
41+
);
42+
this.logger.warn(
43+
'To enable interactive messages, configure PAPI_LICENSE_KEY and PAPI_LICENSE_ADMIN_URL in your .env file',
44+
);
45+
this.logger.warn('Get your license key at: https://padmin.intrategica.com.br/register.html');
46+
this.initializing = false;
47+
return;
48+
}
49+
50+
this.logger.verbose('Initializing PAPI License Manager...');
51+
52+
await licenseManager.initialize(licenseConfig.KEY, licenseConfig.ADMIN_URL);
53+
54+
// Set callback for when license is blocked
55+
licenseManager.onBlock(() => {
56+
this.logger.error('⚠️ PAPI License has been BLOCKED - Interactive messages are now disabled');
57+
});
58+
59+
const initialStatus = licenseManager.getStatus();
60+
61+
if (initialStatus.status === 'PENDING_ACTIVATION') {
62+
this.logger.warn(
63+
'⏳ PAPI License is pending activation - Interactive messages will be blocked until activation',
64+
);
65+
} else if (initialStatus.status === 'MACHINE_MISMATCH') {
66+
this.logger.error('❌ PAPI License is bound to another server - Interactive messages are disabled');
67+
} else if (initialStatus.status === 'ACTIVE') {
68+
this.logger.info(`✓ PAPI License is ACTIVE - Interactive messages enabled`);
69+
} else {
70+
this.logger.warn(`PAPI License status: ${initialStatus.status} - ${initialStatus.message}`);
71+
}
72+
73+
this.initialized = true;
74+
} catch (error) {
75+
this.logger.error(`Failed to initialize PAPI License Manager: ${error}`);
76+
// Não bloqueia a inicialização, mas marca como não inicializado
77+
} finally {
78+
this.initializing = false;
79+
}
80+
}
81+
82+
/**
83+
* Check if interactive messages are allowed (buttons, lists, carousel)
84+
* Initializes license manager on first use (lazy initialization)
85+
* Updates instance count when checking (replaces periodic updates)
86+
*/
87+
public async isAllowed(instancesCount?: number): Promise<boolean> {
88+
const licenseConfig = configService.get<PapiLicense>('PAPI_LICENSE');
89+
90+
// Se não há configuração de licença, bloqueia mensagens interativas
91+
if (!licenseConfig.KEY || !licenseConfig.ADMIN_URL) {
92+
return false;
93+
}
94+
95+
// Inicializa se ainda não foi inicializado (lazy initialization)
96+
await this.ensureInitialized();
97+
98+
// Se não foi inicializado após tentativa, bloqueia por segurança
99+
if (!this.initialized) {
100+
return false;
101+
}
102+
103+
// Atualiza contagem de instâncias quando verificar (substitui atualização periódica)
104+
if (instancesCount !== undefined) {
105+
licenseManager.setInstancesCount(instancesCount);
106+
}
107+
108+
return licenseManager.isAllowed();
109+
}
110+
111+
/**
112+
* Get current license status
113+
* Initializes license manager on first use (lazy initialization)
114+
*/
115+
public async getStatus() {
116+
const licenseConfig = configService.get<PapiLicense>('PAPI_LICENSE');
117+
118+
if (!licenseConfig.KEY || !licenseConfig.ADMIN_URL) {
119+
return {
120+
isValid: false,
121+
status: 'NOT_CONFIGURED' as const,
122+
message: 'License key and admin URL must be configured',
123+
};
124+
}
125+
126+
// Inicializa se ainda não foi inicializado (lazy initialization)
127+
await this.ensureInitialized();
128+
129+
if (!this.initialized) {
130+
return {
131+
isValid: false,
132+
status: 'NOT_INITIALIZED' as const,
133+
message: 'License manager not initialized',
134+
};
135+
}
136+
137+
return licenseManager.getStatus();
138+
}
139+
140+
/**
141+
* Update instances count for heartbeat
142+
* Called only when interactive messages are used (not periodically)
143+
* @deprecated Use isAllowed(instancesCount) instead
144+
*/
145+
public setInstancesCount(count: number): void {
146+
if (this.initialized) {
147+
licenseManager.setInstancesCount(count);
148+
}
149+
}
150+
}
151+
152+
export const licenseService = new LicenseService();

src/config/env.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,11 @@ export type EventEmitter = {
370370
MAX_LISTENERS: number;
371371
};
372372

373+
export type PapiLicense = {
374+
KEY?: string;
375+
ADMIN_URL?: string;
376+
};
377+
373378
export type Production = boolean;
374379

375380
export interface Env {
@@ -403,6 +408,7 @@ export interface Env {
403408
FACEBOOK: Facebook;
404409
SENTRY: Sentry;
405410
EVENT_EMITTER: EventEmitter;
411+
PAPI_LICENSE: PapiLicense;
406412
PRODUCTION?: Production;
407413
}
408414

@@ -846,6 +852,10 @@ export class ConfigService {
846852
EVENT_EMITTER: {
847853
MAX_LISTENERS: Number.parseInt(process.env?.EVENT_EMITTER_MAX_LISTENERS) || 50,
848854
},
855+
PAPI_LICENSE: {
856+
KEY: process.env?.PAPI_LICENSE_KEY,
857+
ADMIN_URL: process.env?.PAPI_LICENSE_ADMIN_URL || 'https://padmin.intrategica.com.br/',
858+
},
849859
};
850860
}
851861
}

src/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ async function bootstrap() {
4444
const prismaRepository = new PrismaRepository(configService);
4545
await prismaRepository.onModuleInit();
4646

47+
// PAPI License Manager will be initialized lazily when interactive messages are first used
48+
4749
app.use(
4850
cors({
4951
origin(requestOrigin, callback) {
@@ -163,6 +165,9 @@ async function bootstrap() {
163165
logger.error('Error loading instances: ' + error);
164166
});
165167

168+
// License manager heartbeat is handled internally by the PAPI licenseManager
169+
// Instance count is updated only when interactive messages are used (lazy initialization)
170+
166171
onUnexpectedError();
167172
}
168173

0 commit comments

Comments
 (0)