Skip to content

Commit 3ac4ab0

Browse files
devlikeproropu
andcommitted
feat(apps): phone numbers apps to check chat id before sending a message
closes #2260 Co-authored-by: ropu <rovagnati@gmail.com>
1 parent 1c6081a commit 3ac4ab0

43 files changed

Lines changed: 1251 additions & 489 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/apps/app_sdk/apps/apps.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1+
import { ArgentinePhoneNumbersAppConfig } from '@waha/apps/argentine-phone-numbers/dto';
12
import { Type } from '@nestjs/common';
2-
import { BrazilianPhoneNumbersAppConfig } from '@waha/apps/brazilian-phone-numbers/dto/config.dto';
3+
import { BrazilianPhoneNumbersAppConfig } from '@waha/apps/brazilian-phone-numbers/dto';
34
import { CallsAppConfig } from '@waha/apps/calls/dto/config.dto';
45
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
56
import { McpAppConfig } from '@waha/apps/mcp/dto/config.dto';
7+
import { PhoneNumbersAppConfig } from '@waha/apps/phone-numbers/dto/config.dto';
68

79
export enum AppName {
10+
argentinePhoneNumbers = 'argentine-phone-numbers',
11+
brazilianPhoneNumbers = 'brazilian-phone-numbers',
812
chatwoot = 'chatwoot',
913
calls = 'calls',
1014
mcp = 'mcp',
11-
brazilianPhoneNumbers = 'brazilian-phone-numbers',
15+
phoneNumbers = 'phone-numbers',
1216
}
1317

1418
/**
@@ -17,7 +21,9 @@ export enum AppName {
1721
* without pulling in every app module (controllers, services, queues) - that creates require cycles.
1822
*/
1923
export const AppConfigClasses: Record<AppName, Type<any>> = {
24+
[AppName.argentinePhoneNumbers]: ArgentinePhoneNumbersAppConfig,
2025
[AppName.brazilianPhoneNumbers]: BrazilianPhoneNumbersAppConfig,
26+
[AppName.phoneNumbers]: PhoneNumbersAppConfig,
2127
[AppName.calls]: CallsAppConfig,
2228
[AppName.chatwoot]: ChatWootAppConfig,
2329
[AppName.mcp]: McpAppConfig,

‎src/apps/app_sdk/apps/registry.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
1+
import ArgentinePhoneNumbersAppModule from '@waha/apps/argentine-phone-numbers/app.module';
12
import { ApiTags } from '@nestjs/swagger';
3+
import BrazilianPhoneNumbersAppModule from '@waha/apps/brazilian-phone-numbers/app.module';
24
import { AppModule } from '@waha/apps/app_sdk/apps/definition';
35
import { AppApiTag } from '@waha/apps/app_sdk/apps/openapi';
4-
import BrazilianPhoneNumbersAppModule from '@waha/apps/brazilian-phone-numbers/app.module';
56
import CallsAppModule from '@waha/apps/calls/app.module';
67
import ChatWootAppModule from '@waha/apps/chatwoot/app.module';
78
import McpAppModule from '@waha/apps/mcp/app.module';
9+
import PhoneNumbersAppModule from '@waha/apps/phone-numbers/app.module';
810

911
/**
1012
* Registry of available apps.
1113
* Add new apps here - the rest of app_sdk works off this list.
1214
*/
1315
const APPS: AppModule[] = [
16+
ArgentinePhoneNumbersAppModule,
1417
BrazilianPhoneNumbersAppModule,
1518
CallsAppModule,
1619
ChatWootAppModule,
1720
McpAppModule,
21+
PhoneNumbersAppModule,
1822
];
1923

2024
// Tag every app controller with the app's OpenAPI tag ("🧩 Apps: {Title}") from AppModule.openapi,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { AppModule } from '@waha/apps/app_sdk/apps/definition';
2+
import { AppName } from '@waha/apps/app_sdk/apps/apps';
3+
import { ArgentinePhoneNumbersController } from '@waha/apps/argentine-phone-numbers/controller';
4+
import { ArgentinePhoneNumbersAppService } from '@waha/apps/argentine-phone-numbers/services/ArgentinePhoneNumbersAppService';
5+
6+
const argentinephonenumbersAppModule: AppModule = {
7+
name: AppName.argentinePhoneNumbers,
8+
openapi: {
9+
title: 'Argentine Phone Numbers',
10+
description:
11+
'Resolve Argentine phone numbers (with and without the mobile 9)',
12+
},
13+
definition: {
14+
plainkey: false,
15+
queue: false,
16+
migrations: true,
17+
restartOnChange: true,
18+
unique: true,
19+
},
20+
nestjs: {
21+
imports: [],
22+
controllers: [ArgentinePhoneNumbersController],
23+
providers: [ArgentinePhoneNumbersAppService],
24+
},
25+
Service: ArgentinePhoneNumbersAppService,
26+
};
27+
28+
export default argentinephonenumbersAppModule;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Controller, UseGuards } from '@nestjs/common';
2+
import { ApiSecurity } from '@nestjs/swagger';
3+
import { AppName } from '@waha/apps/app_sdk/apps/apps';
4+
import { UniqueAppResolver } from '@waha/apps/app_sdk/services/UniqueAppResolver';
5+
import { ArgentinePhoneNumbersAppService } from '@waha/apps/argentine-phone-numbers/services/ArgentinePhoneNumbersAppService';
6+
import { PhoneNumbersCacheController } from '@waha/apps/phone-numbers/api/PhoneNumbersCacheController';
7+
import { SessionManager } from '@waha/core/abc/manager.abc';
8+
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
9+
10+
@ApiSecurity('api_key')
11+
@Controller('api/apps/argentine-phone-numbers/:session')
12+
@UseGuards(PoliciesGuard)
13+
export class ArgentinePhoneNumbersController extends PhoneNumbersCacheController {
14+
constructor(
15+
manager: SessionManager,
16+
resolver: UniqueAppResolver,
17+
appService: ArgentinePhoneNumbersAppService,
18+
) {
19+
super(manager, resolver, appService, AppName.argentinePhoneNumbers);
20+
}
21+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { PhoneNumbersBaseConfig } from '@waha/apps/phone-numbers/dto/config.dto';
2+
3+
export class ArgentinePhoneNumbersAppConfig extends PhoneNumbersBaseConfig {}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { migrations_001_init_cache } from '@waha/apps/phone-numbers/storage/migrations_001_init_cache';
2+
3+
const migration = migrations_001_init_cache({
4+
table: 'app_argentine_phone_numbers_cache',
5+
index: 'arphone',
6+
});
7+
8+
exports.up = migration.up;
9+
exports.down = migration.down;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { UnprocessableEntityException } from '@nestjs/common';
2+
import { ArgentinePhoneNumberRules } from '@waha/apps/argentine-phone-numbers/rules/ArgentinePhoneNumberRules';
3+
import { PhoneNumbersBaseConfig } from '@waha/apps/phone-numbers/dto/config.dto';
4+
import {
5+
buildPlugin as buildPhoneNumbersPlugin,
6+
resolveChat,
7+
stubLookup,
8+
} from '@waha/apps/phone-numbers/plugins/testing';
9+
10+
function buildPlugin(config: Partial<PhoneNumbersBaseConfig> = {}) {
11+
return buildPhoneNumbersPlugin({
12+
config: config,
13+
rules: ArgentinePhoneNumberRules(),
14+
});
15+
}
16+
17+
describe('ArgentinePhoneNumbers', () => {
18+
it('uses the supplied form when it exists, without checking the other one', async () => {
19+
const { session } = buildPlugin();
20+
stubLookup(session, { numberExists: true, chatId: '541112345678@c.us' });
21+
22+
const resolved = await resolveChat(session, '+541112345678');
23+
24+
expect(resolved).toBe('541112345678@c.us');
25+
expect(session.checkNumberStatus).toHaveBeenCalledTimes(1);
26+
expect(session.checkNumberStatus).toHaveBeenCalledWith({
27+
phone: '541112345678',
28+
session: 'test',
29+
});
30+
});
31+
32+
it('tries the form with 9 after the supplied one is absent, keeps a LID answer', async () => {
33+
const { session } = buildPlugin();
34+
session.checkNumberStatus = jest
35+
.fn()
36+
.mockResolvedValueOnce({ numberExists: false })
37+
.mockResolvedValueOnce({ numberExists: true, chatId: '123456@lid' });
38+
39+
const resolved = await resolveChat(session, '541112345678@c.us');
40+
41+
expect(resolved).toBe('123456@lid');
42+
expect(session.checkNumberStatus).toHaveBeenLastCalledWith({
43+
phone: '5491112345678',
44+
session: 'test',
45+
});
46+
// Both forms hit the cache now
47+
expect(await resolveChat(session, '5491112345678@c.us')).toBe('123456@lid');
48+
expect(session.checkNumberStatus).toHaveBeenCalledTimes(2);
49+
});
50+
51+
it('tries the form without 9 as the fallback', async () => {
52+
const { session } = buildPlugin();
53+
session.checkNumberStatus = jest
54+
.fn()
55+
.mockResolvedValueOnce({ numberExists: false })
56+
.mockResolvedValueOnce({
57+
numberExists: true,
58+
chatId: '542920123456@c.us',
59+
});
60+
61+
const resolved = await resolveChat(session, '5492920123456@c.us');
62+
63+
expect(resolved).toBe('542920123456@c.us');
64+
});
65+
66+
it('prefers the pn when the engine answers with both pn and a LID', async () => {
67+
const { session } = buildPlugin();
68+
stubLookup(session, {
69+
numberExists: true,
70+
chatId: '123@lid',
71+
pn: '5491112345678@c.us',
72+
});
73+
74+
expect(await resolveChat(session, '541112345678@c.us')).toBe(
75+
'5491112345678@c.us',
76+
);
77+
});
78+
79+
it('soft mode keeps the supplied form when both are absent, strict rejects', async () => {
80+
const soft = buildPlugin();
81+
stubLookup(soft.session, { numberExists: false });
82+
expect(await resolveChat(soft.session, '541112345678@c.us')).toBe(
83+
'541112345678@c.us',
84+
);
85+
expect(soft.session.checkNumberStatus).toHaveBeenCalledTimes(2);
86+
87+
const strict = buildPlugin({ strict: true });
88+
stubLookup(strict.session, { numberExists: false });
89+
await expect(
90+
resolveChat(strict.session, '541112345678@c.us'),
91+
).rejects.toThrow(UnprocessableEntityException);
92+
});
93+
94+
it('leaves local forms and other countries untouched', async () => {
95+
const { session } = buildPlugin();
96+
stubLookup(session, { numberExists: false });
97+
98+
expect(await resolveChat(session, '1112345678@c.us')).toBe(
99+
'1112345678@c.us',
100+
);
101+
expect(await resolveChat(session, '5511912345678@c.us')).toBe(
102+
'5511912345678@c.us',
103+
);
104+
expect(session.checkNumberStatus).not.toHaveBeenCalled();
105+
});
106+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { ArgentinePhoneNumberRules } from './ArgentinePhoneNumberRules';
2+
3+
describe('ArgentinePhoneNumberRules', () => {
4+
const rules = ArgentinePhoneNumberRules();
5+
6+
function resolve(digits: string) {
7+
const rule = rules.find((r) => r.matches(digits));
8+
return rule?.resolve(digits) ?? null;
9+
}
10+
11+
it.each([
12+
['541112345678', ['541112345678', '5491112345678']],
13+
['5491112345678', ['5491112345678', '541112345678']],
14+
['543511234567', ['543511234567', '5493511234567']],
15+
['5492920123456', ['5492920123456', '542920123456']],
16+
])('keeps the supplied form first for %s', (digits, expected) => {
17+
expect(resolve(digits)).toEqual({ candidates: expected, fallback: digits });
18+
});
19+
20+
it.each([
21+
'0111512345678',
22+
'1112345678',
23+
'91112345678',
24+
'54111512345678',
25+
'548001234567',
26+
'54911123',
27+
'54911123456789',
28+
'5511912345678',
29+
'',
30+
])('does not match %s', (digits) => {
31+
expect(resolve(digits)).toBeNull();
32+
});
33+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { PhoneNumberRule } from '@waha/apps/phone-numbers/rules/PhoneNumberRule';
2+
import { RegexpPhoneNumberRule } from '@waha/apps/phone-numbers/rules/RegexpPhoneNumberRule';
3+
4+
/**
5+
* Argentina: mobiles have a 9 after the country code, callers send either form - check the supplied one first.
6+
* 54 + area code (1-3...) + number, 10 digits, international form only
7+
*/
8+
export function ArgentinePhoneNumberRules(): PhoneNumberRule[] {
9+
return [
10+
new RegexpPhoneNumberRule('^54([1-3]\\d{9})$', '549$1'),
11+
new RegexpPhoneNumberRule('^549([1-3]\\d{9})$', '54$1'),
12+
];
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { UniqueAppResolver } from '@waha/apps/app_sdk/services/UniqueAppResolver';
3+
import { ArgentinePhoneNumbersAppConfig } from '@waha/apps/argentine-phone-numbers/dto';
4+
import { ArgentinePhoneNumberRules } from '@waha/apps/argentine-phone-numbers/rules/ArgentinePhoneNumberRules';
5+
import { ArgentinePhoneNumbersCacheRepository } from '@waha/apps/argentine-phone-numbers/storage/ArgentinePhoneNumbersCacheRepository';
6+
import { PhoneNumberRule } from '@waha/apps/phone-numbers/rules/PhoneNumberRule';
7+
import { PhoneNumbersAppServiceBase } from '@waha/apps/phone-numbers/services/PhoneNumbersAppServiceBase';
8+
9+
@Injectable()
10+
export class ArgentinePhoneNumbersAppService extends PhoneNumbersAppServiceBase<ArgentinePhoneNumbersAppConfig> {
11+
protected readonly Repository = ArgentinePhoneNumbersCacheRepository;
12+
13+
constructor(resolver: UniqueAppResolver) {
14+
super(resolver);
15+
}
16+
17+
protected rules(config: ArgentinePhoneNumbersAppConfig): PhoneNumberRule[] {
18+
void config;
19+
return ArgentinePhoneNumberRules();
20+
}
21+
}

0 commit comments

Comments
 (0)