Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96da96281d | ||
|
|
50e09c3680 | ||
|
|
1da6a16640 | ||
|
|
10f80132f8 | ||
|
|
750b2b86b5 | ||
|
|
2fc2caed85 | ||
|
|
1431e8c0f1 | ||
|
|
01c9c2ca56 | ||
|
|
f5d216df6f | ||
|
|
7614822d2b | ||
|
|
d2eb463d32 | ||
|
|
07b1e50534 | ||
|
|
41b6b3459e | ||
|
|
0f5fcf60f6 | ||
|
|
18d8330ab3 | ||
|
|
7de5f461aa | ||
|
|
caa055abed | ||
|
|
49af88dea3 | ||
|
|
05d856d8ae | ||
|
|
c5c99136fa | ||
|
|
a8b520012f | ||
|
|
449df53c19 | ||
|
|
69ac19b5c0 | ||
|
|
163b11aa36 | ||
|
|
f4171a9086 | ||
|
|
dfbae9c71e | ||
|
|
91caa7ba25 | ||
|
|
2ac57bb6c3 | ||
|
|
e01647eb48 | ||
|
|
90bc05ecf3 | ||
|
|
21998cbfae | ||
|
|
87c49a8766 | ||
|
|
4e78d559a8 | ||
|
|
18ce902c60 |
No files matched your search
@@ -120,5 +120,11 @@ This guide summarizes how to explore, modify, and validate the WhatsApp HTTP API
|
||||
- Keep docs and code ASCII unless a file already uses other characters. When
|
||||
updating documentation, mirror the concise, actionable tone used here.
|
||||
|
||||
## Local Engine Sources
|
||||
|
||||
- WebJS source: `../whatsapp-web.js`
|
||||
- Baileys (NoWeb) source: `../WhiskeySockets-Baileys`
|
||||
- GoWhatsApp (GoWS) source: `../gows` + `../whatsmeow`
|
||||
|
||||
Following this playbook keeps contributions aligned with WAHA’s structure,
|
||||
automation hooks, and release process.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
ARG NODE_IMAGE_TAG=22.16-bookworm-slim
|
||||
ARG NODE_IMAGE_TAG=24.11-bookworm-slim
|
||||
ARG GOLANG_IMAGE_TAG=1.24-bookworm
|
||||
|
||||
#
|
||||
|
||||
@@ -32,7 +32,7 @@ push:
|
||||
docker push devlikeapro/waha
|
||||
|
||||
for-swagger:
|
||||
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true WHATSAPP_SWAGGER_PASSWORD=666 . ${NVM_DIR}/nvm.sh && nvm exec yarn start
|
||||
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true WHATSAPP_SWAGGER_PASSWORD=666 nvm exec yarn start
|
||||
|
||||
up-noweb:
|
||||
yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-11-21
|
||||
|
||||
@@ -70,16 +70,6 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# Enable CPU profiling
|
||||
#
|
||||
if [ "$WAHA_DEBUG_MODE" = "true" ] || [ "$WAHA_DEBUG_MODE" = "True" ] || [ "$WAHA_DEBUG_MODE" = "1" ]; then
|
||||
case " $NODE_OPTIONS " in
|
||||
*" --cpu-prof "*) ;;
|
||||
*) export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--cpu-prof" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
#
|
||||
# Start your application using node with exec to ensure proper signal handling
|
||||
#
|
||||
|
||||
+2
-1
@@ -70,7 +70,7 @@
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"google-protobuf": "^4.0.1",
|
||||
"google-protobuf": "^3.21.4",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"ical.js": "^2.2.1",
|
||||
"ioredis": "^5",
|
||||
@@ -106,6 +106,7 @@
|
||||
"swagger-ui-express": "^4.1.4",
|
||||
"ulid": "^2.3.0",
|
||||
"undici": "^7.16.0",
|
||||
"uniqid": "^5.4.0",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-11-05",
|
||||
"write-file-atomic": "^6.0.0",
|
||||
"yaml": "^2.7.1"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Post,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { RejectCallRequest } from '../structures/calls.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/calls')
|
||||
@ApiTags('📞 Calls')
|
||||
export class CallsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post('reject')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Reject incoming call' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
rejectCall(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: RejectCallRequest,
|
||||
) {
|
||||
return session.rejectCall(request.from, request.id);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
@@ -11,12 +12,16 @@ import {
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiBody, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import {
|
||||
AppsService,
|
||||
IAppsService,
|
||||
} from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import {
|
||||
SessionLogoutDeprecatedRequest,
|
||||
SessionStartDeprecatedRequest,
|
||||
@@ -29,17 +34,23 @@ import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import {
|
||||
ListSessionsQuery,
|
||||
MeInfo,
|
||||
SessionExpand,
|
||||
SessionInfoQuery,
|
||||
SessionCreateRequest,
|
||||
SessionDTO,
|
||||
SessionInfo,
|
||||
SessionUpdateRequest,
|
||||
} from '../structures/sessions.dto';
|
||||
import { SessionExamples } from './sessions.examples';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/sessions')
|
||||
@ApiTags('🖥️ Sessions')
|
||||
class SessionsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
constructor(
|
||||
private manager: SessionManager,
|
||||
@Inject(AppsService) private appsService: IAppsService,
|
||||
) {}
|
||||
|
||||
private withLock(name: string, fn: () => any) {
|
||||
return this.manager.withLock(name, fn);
|
||||
@@ -47,21 +58,33 @@ class SessionsController {
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({ summary: 'List all sessions' })
|
||||
list(
|
||||
async list(
|
||||
@Query(new WAHAValidationPipe()) query: ListSessionsQuery,
|
||||
): Promise<SessionInfo[]> {
|
||||
return this.manager.getSessions(query.all);
|
||||
const sessions = await this.manager.getSessions(query.all);
|
||||
if (query.expand?.includes(SessionExpand.apps)) {
|
||||
for (const session of sessions) {
|
||||
session.apps = await this.appsService.list(this.manager, session.name);
|
||||
}
|
||||
}
|
||||
return sessions;
|
||||
}
|
||||
|
||||
@Get('/:session')
|
||||
@ApiOperation({ summary: 'Get session information' })
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async get(@Param('session') name: string): Promise<SessionInfo> {
|
||||
async get(
|
||||
@Param('session') name: string,
|
||||
@Query() query: SessionInfoQuery,
|
||||
): Promise<SessionInfo> {
|
||||
const session = await this.manager.getSessionInfo(name);
|
||||
if (session === null) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
if (query.expand?.includes(SessionExpand.apps)) {
|
||||
session.apps = await this.appsService.list(this.manager, name);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
@@ -78,6 +101,7 @@ class SessionsController {
|
||||
description:
|
||||
'Create session a new session (and start it at the same time if required).',
|
||||
})
|
||||
@ApiBody({ type: SessionCreateRequest, examples: SessionExamples })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() request: SessionCreateRequest): Promise<SessionDTO> {
|
||||
const name = request.name || generatePrefixedId('session');
|
||||
@@ -86,16 +110,26 @@ class SessionsController {
|
||||
const msg = `Session '${name}' already exists. Use PUT to update it.`;
|
||||
throw new UnprocessableEntityException(msg);
|
||||
}
|
||||
|
||||
const config = request.config;
|
||||
const start = request.start || false;
|
||||
await this.manager.upsert(name, config);
|
||||
if (request.apps) {
|
||||
await this.appsService.syncSessionApps(
|
||||
this.manager,
|
||||
name,
|
||||
request.apps,
|
||||
);
|
||||
}
|
||||
if (start) {
|
||||
await this.manager.assign(name);
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
const session = await this.manager.getSessionInfo(name);
|
||||
if (request.apps) {
|
||||
session.apps = await this.appsService.list(this.manager, name);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
@Put(':session')
|
||||
@@ -104,7 +138,8 @@ class SessionsController {
|
||||
description: '',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiBody({ type: SessionUpdateRequest, examples: SessionExamples })
|
||||
@UsePipes(new WAHAValidationPipe({ forbidNonWhitelisted: false }))
|
||||
async update(
|
||||
@Param('session') name: string,
|
||||
@Body() request: SessionUpdateRequest,
|
||||
@@ -117,11 +152,22 @@ class SessionsController {
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.upsert(name, config);
|
||||
if (request.apps) {
|
||||
await this.appsService.syncSessionApps(
|
||||
this.manager,
|
||||
name,
|
||||
request.apps,
|
||||
);
|
||||
}
|
||||
if (isRunning) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
const session = await this.manager.getSessionInfo(name);
|
||||
if (request.apps) {
|
||||
session.apps = await this.appsService.list(this.manager, name);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
@Delete(':session')
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { AppName } from '@waha/apps/app_sdk/apps/name';
|
||||
|
||||
export const SessionExamples = {
|
||||
basic: {
|
||||
summary: 'Basic Session',
|
||||
value: {
|
||||
name: 'default',
|
||||
},
|
||||
},
|
||||
withWebhooks: {
|
||||
summary: 'Session Webhooks',
|
||||
value: {
|
||||
name: 'default',
|
||||
config: {
|
||||
webhooks: [
|
||||
{
|
||||
url: 'https://example.com/webhooks/waha',
|
||||
events: ['message', 'session.status'],
|
||||
hmac: {
|
||||
key: 'super-secret',
|
||||
},
|
||||
retries: {
|
||||
delaySeconds: 2,
|
||||
attempts: 5,
|
||||
policy: 'linear',
|
||||
},
|
||||
customHeaders: [
|
||||
{
|
||||
name: 'X-Request-ID',
|
||||
value: '123',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
withApps: {
|
||||
summary: 'Session Apps',
|
||||
value: {
|
||||
name: 'default',
|
||||
apps: [
|
||||
{
|
||||
app: AppName.calls,
|
||||
id: 'app_unique_id',
|
||||
session: '',
|
||||
config: {
|
||||
dm: {
|
||||
reject: true,
|
||||
message:
|
||||
"📞❌ We don't take calls right now.\n🎤 Please send a voice message or 📝 text — we'll reply ASAP!",
|
||||
},
|
||||
group: {
|
||||
reject: true,
|
||||
message:
|
||||
"📞❌ We don't take calls right now.\n🎤 Please send a voice message or 📝 text — we'll reply ASAP!",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
start: true,
|
||||
},
|
||||
},
|
||||
full: {
|
||||
summary: 'Full Session Example',
|
||||
value: {
|
||||
name: 'default',
|
||||
start: true,
|
||||
config: {
|
||||
metadata: {
|
||||
'user.id': '123',
|
||||
},
|
||||
debug: true,
|
||||
webhooks: [
|
||||
{
|
||||
url: 'https://example.com/webhooks/waha',
|
||||
events: ['message', 'session.status'],
|
||||
hmac: {
|
||||
key: 'super-secret',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
apps: [
|
||||
{
|
||||
app: AppName.calls,
|
||||
enabled: true,
|
||||
config: {
|
||||
dm: {
|
||||
reject: true,
|
||||
message:
|
||||
"📞❌ We don't take calls right now.\n🎤 Please send a voice message or 📝 text — we'll reply ASAP!",
|
||||
},
|
||||
group: {
|
||||
reject: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -45,7 +45,12 @@ export class AppsController {
|
||||
@ApiOperation({ summary: 'Create a new app' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() app: App): Promise<App> {
|
||||
return await this.appsService.create(this.manager, app);
|
||||
const result = await this.appsService.create(this.manager, app);
|
||||
const isRunning = this.manager.isRunning(app.session);
|
||||
if (isRunning && app.enabled) {
|
||||
await this.manager.restart(app.session);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@@ -67,13 +72,22 @@ export class AppsController {
|
||||
);
|
||||
}
|
||||
|
||||
return await this.appsService.update(this.manager, app);
|
||||
const result = await this.appsService.upsert(this.manager, app);
|
||||
const isRunning = this.manager.isRunning(app.session);
|
||||
if (isRunning) {
|
||||
await this.manager.restart(app.session);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@ApiOperation({ summary: 'Delete an app' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async delete(@Param('id') id: string): Promise<void> {
|
||||
await this.appsService.delete(this.manager, id);
|
||||
const app = await this.appsService.delete(this.manager, id);
|
||||
const isRunning = this.manager.isRunning(app.session);
|
||||
if (isRunning) {
|
||||
await this.manager.restart(app.session);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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';
|
||||
|
||||
class AppRuntimeConfigC {
|
||||
private constructor(private apps: AppDefinition[] | null) {}
|
||||
|
||||
static FromEnv(env: typeof AppEnv) {
|
||||
if (!env.enabled) {
|
||||
return new AppRuntimeConfigC(null);
|
||||
}
|
||||
let apps = Object.values(APPS);
|
||||
// Include
|
||||
if (env.on && env.on.length > 0) {
|
||||
apps = apps.filter((app) => env.on!.includes(app.name));
|
||||
}
|
||||
// Exclude
|
||||
if (env.off && env.off.length > 0) {
|
||||
apps = apps.filter((app) => !env.off!.includes(app.name));
|
||||
}
|
||||
return new AppRuntimeConfigC(apps);
|
||||
}
|
||||
|
||||
Enabled() {
|
||||
return this.apps !== null;
|
||||
}
|
||||
|
||||
GetApps() {
|
||||
return this.apps || [];
|
||||
}
|
||||
|
||||
GetAppsWithMigration() {
|
||||
return this.GetApps().filter((app) => app.migrations);
|
||||
}
|
||||
|
||||
GetAppsRequiringPlainKey() {
|
||||
return this.GetApps().filter((app) => app.plainkey);
|
||||
}
|
||||
|
||||
GetAppsRequiringQueue() {
|
||||
return this.GetApps().filter((app) => app.queue);
|
||||
}
|
||||
|
||||
HasApp(name: AppName) {
|
||||
return this.GetApps().some((app) => app.name === name);
|
||||
}
|
||||
|
||||
HasAppsRequiringPlainKey() {
|
||||
return this.GetAppsRequiringPlainKey().length > 0;
|
||||
}
|
||||
|
||||
HasAppsRequiringQueue() {
|
||||
return this.GetAppsRequiringQueue().length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const AppRuntimeConfig = AppRuntimeConfigC.FromEnv(AppEnv);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AppName } from '@waha/apps/app_sdk/apps/name';
|
||||
|
||||
export interface AppDefinition {
|
||||
// App name
|
||||
name: AppName;
|
||||
// If app requires WAHA_API_KEY_PLAIN to work
|
||||
plainkey: boolean;
|
||||
// If app requires queue to work
|
||||
queue: boolean;
|
||||
// If app has any migrations
|
||||
migrations: boolean;
|
||||
}
|
||||
|
||||
// All Apps
|
||||
export const APPS: Record<AppName, AppDefinition> = {
|
||||
[AppName.calls]: {
|
||||
name: AppName.calls,
|
||||
plainkey: false,
|
||||
queue: false,
|
||||
migrations: false,
|
||||
},
|
||||
[AppName.chatwoot]: {
|
||||
name: AppName.chatwoot,
|
||||
plainkey: true,
|
||||
queue: true,
|
||||
migrations: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum AppName {
|
||||
chatwoot = 'chatwoot',
|
||||
calls = 'calls',
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
|
||||
import { CallsAppConfig } from '@waha/apps/calls/dto/config.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
@@ -8,14 +9,11 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ApiExtraModels, ApiProperty } from '@nestjs/swagger';
|
||||
import { AppName } from '@waha/apps/app_sdk/apps/name';
|
||||
|
||||
export type AllowedAppConfig = ChatWootAppConfig;
|
||||
export type AllowedAppConfig = ChatWootAppConfig | CallsAppConfig;
|
||||
|
||||
export enum AppName {
|
||||
chatwoot = 'chatwoot',
|
||||
}
|
||||
|
||||
@ApiExtraModels(ChatWootAppConfig)
|
||||
@ApiExtraModels(ChatWootAppConfig, CallsAppConfig)
|
||||
export class App<T extends AllowedAppConfig = any> {
|
||||
@IsString()
|
||||
id: string;
|
||||
@@ -43,6 +41,8 @@ export class App<T extends AllowedAppConfig = any> {
|
||||
switch (options.object.app) {
|
||||
case AppName.chatwoot:
|
||||
return ChatWootAppConfig;
|
||||
case AppName.calls:
|
||||
return CallsAppConfig;
|
||||
default:
|
||||
return Object;
|
||||
}
|
||||
@@ -57,4 +57,9 @@ export class ChatWootAppDto extends App<ChatWootAppConfig> {
|
||||
config: ChatWootAppConfig;
|
||||
}
|
||||
|
||||
export type AppDto = ChatWootAppDto;
|
||||
export class CallsAppDto extends App<CallsAppConfig> {
|
||||
@Type(() => CallsAppConfig)
|
||||
config: CallsAppConfig;
|
||||
}
|
||||
|
||||
export type AppDto = ChatWootAppDto | CallsAppDto;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { parseBool } from '@waha/helpers';
|
||||
|
||||
function parseCommaSeparatedList(value: string | undefined): string[] {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return value.split(',').map((item) => item.trim());
|
||||
}
|
||||
|
||||
export const AppEnv = {
|
||||
enabled: parseBool(process.env.WAHA_APPS_ENABLED),
|
||||
on: parseCommaSeparatedList(process.env.WAHA_APPS_ON),
|
||||
off: parseCommaSeparatedList(process.env.WAHA_APPS_OFF),
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { Knex } from 'knex';
|
||||
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
|
||||
|
||||
function migrateSDK(knex: Knex): Promise<any> {
|
||||
const config = {
|
||||
@@ -25,5 +26,7 @@ function migrateApp(knex: Knex, app: string): Promise<any> {
|
||||
|
||||
export async function migrate(knex: Knex) {
|
||||
await migrateSDK(knex);
|
||||
await migrateApp(knex, 'chatwoot');
|
||||
for (const app of AppRuntimeConfig.GetAppsWithMigration()) {
|
||||
await migrateApp(knex, app.name);
|
||||
}
|
||||
}
|
||||
@@ -29,14 +29,22 @@ export class AppsDisabledService implements IAppsService {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async upsert(manager: SessionManager, app: App): Promise<App> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async update(manager: SessionManager, app: App): Promise<App> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async delete(manager: SessionManager, appId: string) {
|
||||
async delete(manager: SessionManager, appId: string): Promise<App> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async removeBySession(manager: SessionManager, session: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
migrate(knex: Knex<any, any[]>): Promise<void> {
|
||||
return;
|
||||
}
|
||||
@@ -48,4 +56,12 @@ export class AppsDisabledService implements IAppsService {
|
||||
async afterSessionStart(session: WhatsappSession, store: DataStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
async syncSessionApps(
|
||||
manager: SessionManager,
|
||||
sessionName: string,
|
||||
apps?: App[] | null,
|
||||
): Promise<void> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
Optional,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
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 { ChatWootAppService } from '@waha/apps/chatwoot/services/ChatWootAppService';
|
||||
import { CallsAppService } from '@waha/apps/calls/services/CallsAppService';
|
||||
import { DataStore } from '@waha/core/abc/DataStore';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
@@ -10,15 +16,26 @@ import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { Knex } from 'knex';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { App, AppName } from '../dto/app.dto';
|
||||
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';
|
||||
|
||||
export class AppDisableError extends UnprocessableEntityException {
|
||||
constructor(app: string) {
|
||||
super(
|
||||
`App '${app}' is disabled in runtime configuration - adjust WAHA_APPS_ON / WAHA_APPS_OFF environment variables to enable it.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AppsEnabledService implements IAppsService {
|
||||
constructor(
|
||||
protected readonly chatwootService: ChatWootAppService,
|
||||
@InjectPinoLogger('AppsService')
|
||||
protected logger: PinoLogger,
|
||||
@Optional() protected readonly chatwootService: ChatWootAppService,
|
||||
@Optional() protected readonly callsAppService: CallsAppService,
|
||||
) {}
|
||||
|
||||
async list(manager: SessionManager, session: string): Promise<App[]> {
|
||||
@@ -43,9 +60,12 @@ export class AppsEnabledService implements IAppsService {
|
||||
throw new Error(`App with ID '${app.id}' already exists.`);
|
||||
}
|
||||
|
||||
let existingApps: App[] = [];
|
||||
if (app.app === AppName.chatwoot || app.app === AppName.calls) {
|
||||
existingApps = await repo.getAllBySession(app.session);
|
||||
}
|
||||
// Validate only one Chatwoot app per session
|
||||
if (app.app === AppName.chatwoot) {
|
||||
const existingApps = await repo.getAllBySession(app.session);
|
||||
const existingChatwootApp = existingApps.find(
|
||||
(existingApp) => existingApp.app === AppName.chatwoot,
|
||||
);
|
||||
@@ -56,8 +76,23 @@ export class AppsEnabledService implements IAppsService {
|
||||
);
|
||||
}
|
||||
}
|
||||
// Validate only one Calls app per session
|
||||
if (app.app === AppName.calls) {
|
||||
const existingCallsApp = existingApps.find(
|
||||
(existingApp) => existingApp.app === AppName.calls,
|
||||
);
|
||||
|
||||
if (existingCallsApp) {
|
||||
throw new Error(
|
||||
`Only one Calls app is allowed per session. Session '${app.session}' already has a Calls app with ID '${existingCallsApp.id}'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const service = this.getAppService(app);
|
||||
if (!service && !AppRuntimeConfig.HasApp(app.app)) {
|
||||
throw new AppDisableError(app.app);
|
||||
}
|
||||
service.validate(app);
|
||||
// Only run beforeCreated when app is enabled (default true if omitted)
|
||||
if (app.enabled !== false) {
|
||||
@@ -65,9 +100,6 @@ export class AppsEnabledService implements IAppsService {
|
||||
}
|
||||
|
||||
const result = await repo.save(app);
|
||||
if (app.enabled) {
|
||||
await this.restartIfRunning(manager, app.session);
|
||||
}
|
||||
delete result.pk;
|
||||
return result;
|
||||
}
|
||||
@@ -83,11 +115,23 @@ export class AppsEnabledService implements IAppsService {
|
||||
return app;
|
||||
}
|
||||
|
||||
async update(manager: SessionManager, app: App): Promise<App> {
|
||||
async upsert(manager: SessionManager, app: App) {
|
||||
return await this.update(manager, app, true);
|
||||
}
|
||||
|
||||
async update(
|
||||
manager: SessionManager,
|
||||
app: App,
|
||||
upsert: boolean = false,
|
||||
): Promise<App> {
|
||||
await this.checkSessionExists(manager, app.session);
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const savedApp = await repo.getById(app.id);
|
||||
if (!savedApp && upsert) {
|
||||
return this.create(manager, app);
|
||||
}
|
||||
|
||||
if (!savedApp) {
|
||||
throw new NotFoundException(`App '${app.id}' not found`);
|
||||
}
|
||||
@@ -103,6 +147,9 @@ export class AppsEnabledService implements IAppsService {
|
||||
}
|
||||
|
||||
const service = this.getAppService(app);
|
||||
if (!service && !AppRuntimeConfig.HasApp(app.app)) {
|
||||
throw new AppDisableError(app.app);
|
||||
}
|
||||
service.validate(app);
|
||||
|
||||
const hasEnabledChange = savedApp.enabled !== app.enabled;
|
||||
@@ -116,16 +163,13 @@ export class AppsEnabledService implements IAppsService {
|
||||
} else {
|
||||
await service.beforeUpdated(savedApp, app);
|
||||
}
|
||||
|
||||
await repo.update(app.id, app);
|
||||
await this.restartIfRunning(manager, app.session);
|
||||
|
||||
const updated = await repo.getById(app.id);
|
||||
delete (updated as any)?.pk;
|
||||
return updated!;
|
||||
}
|
||||
|
||||
async delete(manager: SessionManager, appId: string) {
|
||||
async delete(manager: SessionManager, appId: string): Promise<App> {
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const app = await repo.getById(appId);
|
||||
@@ -133,10 +177,16 @@ export class AppsEnabledService implements IAppsService {
|
||||
throw new NotFoundException(`App '${appId}' not found`);
|
||||
}
|
||||
const service = this.getAppService(app);
|
||||
await service.beforeDeleted(app);
|
||||
await service?.beforeDeleted(app);
|
||||
await repo.delete(app.id);
|
||||
await this.restartIfRunning(manager, app.session);
|
||||
return;
|
||||
delete app.pk;
|
||||
return app;
|
||||
}
|
||||
|
||||
async removeBySession(manager: SessionManager, session: string) {
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
await repo.deleteBySession(session);
|
||||
}
|
||||
|
||||
async beforeSessionStart(session: WhatsappSession, store: DataStore) {
|
||||
@@ -145,6 +195,9 @@ export class AppsEnabledService implements IAppsService {
|
||||
const apps = await repo.getEnabledBySession(session.name);
|
||||
for (const app of apps) {
|
||||
const service = this.getAppService(app);
|
||||
if (!service && !AppRuntimeConfig.HasApp(app.app)) {
|
||||
throw new AppDisableError(app.app);
|
||||
}
|
||||
service.beforeSessionStart(app, session);
|
||||
}
|
||||
}
|
||||
@@ -155,26 +208,55 @@ export class AppsEnabledService implements IAppsService {
|
||||
const apps = await repo.getEnabledBySession(session.name);
|
||||
for (const app of apps) {
|
||||
const service = this.getAppService(app);
|
||||
if (!service && !AppRuntimeConfig.HasApp(app.app)) {
|
||||
throw new AppDisableError(app.app);
|
||||
}
|
||||
service.afterSessionStart(app, session);
|
||||
}
|
||||
}
|
||||
|
||||
async syncSessionApps(
|
||||
manager: SessionManager,
|
||||
session: string,
|
||||
apps: App[],
|
||||
): Promise<void> {
|
||||
const existing = await this.list(manager, session);
|
||||
const ids = new Set<string>();
|
||||
|
||||
// Upsert provided apps
|
||||
for (const app of apps) {
|
||||
if (!app.id) {
|
||||
// Try to find the app by type
|
||||
const found = existing.find((a) => a.app === app.app);
|
||||
if (found) {
|
||||
app.id = found.id;
|
||||
}
|
||||
}
|
||||
// Force session
|
||||
app.session = session;
|
||||
await this.upsert(manager, app);
|
||||
ids.add(app.id);
|
||||
}
|
||||
|
||||
// Remove apps that are not in the provided list
|
||||
for (const app of existing) {
|
||||
if (ids.has(app.id)) {
|
||||
continue;
|
||||
}
|
||||
await this.delete(manager, app.id);
|
||||
}
|
||||
}
|
||||
|
||||
async migrate(knex: Knex): Promise<void> {
|
||||
await migrate(knex);
|
||||
}
|
||||
|
||||
private restartIfRunning(manager: SessionManager, session: string) {
|
||||
const isRunning = manager.isRunning(session);
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
return manager.restart(session);
|
||||
}
|
||||
|
||||
private getAppService(app: App): IAppService {
|
||||
private getAppService(app: App): IAppService | null {
|
||||
switch (app.app) {
|
||||
case AppName.chatwoot:
|
||||
return this.chatwootService;
|
||||
case AppName.calls:
|
||||
return this.callsAppService;
|
||||
default:
|
||||
throw new Error(`App '${app.app}' not supported`);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,24 @@ export interface IAppsService {
|
||||
|
||||
create(manager: SessionManager, app: App): Promise<App>;
|
||||
|
||||
upsert(manager: SessionManager, app: App): Promise<App>;
|
||||
|
||||
update(manager: SessionManager, app: App): Promise<App>;
|
||||
|
||||
delete(manager: SessionManager, appId: string): Promise<void>;
|
||||
delete(manager: SessionManager, appId: string): Promise<App>;
|
||||
|
||||
removeBySession(manager: SessionManager, session: string): Promise<void>;
|
||||
|
||||
beforeSessionStart(session: WhatsappSession, store: DataStore): Promise<void>;
|
||||
|
||||
afterSessionStart(session: WhatsappSession, store: DataStore): Promise<void>;
|
||||
|
||||
syncSessionApps(
|
||||
manager: SessionManager,
|
||||
sessionName: string,
|
||||
apps?: App[] | null,
|
||||
): Promise<void>;
|
||||
|
||||
migrate(knex: Knex): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,4 +123,11 @@ export class AppRepository {
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.knex(this.tableName).where('id', id).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all apps for a session
|
||||
*/
|
||||
async deleteBySession(session: string): Promise<void> {
|
||||
await this.knex(this.tableName).where('session', session).delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { AppsService } from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { AppsDisabledService } from '@waha/apps/app_sdk/services/AppsDisabledService';
|
||||
import { AppsController } from '@waha/apps/app_sdk/api/apps.controller';
|
||||
import { ChatwootLocalesController } from '@waha/apps/chatwoot/api/chatwoot.locales.controller';
|
||||
|
||||
export const AppsDisabled = {
|
||||
providers: [
|
||||
{
|
||||
provide: AppsService,
|
||||
useClass: AppsDisabledService,
|
||||
},
|
||||
],
|
||||
imports: [],
|
||||
controllers: [AppsController, ChatwootLocalesController],
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { RedisModule } from '@liaoliaots/nestjs-redis';
|
||||
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 { ChatWootExports } from '@waha/apps/chatwoot/chatwoot.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';
|
||||
|
||||
const QUEUES_IMPORTS_REQUIRED = [
|
||||
BullModule.forRoot({
|
||||
connection: {
|
||||
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
|
||||
maxRetriesPerRequest: null,
|
||||
},
|
||||
prefix: `waha-${process.env.WAHA_WORKER_ID}`,
|
||||
}),
|
||||
RedisModule.forRoot({
|
||||
closeClient: true,
|
||||
config: {
|
||||
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
|
||||
onClientCreated: async (client) => {
|
||||
try {
|
||||
await client.ping();
|
||||
} catch (err) {
|
||||
console.error('[Redis] Connection failed:', err);
|
||||
process.exit(1); // Stop the app if Redis is unavailable
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
RMutexModule,
|
||||
BullBoardModule.forRoot({
|
||||
route: '/jobs',
|
||||
adapter: ExpressAdapter,
|
||||
middleware: BullAuthMiddleware(),
|
||||
boardOptions: {
|
||||
uiConfig: {
|
||||
boardTitle: 'Jobs | WAHA',
|
||||
boardLogo: {
|
||||
path: '/dashboard/layout/images/logo-white.svg',
|
||||
width: 35,
|
||||
height: 35,
|
||||
},
|
||||
favIcon: {
|
||||
default: '/dashboard/favicon.ico',
|
||||
alternative: '/dashboard/favicon.ico',
|
||||
},
|
||||
miscLinks: [
|
||||
{
|
||||
text: '📊 Dashboard',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
text: '📚 Swagger (OpenAPI)',
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
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;
|
||||
default:
|
||||
throw Error(`App module not found for ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const AppsEnabled = {
|
||||
imports: [
|
||||
...QUEUES_IMPORTS,
|
||||
...getAppModule(AppName.chatwoot).imports,
|
||||
...getAppModule(AppName.calls).imports,
|
||||
],
|
||||
controllers: [
|
||||
AppsController,
|
||||
...getAppModule(AppName.chatwoot).controllers,
|
||||
...getAppModule(AppName.calls).controllers,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: AppsService,
|
||||
useClass: AppsEnabledService,
|
||||
},
|
||||
...getAppModule(AppName.calls).providers,
|
||||
...getAppModule(AppName.chatwoot).providers,
|
||||
],
|
||||
};
|
||||
|
||||
function checkApiKey() {
|
||||
const key = Auth.key.value;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const plain = Auth.keyplain.value;
|
||||
if (!plain) {
|
||||
throw Error(
|
||||
'WAHA_API_KEY set, please provide WAHA_API_KEY_PLAIN when WAHA_APPS_ENABLED',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (AppRuntimeConfig.HasAppsRequiringPlainKey()) {
|
||||
checkApiKey();
|
||||
}
|
||||
+6
-112
@@ -1,113 +1,7 @@
|
||||
import * as process from 'node:process';
|
||||
import { AppRuntimeConfig } from '@waha/apps/app_sdk/apps/AppRuntime';
|
||||
import { AppsDisabled } from '@waha/apps/apps.module.disabled';
|
||||
import { AppsEnabled } from '@waha/apps/apps.module.enabled';
|
||||
|
||||
import { ExpressAdapter } from '@bull-board/express';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { RedisModule } from '@liaoliaots/nestjs-redis';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { AppsController } from '@waha/apps/app_sdk/api/apps.controller';
|
||||
import { BullAuthMiddleware } from '@waha/apps/app_sdk/auth';
|
||||
import { AppsDisabledService } from '@waha/apps/app_sdk/services/AppsDisabledService';
|
||||
import { AppsEnabledService } from '@waha/apps/app_sdk/services/AppsEnabledService';
|
||||
import { AppsService } from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { ChatwootLocalesController } from '@waha/apps/chatwoot/api/chatwoot.locales.controller';
|
||||
import { ChatWootExports } from '@waha/apps/chatwoot/chatwoot.module';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import { RMutexModule } from '@waha/modules/rmutex';
|
||||
import { Auth } from '@waha/core/auth/config';
|
||||
|
||||
const IMPORTS = [
|
||||
BullModule.forRoot({
|
||||
connection: {
|
||||
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
|
||||
maxRetriesPerRequest: null,
|
||||
},
|
||||
prefix: `waha-${process.env.WAHA_WORKER_ID}`,
|
||||
}),
|
||||
RedisModule.forRoot({
|
||||
closeClient: true,
|
||||
config: {
|
||||
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
|
||||
onClientCreated: async (client) => {
|
||||
try {
|
||||
await client.ping();
|
||||
} catch (err) {
|
||||
console.error('[Redis] Connection failed:', err);
|
||||
process.exit(1); // Stop the app if Redis is unavailable
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
RMutexModule,
|
||||
BullBoardModule.forRoot({
|
||||
route: '/jobs',
|
||||
adapter: ExpressAdapter,
|
||||
middleware: BullAuthMiddleware(),
|
||||
boardOptions: {
|
||||
uiConfig: {
|
||||
boardTitle: 'Jobs | WAHA',
|
||||
boardLogo: {
|
||||
path: '/dashboard/layout/images/logo-white.svg',
|
||||
width: 35,
|
||||
height: 35,
|
||||
},
|
||||
favIcon: {
|
||||
default: '/dashboard/favicon.ico',
|
||||
alternative: '/dashboard/favicon.ico',
|
||||
},
|
||||
miscLinks: [
|
||||
{
|
||||
text: '📊 Dashboard',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
text: '📚 Swagger (OpenAPI)',
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
...ChatWootExports.imports,
|
||||
];
|
||||
|
||||
const AppsEnabled = {
|
||||
imports: IMPORTS,
|
||||
controllers: [AppsController, ...ChatWootExports.controllers],
|
||||
providers: [
|
||||
{
|
||||
provide: AppsService,
|
||||
useClass: AppsEnabledService,
|
||||
},
|
||||
...ChatWootExports.providers,
|
||||
],
|
||||
};
|
||||
|
||||
const AppsDisabled = {
|
||||
providers: [
|
||||
{
|
||||
provide: AppsService,
|
||||
useClass: AppsDisabledService,
|
||||
},
|
||||
],
|
||||
imports: [],
|
||||
controllers: [AppsController, ChatwootLocalesController],
|
||||
};
|
||||
|
||||
function checkApiKey() {
|
||||
const key = Auth.key.value;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const plain = Auth.keyplain.value;
|
||||
if (!plain) {
|
||||
throw Error(
|
||||
'WAHA_API_KEY set, please provide WAHA_API_KEY_PLAIN when WAHA_APPS_ENABLED',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enabled = parseBool(process.env.WAHA_APPS_ENABLED);
|
||||
if (enabled) {
|
||||
checkApiKey();
|
||||
}
|
||||
export const AppsModuleExports = enabled ? AppsEnabled : AppsDisabled;
|
||||
export const AppsModuleExports = AppRuntimeConfig.Enabled()
|
||||
? AppsEnabled
|
||||
: AppsDisabled;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { CallsAppService } from '@waha/apps/calls/services/CallsAppService';
|
||||
|
||||
export const CallsAppExports = {
|
||||
providers: [CallsAppService],
|
||||
imports: [],
|
||||
controllers: [],
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CallsAppChannelConfig {
|
||||
@ApiProperty({
|
||||
description: 'Reject incoming calls for this chat type',
|
||||
default: true,
|
||||
})
|
||||
@IsBoolean()
|
||||
reject: boolean = true;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Optional auto-reply message sent after the call is rejected. If empty, no message is sent.',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export class CallsAppConfig {
|
||||
@ApiProperty({
|
||||
description: 'Rules applied to direct messages (non-group calls)',
|
||||
type: CallsAppChannelConfig,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => CallsAppChannelConfig)
|
||||
dm: CallsAppChannelConfig = new CallsAppChannelConfig();
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Rules applied to group calls',
|
||||
type: CallsAppChannelConfig,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => CallsAppChannelConfig)
|
||||
group: CallsAppChannelConfig = new CallsAppChannelConfig();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
import { IAppService } from '@waha/apps/app_sdk/services/IAppService';
|
||||
import { CallsAppConfig } from '@waha/apps/calls/dto/config.dto';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
import { CallsListener } from '@waha/apps/calls/services/CallsListener';
|
||||
|
||||
@Injectable()
|
||||
export class CallsAppService implements IAppService {
|
||||
constructor(
|
||||
@InjectPinoLogger('CallsAppService')
|
||||
private readonly logger: PinoLogger,
|
||||
) {}
|
||||
|
||||
validate(app: App<CallsAppConfig>): void {
|
||||
// The DTO validation covers structure; no extra validation rules.
|
||||
void app;
|
||||
return;
|
||||
}
|
||||
|
||||
async beforeCreated(app: App<CallsAppConfig>): Promise<void> {
|
||||
void app;
|
||||
return;
|
||||
}
|
||||
|
||||
async beforeEnabled(
|
||||
savedApp: App<CallsAppConfig>,
|
||||
newApp: App<CallsAppConfig>,
|
||||
): Promise<void> {
|
||||
// Enabling behaves the same as creating for this lightweight app.
|
||||
void savedApp;
|
||||
void newApp;
|
||||
return;
|
||||
}
|
||||
|
||||
async beforeDisabled(
|
||||
savedApp: App<CallsAppConfig>,
|
||||
newApp: App<CallsAppConfig>,
|
||||
): Promise<void> {
|
||||
void newApp;
|
||||
void savedApp;
|
||||
}
|
||||
|
||||
async beforeUpdated(
|
||||
savedApp: App<CallsAppConfig>,
|
||||
newApp: App<CallsAppConfig>,
|
||||
): Promise<void> {
|
||||
void savedApp;
|
||||
void newApp;
|
||||
}
|
||||
|
||||
async beforeDeleted(app: App<CallsAppConfig>): Promise<void> {
|
||||
void app;
|
||||
}
|
||||
|
||||
beforeSessionStart(app: App<CallsAppConfig>, session: WhatsappSession): void {
|
||||
const listener = new CallsListener(app, session, this.logger);
|
||||
listener.attach();
|
||||
}
|
||||
|
||||
afterSessionStart(app: App<CallsAppConfig>, session: WhatsappSession): void {
|
||||
void app;
|
||||
void session;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Subscription } from 'rxjs';
|
||||
import {
|
||||
CallsAppChannelConfig,
|
||||
CallsAppConfig,
|
||||
} from '@waha/apps/calls/dto/config.dto';
|
||||
import { Logger } from 'pino';
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { WAHAEvents, WAHAPresenceStatus } from '@waha/structures/enums.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import { MessageTextRequest } from '@waha/structures/chatting.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
|
||||
export class CallsListener {
|
||||
private subscription?: Subscription;
|
||||
private config: CallsAppConfig;
|
||||
private readonly log: Logger;
|
||||
private readonly session: WhatsappSession;
|
||||
|
||||
constructor(
|
||||
app: App<CallsAppConfig>,
|
||||
session: WhatsappSession,
|
||||
logger: PinoLogger,
|
||||
) {
|
||||
this.session = session;
|
||||
this.config = app.config;
|
||||
this.log = logger.logger.child({
|
||||
app: 'calls',
|
||||
session: app.session,
|
||||
});
|
||||
}
|
||||
|
||||
attach(): void {
|
||||
this.detach();
|
||||
|
||||
const observable = this.session.getEventObservable(
|
||||
WAHAEvents.CALL_RECEIVED,
|
||||
);
|
||||
if (!observable) {
|
||||
this.log.warn('CALL_RECEIVED event stream is not available, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscription = observable.subscribe((payload) => {
|
||||
this.handleCall(payload as CallData).catch((error) => {
|
||||
this.log.error(
|
||||
{ err: error, callId: (payload as any)?.id },
|
||||
'Failed to handle incoming call',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
this.log.info('Calls app listener is attached');
|
||||
}
|
||||
|
||||
detach(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
|
||||
private configFor(call: CallData): CallsAppChannelConfig {
|
||||
return call?.isGroup ? this.config?.group : this.config?.dm;
|
||||
}
|
||||
|
||||
private async handleCall(call: CallData): Promise<void> {
|
||||
if (!call.from) {
|
||||
this.log.warn({ call: call?.id }, 'Incoming call has no chat id');
|
||||
return;
|
||||
}
|
||||
if (!call.id) {
|
||||
this.log.warn({ from: call.from }, 'Incoming call has no from');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = this.configFor(call);
|
||||
if (!config) {
|
||||
this.log.warn({ callId: call.id }, 'No calls config found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = (config.message || '').trim();
|
||||
const shouldReject = !!config.reject;
|
||||
const shouldMessage = message.length > 0;
|
||||
|
||||
if (!shouldReject && !shouldMessage) {
|
||||
this.log.debug(
|
||||
{ callId: call.id, chatId: call.from },
|
||||
'No actions configured for this call',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldReject) {
|
||||
await this.rejectCall(call);
|
||||
}
|
||||
|
||||
if (shouldMessage) {
|
||||
await this.replyWithTyping(call.from, message);
|
||||
}
|
||||
}
|
||||
|
||||
private async rejectCall(call: CallData): Promise<void> {
|
||||
this.log.debug({ from: call.from, id: call.id }, 'Rejecting incoming call');
|
||||
await this.session.rejectCall(call.from, call.id);
|
||||
this.log.info({ from: call.from, id: call.id }, 'Call rejected');
|
||||
}
|
||||
|
||||
private async replyWithTyping(
|
||||
chatId: string,
|
||||
message: string,
|
||||
): Promise<void> {
|
||||
this.log.info(
|
||||
{ chatId: chatId },
|
||||
'Sending auto-response for rejected call',
|
||||
);
|
||||
|
||||
await this.setTyping(chatId);
|
||||
await this.setPaused(chatId);
|
||||
await this.session.sendText({
|
||||
session: this.session.name,
|
||||
chatId: chatId,
|
||||
text: message,
|
||||
} as MessageTextRequest);
|
||||
}
|
||||
|
||||
private async setTyping(chatId: string): Promise<void> {
|
||||
try {
|
||||
await this.session.setPresence(WAHAPresenceStatus.TYPING, chatId);
|
||||
} catch (error) {
|
||||
this.log.warn(
|
||||
{ err: error, chatId: chatId },
|
||||
'Failed to set typing presence before reply',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
private async setPaused(chatId: string): Promise<void> {
|
||||
try {
|
||||
await this.session.setPresence(WAHAPresenceStatus.PAUSED, chatId);
|
||||
} catch (error) {
|
||||
this.log.warn(
|
||||
{ err: error, chatId: chatId },
|
||||
'Failed to clear typing presence after reply',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ import { WAHAMessageReactionConsumer } from './consumers/waha/message.reaction';
|
||||
import { WAHAMessageRevokedConsumer } from './consumers/waha/message.revoked';
|
||||
import { WAHAMessageAckConsumer } from './consumers/waha/message.ack';
|
||||
import { WAHASessionStatusConsumer } from './consumers/waha/session.status';
|
||||
import { WAHACallAcceptedConsumer } from './consumers/waha/call.accepted';
|
||||
import { WAHACallReceivedConsumer } from './consumers/waha/call.received';
|
||||
import { WAHACallRejectedConsumer } from './consumers/waha/call.rejected';
|
||||
import { ChatWootQueueService } from './services/ChatWootQueueService';
|
||||
import { ChatWootScheduleService } from './services/ChatWootScheduleService';
|
||||
import { ChatWootWAHAQueueService } from './services/ChatWootWAHAQueueService';
|
||||
@@ -76,6 +79,18 @@ const IMPORTS = lodash.flatten([
|
||||
name: QueueName.WAHA_MESSAGE_ACK,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.WAHA_CALL_RECEIVED,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.WAHA_CALL_ACCEPTED,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.WAHA_CALL_REJECTED,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.WAHA_SESSION_STATUS,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
@@ -124,6 +139,9 @@ const PROVIDERS = [
|
||||
WAHAMessageEditedConsumer,
|
||||
WAHAMessageRevokedConsumer,
|
||||
WAHAMessageAckConsumer,
|
||||
WAHACallReceivedConsumer,
|
||||
WAHACallAcceptedConsumer,
|
||||
WAHACallRejectedConsumer,
|
||||
// Scheduled
|
||||
MessageCleanupConsumer,
|
||||
CheckVersionConsumer,
|
||||
|
||||
@@ -20,6 +20,9 @@ export enum QueueName {
|
||||
WAHA_MESSAGE_EDITED = 'chatwoot.waha | message.edited',
|
||||
WAHA_MESSAGE_REVOKED = 'chatwoot.waha | message.revoked',
|
||||
WAHA_MESSAGE_ACK = 'chatwoot.waha | message.ack',
|
||||
WAHA_CALL_RECEIVED = 'chatwoot.waha | call.received',
|
||||
WAHA_CALL_ACCEPTED = 'chatwoot.waha | call.accepted',
|
||||
WAHA_CALL_REJECTED = 'chatwoot.waha | call.rejected',
|
||||
//
|
||||
// ChatWoot Events - Real
|
||||
//
|
||||
|
||||
@@ -24,7 +24,7 @@ import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { MessageSource, WAMessageBase } from '@waha/structures/responses.dto';
|
||||
import { MessageSource } from '@waha/structures/responses.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
@@ -34,16 +34,26 @@ import { isJidBroadcast, isJidGroup, toCusFormat } from '@waha/core/utils/jids';
|
||||
import { EngineHelper } from '@waha/apps/chatwoot/waha';
|
||||
import { EnsureSeconds } from '@waha/utils/timehelper';
|
||||
import { CHATWOOT_MESSAGE_CALENDAR_THRESHOLD_SECONDS } from '@waha/apps/chatwoot/env';
|
||||
import {
|
||||
ChatWootAppConfig,
|
||||
ChatWootConfig,
|
||||
} from '@waha/apps/chatwoot/dto/config.dto';
|
||||
|
||||
export function ListenEventsForChatWoot() {
|
||||
return [
|
||||
export function ListenEventsForChatWoot(config: ChatWootConfig) {
|
||||
const events = [
|
||||
WAHAEvents.MESSAGE_ANY,
|
||||
WAHAEvents.MESSAGE_REACTION,
|
||||
WAHAEvents.MESSAGE_EDITED,
|
||||
WAHAEvents.MESSAGE_REVOKED,
|
||||
WAHAEvents.MESSAGE_ACK,
|
||||
WAHAEvents.SESSION_STATUS,
|
||||
WAHAEvents.CALL_RECEIVED,
|
||||
WAHAEvents.CALL_ACCEPTED,
|
||||
WAHAEvents.CALL_REJECTED,
|
||||
];
|
||||
if (config.conversations.markAsRead) {
|
||||
events.push(WAHAEvents.MESSAGE_ACK);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,7 +205,17 @@ export interface ChatWootMessagePartial {
|
||||
private?: boolean;
|
||||
}
|
||||
|
||||
export abstract class MessageBaseHandler<Payload extends WAMessageBase> {
|
||||
export interface MessageBaseHandlerPayload {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
from?: string;
|
||||
fromMe?: boolean;
|
||||
source?: MessageSource;
|
||||
}
|
||||
|
||||
export abstract class MessageBaseHandler<
|
||||
Payload extends MessageBaseHandlerPayload,
|
||||
> {
|
||||
constructor(
|
||||
protected job: Job,
|
||||
protected mappingService: MessageMappingService,
|
||||
@@ -305,7 +325,7 @@ export abstract class MessageBaseHandler<Payload extends WAMessageBase> {
|
||||
|
||||
private async saveMapping(
|
||||
chatwootMessage: generic_id & message,
|
||||
whatsappMessage: WAMessageBase,
|
||||
whatsappMessage: MessageBaseHandlerPayload,
|
||||
) {
|
||||
const chatwoot: Omit<ChatwootMessage, 'id'> = {
|
||||
timestamp: new Date(chatwootMessage.created_at * 1000),
|
||||
@@ -369,12 +389,13 @@ export abstract class MessageBaseHandler<Payload extends WAMessageBase> {
|
||||
},
|
||||
);
|
||||
|
||||
const type = payload.fromMe ? MessageType.OUTGOING : MessageType.INCOMING;
|
||||
const private_ = message.private ?? payload.fromMe;
|
||||
const type = private_ ? MessageType.OUTGOING : MessageType.INCOMING;
|
||||
content = this.finalizeContent(content, payload);
|
||||
return {
|
||||
content: content,
|
||||
message_type: type,
|
||||
private: message.private ?? payload.fromMe,
|
||||
private: private_,
|
||||
attachments: message.attachments as any,
|
||||
content_attributes: {
|
||||
in_reply_to: replyTo,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import { MessageSource } from '@waha/structures/responses.dto';
|
||||
import { SerializeMessageKey } from '@waha/core/utils/ids';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { MessageBaseHandlerPayload } from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
import { EngineHelper } from '@waha/apps/chatwoot/waha';
|
||||
|
||||
export function ShouldProcessCall(event: { payload: CallData }): boolean {
|
||||
if (event.payload.isGroup) {
|
||||
// No group calls for now
|
||||
// GroupJID (group-jid tag) is empty in all cases
|
||||
// and the "call" appears in the 1-1 chat
|
||||
// with the participant who started the call
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The MessageBaseHandler works on top of "MessageBase",
|
||||
// but we need the call original properties, so we keep it here
|
||||
export type CallMessagePayload = CallData &
|
||||
MessageBaseHandlerPayload & { callId: string };
|
||||
|
||||
const SUFFIX = {
|
||||
[WAHAEvents.CALL_RECEIVED]: '0',
|
||||
[WAHAEvents.CALL_ACCEPTED]: '1',
|
||||
[WAHAEvents.CALL_REJECTED]: '2',
|
||||
};
|
||||
type Events = keyof typeof SUFFIX;
|
||||
|
||||
/**
|
||||
* So we can reference from "accepted" or "rejected" to the first one, "received"
|
||||
* and "reply" to it
|
||||
*/
|
||||
export function BuildFakeCallMessageId(id: string, event: Events) {
|
||||
return id + SUFFIX[event];
|
||||
}
|
||||
|
||||
export function BuildCallMessagePayload(
|
||||
call: CallData,
|
||||
event: Events,
|
||||
): CallMessagePayload {
|
||||
const key = {
|
||||
fromMe: false,
|
||||
remoteJid: EngineHelper.CallChatID(call),
|
||||
id: BuildFakeCallMessageId(call.id, event),
|
||||
};
|
||||
const id = SerializeMessageKey(key);
|
||||
return {
|
||||
...call,
|
||||
callId: call.id,
|
||||
id: id,
|
||||
// Use the "Creator" one if there's any
|
||||
from: EngineHelper.CallChatID(call),
|
||||
fromMe: false,
|
||||
source: MessageSource.APP,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { WAHASessionAPI } from '@waha/apps/app_sdk/waha/WAHASelf';
|
||||
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||
import { EventData } from '@waha/apps/chatwoot/consumers/types';
|
||||
import {
|
||||
ChatWootMessagePartial,
|
||||
ChatWootWAHABaseConsumer,
|
||||
IMessageInfo,
|
||||
MessageBaseHandler,
|
||||
} from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
import {
|
||||
BuildCallMessagePayload,
|
||||
BuildFakeCallMessageId,
|
||||
CallMessagePayload,
|
||||
ShouldProcessCall,
|
||||
} from '@waha/apps/chatwoot/consumers/waha/call.0.base';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { EngineHelper } from '@waha/apps/chatwoot/waha';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { WAHAWebhookCallAccepted } from '@waha/structures/webhooks.dto';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
|
||||
|
||||
@Processor(QueueName.WAHA_CALL_ACCEPTED, { concurrency: JOB_CONCURRENCY })
|
||||
export class WAHACallAcceptedConsumer extends ChatWootWAHABaseConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'WAHACallAcceptedConsumer');
|
||||
}
|
||||
|
||||
ShouldProcess(event: WAHAWebhookCallAccepted): boolean {
|
||||
return ShouldProcessCall(event);
|
||||
}
|
||||
|
||||
GetChatId(event: WAHAWebhookCallAccepted): string {
|
||||
return EngineHelper.CallChatID(event.payload as any);
|
||||
}
|
||||
|
||||
async Process(
|
||||
job: Job<EventData, any, WAHAEvents>,
|
||||
info: IMessageInfo,
|
||||
): Promise<any> {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const event = job.data.event as WAHAWebhookCallAccepted;
|
||||
const locale = container.Locale();
|
||||
const handler = new CallAcceptedMessageHandler(
|
||||
job,
|
||||
container.MessageMappingService(),
|
||||
container.ContactConversationService(),
|
||||
container.Logger(),
|
||||
info,
|
||||
new WAHASessionAPI(event.session, container.WAHASelf()),
|
||||
locale,
|
||||
container.WAHASelf(),
|
||||
);
|
||||
const msg = BuildCallMessagePayload(
|
||||
event.payload,
|
||||
WAHAEvents.CALL_ACCEPTED,
|
||||
);
|
||||
return await handler.handle(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export class CallAcceptedMessageHandler extends MessageBaseHandler<CallMessagePayload> {
|
||||
protected async getMessage(
|
||||
payload: CallMessagePayload,
|
||||
): Promise<ChatWootMessagePartial> {
|
||||
const content = this.l
|
||||
.key(TKey.WA_TO_CW_CALL_ACCEPTED)
|
||||
.render({ call: payload });
|
||||
return {
|
||||
content: WhatsappToMarkdown(content),
|
||||
attachments: [],
|
||||
private: true,
|
||||
};
|
||||
}
|
||||
|
||||
getReplyToWhatsAppID(payload: CallMessagePayload): string | undefined {
|
||||
return BuildFakeCallMessageId(payload.callId, WAHAEvents.CALL_RECEIVED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { WAHASessionAPI } from '@waha/apps/app_sdk/waha/WAHASelf';
|
||||
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||
import { EventData } from '@waha/apps/chatwoot/consumers/types';
|
||||
import {
|
||||
ChatWootMessagePartial,
|
||||
ChatWootWAHABaseConsumer,
|
||||
IMessageInfo,
|
||||
MessageBaseHandler,
|
||||
MessageBaseHandlerPayload,
|
||||
} from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
import {
|
||||
BuildCallMessagePayload,
|
||||
CallMessagePayload,
|
||||
ShouldProcessCall,
|
||||
} from '@waha/apps/chatwoot/consumers/waha/call.0.base';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { EngineHelper } from '@waha/apps/chatwoot/waha';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { WAHAWebhookCallReceived } from '@waha/structures/webhooks.dto';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
|
||||
|
||||
@Processor(QueueName.WAHA_CALL_RECEIVED, { concurrency: JOB_CONCURRENCY })
|
||||
export class WAHACallReceivedConsumer extends ChatWootWAHABaseConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'WAHACallReceivedConsumer');
|
||||
}
|
||||
|
||||
ShouldProcess(event: WAHAWebhookCallReceived): boolean {
|
||||
return ShouldProcessCall(event);
|
||||
}
|
||||
|
||||
GetChatId(event: WAHAWebhookCallReceived): string {
|
||||
return EngineHelper.CallChatID(event.payload as any);
|
||||
}
|
||||
|
||||
async Process(
|
||||
job: Job<EventData, any, WAHAEvents>,
|
||||
info: IMessageInfo,
|
||||
): Promise<any> {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const event = job.data.event as WAHAWebhookCallReceived;
|
||||
const locale = container.Locale();
|
||||
const handler = new CallReceivedMessageHandler(
|
||||
job,
|
||||
container.MessageMappingService(),
|
||||
container.ContactConversationService(),
|
||||
container.Logger(),
|
||||
info,
|
||||
new WAHASessionAPI(event.session, container.WAHASelf()),
|
||||
locale,
|
||||
container.WAHASelf(),
|
||||
);
|
||||
const msg = BuildCallMessagePayload(
|
||||
event.payload,
|
||||
event.event as WAHAEvents.CALL_RECEIVED,
|
||||
);
|
||||
return await handler.handle(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export class CallReceivedMessageHandler extends MessageBaseHandler<CallMessagePayload> {
|
||||
protected async getMessage(
|
||||
payload: MessageBaseHandlerPayload & CallData,
|
||||
): Promise<ChatWootMessagePartial> {
|
||||
const content = this.l
|
||||
.key(TKey.WA_TO_CW_CALL_RECEIVED)
|
||||
.render({ call: payload });
|
||||
return {
|
||||
content: WhatsappToMarkdown(content),
|
||||
attachments: [],
|
||||
private: false,
|
||||
};
|
||||
}
|
||||
|
||||
getReplyToWhatsAppID(
|
||||
payload: MessageBaseHandlerPayload & CallData,
|
||||
): string | undefined {
|
||||
// No reply required for the first call received
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { WAHASessionAPI } from '@waha/apps/app_sdk/waha/WAHASelf';
|
||||
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||
import { EventData } from '@waha/apps/chatwoot/consumers/types';
|
||||
import {
|
||||
ChatWootMessagePartial,
|
||||
ChatWootWAHABaseConsumer,
|
||||
IMessageInfo,
|
||||
MessageBaseHandler,
|
||||
} from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
import {
|
||||
BuildCallMessagePayload,
|
||||
BuildFakeCallMessageId,
|
||||
CallMessagePayload,
|
||||
ShouldProcessCall,
|
||||
} from '@waha/apps/chatwoot/consumers/waha/call.0.base';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { EngineHelper } from '@waha/apps/chatwoot/waha';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { WAHAWebhookCallRejected } from '@waha/structures/webhooks.dto';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
|
||||
|
||||
@Processor(QueueName.WAHA_CALL_REJECTED, { concurrency: JOB_CONCURRENCY })
|
||||
export class WAHACallRejectedConsumer extends ChatWootWAHABaseConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'WAHACallRejectedConsumer');
|
||||
}
|
||||
|
||||
ShouldProcess(event: WAHAWebhookCallRejected): boolean {
|
||||
return ShouldProcessCall(event);
|
||||
}
|
||||
|
||||
GetChatId(event: WAHAWebhookCallRejected): string {
|
||||
return EngineHelper.CallChatID(event.payload as any);
|
||||
}
|
||||
|
||||
async Process(
|
||||
job: Job<EventData, any, WAHAEvents>,
|
||||
info: IMessageInfo,
|
||||
): Promise<any> {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const event = job.data.event as WAHAWebhookCallRejected;
|
||||
const locale = container.Locale();
|
||||
const handler = new CallRejectedMessageHandler(
|
||||
job,
|
||||
container.MessageMappingService(),
|
||||
container.ContactConversationService(),
|
||||
container.Logger(),
|
||||
info,
|
||||
new WAHASessionAPI(event.session, container.WAHASelf()),
|
||||
locale,
|
||||
container.WAHASelf(),
|
||||
);
|
||||
const msg = BuildCallMessagePayload(
|
||||
event.payload,
|
||||
WAHAEvents.CALL_REJECTED,
|
||||
);
|
||||
return await handler.handle(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export class CallRejectedMessageHandler extends MessageBaseHandler<CallMessagePayload> {
|
||||
protected async getMessage(
|
||||
payload: CallMessagePayload,
|
||||
): Promise<ChatWootMessagePartial> {
|
||||
const content = this.l
|
||||
.key(TKey.WA_TO_CW_CALL_REJECTED)
|
||||
.render({ call: payload });
|
||||
|
||||
let private_ = false;
|
||||
// GOWS
|
||||
if (payload._data.Data?.Tag == 'reject') {
|
||||
private_ = true;
|
||||
}
|
||||
if (payload._data.Reason == 'rejected_elsewhere') {
|
||||
private_ = true;
|
||||
}
|
||||
// WEBJS
|
||||
if (payload._data.api) {
|
||||
private_ = true;
|
||||
}
|
||||
|
||||
return {
|
||||
content: WhatsappToMarkdown(content),
|
||||
attachments: [],
|
||||
private: private_,
|
||||
};
|
||||
}
|
||||
|
||||
getReplyToWhatsAppID(payload: CallMessagePayload): string | undefined {
|
||||
return BuildFakeCallMessageId(payload.callId, WAHAEvents.CALL_RECEIVED);
|
||||
}
|
||||
}
|
||||
@@ -207,18 +207,28 @@ export class DIContainer {
|
||||
|
||||
@CacheSync()
|
||||
public ChatWootConfig(): ChatWootConfig {
|
||||
const defaults: ChatWootConfig = {
|
||||
templates: {},
|
||||
linkPreview: LinkPreview.OFF,
|
||||
commands: {
|
||||
server: true,
|
||||
queue: false,
|
||||
},
|
||||
conversations: {
|
||||
sort: ConversationSort.created_newest,
|
||||
status: null,
|
||||
},
|
||||
};
|
||||
return lodash.defaults({}, this.config, defaults);
|
||||
return ChatWootConfigDefaults(this.config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies default values to the ChatWoot configuration
|
||||
*/
|
||||
export function ChatWootConfigDefaults(
|
||||
config: ChatWootAppConfig,
|
||||
): ChatWootConfig {
|
||||
const defaults: ChatWootConfig = {
|
||||
templates: {},
|
||||
linkPreview: LinkPreview.OFF,
|
||||
commands: {
|
||||
server: true,
|
||||
queue: false,
|
||||
},
|
||||
conversations: {
|
||||
sort: ConversationSort.created_newest,
|
||||
status: null,
|
||||
markAsRead: true,
|
||||
},
|
||||
};
|
||||
return lodash.defaultsDeep({}, config, defaults);
|
||||
}
|
||||
@@ -7,12 +7,10 @@ import {
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsDynamicObject } from '@waha/nestjs/validation/IsDynamicObject';
|
||||
import {
|
||||
ConversationSelectorConfig,
|
||||
ConversationSort,
|
||||
} from '@waha/apps/chatwoot/services/ConversationSelector';
|
||||
import { ConversationSort } from '@waha/apps/chatwoot/services/ConversationSelector';
|
||||
import { ConversationStatus } from '@waha/apps/chatwoot/client/types';
|
||||
|
||||
export const DEFAULT_LOCALE = 'en-US';
|
||||
@@ -39,6 +37,15 @@ export class ChatWootConversationsConfig {
|
||||
@IsOptional()
|
||||
@IsEnum(ConversationStatus, { each: true })
|
||||
status: Array<ConversationStatus> | null;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
'Process message.ack events to mark ChatWoot conversations as read. Enabled by default.',
|
||||
default: true,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
markAsRead?: boolean = true;
|
||||
}
|
||||
|
||||
export interface ChatWootConfig {
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **رسالة العميل:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *مكالمة {{#call.isVideo}}فيديو{{/call.isVideo}}{{^call.isVideo}}صوتية{{/call.isVideo}} واردة*
|
||||
📱 يرجى فتح **واتساب** للرد.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *تم الرد على المكالمة*
|
||||
📱 تم الرد على المكالمة من جهازك الآخر.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *تم إنهاء المكالمة*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **গ্রাহকের বার্তা:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *ইনকামিং {{#call.isVideo}}ভিডিও{{/call.isVideo}}{{^call.isVideo}}ভয়েস{{/call.isVideo}} কল*
|
||||
📱 উত্তর দিতে দয়া করে **WhatsApp** খুলুন।
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *কল রিসিভ করা হয়েছে*
|
||||
📱 কলটি আপনার অন্য ডিভাইসে রিসিভ করা হয়েছে।
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *কল শেষ হয়েছে*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -525,6 +525,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **Kundennachricht:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *Eingehender {{#call.isVideo}}Video{{/call.isVideo}}{{^call.isVideo}}Sprach{{/call.isVideo}}anruf*
|
||||
📱 Bitte öffne **WhatsApp**, um anzunehmen.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *Anruf angenommen*
|
||||
📱 Der Anruf wurde auf deinem anderen Gerät angenommen.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *Anruf beendet*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -316,6 +316,17 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
💬 **CUSTOMER MESSAGE:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *Incoming {{#call.isVideo}}Video{{/call.isVideo}}{{^call.isVideo}}Voice{{/call.isVideo}} Call*
|
||||
📱 Please open **WhatsApp** to answer.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *Call Answered*
|
||||
📱 The call was picked up on your other device.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *Call Ended*
|
||||
|
||||
app.connected.message: |-
|
||||
🔗 **CONNECTED: WhatsApp Session to Inbox**
|
||||
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **Mensaje del cliente:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *Llamada {{#call.isVideo}}de video{{/call.isVideo}}{{^call.isVideo}}de voz{{/call.isVideo}} entrante*
|
||||
📱 Abre **WhatsApp** para contestar.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *Llamada respondida*
|
||||
📱 La llamada fue contestada en tu otro dispositivo.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *Llamada finalizada*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **پیام مشتری:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *تماس {{#call.isVideo}}تصویری{{/call.isVideo}}{{^call.isVideo}}صوتی{{/call.isVideo}} ورودی*
|
||||
📱 برای پاسخ دادن لطفاً **واتساپ** را باز کنید.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *تماس پاسخ داده شد*
|
||||
📱 تماس روی دستگاه دیگر شما پاسخ داده شد.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *تماس پایان یافت*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **Message du client :**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *Appel {{#call.isVideo}}vidéo{{/call.isVideo}}{{^call.isVideo}}vocal{{/call.isVideo}} entrant*
|
||||
📱 Ouvre **WhatsApp** pour répondre.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *Appel décroché*
|
||||
📱 L'appel a été pris sur ton autre appareil.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *Appel terminé*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **הודעת הלקוח:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *שיחת {{#call.isVideo}}וידאו{{/call.isVideo}}{{^call.isVideo}}קול{{/call.isVideo}} נכנסת*
|
||||
📱 פתח/י את **WhatsApp** כדי לענות.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *השיחה נענתה*
|
||||
📱 השיחה נענתה במכשיר אחר שלך.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *השיחה הסתיימה*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **ग्राहक संदेश:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *आने वाली {{#call.isVideo}}वीडियो{{/call.isVideo}}{{^call.isVideo}}वॉयस{{/call.isVideo}} कॉल*
|
||||
📱 जवाब देने के लिए कृपया **WhatsApp** खोलें।
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *कॉल का जवाब दिया गया*
|
||||
📱 कॉल आपके दूसरे डिवाइस पर उठाई गई।
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *कॉल समाप्त हुई*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **Pesan Pelanggan:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *Panggilan {{#call.isVideo}}Video{{/call.isVideo}}{{^call.isVideo}}Suara{{/call.isVideo}} Masuk*
|
||||
📱 Silakan buka **WhatsApp** untuk menjawab.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *Panggilan Dijawab*
|
||||
📱 Panggilan dijawab di perangkat lainmu.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *Panggilan Berakhir*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **ਗਾਹਕ ਦਾ ਸੁਨੇਹਾ:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *ਆ ਰਹੀ {{#call.isVideo}}ਵੀਡੀਓ{{/call.isVideo}}{{^call.isVideo}}ਵਾਇਸ{{/call.isVideo}} ਕਾਲ*
|
||||
📱 ਜਵਾਬ ਦੇਣ ਲਈ ਕਿਰਪਾ ਕਰਕੇ **WhatsApp** ਖੋਲ੍ਹੋ।
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *ਕਾਲ ਦਾ ਜਵਾਬ ਦੇ ਦਿੱਤਾ ਗਿਆ ਹੈ*
|
||||
📱 ਕਾਲ ਤੁਹਾਡੇ ਹੋਰ ਜੰਤਰ ਤੇ ਉਠਾਈ ਗਈ ਸੀ।
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *ਕਾਲ ਸਮਾਪਤ ਹੋ ਗਈ*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -527,6 +527,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **MENSAGEM DO CLIENTE:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *Chamada {{#call.isVideo}}de vídeo{{/call.isVideo}}{{^call.isVideo}}de voz{{/call.isVideo}} recebida*
|
||||
📱 Abra o **WhatsApp** para atender.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *Chamada atendida*
|
||||
📱 A chamada foi atendida no seu outro dispositivo.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *Chamada encerrada*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **Сообщение клиента:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *Входящий {{#call.isVideo}}видеозвонок{{/call.isVideo}}{{^call.isVideo}}голосовой звонок{{/call.isVideo}}*
|
||||
📱 Откройте **WhatsApp**, чтобы ответить.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *Звонок принят*
|
||||
📱 Звонок был принят на другом вашем устройстве.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *Звонок завершен*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **Müşteri Mesajı:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *Gelen {{#call.isVideo}}Görüntülü{{/call.isVideo}}{{^call.isVideo}}Sesli{{/call.isVideo}} Arama*
|
||||
📱 Yanıtlamak için lütfen **WhatsApp**'ı açın.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *Arama Yanıtlandı*
|
||||
📱 Arama diğer cihazınızda açıldı.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *Arama Sonlandı*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **Повідомлення клієнта:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *Вхідний {{#call.isVideo}}відеодзвінок{{/call.isVideo}}{{^call.isVideo}}голосовий дзвінок{{/call.isVideo}}*
|
||||
📱 Відкрийте **WhatsApp**, щоб відповісти.
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *Дзвінок прийнято*
|
||||
📱 Дзвінок прийняли на вашому іншому пристрої.
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *Дзвінок завершено*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **گاہک کا پیغام:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *آنے والی {{#call.isVideo}}ویڈیو{{/call.isVideo}}{{^call.isVideo}}آواز{{/call.isVideo}} کال*
|
||||
📱 جواب دینے کے لیے براہ کرم **WhatsApp** کھولیں۔
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *کال کا جواب دے دیا گیا*
|
||||
📱 کال آپ کے دوسرے ڈیوائس پر اٹھائی گئی۔
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *کال ختم ہو گئی*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -522,6 +522,18 @@ whatsapp.to.chatwoot.message.facebook.ad: |-
|
||||
---
|
||||
💬 **客户消息:**
|
||||
{{{payload.body}}}
|
||||
|
||||
whatsapp.to.chatwoot.call.received: |-
|
||||
📞🔔 *收到{{#call.isVideo}}视频{{/call.isVideo}}{{^call.isVideo}}语音{{/call.isVideo}}来电*
|
||||
📱 请打开 **WhatsApp** 接听。
|
||||
|
||||
whatsapp.to.chatwoot.call.accepted: |-
|
||||
📞✅ *已接听来电*
|
||||
📱 来电已在你的其他设备上接听。
|
||||
|
||||
whatsapp.to.chatwoot.call.rejected: |-
|
||||
📞❌ *通话已结束*
|
||||
|
||||
whatsapp.history.message.wrapper: |-
|
||||
{{{content}}}
|
||||
`{{#history}}📥{{/history}}🗓️ {{{timestamp}}}`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { proto } from '@adiwajshing/baileys';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
import { SimpleVCardInfo } from '@waha/core/vcard';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
|
||||
export enum TKey {
|
||||
LOCALE_NAME = 'locale.name',
|
||||
@@ -57,6 +58,9 @@ export enum TKey {
|
||||
WA_TO_CW_MESSAGE_FACEBOOK_AD = 'whatsapp.to.chatwoot.message.facebook.ad',
|
||||
WA_TO_CW_MESSAGE_LIST = 'whatsapp.to.chatwoot.message.list',
|
||||
WA_TO_CW_MESSAGE_ALBUM = 'whatsapp.to.chatwoot.message.album',
|
||||
WA_TO_CW_CALL_RECEIVED = 'whatsapp.to.chatwoot.call.received',
|
||||
WA_TO_CW_CALL_ACCEPTED = 'whatsapp.to.chatwoot.call.accepted',
|
||||
WA_TO_CW_CALL_REJECTED = 'whatsapp.to.chatwoot.call.rejected',
|
||||
|
||||
//
|
||||
// App Inbox
|
||||
@@ -195,6 +199,9 @@ export type TemplatePayloads = {
|
||||
expectedVideoCount: number;
|
||||
totalCount: number;
|
||||
};
|
||||
[TKey.WA_TO_CW_CALL_RECEIVED]: { call: CallData };
|
||||
[TKey.WA_TO_CW_CALL_ACCEPTED]: { call: CallData };
|
||||
[TKey.WA_TO_CW_CALL_REJECTED]: { call: CallData };
|
||||
[TKey.JOB_SCHEDULED_ERROR_HEADER]: void;
|
||||
[TKey.JOB_REPORT_ERROR]: {
|
||||
header: string;
|
||||
|
||||
@@ -108,15 +108,15 @@ describe('PixMessage - pt-BR', () => {
|
||||
const result = converter.convert(payload, protoMessage);
|
||||
|
||||
// Assert
|
||||
const expected = `💳 ****PIX - Copia e Cola****
|
||||
const expected = `💳 **PIX - Copia e Cola**
|
||||
|
||||
****Merchant Name Here****
|
||||
**Merchant Name Here**
|
||||
|
||||
****Chave PIX:**** +5000000000000
|
||||
****Tipo:**** PHONE
|
||||
****Referência:**** 4TB11111111
|
||||
**Chave PIX:** +5000000000000
|
||||
**Tipo:** PHONE
|
||||
**Referência:** 4TB11111111
|
||||
|
||||
💳 ****PIX Copia e Cola enviado****`;
|
||||
💳 **PIX Copia e Cola enviado**`;
|
||||
|
||||
expect(protoMessage).not.toBeNull();
|
||||
expect(result).not.toBeNull();
|
||||
@@ -227,16 +227,16 @@ describe('PixMessage - pt-BR', () => {
|
||||
const result = converter.convert(payload, protoMessage);
|
||||
|
||||
// Assert
|
||||
const expected = `💳 ****PIX - Copia e Cola****
|
||||
const expected = `💳 **PIX - Copia e Cola**
|
||||
|
||||
****Merchant Name Here****
|
||||
**Merchant Name Here**
|
||||
|
||||
****Chave PIX:**** +5000000000000
|
||||
****Tipo:**** PHONE
|
||||
****Valor:**** R$\u00A025,12
|
||||
****Referência:**** 4TB11111111
|
||||
**Chave PIX:** +5000000000000
|
||||
**Tipo:** PHONE
|
||||
**Valor:** R$\u00A025,12
|
||||
**Referência:** 4TB11111111
|
||||
|
||||
💳 ****PIX Copia e Cola enviado****`;
|
||||
💳 **PIX Copia e Cola enviado**`;
|
||||
|
||||
expect(protoMessage).not.toBeNull();
|
||||
expect(result).not.toBeNull();
|
||||
|
||||
@@ -53,4 +53,40 @@ describe('WhatsappToMarkdown', () => {
|
||||
const expected = 'URL: https://example.com/test_page e texto *em itálico*';
|
||||
expect(WhatsappToMarkdown(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('keeps already bold markdown intact', () => {
|
||||
const input = '**PIX - Copia e Cola**';
|
||||
expect(WhatsappToMarkdown(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('keeps already strikethrough markdown intact', () => {
|
||||
const input = '~~removed~~';
|
||||
expect(WhatsappToMarkdown(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('keeps underscores inside identifiers intact', () => {
|
||||
const input = 'api key: abc_test_key';
|
||||
expect(WhatsappToMarkdown(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('does not convert underscores inside words', () => {
|
||||
const input = 'Instance name: customer_prod_instance';
|
||||
expect(WhatsappToMarkdown(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('does not convert when markers are stuck to word characters', () => {
|
||||
const input = 'My name is*Rafael* and email is test_user@example.com';
|
||||
expect(WhatsappToMarkdown(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('keeps formatting when markers touch punctuation boundaries', () => {
|
||||
const input = 'Hello *Rafael*, welcome to _WAHA_!';
|
||||
const expected = 'Hello **Rafael**, welcome to *WAHA*!';
|
||||
expect(WhatsappToMarkdown(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('ignores markers with inner spaces next to the marker', () => {
|
||||
const input = '* text* and _ text_ should stay the same';
|
||||
expect(WhatsappToMarkdown(input)).toBe(input);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,18 @@
|
||||
import { UrlProtection } from '@waha/apps/chatwoot/messages/to/markdown';
|
||||
|
||||
const boldPattern = new RegExp(
|
||||
`(?<![\\p{L}\\p{N}_*])\\*(?!\\*)(?=\\S)(.+?)(?<=\\S)\\*(?![\\p{L}\\p{N}_*])`,
|
||||
'gu',
|
||||
);
|
||||
const italicPattern = new RegExp(
|
||||
`(?<![\\p{L}\\p{N}_])_(?!_)(?=\\S)(.+?)(?<=\\S)_(?![\\p{L}\\p{N}_])`,
|
||||
'gu',
|
||||
);
|
||||
const strikePattern = new RegExp(
|
||||
`(?<![\\p{L}\\p{N}_~])~(?!~)(?=\\S)(.+?)(?<=\\S)~(?![\\p{L}\\p{N}_~])`,
|
||||
'gu',
|
||||
);
|
||||
|
||||
export function WhatsappToMarkdown(text: string): string {
|
||||
if (!text) {
|
||||
return text;
|
||||
@@ -16,11 +29,11 @@ export function WhatsappToMarkdown(text: string): string {
|
||||
// Apply markdown transformations to "clean" text
|
||||
let result = text
|
||||
// Bold: *bold* → **bold**
|
||||
.replace(/\*(.*?)\*/g, '**$1**')
|
||||
.replace(boldPattern, '**$1**')
|
||||
// Strikethrough: ~strike~ → ~~strike~~
|
||||
.replace(/~(.*?)~/g, '~~$1~~')
|
||||
.replace(strikePattern, '~~$1~~')
|
||||
// Italic: _italic_ → *italic*
|
||||
.replace(/_(.*?)_/g, '*$1*');
|
||||
.replace(italicPattern, '*$1*');
|
||||
|
||||
// Restore original URLs after all transformations
|
||||
result = urls.restore(result);
|
||||
|
||||
@@ -120,7 +120,7 @@ export class ChatWootAppService implements IAppService {
|
||||
}
|
||||
|
||||
beforeSessionStart(app: App<ChatWootAppConfig>, session: WhatsappSession) {
|
||||
this.chatWootWAHAQueueService.listenEvents(app.id, session);
|
||||
this.chatWootWAHAQueueService.listenEvents(app, session);
|
||||
}
|
||||
|
||||
afterSessionStart(app: App<ChatWootAppConfig>, session: WhatsappSession) {
|
||||
|
||||
@@ -7,6 +7,9 @@ import { Queue } from 'bullmq';
|
||||
|
||||
import { QueueName } from '../consumers/QueueName';
|
||||
import { QueueRegistry } from './QueueRegistry';
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
|
||||
import { ChatWootConfigDefaults } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
|
||||
/**
|
||||
* Service for managing ChatWoot queues for WAHA events
|
||||
@@ -35,6 +38,12 @@ export class ChatWootWAHAQueueService {
|
||||
return this.queueRegistry.queue(QueueName.WAHA_MESSAGE_ACK);
|
||||
case WAHAEvents.SESSION_STATUS:
|
||||
return this.queueRegistry.queue(QueueName.WAHA_SESSION_STATUS);
|
||||
case WAHAEvents.CALL_RECEIVED:
|
||||
return this.queueRegistry.queue(QueueName.WAHA_CALL_RECEIVED);
|
||||
case WAHAEvents.CALL_ACCEPTED:
|
||||
return this.queueRegistry.queue(QueueName.WAHA_CALL_ACCEPTED);
|
||||
case WAHAEvents.CALL_REJECTED:
|
||||
return this.queueRegistry.queue(QueueName.WAHA_CALL_REJECTED);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -57,13 +66,14 @@ export class ChatWootWAHAQueueService {
|
||||
/**
|
||||
* Configure ChatWoot event handling for a session
|
||||
*/
|
||||
listenEvents(appId: string, session: WhatsappSession): void {
|
||||
const events = ListenEventsForChatWoot();
|
||||
listenEvents(app: App<ChatWootAppConfig>, session: WhatsappSession): void {
|
||||
const config = ChatWootConfigDefaults(app.config);
|
||||
const events = ListenEventsForChatWoot(config);
|
||||
for (const event of events) {
|
||||
const obs$ = session.getEventObservable(event);
|
||||
obs$.subscribe(async (payload) => {
|
||||
const data = populateSessionInfo(event, session)(payload);
|
||||
await this.addJobToQueue(event, data, appId);
|
||||
await this.addJobToQueue(event, data, app.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ export class QueueManager {
|
||||
[QueueName.WAHA_MESSAGE_EDITED]: Managable,
|
||||
[QueueName.WAHA_MESSAGE_REVOKED]: Managable,
|
||||
[QueueName.WAHA_MESSAGE_ACK]: Managable,
|
||||
[QueueName.WAHA_CALL_RECEIVED]: Managable,
|
||||
[QueueName.WAHA_CALL_ACCEPTED]: Managable,
|
||||
[QueueName.WAHA_CALL_REJECTED]: Managable,
|
||||
[QueueName.INBOX_MESSAGE_CREATED]: Managable,
|
||||
[QueueName.INBOX_MESSAGE_UPDATED]: Managable,
|
||||
[QueueName.INBOX_CONVERSATION_CREATED]: Managable,
|
||||
|
||||
@@ -32,6 +32,12 @@ export class QueueRegistry {
|
||||
private readonly wahaMessageRevokedQueue: Queue,
|
||||
@InjectQueue(QueueName.WAHA_MESSAGE_ACK)
|
||||
private readonly wahaMessageAckQueue: Queue,
|
||||
@InjectQueue(QueueName.WAHA_CALL_RECEIVED)
|
||||
private readonly wahaCallReceivedQueue: Queue,
|
||||
@InjectQueue(QueueName.WAHA_CALL_ACCEPTED)
|
||||
private readonly wahaCallAcceptedQueue: Queue,
|
||||
@InjectQueue(QueueName.WAHA_CALL_REJECTED)
|
||||
private readonly wahaCallRejectedQueue: Queue,
|
||||
@InjectQueue(QueueName.INBOX_MESSAGE_CREATED)
|
||||
private readonly inboxMessageCreatedQueue: Queue,
|
||||
@InjectQueue(QueueName.INBOX_MESSAGE_UPDATED)
|
||||
@@ -57,6 +63,9 @@ export class QueueRegistry {
|
||||
[QueueName.WAHA_MESSAGE_EDITED]: this.wahaMessageEditedQueue,
|
||||
[QueueName.WAHA_MESSAGE_REVOKED]: this.wahaMessageRevokedQueue,
|
||||
[QueueName.WAHA_MESSAGE_ACK]: this.wahaMessageAckQueue,
|
||||
[QueueName.WAHA_CALL_RECEIVED]: this.wahaCallReceivedQueue,
|
||||
[QueueName.WAHA_CALL_ACCEPTED]: this.wahaCallAcceptedQueue,
|
||||
[QueueName.WAHA_CALL_REJECTED]: this.wahaCallRejectedQueue,
|
||||
[QueueName.INBOX_MESSAGE_CREATED]: this.inboxMessageCreatedQueue,
|
||||
[QueueName.INBOX_MESSAGE_UPDATED]: this.inboxMessageUpdatedQueue,
|
||||
[QueueName.INBOX_CONVERSATION_CREATED]:
|
||||
|
||||
@@ -5,10 +5,13 @@ import { getEngineName } from '@waha/version';
|
||||
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||
import { isLidUser, isPnUser, toCusFormat } from '@waha/core/utils/jids';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
|
||||
interface IEngineHelper {
|
||||
ChatID(message: WAMessage | any): string;
|
||||
|
||||
CallChatID(call: CallData | any): string;
|
||||
|
||||
WhatsAppMessageKeys(message: any): WhatsAppMessage;
|
||||
|
||||
IterateMessages<T extends { timestamp: number }>(
|
||||
@@ -27,6 +30,10 @@ class NOWEBHelper implements IEngineHelper {
|
||||
return message.from;
|
||||
}
|
||||
|
||||
CallChatID(call: CallData): string {
|
||||
return call.from;
|
||||
}
|
||||
|
||||
WhatsAppMessageKeys(message: any): WhatsAppMessage {
|
||||
const timestamp = parseInt(message.messageTimestamp) * 1000;
|
||||
return {
|
||||
@@ -62,6 +69,10 @@ class GOWSHelper implements IEngineHelper {
|
||||
return message.from;
|
||||
}
|
||||
|
||||
CallChatID(call: CallData): string {
|
||||
return call._data?.CallCreator || call.from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse API response and get the data
|
||||
* API Response depends on engine right now
|
||||
@@ -102,6 +113,10 @@ class WEBJSHelper implements IEngineHelper {
|
||||
return message._data?.id?.remote || message.from;
|
||||
}
|
||||
|
||||
CallChatID(call: CallData): string {
|
||||
return call.from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse API response and get the data for WEBJS engine
|
||||
*/
|
||||
|
||||
@@ -79,6 +79,7 @@ export class SwaggerConfiguratorCore {
|
||||
.addTag('🆔 Profile', 'Your profile information')
|
||||
.addTag('🖼️ Screenshot', 'Get screenshot of WhatsApp and show QR code')
|
||||
.addTag('📤 Chatting', 'Chatting methods')
|
||||
.addTag('📞 Calls', 'Call handling methods')
|
||||
.addTag('📢 Channels', 'Channels (newsletters) methods')
|
||||
.addTag('🟢 Status', 'Status (aka stories) methods')
|
||||
.addTag('💬 Chats', `Chats methods`)
|
||||
|
||||
@@ -181,7 +181,7 @@ export abstract class WhatsappSession {
|
||||
public name: string;
|
||||
protected mediaManager: IMediaManager;
|
||||
public loggerBuilder: LoggerBuilder;
|
||||
protected logger: Logger;
|
||||
public logger: Logger;
|
||||
protected sessionStore: DataStore;
|
||||
protected proxyConfig?: ProxyConfig;
|
||||
public sessionConfig?: SessionConfig;
|
||||
@@ -321,7 +321,7 @@ export abstract class WhatsappSession {
|
||||
return this.events2.get(event);
|
||||
}
|
||||
|
||||
protected set status(value: WAHASessionStatus) {
|
||||
public set status(value: WAHASessionStatus) {
|
||||
if (this.unpairing && value !== WAHASessionStatus.STOPPED) {
|
||||
// In case of unpairing
|
||||
// wait for STOPPED event, ignore the rest
|
||||
@@ -664,6 +664,10 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public rejectCall(from: string, id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Chats methods
|
||||
*/
|
||||
|
||||
@@ -40,6 +40,7 @@ import { join } from 'path';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
import { AuthController } from '../api/auth.controller';
|
||||
import { CallsController } from '../api/calls.controller';
|
||||
import { ChatsController } from '../api/chats.controller';
|
||||
import { ChattingController } from '../api/chatting.controller';
|
||||
import { ContactsController } from '../api/contacts.controller';
|
||||
@@ -143,6 +144,7 @@ export const CONTROLLERS = [
|
||||
ProfileController,
|
||||
ChattingController,
|
||||
ChatsController,
|
||||
CallsController,
|
||||
ChannelsController,
|
||||
StatusController,
|
||||
LabelsController,
|
||||
|
||||
@@ -7,13 +7,15 @@ let eventStreamClient: messages.EventStreamClient | null = null;
|
||||
export const MessageServiceClientSingleton = (
|
||||
address: string,
|
||||
credentials: grpc.ChannelCredentials,
|
||||
options?: grpc.ChannelOptions,
|
||||
): messages.MessageServiceClient => {
|
||||
if (!messageServiceClient) {
|
||||
messageServiceClient = new messages.MessageServiceClient(
|
||||
address,
|
||||
credentials,
|
||||
options,
|
||||
{
|
||||
'grpc.max_send_message_length': 128 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 128 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
}
|
||||
return messageServiceClient;
|
||||
@@ -22,14 +24,12 @@ export const MessageServiceClientSingleton = (
|
||||
export const EventStreamClientSingleton = (
|
||||
address: string,
|
||||
credentials: grpc.ChannelCredentials,
|
||||
options?: grpc.ChannelOptions,
|
||||
): messages.EventStreamClient => {
|
||||
if (!eventStreamClient) {
|
||||
eventStreamClient = new messages.EventStreamClient(
|
||||
address,
|
||||
credentials,
|
||||
options,
|
||||
);
|
||||
eventStreamClient = new messages.EventStreamClient(address, credentials, {
|
||||
'grpc.max_send_message_length': 128 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 128 * 1024 * 1024,
|
||||
});
|
||||
}
|
||||
return eventStreamClient;
|
||||
};
|
||||
@@ -2687,6 +2687,7 @@ export namespace messages {
|
||||
mimetype?: string;
|
||||
audio?: AudioInfo;
|
||||
filename?: string;
|
||||
contentPath?: string;
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
@@ -2706,6 +2707,9 @@ export namespace messages {
|
||||
if ("filename" in data && data.filename != undefined) {
|
||||
this.filename = data.filename;
|
||||
}
|
||||
if ("contentPath" in data && data.contentPath != undefined) {
|
||||
this.contentPath = data.contentPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
get content() {
|
||||
@@ -2741,12 +2745,19 @@ export namespace messages {
|
||||
set filename(value: string) {
|
||||
pb_1.Message.setField(this, 5, value);
|
||||
}
|
||||
get contentPath() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 6, "") as string;
|
||||
}
|
||||
set contentPath(value: string) {
|
||||
pb_1.Message.setField(this, 6, value);
|
||||
}
|
||||
static fromObject(data: {
|
||||
content?: Uint8Array;
|
||||
type?: MediaType;
|
||||
mimetype?: string;
|
||||
audio?: ReturnType<typeof AudioInfo.prototype.toObject>;
|
||||
filename?: string;
|
||||
contentPath?: string;
|
||||
}): Media {
|
||||
const message = new Media({});
|
||||
if (data.content != null) {
|
||||
@@ -2764,6 +2775,9 @@ export namespace messages {
|
||||
if (data.filename != null) {
|
||||
message.filename = data.filename;
|
||||
}
|
||||
if (data.contentPath != null) {
|
||||
message.contentPath = data.contentPath;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
@@ -2773,6 +2787,7 @@ export namespace messages {
|
||||
mimetype?: string;
|
||||
audio?: ReturnType<typeof AudioInfo.prototype.toObject>;
|
||||
filename?: string;
|
||||
contentPath?: string;
|
||||
} = {};
|
||||
if (this.content != null) {
|
||||
data.content = this.content;
|
||||
@@ -2789,6 +2804,9 @@ export namespace messages {
|
||||
if (this.filename != null) {
|
||||
data.filename = this.filename;
|
||||
}
|
||||
if (this.contentPath != null) {
|
||||
data.contentPath = this.contentPath;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
@@ -2805,6 +2823,8 @@ export namespace messages {
|
||||
writer.writeMessage(4, this.audio, () => this.audio.serialize(writer));
|
||||
if (this.filename.length)
|
||||
writer.writeString(5, this.filename);
|
||||
if (this.contentPath.length)
|
||||
writer.writeString(6, this.contentPath);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
@@ -2829,6 +2849,9 @@ export namespace messages {
|
||||
case 5:
|
||||
message.filename = reader.readString();
|
||||
break;
|
||||
case 6:
|
||||
message.contentPath = reader.readString();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -7949,6 +7972,7 @@ export namespace messages {
|
||||
message?: string;
|
||||
jid?: string;
|
||||
messageId?: string;
|
||||
contentPath?: string;
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
@@ -7965,6 +7989,9 @@ export namespace messages {
|
||||
if ("messageId" in data && data.messageId != undefined) {
|
||||
this.messageId = data.messageId;
|
||||
}
|
||||
if ("contentPath" in data && data.contentPath != undefined) {
|
||||
this.contentPath = data.contentPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
get session() {
|
||||
@@ -7994,11 +8021,18 @@ export namespace messages {
|
||||
set messageId(value: string) {
|
||||
pb_1.Message.setField(this, 4, value);
|
||||
}
|
||||
get contentPath() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 5, "") as string;
|
||||
}
|
||||
set contentPath(value: string) {
|
||||
pb_1.Message.setField(this, 5, value);
|
||||
}
|
||||
static fromObject(data: {
|
||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||
message?: string;
|
||||
jid?: string;
|
||||
messageId?: string;
|
||||
contentPath?: string;
|
||||
}): DownloadMediaRequest {
|
||||
const message = new DownloadMediaRequest({});
|
||||
if (data.session != null) {
|
||||
@@ -8013,6 +8047,9 @@ export namespace messages {
|
||||
if (data.messageId != null) {
|
||||
message.messageId = data.messageId;
|
||||
}
|
||||
if (data.contentPath != null) {
|
||||
message.contentPath = data.contentPath;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
@@ -8021,6 +8058,7 @@ export namespace messages {
|
||||
message?: string;
|
||||
jid?: string;
|
||||
messageId?: string;
|
||||
contentPath?: string;
|
||||
} = {};
|
||||
if (this.session != null) {
|
||||
data.session = this.session.toObject();
|
||||
@@ -8034,6 +8072,9 @@ export namespace messages {
|
||||
if (this.messageId != null) {
|
||||
data.messageId = this.messageId;
|
||||
}
|
||||
if (this.contentPath != null) {
|
||||
data.contentPath = this.contentPath;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
@@ -8048,6 +8089,8 @@ export namespace messages {
|
||||
writer.writeString(3, this.jid);
|
||||
if (this.messageId.length)
|
||||
writer.writeString(4, this.messageId);
|
||||
if (this.contentPath.length)
|
||||
writer.writeString(5, this.contentPath);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
@@ -8069,6 +8112,9 @@ export namespace messages {
|
||||
case 4:
|
||||
message.messageId = reader.readString();
|
||||
break;
|
||||
case 5:
|
||||
message.contentPath = reader.readString();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -8085,6 +8131,7 @@ export namespace messages {
|
||||
#one_of_decls: number[][] = [];
|
||||
constructor(data?: any[] | {
|
||||
content?: Uint8Array;
|
||||
contentPath?: string;
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
@@ -8092,6 +8139,9 @@ export namespace messages {
|
||||
if ("content" in data && data.content != undefined) {
|
||||
this.content = data.content;
|
||||
}
|
||||
if ("contentPath" in data && data.contentPath != undefined) {
|
||||
this.contentPath = data.contentPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
get content() {
|
||||
@@ -8100,22 +8150,36 @@ export namespace messages {
|
||||
set content(value: Uint8Array) {
|
||||
pb_1.Message.setField(this, 1, value);
|
||||
}
|
||||
get contentPath() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 5, "") as string;
|
||||
}
|
||||
set contentPath(value: string) {
|
||||
pb_1.Message.setField(this, 5, value);
|
||||
}
|
||||
static fromObject(data: {
|
||||
content?: Uint8Array;
|
||||
contentPath?: string;
|
||||
}): DownloadMediaResponse {
|
||||
const message = new DownloadMediaResponse({});
|
||||
if (data.content != null) {
|
||||
message.content = data.content;
|
||||
}
|
||||
if (data.contentPath != null) {
|
||||
message.contentPath = data.contentPath;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
const data: {
|
||||
content?: Uint8Array;
|
||||
contentPath?: string;
|
||||
} = {};
|
||||
if (this.content != null) {
|
||||
data.content = this.content;
|
||||
}
|
||||
if (this.contentPath != null) {
|
||||
data.contentPath = this.contentPath;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
@@ -8124,6 +8188,8 @@ export namespace messages {
|
||||
const writer = w || new pb_1.BinaryWriter();
|
||||
if (this.content.length)
|
||||
writer.writeBytes(1, this.content);
|
||||
if (this.contentPath.length)
|
||||
writer.writeString(5, this.contentPath);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
@@ -8136,6 +8202,9 @@ export namespace messages {
|
||||
case 1:
|
||||
message.content = reader.readBytes();
|
||||
break;
|
||||
case 5:
|
||||
message.contentPath = reader.readString();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -10036,6 +10105,122 @@ export namespace messages {
|
||||
return GetLidsRequest.deserialize(bytes);
|
||||
}
|
||||
}
|
||||
export class RejectCallRequest extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [];
|
||||
constructor(data?: any[] | {
|
||||
session?: Session;
|
||||
from?: string;
|
||||
id?: string;
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
if (!Array.isArray(data) && typeof data == "object") {
|
||||
if ("session" in data && data.session != undefined) {
|
||||
this.session = data.session;
|
||||
}
|
||||
if ("from" in data && data.from != undefined) {
|
||||
this.from = data.from;
|
||||
}
|
||||
if ("id" in data && data.id != undefined) {
|
||||
this.id = data.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
get session() {
|
||||
return pb_1.Message.getWrapperField(this, Session, 1) as Session;
|
||||
}
|
||||
set session(value: Session) {
|
||||
pb_1.Message.setWrapperField(this, 1, value);
|
||||
}
|
||||
get has_session() {
|
||||
return pb_1.Message.getField(this, 1) != null;
|
||||
}
|
||||
get from() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 2, "") as string;
|
||||
}
|
||||
set from(value: string) {
|
||||
pb_1.Message.setField(this, 2, value);
|
||||
}
|
||||
get id() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 3, "") as string;
|
||||
}
|
||||
set id(value: string) {
|
||||
pb_1.Message.setField(this, 3, value);
|
||||
}
|
||||
static fromObject(data: {
|
||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||
from?: string;
|
||||
id?: string;
|
||||
}): RejectCallRequest {
|
||||
const message = new RejectCallRequest({});
|
||||
if (data.session != null) {
|
||||
message.session = Session.fromObject(data.session);
|
||||
}
|
||||
if (data.from != null) {
|
||||
message.from = data.from;
|
||||
}
|
||||
if (data.id != null) {
|
||||
message.id = data.id;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
const data: {
|
||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||
from?: string;
|
||||
id?: string;
|
||||
} = {};
|
||||
if (this.session != null) {
|
||||
data.session = this.session.toObject();
|
||||
}
|
||||
if (this.from != null) {
|
||||
data.from = this.from;
|
||||
}
|
||||
if (this.id != null) {
|
||||
data.id = this.id;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
serialize(w: pb_1.BinaryWriter): void;
|
||||
serialize(w?: pb_1.BinaryWriter): Uint8Array | void {
|
||||
const writer = w || new pb_1.BinaryWriter();
|
||||
if (this.has_session)
|
||||
writer.writeMessage(1, this.session, () => this.session.serialize(writer));
|
||||
if (this.from.length)
|
||||
writer.writeString(2, this.from);
|
||||
if (this.id.length)
|
||||
writer.writeString(3, this.id);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
static deserialize(bytes: Uint8Array | pb_1.BinaryReader): RejectCallRequest {
|
||||
const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new RejectCallRequest();
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup())
|
||||
break;
|
||||
switch (reader.getFieldNumber()) {
|
||||
case 1:
|
||||
reader.readMessage(message.session, () => message.session = Session.deserialize(reader));
|
||||
break;
|
||||
case 2:
|
||||
message.from = reader.readString();
|
||||
break;
|
||||
case 3:
|
||||
message.id = reader.readString();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
serializeBinary(): Uint8Array {
|
||||
return this.serialize();
|
||||
}
|
||||
static deserializeBinary(bytes: Uint8Array): RejectCallRequest {
|
||||
return RejectCallRequest.deserialize(bytes);
|
||||
}
|
||||
}
|
||||
export class PollMessage extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [];
|
||||
constructor(data?: any[] | {
|
||||
@@ -10864,6 +11049,15 @@ export namespace messages {
|
||||
responseSerialize: (message: DownloadMediaResponse) => Buffer.from(message.serialize()),
|
||||
responseDeserialize: (bytes: Buffer) => DownloadMediaResponse.deserialize(new Uint8Array(bytes))
|
||||
},
|
||||
RejectCall: {
|
||||
path: "/messages.MessageService/RejectCall",
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestSerialize: (message: RejectCallRequest) => Buffer.from(message.serialize()),
|
||||
requestDeserialize: (bytes: Buffer) => RejectCallRequest.deserialize(new Uint8Array(bytes)),
|
||||
responseSerialize: (message: Empty) => Buffer.from(message.serialize()),
|
||||
responseDeserialize: (bytes: Buffer) => Empty.deserialize(new Uint8Array(bytes))
|
||||
},
|
||||
GetMessageById: {
|
||||
path: "/messages.MessageService/GetMessageById",
|
||||
requestStream: false,
|
||||
@@ -10953,6 +11147,7 @@ export namespace messages {
|
||||
abstract GetContactById(call: grpc_1.ServerUnaryCall<EntityByIdRequest, Json>, callback: grpc_1.sendUnaryData<Json>): void;
|
||||
abstract CancelEventMessage(call: grpc_1.ServerUnaryCall<CancelEventMessageRequest, MessageResponse>, callback: grpc_1.sendUnaryData<MessageResponse>): void;
|
||||
abstract DownloadMedia(call: grpc_1.ServerUnaryCall<DownloadMediaRequest, DownloadMediaResponse>, callback: grpc_1.sendUnaryData<DownloadMediaResponse>): void;
|
||||
abstract RejectCall(call: grpc_1.ServerUnaryCall<RejectCallRequest, Empty>, callback: grpc_1.sendUnaryData<Empty>): void;
|
||||
abstract GetMessageById(call: grpc_1.ServerUnaryCall<EntityByIdRequest, Json>, callback: grpc_1.sendUnaryData<Json>): void;
|
||||
abstract GetMessages(call: grpc_1.ServerUnaryCall<GetMessagesRequest, JsonList>, callback: grpc_1.sendUnaryData<JsonList>): void;
|
||||
abstract GetChats(call: grpc_1.ServerUnaryCall<GetChatsRequest, JsonList>, callback: grpc_1.sendUnaryData<JsonList>): void;
|
||||
@@ -11141,6 +11336,9 @@ export namespace messages {
|
||||
DownloadMedia: GrpcUnaryServiceInterface<DownloadMediaRequest, DownloadMediaResponse> = (message: DownloadMediaRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<DownloadMediaResponse>, options?: grpc_1.CallOptions | grpc_1.requestCallback<DownloadMediaResponse>, callback?: grpc_1.requestCallback<DownloadMediaResponse>): grpc_1.ClientUnaryCall => {
|
||||
return super.DownloadMedia(message, metadata, options, callback);
|
||||
};
|
||||
RejectCall: GrpcUnaryServiceInterface<RejectCallRequest, Empty> = (message: RejectCallRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<Empty>, options?: grpc_1.CallOptions | grpc_1.requestCallback<Empty>, callback?: grpc_1.requestCallback<Empty>): grpc_1.ClientUnaryCall => {
|
||||
return super.RejectCall(message, metadata, options, callback);
|
||||
};
|
||||
GetMessageById: GrpcUnaryServiceInterface<EntityByIdRequest, Json> = (message: EntityByIdRequest, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback<Json>, options?: grpc_1.CallOptions | grpc_1.requestCallback<Json>, callback?: grpc_1.requestCallback<Json>): grpc_1.ClientUnaryCall => {
|
||||
return super.GetMessageById(message, metadata, options, callback);
|
||||
};
|
||||
|
||||
@@ -543,6 +543,17 @@ function deserialize_messages_ProfileStatusRequest(buffer_arg) {
|
||||
return gows_pb.ProfileStatusRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_RejectCallRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.RejectCallRequest)) {
|
||||
throw new Error('Expected argument of type messages.RejectCallRequest');
|
||||
}
|
||||
return Buffer.from(arg.serializeBinary());
|
||||
}
|
||||
|
||||
function deserialize_messages_RejectCallRequest(buffer_arg) {
|
||||
return gows_pb.RejectCallRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_RevokeMessageRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.RevokeMessageRequest)) {
|
||||
throw new Error('Expected argument of type messages.RevokeMessageRequest');
|
||||
@@ -1391,6 +1402,20 @@ downloadMedia: {
|
||||
responseDeserialize: deserialize_messages_DownloadMediaResponse,
|
||||
},
|
||||
//
|
||||
// Calls
|
||||
//
|
||||
rejectCall: {
|
||||
path: '/messages.MessageService/RejectCall',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.RejectCallRequest,
|
||||
responseType: gows_pb.Empty,
|
||||
requestSerialize: serialize_messages_RejectCallRequest,
|
||||
requestDeserialize: deserialize_messages_RejectCallRequest,
|
||||
responseSerialize: serialize_messages_Empty,
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
//
|
||||
// Storage
|
||||
//
|
||||
getMessageById: {
|
||||
|
||||
@@ -94,6 +94,7 @@ goog.exportSymbol('proto.messages.ProfilePictureRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.ProfilePictureResponse', null, global);
|
||||
goog.exportSymbol('proto.messages.ProfileStatusRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.ReceiptType', null, global);
|
||||
goog.exportSymbol('proto.messages.RejectCallRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.RevokeMessageRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.Row', null, global);
|
||||
goog.exportSymbol('proto.messages.SearchNewslettersByTextRequest', null, global);
|
||||
@@ -1966,6 +1967,27 @@ if (goog.DEBUG && !COMPILED) {
|
||||
*/
|
||||
proto.messages.GetLidsRequest.displayName = 'proto.messages.GetLidsRequest';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
* server response, or constructed directly in Javascript. The array is used
|
||||
* in place and becomes part of the constructed object. It is not cloned.
|
||||
* If no data is provided, the constructed object will be empty, but still
|
||||
* valid.
|
||||
* @extends {jspb.Message}
|
||||
* @constructor
|
||||
*/
|
||||
proto.messages.RejectCallRequest = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
|
||||
};
|
||||
goog.inherits(proto.messages.RejectCallRequest, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
/**
|
||||
* @public
|
||||
* @override
|
||||
*/
|
||||
proto.messages.RejectCallRequest.displayName = 'proto.messages.RejectCallRequest';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
@@ -7115,7 +7137,8 @@ proto.messages.Media.toObject = function(includeInstance, msg) {
|
||||
type: jspb.Message.getFieldWithDefault(msg, 2, 0),
|
||||
mimetype: jspb.Message.getFieldWithDefault(msg, 3, ""),
|
||||
audio: (f = msg.getAudio()) && proto.messages.AudioInfo.toObject(includeInstance, f),
|
||||
filename: jspb.Message.getFieldWithDefault(msg, 5, "")
|
||||
filename: jspb.Message.getFieldWithDefault(msg, 5, ""),
|
||||
contentpath: jspb.Message.getFieldWithDefault(msg, 6, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -7173,6 +7196,10 @@ proto.messages.Media.deserializeBinaryFromReader = function(msg, reader) {
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setFilename(value);
|
||||
break;
|
||||
case 6:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setContentpath(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
@@ -7238,6 +7265,13 @@ proto.messages.Media.serializeBinaryToWriter = function(message, writer) {
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getContentpath();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
6,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7374,6 +7408,24 @@ proto.messages.Media.prototype.setFilename = function(value) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional string contentPath = 6;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.messages.Media.prototype.getContentpath = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 6, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.messages.Media} returns this
|
||||
*/
|
||||
proto.messages.Media.prototype.setContentpath = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 6, value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16673,7 +16725,8 @@ proto.messages.DownloadMediaRequest.toObject = function(includeInstance, msg) {
|
||||
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
|
||||
message: jspb.Message.getFieldWithDefault(msg, 2, ""),
|
||||
jid: jspb.Message.getFieldWithDefault(msg, 3, ""),
|
||||
messageid: jspb.Message.getFieldWithDefault(msg, 4, "")
|
||||
messageid: jspb.Message.getFieldWithDefault(msg, 4, ""),
|
||||
contentpath: jspb.Message.getFieldWithDefault(msg, 5, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -16727,6 +16780,10 @@ proto.messages.DownloadMediaRequest.deserializeBinaryFromReader = function(msg,
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setMessageid(value);
|
||||
break;
|
||||
case 5:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setContentpath(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
@@ -16785,6 +16842,13 @@ proto.messages.DownloadMediaRequest.serializeBinaryToWriter = function(message,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getContentpath();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
5,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16879,6 +16943,24 @@ proto.messages.DownloadMediaRequest.prototype.setMessageid = function(value) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional string contentPath = 5;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.messages.DownloadMediaRequest.prototype.getContentpath = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.messages.DownloadMediaRequest} returns this
|
||||
*/
|
||||
proto.messages.DownloadMediaRequest.prototype.setContentpath = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 5, value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16911,7 +16993,8 @@ proto.messages.DownloadMediaResponse.prototype.toObject = function(opt_includeIn
|
||||
*/
|
||||
proto.messages.DownloadMediaResponse.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
content: msg.getContent_asB64()
|
||||
content: msg.getContent_asB64(),
|
||||
contentpath: jspb.Message.getFieldWithDefault(msg, 5, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -16952,6 +17035,10 @@ proto.messages.DownloadMediaResponse.deserializeBinaryFromReader = function(msg,
|
||||
var value = /** @type {!Uint8Array} */ (reader.readBytes());
|
||||
msg.setContent(value);
|
||||
break;
|
||||
case 5:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setContentpath(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
@@ -16988,6 +17075,13 @@ proto.messages.DownloadMediaResponse.serializeBinaryToWriter = function(message,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getContentpath();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
5,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17033,6 +17127,24 @@ proto.messages.DownloadMediaResponse.prototype.setContent = function(value) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional string contentPath = 5;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.messages.DownloadMediaResponse.prototype.getContentpath = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 5, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.messages.DownloadMediaResponse} returns this
|
||||
*/
|
||||
proto.messages.DownloadMediaResponse.prototype.setContentpath = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 5, value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20764,6 +20876,217 @@ proto.messages.GetLidsRequest.prototype.hasSession = function() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
* Field names that are reserved in JavaScript and will be renamed to pb_name.
|
||||
* Optional fields that are not set will be set to undefined.
|
||||
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
|
||||
* For the list of reserved names please see:
|
||||
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
|
||||
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
|
||||
* JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @return {!Object}
|
||||
*/
|
||||
proto.messages.RejectCallRequest.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.messages.RejectCallRequest.toObject(opt_includeInstance, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Static version of the {@see toObject} method.
|
||||
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
|
||||
* the JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @param {!proto.messages.RejectCallRequest} msg The msg instance to transform.
|
||||
* @return {!Object}
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.messages.RejectCallRequest.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
|
||||
from: jspb.Message.getFieldWithDefault(msg, 2, ""),
|
||||
id: jspb.Message.getFieldWithDefault(msg, 3, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
obj.$jspbMessageInstance = msg;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format).
|
||||
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||
* @return {!proto.messages.RejectCallRequest}
|
||||
*/
|
||||
proto.messages.RejectCallRequest.deserializeBinary = function(bytes) {
|
||||
var reader = new jspb.BinaryReader(bytes);
|
||||
var msg = new proto.messages.RejectCallRequest;
|
||||
return proto.messages.RejectCallRequest.deserializeBinaryFromReader(msg, reader);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format) from the
|
||||
* given reader into the given message object.
|
||||
* @param {!proto.messages.RejectCallRequest} msg The message object to deserialize into.
|
||||
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||
* @return {!proto.messages.RejectCallRequest}
|
||||
*/
|
||||
proto.messages.RejectCallRequest.deserializeBinaryFromReader = function(msg, reader) {
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup()) {
|
||||
break;
|
||||
}
|
||||
var field = reader.getFieldNumber();
|
||||
switch (field) {
|
||||
case 1:
|
||||
var value = new proto.messages.Session;
|
||||
reader.readMessage(value,proto.messages.Session.deserializeBinaryFromReader);
|
||||
msg.setSession(value);
|
||||
break;
|
||||
case 2:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setFrom(value);
|
||||
break;
|
||||
case 3:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setId(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the message to binary data (in protobuf wire format).
|
||||
* @return {!Uint8Array}
|
||||
*/
|
||||
proto.messages.RejectCallRequest.prototype.serializeBinary = function() {
|
||||
var writer = new jspb.BinaryWriter();
|
||||
proto.messages.RejectCallRequest.serializeBinaryToWriter(this, writer);
|
||||
return writer.getResultBuffer();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the given message to binary data (in protobuf wire
|
||||
* format), writing to the given BinaryWriter.
|
||||
* @param {!proto.messages.RejectCallRequest} message
|
||||
* @param {!jspb.BinaryWriter} writer
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.messages.RejectCallRequest.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = message.getSession();
|
||||
if (f != null) {
|
||||
writer.writeMessage(
|
||||
1,
|
||||
f,
|
||||
proto.messages.Session.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
f = message.getFrom();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
2,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getId();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
3,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional Session session = 1;
|
||||
* @return {?proto.messages.Session}
|
||||
*/
|
||||
proto.messages.RejectCallRequest.prototype.getSession = function() {
|
||||
return /** @type{?proto.messages.Session} */ (
|
||||
jspb.Message.getWrapperField(this, proto.messages.Session, 1));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {?proto.messages.Session|undefined} value
|
||||
* @return {!proto.messages.RejectCallRequest} returns this
|
||||
*/
|
||||
proto.messages.RejectCallRequest.prototype.setSession = function(value) {
|
||||
return jspb.Message.setWrapperField(this, 1, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the message field making it undefined.
|
||||
* @return {!proto.messages.RejectCallRequest} returns this
|
||||
*/
|
||||
proto.messages.RejectCallRequest.prototype.clearSession = function() {
|
||||
return this.setSession(undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.RejectCallRequest.prototype.hasSession = function() {
|
||||
return jspb.Message.getField(this, 1) != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional string from = 2;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.messages.RejectCallRequest.prototype.getFrom = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.messages.RejectCallRequest} returns this
|
||||
*/
|
||||
proto.messages.RejectCallRequest.prototype.setFrom = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 2, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional string id = 3;
|
||||
* @return {string}
|
||||
*/
|
||||
proto.messages.RejectCallRequest.prototype.getId = function() {
|
||||
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, ""));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @return {!proto.messages.RejectCallRequest} returns this
|
||||
*/
|
||||
proto.messages.RejectCallRequest.prototype.setId = function(value) {
|
||||
return jspb.Message.setProto3StringField(this, 3, value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* List of repeated fields within this message type.
|
||||
* @private {!Array<number>}
|
||||
|
||||
@@ -123,6 +123,7 @@ import {
|
||||
WAMessage,
|
||||
WAMessageReaction,
|
||||
} from '@waha/structures/responses.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import { MeInfo, ProxyConfig } from '@waha/structures/sessions.dto';
|
||||
import {
|
||||
BROADCAST_ID,
|
||||
@@ -142,6 +143,7 @@ import { onlyEvent } from '@waha/utils/reactive/ops/onlyEvent';
|
||||
import * as NodeCache from 'node-cache';
|
||||
import {
|
||||
filter,
|
||||
groupBy,
|
||||
merge,
|
||||
mergeMap,
|
||||
Observable,
|
||||
@@ -150,7 +152,7 @@ import {
|
||||
share,
|
||||
Subject,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { map, debounceTime } from 'rxjs/operators';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import * as gows from './types';
|
||||
@@ -177,11 +179,14 @@ import {
|
||||
} from '@waha/core/engines/gows/clients';
|
||||
import esm from '@waha/vendor/esm';
|
||||
import { IsEditedMessage } from '@waha/core/utils/pwa';
|
||||
import MessageServiceClient = messages.MessageServiceClient;
|
||||
import { GoToJSWAProto } from '@waha/core/engines/gows/waproto';
|
||||
import { extractWALocation } from '@waha/core/engines/waproto/locaiton';
|
||||
import { extractVCards } from '@waha/core/engines/waproto/vcards';
|
||||
import { Activity } from '@waha/core/abc/activity';
|
||||
import { TmpDir } from '@waha/utils/tmpdir';
|
||||
import * as path from 'path';
|
||||
import MessageServiceClient = messages.MessageServiceClient;
|
||||
import * as fsp from 'fs/promises';
|
||||
|
||||
enum WhatsMeowEvent {
|
||||
CONNECTED = 'gows.ConnectedEventData',
|
||||
@@ -205,13 +210,14 @@ enum WhatsMeowEvent {
|
||||
EVENT_MESSAGE_RESPONSE = 'gows.EventMessageResponse',
|
||||
// Polls
|
||||
POLL_VOTE_EVENT = 'gows.PollVoteEvent',
|
||||
// Calls
|
||||
CALL_OFFER = 'events.CallOffer',
|
||||
CALL_ACCEPT = 'events.CallAccept',
|
||||
CALL_REJECT = 'events.CallReject',
|
||||
CALL_TERMINATE = 'events.CallTerminate',
|
||||
CALL_OFFER_NOTICE = 'events.CallOfferNotice',
|
||||
}
|
||||
|
||||
const gRPCClientConfig = {
|
||||
'grpc.max_send_message_length': 512 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 512 * 1024 * 1024,
|
||||
};
|
||||
|
||||
export interface GowsConfig {
|
||||
connection: string;
|
||||
}
|
||||
@@ -279,7 +285,6 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
this.client = MessageServiceClientSingleton(
|
||||
this.engineConfig.connection,
|
||||
grpc.credentials.createInsecure(),
|
||||
gRPCClientConfig,
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -309,7 +314,6 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
const client = EventStreamClientSingleton(
|
||||
this.engineConfig.connection,
|
||||
grpc.credentials.createInsecure(),
|
||||
gRPCClientConfig,
|
||||
);
|
||||
const stream = client.StreamEvents(this.session);
|
||||
return { client, stream };
|
||||
@@ -546,6 +550,52 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_REACTION).switch(messageReactions$);
|
||||
|
||||
//
|
||||
// Calls
|
||||
//
|
||||
const callOffer$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.CALL_OFFER),
|
||||
filter(this.shouldProcessCallEvent.bind(this)),
|
||||
map(this.toCallData.bind(this)),
|
||||
);
|
||||
const callOfferNotice$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.CALL_OFFER_NOTICE),
|
||||
filter(this.shouldProcessCallEvent.bind(this)),
|
||||
map(this.toCallData.bind(this)),
|
||||
);
|
||||
this.events2
|
||||
.get(WAHAEvents.CALL_RECEIVED)
|
||||
.switch(merge(callOffer$, callOfferNotice$));
|
||||
|
||||
const callAccept$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.CALL_ACCEPT),
|
||||
filter(this.shouldProcessCallEvent.bind(this)),
|
||||
map(this.toCallData.bind(this)),
|
||||
);
|
||||
this.events2.get(WAHAEvents.CALL_ACCEPTED).switch(callAccept$);
|
||||
|
||||
const callReject$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.CALL_REJECT),
|
||||
filter(this.shouldProcessCallEvent.bind(this)),
|
||||
);
|
||||
const callTerminate$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.CALL_TERMINATE),
|
||||
filter(this.shouldProcessCallEvent.bind(this)),
|
||||
// Skip terminate events that only mean the call was accepted or rejected on another device
|
||||
filter(
|
||||
(call: any) =>
|
||||
call?.Reason !== 'accepted_elsewhere' &&
|
||||
call?.Reason !== 'rejected_elsewhere',
|
||||
),
|
||||
);
|
||||
// Debounce per call to collapse duplicate reject/terminate events
|
||||
const callRejected$ = merge(callReject$, callTerminate$).pipe(
|
||||
groupBy((call: any) => this.getCallId(call) || 'unknown'),
|
||||
mergeMap((group$) => group$.pipe(debounceTime(1_000))),
|
||||
map(this.toCallData.bind(this)),
|
||||
);
|
||||
this.events2.get(WAHAEvents.CALL_REJECTED).switch(callRejected$);
|
||||
|
||||
const presence$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.PRESENCE),
|
||||
filter((event: any) => this.jids.include(event?.From)),
|
||||
@@ -812,6 +862,15 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return data.id;
|
||||
}
|
||||
|
||||
async rejectCall(from: string, id: string): Promise<void> {
|
||||
const request = new messages.RejectCallRequest({
|
||||
session: this.session,
|
||||
from: toJID(this.ensureSuffix(from)),
|
||||
id: id,
|
||||
});
|
||||
await promisify(this.client.RejectCall)(request);
|
||||
}
|
||||
|
||||
@Activity()
|
||||
async sendText(request: MessageTextRequest) {
|
||||
const jid = toJID(this.ensureSuffix(request.chatId));
|
||||
@@ -2169,6 +2228,59 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
};
|
||||
}
|
||||
|
||||
private getCallId(call: any): string | null {
|
||||
return call?.CallID || call?.Data?.Attrs?.['call-id'] || null;
|
||||
}
|
||||
|
||||
private shouldProcessCallEvent(call: any): boolean {
|
||||
if (!call) {
|
||||
return false;
|
||||
}
|
||||
if (call.GroupJID && this.jids.include(call.GroupJID)) {
|
||||
return true;
|
||||
}
|
||||
return this.jids.include(call.From);
|
||||
}
|
||||
|
||||
private toCallData(call: any): CallData {
|
||||
const date = call?.Timestamp ? new Date(call.Timestamp) : new Date();
|
||||
const timestamp = date.getTime() / 1000;
|
||||
const isVideo = this.isVideoCall(call);
|
||||
const isGroup = this.isGroupCall(call);
|
||||
const from = call?.From || call?.GroupJID;
|
||||
return {
|
||||
id: this.getCallId(call),
|
||||
from: from ? toCusFormat(from) : undefined,
|
||||
timestamp: timestamp,
|
||||
isVideo: Boolean(isVideo),
|
||||
isGroup: isGroup,
|
||||
_data: call,
|
||||
};
|
||||
}
|
||||
|
||||
private isVideoCall(call: any): boolean {
|
||||
if (!call) return false;
|
||||
if (call?.Media === 'video') return true;
|
||||
const attrsMedia = call?.Data?.Attrs?.media || call?.Data?.Attrs?.type;
|
||||
if (attrsMedia === 'video') return true;
|
||||
const content = call?.Data?.Content;
|
||||
if (Array.isArray(content)) {
|
||||
const hasVideoTag = content.some((item: any) => item?.Tag === 'video');
|
||||
if (hasVideoTag) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private isGroupCall(call: any): boolean {
|
||||
if (!call) return false;
|
||||
if (call?.GroupJID) return true;
|
||||
const attrs = call?.Data?.Attrs;
|
||||
if (!attrs) return false;
|
||||
return attrs.type === 'group' || Boolean(attrs['group-jid']);
|
||||
}
|
||||
|
||||
private toEventResponsePayload(event: any): EventResponsePayload {
|
||||
const msg = this.toWAMessage(event);
|
||||
let response: EventResponse | null = null;
|
||||
@@ -2375,30 +2487,44 @@ export class GOWSEngineMediaProcessor implements IMediaEngineProcessor<any> {
|
||||
const mediaDownloadTimeoutMs = 600_000; // 10 minutes
|
||||
|
||||
const data = JSON.stringify(message.Message);
|
||||
const request = new messages.DownloadMediaRequest({
|
||||
// double "session" it's not a mistake here
|
||||
session: this.session.session,
|
||||
message: data,
|
||||
jid: message.Info.Chat,
|
||||
messageId: message.Info.ID,
|
||||
});
|
||||
|
||||
const opts = {
|
||||
deadline: new Date(Date.now() + mediaDownloadTimeoutMs),
|
||||
};
|
||||
const call = promisify(
|
||||
this.session.client.DownloadMedia.bind(this.session.client),
|
||||
const tmpdir = new TmpDir(
|
||||
this.session.logger,
|
||||
`waha-media-${this.session.name}-`,
|
||||
);
|
||||
try {
|
||||
const response = await call(request, opts);
|
||||
const obj = response.toObject();
|
||||
return Buffer.from(obj.content);
|
||||
} catch (err) {
|
||||
if (err?.code === grpc.status.DEADLINE_EXCEEDED) {
|
||||
err.message = `DownloadMedia timed out after ${mediaDownloadTimeoutMs}ms for message '${message?.Info?.ID}'`;
|
||||
return await tmpdir.use(async (dir) => {
|
||||
const file = path.join(dir, 'content.tmp');
|
||||
const request = new messages.DownloadMediaRequest({
|
||||
// double "session" it's not a mistake here
|
||||
session: this.session.session,
|
||||
message: data,
|
||||
jid: message.Info.Chat,
|
||||
messageId: message.Info.ID,
|
||||
contentPath: file,
|
||||
});
|
||||
|
||||
const opts = {
|
||||
deadline: new Date(Date.now() + mediaDownloadTimeoutMs),
|
||||
};
|
||||
const call = promisify(
|
||||
this.session.client.DownloadMedia.bind(this.session.client),
|
||||
);
|
||||
try {
|
||||
const response = await call(request, opts);
|
||||
const obj = response.toObject();
|
||||
if (!obj.contentPath) {
|
||||
// Read directly from grpc response
|
||||
return Buffer.from(obj.content);
|
||||
} else {
|
||||
// Read from the file
|
||||
return await fsp.readFile(obj.contentPath);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.code === grpc.status.DEADLINE_EXCEEDED) {
|
||||
err.message = `DownloadMedia timed out after ${mediaDownloadTimeoutMs}ms for message '${message?.Info?.ID}'`;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getFilename(message: any): string | null {
|
||||
|
||||
@@ -67,6 +67,7 @@ import type { Agent } from 'https';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { AckToStatus, StatusToAck } from '@waha/core/utils/acks';
|
||||
import { pairs } from '@waha/utils/pairs';
|
||||
import { ExtractMessageKeysForRead } from '@waha/core/utils/convertors';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { isJidNewsletter, toCusFormat, toJID } from '@waha/core/utils/jids';
|
||||
@@ -186,9 +187,11 @@ import {
|
||||
mergeMap,
|
||||
Observable,
|
||||
partition,
|
||||
groupBy,
|
||||
share,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { debounceTime, map } from 'rxjs/operators';
|
||||
|
||||
import { INowebStore } from './store/INowebStore';
|
||||
import { NowebPersistentStore } from './store/NowebPersistentStore';
|
||||
@@ -631,10 +634,14 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
private issueMessageUpdateOnPoll() {
|
||||
// Fix for https://github.com/devlikeapro/waha/issues/960
|
||||
this.sock.ev.on('messages.upsert', async ({ messages }) => {
|
||||
const meId = this.getSessionMeInfo().id;
|
||||
if (!meId) {
|
||||
const me = this.getSessionMeInfo();
|
||||
if (!me) {
|
||||
this.logger.warn(
|
||||
'Cannot issue poll updates, session "me" info not found',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
const content = normalizeMessageContent(message.message);
|
||||
if (!content?.pollUpdateMessage) {
|
||||
@@ -653,38 +660,75 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
continue;
|
||||
}
|
||||
|
||||
const meIdNormalised = jidNormalizedUser(meId);
|
||||
const pollCreatorJid = getKeyAuthor(creationMsgKey, meIdNormalised);
|
||||
const voterJid = getKeyAuthor(message.key, meIdNormalised);
|
||||
const pollEncKey = pollMsg.messageContextInfo?.messageSecret;
|
||||
|
||||
try {
|
||||
const voteMsg = decryptPollVote(content.pollUpdateMessage.vote, {
|
||||
pollEncKey,
|
||||
pollCreatorJid,
|
||||
pollMsgId: creationMsgKey.id,
|
||||
voterJid,
|
||||
});
|
||||
this.sock.ev.emit('messages.update', [
|
||||
{
|
||||
key: creationMsgKey,
|
||||
update: {
|
||||
pollUpdates: [
|
||||
{
|
||||
pollUpdateMessageKey: message.key,
|
||||
vote: voteMsg,
|
||||
senderTimestampMs: (
|
||||
content.pollUpdateMessage.senderTimestampMs as Long
|
||||
).toNumber(),
|
||||
},
|
||||
],
|
||||
// Because of new @lid system it's hard to detect exactly how
|
||||
// the vote has been encrypted, so we'll iterator over all possible
|
||||
// not null combinations
|
||||
const key = message.key;
|
||||
const myIds = [jidNormalizedUser(me.id), jidNormalizedUser(me.lid)];
|
||||
const participantIds = [
|
||||
key?.participantAlt,
|
||||
key?.remoteJidAlt,
|
||||
key?.participant,
|
||||
key?.remoteJid,
|
||||
];
|
||||
let creators: string[] = creationMsgKey.fromMe
|
||||
? [...myIds, ...participantIds]
|
||||
: [...participantIds, ...myIds];
|
||||
let votes: string[] = key.fromMe
|
||||
? [...myIds, ...participantIds]
|
||||
: [...participantIds, ...myIds];
|
||||
creators = lodash.uniq(creators.filter(Boolean));
|
||||
votes = lodash.uniq(votes.filter(Boolean));
|
||||
let found = false;
|
||||
for (const [pollCreatorJid, voterJid] of pairs(creators, votes)) {
|
||||
try {
|
||||
const pollEncKey = pollMsg.messageContextInfo?.messageSecret;
|
||||
const voteMsg = decryptPollVote(content.pollUpdateMessage.vote, {
|
||||
pollCreatorJid: pollCreatorJid,
|
||||
pollMsgId: creationMsgKey.id,
|
||||
pollEncKey: pollEncKey,
|
||||
voterJid: voterJid,
|
||||
});
|
||||
this.sock.ev.emit('messages.update', [
|
||||
{
|
||||
key: creationMsgKey,
|
||||
update: {
|
||||
pollUpdates: [
|
||||
{
|
||||
pollUpdateMessageKey: message.key,
|
||||
vote: voteMsg,
|
||||
senderTimestampMs: (
|
||||
content.pollUpdateMessage.senderTimestampMs as Long
|
||||
).toNumber(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
} catch (err) {
|
||||
]);
|
||||
found = true;
|
||||
break;
|
||||
} catch (err) {
|
||||
this.logger.trace(
|
||||
{
|
||||
err: err.message,
|
||||
key: key,
|
||||
creationsMsgKey: creationMsgKey,
|
||||
pollCreatorJid: pollCreatorJid,
|
||||
voterJid: voterJid,
|
||||
},
|
||||
'failed to decrypt poll vote using creator and voter',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
this.logger.warn(
|
||||
{ err, creationMsgKey },
|
||||
'failed to decrypt poll vote',
|
||||
{
|
||||
key: key,
|
||||
creationsMsgKey: creationMsgKey,
|
||||
creators: creators,
|
||||
voters: votes,
|
||||
},
|
||||
'failed to decrypt poll vote with any combination of creator/voter',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -850,6 +894,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return this.generateMessageID();
|
||||
}
|
||||
|
||||
async rejectCall(from: string, id: string): Promise<void> {
|
||||
const jid = toJID(this.ensureSuffix(from));
|
||||
await this.sock.rejectCall(id, jid);
|
||||
}
|
||||
|
||||
@Activity()
|
||||
async sendText(request: MessageTextRequest) {
|
||||
const chatId = toJID(this.ensureSuffix(request.chatId));
|
||||
@@ -2123,6 +2172,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
),
|
||||
share(),
|
||||
);
|
||||
|
||||
const acceptedCallIds = new Set<string>();
|
||||
this.events2.get(WAHAEvents.CALL_RECEIVED).switch(
|
||||
call$.pipe(
|
||||
filter((call: WACallEvent) => call.status === 'offer'),
|
||||
@@ -2132,18 +2183,33 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
this.events2.get(WAHAEvents.CALL_ACCEPTED).switch(
|
||||
call$.pipe(
|
||||
filter((call: WACallEvent) => call.status === 'accept'),
|
||||
tap((call: WACallEvent) => acceptedCallIds.add(call.id)),
|
||||
map(this.toCallData.bind(this)),
|
||||
),
|
||||
);
|
||||
this.events2.get(WAHAEvents.CALL_REJECTED).switch(
|
||||
calls$.pipe(
|
||||
// Filter out if there's any "accept" events.
|
||||
// Meaning it's been accepted on one device, but rejected on another
|
||||
exclude((calls) => calls.some((call) => call.status === 'accept')),
|
||||
mergeAll(),
|
||||
filter((call: WACallEvent) => call.status === 'reject'),
|
||||
call$.pipe(
|
||||
filter(
|
||||
(call: WACallEvent) =>
|
||||
call.status === 'reject' || call.status === 'terminate',
|
||||
),
|
||||
// Skip rejections when the call was accepted earlier (local or other device)
|
||||
exclude((call: WACallEvent) => {
|
||||
const shouldSkip = acceptedCallIds.has(call.id);
|
||||
if (call.status === 'terminate') {
|
||||
acceptedCallIds.delete(call.id);
|
||||
}
|
||||
return shouldSkip;
|
||||
}),
|
||||
// We get two "reject" events, one with null isGroup property, ignore it
|
||||
exclude((call: WACallEvent) => call.isGroup == null),
|
||||
groupBy((call: WACallEvent) => call.id || 'unknown'),
|
||||
mergeMap((group$) =>
|
||||
group$.pipe(
|
||||
debounceTime(1_000),
|
||||
tap((call: WACallEvent) => acceptedCallIds.delete(call.id)),
|
||||
),
|
||||
),
|
||||
map(this.toCallData.bind(this)),
|
||||
),
|
||||
);
|
||||
@@ -2540,6 +2606,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
timestamp: timestamp,
|
||||
isVideo: call.isVideo,
|
||||
isGroup: call.isGroup,
|
||||
_data: call,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,15 @@ import { TmpDir } from '@waha/utils/tmpdir';
|
||||
import * as lodash from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { ProtocolError } from 'puppeteer';
|
||||
import { filter, fromEvent, merge, mergeMap, Observable, share } from 'rxjs';
|
||||
import {
|
||||
filter,
|
||||
fromEvent,
|
||||
merge,
|
||||
mergeMap,
|
||||
Observable,
|
||||
share,
|
||||
Subject,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import {
|
||||
AuthStrategy,
|
||||
@@ -150,7 +158,10 @@ import {
|
||||
Reaction,
|
||||
WAState,
|
||||
} from 'whatsapp-web.js';
|
||||
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||
import {
|
||||
Message as MessageInstance,
|
||||
Call as CallInstance,
|
||||
} from 'whatsapp-web.js/src/structures';
|
||||
|
||||
import { WAJSPresenceChatStateType, WebJSPresence } from './types';
|
||||
import {
|
||||
@@ -160,6 +171,7 @@ import {
|
||||
toCusFormat,
|
||||
} from '@waha/core/utils/jids';
|
||||
import { Activity } from '@waha/core/abc/activity';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
|
||||
export interface WebJSConfig {
|
||||
webVersion?: string;
|
||||
@@ -180,6 +192,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
whatsapp: WebjsClientCore;
|
||||
protected qr: QR;
|
||||
private callRejected$ = new Subject<CallData>();
|
||||
|
||||
public constructor(config) {
|
||||
super(config);
|
||||
@@ -475,7 +488,6 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.READY, () => {
|
||||
this.status = WAHASessionStatus.WORKING;
|
||||
this.qr.save('');
|
||||
this.logger.info(`Session '${this.name}' is ready!`);
|
||||
});
|
||||
@@ -497,6 +509,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.AUTHENTICATED, (args) => {
|
||||
this.status = WAHASessionStatus.WORKING;
|
||||
this.qr.save('');
|
||||
this.logger.info({ args: args }, `Session has been authenticated!`);
|
||||
});
|
||||
@@ -651,6 +664,15 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
/**
|
||||
* Other methods
|
||||
*/
|
||||
async rejectCall(from: string, id: string): Promise<void> {
|
||||
const peerJid = normalizeJid(this.ensureSuffix(from));
|
||||
const call = new CallInstance(this.whatsapp, null);
|
||||
call.id = id;
|
||||
call.from = peerJid;
|
||||
await call.reject();
|
||||
this.callRejected$.next(this.toRejectedCallData(peerJid, id));
|
||||
}
|
||||
|
||||
@Activity()
|
||||
sendText(request: MessageTextRequest) {
|
||||
const options = this.getMessageOptions(request);
|
||||
@@ -1770,10 +1792,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
timestamp: call.timestamp,
|
||||
isVideo: call.isVideo,
|
||||
isGroup: call.isGroup,
|
||||
_data: call,
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.events2.get(WAHAEvents.CALL_RECEIVED).switch(calls$);
|
||||
this.events2
|
||||
.get(WAHAEvents.CALL_REJECTED)
|
||||
.switch(this.callRejected$.asObservable());
|
||||
}
|
||||
|
||||
protected async processIncomingMessage(
|
||||
@@ -1790,6 +1816,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return wamessage;
|
||||
}
|
||||
|
||||
private toRejectedCallData(peerJid: string, id: string): CallData {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
return {
|
||||
id: id,
|
||||
from: peerJid,
|
||||
timestamp: timestamp,
|
||||
isVideo: false,
|
||||
isGroup: isJidGroup(peerJid),
|
||||
_data: {
|
||||
id: id,
|
||||
from: peerJid,
|
||||
status: 'reject',
|
||||
api: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private processMessageReaction(reaction: Reaction): WAMessageReaction {
|
||||
if (this.lastQRDate) {
|
||||
// If it's timestamp before last qr - ignore it
|
||||
|
||||
@@ -9,8 +9,9 @@ import axios, { AxiosInstance } from 'axios';
|
||||
import axiosRetry, { retryAfter } from 'axios-retry';
|
||||
import * as crypto from 'crypto';
|
||||
import { Logger } from 'pino';
|
||||
import { ulid } from 'ulid';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const uniqid = require('uniqid');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const HttpAgent = require('agentkeepalive');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
@@ -71,6 +72,7 @@ export class WebhookSender {
|
||||
const ctx = {
|
||||
id: headers['X-Webhook-Request-Id'],
|
||||
['event.id']: json.id,
|
||||
event: json.event,
|
||||
url: this.url,
|
||||
};
|
||||
this.logger.info(ctx, `Sending POST...`);
|
||||
@@ -133,7 +135,9 @@ export class WebhookSender {
|
||||
retryCondition: (error) => true,
|
||||
onRetry: (retryCount, error, requestConfig) => {
|
||||
this.logger.warn(
|
||||
{ id: requestConfig.headers['X-Webhook-Request-Id'] },
|
||||
{
|
||||
id: requestConfig.headers['X-Webhook-Request-Id'],
|
||||
},
|
||||
`Error sending POST request: '${error.message}'. Retrying ${retryCount}/${attempts}...`,
|
||||
);
|
||||
},
|
||||
@@ -157,7 +161,7 @@ export class WebhookSender {
|
||||
const timestamp = json.timestamp?.toString() || Date.now().toString();
|
||||
return {
|
||||
// UUID, no '-' in it
|
||||
'X-Webhook-Request-Id': ulid(),
|
||||
'X-Webhook-Request-Id': uniqid(),
|
||||
// unix timestamp with ms
|
||||
'X-Webhook-Timestamp': timestamp,
|
||||
};
|
||||
|
||||
@@ -218,11 +218,20 @@ export class SessionManagerCore extends SessionManager implements OnModuleInit {
|
||||
webhook.configure(session, webhooks);
|
||||
|
||||
// Apps
|
||||
await this.appsService.beforeSessionStart(session, this.store);
|
||||
try {
|
||||
await this.appsService.beforeSessionStart(session, this.store);
|
||||
} catch (e) {
|
||||
logger.error(`Apps Error: ${e}`);
|
||||
session.status = WAHASessionStatus.FAILED;
|
||||
}
|
||||
|
||||
// start session
|
||||
await session.start();
|
||||
logger.info('Session has been started.');
|
||||
if (session.status !== WAHASessionStatus.FAILED) {
|
||||
await session.start();
|
||||
logger.info('Session has been started.');
|
||||
// Apps
|
||||
await this.appsService.afterSessionStart(session, this.store);
|
||||
}
|
||||
|
||||
// Apps
|
||||
await this.appsService.afterSessionStart(session, this.store);
|
||||
@@ -295,6 +304,7 @@ export class SessionManagerCore extends SessionManager implements OnModuleInit {
|
||||
|
||||
async delete(name: string): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
await this.appsService.removeBySession(this, name);
|
||||
this.session = DefaultSessionStatus.REMOVED;
|
||||
this.updateSession();
|
||||
this.sessionConfig = undefined;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
function CallIdProperty() {
|
||||
return ApiProperty({
|
||||
@@ -11,6 +12,18 @@ function CallIdProperty() {
|
||||
});
|
||||
}
|
||||
|
||||
export class RejectCallRequest {
|
||||
@ChatIdProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
from: string;
|
||||
|
||||
@CallIdProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class CallData {
|
||||
@CallIdProperty()
|
||||
id: string;
|
||||
@@ -23,4 +36,6 @@ export class CallData {
|
||||
isVideo: boolean;
|
||||
|
||||
isGroup: boolean;
|
||||
|
||||
_data: any;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
|
||||
import { IsDynamicObject } from '@waha/nestjs/validation/IsDynamicObject';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
@@ -19,7 +21,26 @@ import { WebhookConfig } from './webhooks.config.dto';
|
||||
/**
|
||||
* Queries
|
||||
*/
|
||||
export class ListSessionsQuery {
|
||||
export enum SessionExpand {
|
||||
apps = 'apps',
|
||||
}
|
||||
|
||||
export class SessionExpandQuery {
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
type: String,
|
||||
enum: SessionExpand,
|
||||
isArray: true,
|
||||
description: 'Expand additional session details.',
|
||||
})
|
||||
@Transform(({ value }) => (Array.isArray(value) ? value : [value]))
|
||||
@IsArray()
|
||||
@IsEnum(SessionExpand, { each: true })
|
||||
@IsOptional()
|
||||
expand?: SessionExpand[];
|
||||
}
|
||||
|
||||
export class ListSessionsQuery extends SessionExpandQuery {
|
||||
@ApiProperty({
|
||||
example: false,
|
||||
required: false,
|
||||
@@ -32,6 +53,8 @@ export class ListSessionsQuery {
|
||||
all?: boolean;
|
||||
}
|
||||
|
||||
export class SessionInfoQuery extends SessionExpandQuery {}
|
||||
|
||||
/**
|
||||
* Requests
|
||||
*/
|
||||
@@ -242,6 +265,15 @@ export class SessionInfo extends SessionDTO {
|
||||
timestamps: {
|
||||
activity: number | null;
|
||||
};
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Apps configured for the session.',
|
||||
required: false,
|
||||
isArray: true,
|
||||
type: App,
|
||||
nullable: true,
|
||||
})
|
||||
apps?: App[];
|
||||
}
|
||||
|
||||
export class SessionDetailedInfo extends SessionInfo {
|
||||
@@ -272,6 +304,19 @@ export class SessionCreateRequest {
|
||||
@IsOptional()
|
||||
config?: SessionConfig;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Apps to be synchronized for this session.',
|
||||
required: false,
|
||||
isArray: true,
|
||||
type: App,
|
||||
nullable: true,
|
||||
})
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => App)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
apps?: App[] | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Start session after creation',
|
||||
example: true,
|
||||
@@ -287,4 +332,17 @@ export class SessionUpdateRequest {
|
||||
@Type(() => SessionConfig)
|
||||
@IsOptional()
|
||||
config?: SessionConfig;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Apps to be synchronized for this session.',
|
||||
required: false,
|
||||
isArray: true,
|
||||
type: App,
|
||||
nullable: true,
|
||||
})
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => App)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
apps?: App[] | null;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export function* pairs<T, U>(
|
||||
iter1: Iterable<T>,
|
||||
iter2: Iterable<U>,
|
||||
): Generator<[T, U]> {
|
||||
for (const id1 of iter1) {
|
||||
for (const id2 of iter2) {
|
||||
yield [id1, id2];
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -30,7 +30,7 @@ export function getWAHAVersion(): WAHAVersion {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2025.11.3',
|
||||
version: '2025.12.1',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser:
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
"waha": {
|
||||
"gows": {
|
||||
"repo": "devlikeapro/gows",
|
||||
"ref": "v1.0.23"
|
||||
"ref": "v1.0.25"
|
||||
},
|
||||
"dashboard": {
|
||||
"repo": "devlikeapro/dashboard",
|
||||
"ref": "d982212d20191adab7065216cad45ff064b483d8"
|
||||
"ref": "e29fd9300cfae9cabe8fbb66c1d30009c06081f3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7279,10 +7279,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"google-protobuf@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "google-protobuf@npm:4.0.1"
|
||||
checksum: d6ed3007b121ac28dbe9a686bdcc26ba4aba8bc41bd43bbaf59c2cbad96eba4d56338b9da56e8006fa89922eb5b15cb5c79f40b82d03ac39e006c9163435ad4b
|
||||
"google-protobuf@npm:^3.21.4":
|
||||
version: 3.21.4
|
||||
resolution: "google-protobuf@npm:3.21.4"
|
||||
checksum: 048fa2cb579f5f88c977774b2ae36851807379d9329a6895fe3685df69ba6c927e2ff463d08d5eecd56becd9c65bca406f34f90e27984a3077e8629bb3a2a766
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -12392,6 +12392,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uniqid@npm:^5.4.0":
|
||||
version: 5.4.0
|
||||
resolution: "uniqid@npm:5.4.0"
|
||||
checksum: 69fc28e7b2b5b24227b4295e51aa7c1d4085a60655ad071db8fc350876cc9044d68ee0781c0d27c2c98691380aa418970e0d1b02a1e4564480f9d19b0dc1707b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unique-filename@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "unique-filename@npm:1.1.1"
|
||||
@@ -12609,7 +12616,7 @@ __metadata:
|
||||
express-basic-auth: ^1.2.1
|
||||
file-type: 16.5.4
|
||||
fs-extra: ^11.2.0
|
||||
google-protobuf: ^4.0.1
|
||||
google-protobuf: ^3.21.4
|
||||
grpc-tools: ^1.13.0
|
||||
https-proxy-agent: ^7.0.0
|
||||
ical.js: ^2.2.1
|
||||
@@ -12656,6 +12663,7 @@ __metadata:
|
||||
typescript: 5.9.2
|
||||
ulid: ^2.3.0
|
||||
undici: ^7.16.0
|
||||
uniqid: ^5.4.0
|
||||
whatsapp-web.js: "github:devlikeapro/whatsapp-web.js#fork-main-2025-11-05"
|
||||
write-file-atomic: ^6.0.0
|
||||
yaml: ^2.7.1
|
||||
@@ -12759,7 +12767,7 @@ __metadata:
|
||||
|
||||
"whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-11-05":
|
||||
version: 1.34.2
|
||||
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=06c56b6a693d438e38793cec150c2890ca59910f"
|
||||
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=59a904a9a1f2869d39754117289267e0a925a7af"
|
||||
dependencies:
|
||||
"@pedroslopez/moduleraid": ^5.0.2
|
||||
archiver: ^5.3.1
|
||||
@@ -12777,7 +12785,7 @@ __metadata:
|
||||
optional: true
|
||||
unzipper:
|
||||
optional: true
|
||||
checksum: 46971900f209cb9988a8386ce13d9d082d7133711b1e6c38e1880684a70c1eda407be3c6fe45cd2526160efd47c2780b6b55500592ffc2b8c784f5e6c7d54670
|
||||
checksum: a59e4cee66a41f2a59d0340c4a765848f9fe76bdf4cfdd749c50deb5246f61bb8f9974fe4b35a7cc13d3adde6244975cea02caf72ca6e746b18b5d9c5c425b69
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in new issue
Block a user