feat: app.modules.ts

This commit is contained in:
devlikepro committed 2026-08-31 10:44:22 +07:00
1 parent 16b50c672b
commit af51c0ae65
20 files changed
+232 -199

No files matched your search

+8 -4
View File
@@ -31,7 +31,7 @@ import {
import { Action, session as SessionName } from '@waha/core/auth/casl.types';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import { APPS } from '../apps/definition';
import { GetApp } from '../apps/registry';
import { App } from '../dto/app.dto';
import { ListAppsQuery } from '../dto/query.dto';
@@ -63,7 +63,11 @@ export class AppsController {
async create(@Body() app: App): Promise<App> {
const result = await this.appsService.create(this.manager, app);
const isRunning = this.manager.isRunning(app.session);
if (isRunning && app.enabled && APPS[app.app].restartOnChange) {
if (
isRunning &&
app.enabled &&
GetApp(app.app)?.definition.restartOnChange
) {
await this.manager.restart(app.session);
}
return result;
@@ -114,7 +118,7 @@ export class AppsController {
const result = await this.appsService.upsert(this.manager, app);
const isRunning = this.manager.isRunning(result.session);
if (isRunning && APPS[result.app].restartOnChange) {
if (isRunning && GetApp(result.app)?.definition.restartOnChange) {
await this.manager.restart(result.session);
}
return result;
@@ -134,7 +138,7 @@ export class AppsController {
}
const app = await this.appsService.delete(this.manager, id);
const isRunning = this.manager.isRunning(app.session);
if (isRunning && APPS[app.app].restartOnChange) {
if (isRunning && GetApp(app.app)?.definition.restartOnChange) {
await this.manager.restart(app.session);
}
}
+8 -8
View File
@@ -1,15 +1,15 @@
import { AppEnv } from '@waha/apps/app_sdk/env';
import { AppDefinition, APPS } from '@waha/apps/app_sdk/apps/definition';
import { AppName } from '@waha/apps/app_sdk/apps/name';
import { AppModule } from '@waha/apps/app_sdk/apps/definition';
import { GetApps } from '@waha/apps/app_sdk/apps/registry';
class AppRuntimeConfigC {
private constructor(private apps: AppDefinition[] | null) {}
private constructor(private apps: AppModule[] | null) {}
static FromEnv(env: typeof AppEnv) {
if (!env.enabled) {
return new AppRuntimeConfigC(null);
}
let apps = Object.values(APPS);
let apps: AppModule[] = GetApps();
// Include
if (env.on && env.on.length > 0) {
apps = apps.filter((app) => env.on!.includes(app.name));
@@ -30,18 +30,18 @@ class AppRuntimeConfigC {
}
GetAppsWithMigration() {
return this.GetApps().filter((app) => app.migrations);
return this.GetApps().filter((app) => app.definition.migrations);
}
GetAppsRequiringPlainKey() {
return this.GetApps().filter((app) => app.plainkey);
return this.GetApps().filter((app) => app.definition.plainkey);
}
GetAppsRequiringQueue() {
return this.GetApps().filter((app) => app.queue);
return this.GetApps().filter((app) => app.definition.queue);
}
HasApp(name: AppName) {
HasApp(name: string) {
return this.GetApps().some((app) => app.name === name);
}
+27 -38
View File
@@ -1,8 +1,9 @@
import { Type } from '@nestjs/common';
import { AppName } from '@waha/apps/app_sdk/apps/name';
import { GetApp } from '@waha/apps/app_sdk/apps/registry';
import { IAppService } from '@waha/apps/app_sdk/services/IAppService';
export interface AppDefinition {
// App name
name: AppName;
// If app requires WAHA_API_KEY_PLAIN to work
plainkey: boolean;
// If app requires queue to work
@@ -15,44 +16,32 @@ export interface AppDefinition {
unique: boolean;
}
// All Apps
export const APPS: Record<AppName, AppDefinition> = {
[AppName.calls]: {
name: AppName.calls,
plainkey: false,
queue: false,
migrations: false,
restartOnChange: true,
unique: true,
},
[AppName.chatwoot]: {
name: AppName.chatwoot,
plainkey: true,
queue: true,
migrations: true,
restartOnChange: true,
unique: true,
},
[AppName.mcp]: {
name: AppName.mcp,
plainkey: false,
queue: false,
migrations: false,
restartOnChange: false,
unique: false,
},
[AppName.brazilianPhoneNumbers]: {
name: AppName.brazilianPhoneNumbers,
plainkey: false,
queue: false,
migrations: true,
restartOnChange: true,
unique: true,
},
};
// NestJS module parts, included when the app is enabled in runtime configuration
export interface AppNestJS {
imports: any[];
controllers: any[];
providers: any[];
}
/**
* Contract for the default export of 'src/apps/<name>/app.module.ts'.
* Each app self-describes with this and gets listed in the registry ('apps/registry.ts').
*/
export interface AppModule {
// App name
name: AppName;
// Static app metadata (requirements, behavior flags)
definition: AppDefinition;
// NestJS module parts
nestjs: AppNestJS;
// Service implementing app lifecycle hooks; must also be listed in 'nestjs.providers'
Service: Type<IAppService>;
// DTO class used to transform and validate App.config
ConfigClass: Type<any>;
}
export function isUniqueApp(name: AppName): boolean {
return APPS[name]?.unique === true;
return GetApp(name)?.definition.unique === true;
}
// Returns the first unique AppName that appears more than once in the list, or null
+24
View File
@@ -0,0 +1,24 @@
import { AppModule } from '@waha/apps/app_sdk/apps/definition';
import BrazilianPhoneNumbersAppModule from '@waha/apps/brazilian-phone-numbers/app.module';
import CallsAppModule from '@waha/apps/calls/app.module';
import ChatWootAppModule from '@waha/apps/chatwoot/app.module';
import McpAppModule from '@waha/apps/mcp/app.module';
/**
* Registry of available apps.
* Add new apps here - the rest of app_sdk works off this list.
*/
const APPS: AppModule[] = [
BrazilianPhoneNumbersAppModule,
CallsAppModule,
ChatWootAppModule,
McpAppModule,
];
export function GetApps(): AppModule[] {
return APPS;
}
export function GetApp(name: string): AppModule | undefined {
return APPS.find((app) => app.name === name);
}
+7 -58
View File
@@ -1,7 +1,3 @@
import { BrazilianPhoneNumbersAppConfig } from '@waha/apps/brazilian-phone-numbers/dto/config.dto';
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
import { CallsAppConfig } from '@waha/apps/calls/dto/config.dto';
import { McpAppConfig } from '@waha/apps/mcp/dto/config.dto';
import { Type } from 'class-transformer';
import {
IsBoolean,
@@ -10,22 +6,11 @@ import {
IsString,
ValidateNested,
} from 'class-validator';
import { ApiExtraModels, ApiProperty } from '@nestjs/swagger';
import { ApiProperty } from '@nestjs/swagger';
import { AppName } from '@waha/apps/app_sdk/apps/name';
import { GetApp } from '@waha/apps/app_sdk/apps/registry';
export type AllowedAppConfig =
| ChatWootAppConfig
| CallsAppConfig
| McpAppConfig
| BrazilianPhoneNumbersAppConfig;
@ApiExtraModels(
ChatWootAppConfig,
CallsAppConfig,
McpAppConfig,
BrazilianPhoneNumbersAppConfig,
)
export class App<T extends AllowedAppConfig = any> {
export class App<T = any> {
@IsString()
id: string;
@@ -48,47 +33,11 @@ export class App<T extends AllowedAppConfig = any> {
@ValidateNested()
@Type((options) => {
if (options && options.object && options.object.app) {
switch (options.object.app) {
case AppName.chatwoot:
return ChatWootAppConfig;
case AppName.calls:
return CallsAppConfig;
case AppName.mcp:
return McpAppConfig;
case AppName.brazilianPhoneNumbers:
return BrazilianPhoneNumbersAppConfig;
default:
return Object;
}
const name = options?.object?.app;
if (!name) {
return Object;
}
return Object;
return GetApp(name)?.ConfigClass ?? Object;
})
config: T;
}
export class ChatWootAppDto extends App<ChatWootAppConfig> {
@Type(() => ChatWootAppConfig)
config: ChatWootAppConfig;
}
export class CallsAppDto extends App<CallsAppConfig> {
@Type(() => CallsAppConfig)
config: CallsAppConfig;
}
export class McpAppDto extends App<McpAppConfig> {
@Type(() => McpAppConfig)
config: McpAppConfig;
}
export class BrazilianPhoneNumbersAppDto extends App<BrazilianPhoneNumbersAppConfig> {
@Type(() => BrazilianPhoneNumbersAppConfig)
config: BrazilianPhoneNumbersAppConfig;
}
export type AppDto =
| ChatWootAppDto
| CallsAppDto
| McpAppDto
| BrazilianPhoneNumbersAppDto;
+3 -3
View File
@@ -1,9 +1,9 @@
import { parseBool } from '@waha/helpers';
import { APPS } from '@waha/apps/app_sdk/apps/definition';
import { GetApps } from '@waha/apps/app_sdk/apps/registry';
// Apps that don't require queue (Redis) - derived from AppDefinition
const IN_MEMORY_APPS = Object.values(APPS)
.filter((app) => !app.queue)
const IN_MEMORY_APPS = GetApps()
.filter((app) => !app.definition.queue)
.map((app) => app.name);
function parseCommaSeparatedList(value: string | undefined): string[] {
+12 -22
View File
@@ -1,16 +1,12 @@
import {
Injectable,
NotFoundException,
Optional,
UnprocessableEntityException,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { migrate } from '@waha/apps/app_sdk/migrations';
import { IAppService } from '@waha/apps/app_sdk/services/IAppService';
import { IAppsService } from '@waha/apps/app_sdk/services/IAppsService';
import { BrazilianPhoneNumbersAppService } from '@waha/apps/brazilian-phone-numbers/services/BrazilianPhoneNumbersAppService';
import { ChatWootAppService } from '@waha/apps/chatwoot/services/ChatWootAppService';
import { CallsAppService } from '@waha/apps/calls/services/CallsAppService';
import { McpAppService } from '@waha/apps/mcp/services/McpAppService';
import { DataStore } from '@waha/core/abc/DataStore';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WhatsappSession } from '@waha/core/abc/session.abc';
@@ -20,12 +16,12 @@ import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
import { App } from '../dto/app.dto';
import { AppRepository } from '../storage/AppRepository';
import { AppName } from '@waha/apps/app_sdk/apps/name';
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
import {
findDuplicateUniqueApp,
isUniqueApp,
} from '@waha/apps/app_sdk/apps/definition';
import { GetApp } from '@waha/apps/app_sdk/apps/registry';
export class AppDisableError extends UnprocessableEntityException {
constructor(app: string) {
@@ -49,11 +45,7 @@ export class AppsEnabledService implements IAppsService {
constructor(
@InjectPinoLogger('AppsService')
protected logger: PinoLogger,
@Optional() protected readonly chatwootService: ChatWootAppService,
@Optional() protected readonly callsAppService: CallsAppService,
@Optional() protected readonly mcpAppService: McpAppService,
@Optional()
protected readonly brazilianPhoneNumbersAppService: BrazilianPhoneNumbersAppService,
private readonly moduleRef: ModuleRef,
) {}
async list(manager: SessionManager, session: string): Promise<App[]> {
@@ -281,17 +273,15 @@ export class AppsEnabledService implements IAppsService {
}
private getAppService(app: App): IAppService | null {
switch (app.app) {
case AppName.chatwoot:
return this.chatwootService;
case AppName.calls:
return this.callsAppService;
case AppName.mcp:
return this.mcpAppService;
case AppName.brazilianPhoneNumbers:
return this.brazilianPhoneNumbersAppService;
default:
throw new Error(`App '${app.app}' not supported`);
const appModule = GetApp(app.app);
if (!appModule) {
throw new Error(`App '${app.app}' not supported`);
}
try {
return this.moduleRef.get(appModule.Service, { strict: false });
} catch {
// Provider is not registered - app is disabled via WAHA_APPS_ON / WAHA_APPS_OFF
return null;
}
}
+8 -38
View File
@@ -4,16 +4,12 @@ import { RMutexModule } from '@waha/modules/rmutex';
import { BullBoardModule } from '@bull-board/nestjs';
import { ExpressAdapter } from '@bull-board/express';
import { BullAuthMiddleware } from '@waha/apps/app_sdk/auth';
import { BrazilianPhoneNumbersAppExports } from '@waha/apps/brazilian-phone-numbers/brazilian-phone-numbers.module';
import { ChatWootExports } from '@waha/apps/chatwoot/chatwoot.module';
import { McpModuleExports } from '@waha/apps/mcp/mcp.module';
import { AppsController } from '@waha/apps/app_sdk/api/apps.controller';
import { CallsAppExports } from '@waha/apps/calls/calls.module';
import { AppsService } from '@waha/apps/app_sdk/services/IAppsService';
import { AppsEnabledService } from '@waha/apps/app_sdk/services/AppsEnabledService';
import { Auth } from '@waha/core/auth/config';
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
import { AppName } from '@waha/apps/app_sdk/apps/name';
import { GetApps } from '@waha/apps/app_sdk/apps/registry';
const QUEUES_IMPORTS_REQUIRED = [
BullModule.forRoot({
@@ -72,52 +68,26 @@ const QUEUES_IMPORTS = AppRuntimeConfig.HasAppsRequiringQueue()
? QUEUES_IMPORTS_REQUIRED
: [];
function getAppModule(name: AppName) {
if (!AppRuntimeConfig.HasApp(name)) {
return {
imports: [],
controllers: [],
providers: [],
};
}
switch (name) {
case AppName.calls:
return CallsAppExports;
case AppName.chatwoot:
return ChatWootExports;
case AppName.mcp:
return McpModuleExports;
case AppName.brazilianPhoneNumbers:
return BrazilianPhoneNumbersAppExports;
default:
throw Error(`App module not found for ${name}`);
}
}
// Apps enabled in the runtime configuration (WAHA_APPS_ON / WAHA_APPS_OFF)
const ENABLED_APPS = GetApps().filter((app) =>
AppRuntimeConfig.HasApp(app.name),
);
export const AppsEnabled = {
imports: [
...QUEUES_IMPORTS,
...getAppModule(AppName.mcp).imports,
...getAppModule(AppName.chatwoot).imports,
...getAppModule(AppName.calls).imports,
...getAppModule(AppName.brazilianPhoneNumbers).imports,
...ENABLED_APPS.flatMap((app) => app.nestjs.imports),
],
controllers: [
AppsController,
...getAppModule(AppName.mcp).controllers,
...getAppModule(AppName.chatwoot).controllers,
...getAppModule(AppName.calls).controllers,
...getAppModule(AppName.brazilianPhoneNumbers).controllers,
...ENABLED_APPS.flatMap((app) => app.nestjs.controllers),
],
providers: [
{
provide: AppsService,
useClass: AppsEnabledService,
},
...getAppModule(AppName.mcp).providers,
...getAppModule(AppName.calls).providers,
...getAppModule(AppName.chatwoot).providers,
...getAppModule(AppName.brazilianPhoneNumbers).providers,
...ENABLED_APPS.flatMap((app) => app.nestjs.providers),
],
};
+7
View File
@@ -1,7 +1,14 @@
import { ApiExtraModels } from '@nestjs/swagger';
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
import { GetApps } from '@waha/apps/app_sdk/apps/registry';
import { App } from '@waha/apps/app_sdk/dto/app.dto';
import { AppsDisabled } from '@waha/apps/apps.module.disabled';
import { AppsEnabled } from '@waha/apps/apps.module.enabled';
// Swagger models for app configs come from the registry -
// applied here instead of app.dto.ts to avoid a require cycle through the registry
ApiExtraModels(...GetApps().map((app) => app.ConfigClass))(App);
export const AppsModuleExports = AppRuntimeConfig.Enabled()
? AppsEnabled
: AppsDisabled;
@@ -0,0 +1,25 @@
import { AppModule } from '@waha/apps/app_sdk/apps/definition';
import { AppName } from '@waha/apps/app_sdk/apps/name';
import { BrazilianPhoneNumbersController } from '@waha/apps/brazilian-phone-numbers/api/brazilian-phone-numbers.controller';
import { BrazilianPhoneNumbersAppConfig } from '@waha/apps/brazilian-phone-numbers/dto/config.dto';
import { BrazilianPhoneNumbersAppService } from '@waha/apps/brazilian-phone-numbers/services/BrazilianPhoneNumbersAppService';
const BrazilianPhoneNumbersAppModule: AppModule = {
name: AppName.brazilianPhoneNumbers,
definition: {
plainkey: false,
queue: false,
migrations: true,
restartOnChange: true,
unique: true,
},
nestjs: {
imports: [],
controllers: [BrazilianPhoneNumbersController],
providers: [BrazilianPhoneNumbersAppService],
},
Service: BrazilianPhoneNumbersAppService,
ConfigClass: BrazilianPhoneNumbersAppConfig,
};
export default BrazilianPhoneNumbersAppModule;
@@ -1,8 +0,0 @@
import { BrazilianPhoneNumbersController } from '@waha/apps/brazilian-phone-numbers/api/brazilian-phone-numbers.controller';
import { BrazilianPhoneNumbersAppService } from '@waha/apps/brazilian-phone-numbers/services/BrazilianPhoneNumbersAppService';
export const BrazilianPhoneNumbersAppExports = {
providers: [BrazilianPhoneNumbersAppService],
imports: [],
controllers: [BrazilianPhoneNumbersController],
};
@@ -0,0 +1,8 @@
import { App } from '@waha/apps/app_sdk/dto/app.dto';
import { BrazilianPhoneNumbersAppConfig } from '@waha/apps/brazilian-phone-numbers/dto/config.dto';
import { Type } from 'class-transformer';
export class BrazilianPhoneNumbersAppDto extends App<BrazilianPhoneNumbersAppConfig> {
@Type(() => BrazilianPhoneNumbersAppConfig)
config: BrazilianPhoneNumbersAppConfig;
}
+24
View File
@@ -0,0 +1,24 @@
import { AppModule } from '@waha/apps/app_sdk/apps/definition';
import { AppName } from '@waha/apps/app_sdk/apps/name';
import { CallsAppConfig } from '@waha/apps/calls/dto/config.dto';
import { CallsAppService } from '@waha/apps/calls/services/CallsAppService';
const CallsAppModule: AppModule = {
name: AppName.calls,
definition: {
plainkey: false,
queue: false,
migrations: false,
restartOnChange: true,
unique: true,
},
nestjs: {
imports: [],
controllers: [],
providers: [CallsAppService],
},
Service: CallsAppService,
ConfigClass: CallsAppConfig,
};
export default CallsAppModule;
-7
View File
@@ -1,7 +0,0 @@
import { CallsAppService } from '@waha/apps/calls/services/CallsAppService';
export const CallsAppExports = {
providers: [CallsAppService],
imports: [],
controllers: [],
};
+8
View File
@@ -0,0 +1,8 @@
import { App } from '@waha/apps/app_sdk/dto/app.dto';
import { CallsAppConfig } from '@waha/apps/calls/dto/config.dto';
import { Type } from 'class-transformer';
export class CallsAppDto extends App<CallsAppConfig> {
@Type(() => CallsAppConfig)
config: CallsAppConfig;
}
@@ -1,4 +1,7 @@
import { AppModule } from '@waha/apps/app_sdk/apps/definition';
import { AppName } from '@waha/apps/app_sdk/apps/name';
import { RegisterAppQueue } from '@waha/apps/app_sdk/BullUtils';
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
import {
ExponentialRetriesJobOptions,
JobRemoveOptions,
@@ -160,8 +163,22 @@ const PROVIDERS = [
QueueManager,
];
export const ChatWootExports = {
providers: PROVIDERS,
imports: IMPORTS,
controllers: CONTROLLERS,
const ChatWootAppModule: AppModule = {
name: AppName.chatwoot,
definition: {
plainkey: true,
queue: true,
migrations: true,
restartOnChange: true,
unique: true,
},
nestjs: {
imports: IMPORTS,
controllers: CONTROLLERS,
providers: PROVIDERS,
},
Service: ChatWootAppService,
ConfigClass: ChatWootAppConfig,
};
export default ChatWootAppModule;
+8
View File
@@ -0,0 +1,8 @@
import { App } from '@waha/apps/app_sdk/dto/app.dto';
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
import { Type } from 'class-transformer';
export class ChatWootAppDto extends App<ChatWootAppConfig> {
@Type(() => ChatWootAppConfig)
config: ChatWootAppConfig;
}
+26
View File
@@ -0,0 +1,26 @@
import { AppModule } from '@waha/apps/app_sdk/apps/definition';
import { AppName } from '@waha/apps/app_sdk/apps/name';
import { McpController } from '@waha/apps/mcp/api/mcp.controller';
import { McpAppConfig } from '@waha/apps/mcp/dto/config.dto';
import { McpService } from '@waha/apps/mcp/mcp.service';
import { McpAppService } from '@waha/apps/mcp/services/McpAppService';
const McpAppModule: AppModule = {
name: AppName.mcp,
definition: {
plainkey: false,
queue: false,
migrations: false,
restartOnChange: false,
unique: false,
},
nestjs: {
imports: [],
controllers: [McpController],
providers: [McpService, McpAppService],
},
Service: McpAppService,
ConfigClass: McpAppConfig,
};
export default McpAppModule;
+8
View File
@@ -0,0 +1,8 @@
import { App } from '@waha/apps/app_sdk/dto/app.dto';
import { McpAppConfig } from '@waha/apps/mcp/dto/config.dto';
import { Type } from 'class-transformer';
export class McpAppDto extends App<McpAppConfig> {
@Type(() => McpAppConfig)
config: McpAppConfig;
}
-9
View File
@@ -1,9 +0,0 @@
import { McpController } from '@waha/apps/mcp/api/mcp.controller';
import { McpService } from '@waha/apps/mcp/mcp.service';
import { McpAppService } from '@waha/apps/mcp/services/McpAppService';
export const McpModuleExports = {
imports: [],
controllers: [McpController],
providers: [McpService, McpAppService],
};