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 | ||
|
|
5784db8055 | ||
|
|
6862063654 | ||
|
|
4aea282356 | ||
|
|
82ff15ae56 | ||
|
|
6cbf8cded2 | ||
|
|
35e9418988 | ||
|
|
a58be84e56 | ||
|
|
2994e0a54d | ||
|
|
f16100e1d9 | ||
|
|
e57be58d48 | ||
|
|
7909910dc6 | ||
|
|
7f1778ceb0 | ||
|
|
aba90574d8 | ||
|
|
33c945bf95 | ||
|
|
d40e82eb02 | ||
|
|
6e82d6f40b | ||
|
|
e193eb5696 | ||
|
|
8c63c4b097 | ||
|
|
4127d609cf | ||
|
|
57f6db4252 | ||
|
|
68a91cb55c | ||
|
|
869a343e43 | ||
|
|
fd2b22b339 | ||
|
|
90d3a5b1ad | ||
|
|
b4a2ad6b96 | ||
|
|
27ff7e7a21 | ||
|
|
94ae624662 | ||
|
|
52e018aa93 | ||
|
|
8bd13a233e | ||
|
|
87f79c1dc5 | ||
|
|
31533b5d19 | ||
|
|
27cc9f4764 | ||
|
|
4fdfce3944 | ||
|
|
1e12a83cdf | ||
|
|
1c41b8be65 | ||
|
|
f72ee7c1f8 | ||
|
|
e75912435b | ||
|
|
da294a58a6 | ||
|
|
7689199e11 | ||
|
|
324ab11f09 | ||
|
|
b1f2e17e3d | ||
|
|
90822cd582 | ||
|
|
9eec72b641 | ||
|
|
050c652480 |
No files matched your search
@@ -1,3 +1,4 @@
|
||||
tmp/*
|
||||
Dockerfile
|
||||
docker-compose
|
||||
src/core/engines/gows/proto
|
||||
@@ -10,6 +11,8 @@ docs
|
||||
examples
|
||||
.github
|
||||
.git
|
||||
.envs/*
|
||||
.env
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
|
||||
@@ -32,6 +32,12 @@ WAHA_BASE_URL=http://localhost:3000
|
||||
# WHATSAPP_API_HOSTNAME=localhost
|
||||
|
||||
|
||||
# ===================
|
||||
# ====== GOWS =======
|
||||
# ===================
|
||||
# By default, GOWS uses cgo (libc) DNS resolver
|
||||
# You can ask to use golang one, but it's less reliable
|
||||
# GODEBUG=netdns=go
|
||||
|
||||
# ===================
|
||||
# ===== LOGGING =====
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
tmp/*
|
||||
src/core/engines/gows/proto
|
||||
.env
|
||||
.envs/*
|
||||
*.heapsnapshot
|
||||
.yarn
|
||||
example.ts
|
||||
|
||||
@@ -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.
|
||||
+22
-2
@@ -1,5 +1,5 @@
|
||||
ARG NODE_IMAGE_TAG=22.16-bookworm-slim
|
||||
ARG GOLANG_IMAGE_TAG=1.23-bookworm
|
||||
ARG NODE_IMAGE_TAG=24.11-bookworm-slim
|
||||
ARG GOLANG_IMAGE_TAG=1.24-bookworm
|
||||
|
||||
#
|
||||
# Build
|
||||
@@ -163,6 +163,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
# Available versions:
|
||||
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
|
||||
ARG CHROME_VERSION="140.0.7339.80-1"
|
||||
ARG OPUSTAGS_VERSION="1.10.1"
|
||||
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
|
||||
&& apt-get update \
|
||||
@@ -176,6 +177,22 @@ RUN apt-get update \
|
||||
&& apt-get install -y curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build and install opustags so audio metadata can be cleaned up inside the container
|
||||
RUN set -eux; \
|
||||
buildDeps='build-essential cmake pkg-config libogg-dev'; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends ${buildDeps}; \
|
||||
mkdir -p /tmp/opustags; \
|
||||
curl -L https://github.com/fmang/opustags/archive/refs/tags/${OPUSTAGS_VERSION}.tar.gz \
|
||||
| tar -xz -C /tmp/opustags; \
|
||||
cd /tmp/opustags/opustags-${OPUSTAGS_VERSION}; \
|
||||
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Release; \
|
||||
cmake --build build --config Release; \
|
||||
cmake --install build; \
|
||||
rm -rf /tmp/opustags; \
|
||||
apt-get purge -y --auto-remove ${buildDeps}; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# GOWS requirements
|
||||
# libc6
|
||||
RUN apt-get update \
|
||||
@@ -212,6 +229,9 @@ ENV CHOKIDAR_INTERVAL=5000
|
||||
# WAHA variables
|
||||
ENV WAHA_ZIPPER=ZIPUNZIP
|
||||
|
||||
# GOWS - use libc DNS resolver
|
||||
ENV GODEBUG netdns=cgo
|
||||
|
||||
# Run command, etc
|
||||
EXPOSE 3000
|
||||
# Use tini as init system to handle zombie processes properly
|
||||
|
||||
@@ -32,16 +32,16 @@ push:
|
||||
docker push devlikeapro/waha
|
||||
|
||||
for-swagger:
|
||||
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${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-10-19
|
||||
yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-11-21
|
||||
|
||||
up-noweb-libsignal:
|
||||
yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
|
||||
|
||||
up-webjs:
|
||||
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-09-10
|
||||
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-11-05
|
||||
|
||||
start-proxy:
|
||||
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
|
||||
|
||||
+7
-5
@@ -26,7 +26,7 @@
|
||||
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-10-19",
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-11-21",
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@aws-sdk/client-s3": "^3.633.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.633.0",
|
||||
@@ -59,7 +59,7 @@
|
||||
"audio-decode": "^2.2.2",
|
||||
"axios": "^1.9.0",
|
||||
"axios-retry": "^4.5.0",
|
||||
"better-sqlite3": "11.3.0",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"bullmq": "^5.48.1",
|
||||
"check-disk-space": "^3.4.0",
|
||||
"chokidar": "^3.6.0",
|
||||
@@ -106,7 +106,8 @@
|
||||
"swagger-ui-express": "^4.1.4",
|
||||
"ulid": "^2.3.0",
|
||||
"undici": "^7.16.0",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-09-10",
|
||||
"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"
|
||||
},
|
||||
@@ -121,8 +122,8 @@
|
||||
"axios": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grpc/grpc-js": "^1.13.4",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@grpc/grpc-js": "^1.14.1",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.0",
|
||||
@@ -132,6 +133,7 @@
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"dotenv": "^17.2.3",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"jest": "^29.7.0",
|
||||
"oxlint": "^1.13.0",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,9 @@ class ChatsController {
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
if (query.sortBy == MessageSortField.MESSAGE_TIMESTAMP) {
|
||||
query.sortBy = MessageSortField.TIMESTAMP;
|
||||
}
|
||||
query.sortBy = query.sortBy || MessageSortField.TIMESTAMP;
|
||||
query.sortOrder = query.sortOrder || SortOrder.DESC;
|
||||
filter = transformAck(filter);
|
||||
|
||||
@@ -44,6 +44,10 @@ import {
|
||||
WANumberExistResult,
|
||||
} from '../structures/chatting.dto';
|
||||
import { WAMessage } from '../structures/responses.dto';
|
||||
import {
|
||||
mentionsAll,
|
||||
validateRequestMentions,
|
||||
} from '@waha/core/utils/mentions.all';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api')
|
||||
@@ -55,6 +59,10 @@ export class ChattingController {
|
||||
@ApiOperation({ summary: 'Send a text message' })
|
||||
async sendText(@Body() request: MessageTextRequest): Promise<WAMessage> {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
validateRequestMentions(request);
|
||||
request.mentions = await whatsapp.resolveMentionsAll(request.chatId);
|
||||
}
|
||||
return whatsapp.sendText(request);
|
||||
}
|
||||
|
||||
@@ -66,6 +74,10 @@ export class ChattingController {
|
||||
})
|
||||
async sendImage(@Body() request: MessageImageRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
validateRequestMentions(request);
|
||||
request.mentions = await whatsapp.resolveMentionsAll(request.chatId);
|
||||
}
|
||||
return whatsapp.sendImage(request);
|
||||
}
|
||||
|
||||
@@ -77,6 +89,10 @@ export class ChattingController {
|
||||
})
|
||||
async sendFile(@Body() request: MessageFileRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
validateRequestMentions(request);
|
||||
request.mentions = await whatsapp.resolveMentionsAll(request.chatId);
|
||||
}
|
||||
return whatsapp.sendFile(request);
|
||||
}
|
||||
|
||||
@@ -99,6 +115,10 @@ export class ChattingController {
|
||||
})
|
||||
async sendVideo(@Body() request: MessageVideoRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
validateRequestMentions(request);
|
||||
request.mentions = await whatsapp.resolveMentionsAll(request.chatId);
|
||||
}
|
||||
return whatsapp.sendVideo(request);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
CreateGroupRequest,
|
||||
DescriptionRequest,
|
||||
GroupField,
|
||||
GroupParticipant,
|
||||
GroupsListFields,
|
||||
GroupsPaginationParams,
|
||||
JoinGroupRequest,
|
||||
@@ -320,6 +321,17 @@ export class GroupsController {
|
||||
return session.getParticipants(id);
|
||||
}
|
||||
|
||||
@Get(':id/participants/v2')
|
||||
@GroupIdApiParam
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get group participants.' })
|
||||
getGroupParticipants(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<GroupParticipant[]> {
|
||||
return session.getGroupParticipants(id);
|
||||
}
|
||||
|
||||
@Post(':id/participants/add')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { PingResponse } from '@waha/structures/ping.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('ping')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class PingController {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as inspector from 'node:inspector';
|
||||
import * as v8 from 'node:v8';
|
||||
import { Readable } from 'node:stream';
|
||||
|
||||
import {
|
||||
Controller,
|
||||
@@ -18,7 +20,10 @@ import {
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
|
||||
import {
|
||||
BrowserTraceQuery,
|
||||
CpuProfileQuery,
|
||||
} from '@waha/structures/server.debug.dto';
|
||||
import { createReadStream } from 'fs';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@@ -36,6 +41,56 @@ export class ServerDebugController {
|
||||
this.enabled = this.config.debugModeEnabled;
|
||||
}
|
||||
|
||||
@Get('cpu')
|
||||
@ApiOperation({
|
||||
summary: 'Collect and return a CPU profile for the current nodejs process',
|
||||
description: 'Uses the Node.js inspector profiler to capture a .cpuprofile',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async cpuProfile(@Query() query: CpuProfileQuery) {
|
||||
if (!this.enabled) {
|
||||
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
|
||||
}
|
||||
const { seconds } = query;
|
||||
this.logger.log(`Collecting CPU profile for ${seconds}s...`);
|
||||
|
||||
const session = new inspector.Session();
|
||||
session.connect();
|
||||
|
||||
const profile = await new Promise<inspector.Profiler.Profile>(
|
||||
(resolve, reject) => {
|
||||
session.post('Profiler.enable', (enableError) => {
|
||||
if (enableError) {
|
||||
session.disconnect();
|
||||
return reject(enableError);
|
||||
}
|
||||
session.post('Profiler.start', (startError) => {
|
||||
if (startError) {
|
||||
session.disconnect();
|
||||
return reject(startError);
|
||||
}
|
||||
setTimeout(() => {
|
||||
session.post('Profiler.stop', (stopError, params) => {
|
||||
session.disconnect();
|
||||
if (stopError) {
|
||||
return reject(stopError);
|
||||
}
|
||||
resolve(params.profile);
|
||||
});
|
||||
}, seconds * 1000);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const filename = `CPU.${Date.now()}.${process.pid}.cpuprofile`;
|
||||
const stream = Readable.from([JSON.stringify(profile)]);
|
||||
return new StreamableFile(stream, {
|
||||
type: 'application/json',
|
||||
disposition: `attachment; filename=${filename}`,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('heapsnapshot')
|
||||
@ApiOperation({
|
||||
summary: 'Return a heapsnapshot for the current nodejs process',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -29,13 +29,22 @@ export class AxiosLogging {
|
||||
const { method, url } = response.config;
|
||||
const status = response.status;
|
||||
const methodStr = method?.toUpperCase() ?? 'GET';
|
||||
const responseData = JSON.stringify(response.data);
|
||||
|
||||
if (status >= 200 && status < 400) {
|
||||
this.logger.debug(`${methodStr} ${status}:OK ${url}`);
|
||||
this.logger.trace(`${methodStr} ${status}:OK ${url} ${responseData}`);
|
||||
const type: any = response.headers['content-type'];
|
||||
// Log warnings for non-2xx/3xx responses
|
||||
if (status < 200 || status >= 400) {
|
||||
const data = JSON.stringify(response.data);
|
||||
this.logger.warn(`${methodStr} ${status} ${url} ${data}`);
|
||||
return response;
|
||||
}
|
||||
|
||||
// Log debug for 2xx/3xx responses
|
||||
this.logger.debug(`${methodStr} ${status}:OK ${url}`);
|
||||
if (type.startsWith('application/json')) {
|
||||
const data = JSON.stringify(response.data);
|
||||
this.logger.trace(`${methodStr} ${status}:OK ${url} ${data}`);
|
||||
} else {
|
||||
this.logger.warn(`${methodStr} ${status} ${url} ${responseData}`);
|
||||
this.logger.trace(`${methodStr} ${status}:OK ${url} [${type}]`);
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -51,6 +51,17 @@ export function HasBeenRetried(job: Job): boolean {
|
||||
return attemptsMade > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide chatwoot and waha from queue name for user faced messages
|
||||
*/
|
||||
export function QueueNameRepr(name: string): string {
|
||||
// No "chatwoot" in the name always (it start with chatwoot)
|
||||
name = name.replace('chatwoot.', '');
|
||||
// Replace "waha" with "whatsapp"
|
||||
name = name.replace('waha |', 'whatsapp |');
|
||||
return name;
|
||||
}
|
||||
|
||||
let base =
|
||||
process.env.WAHA_PUBLIC_URL ||
|
||||
process.env.WAHA_BASE_URL ||
|
||||
@@ -59,7 +70,10 @@ let base =
|
||||
base = base.replace(/\/+$/, '');
|
||||
|
||||
export function JobLink(job: Job): { text: string; url: string } {
|
||||
const text = `${job.queueName} => ${job.id}`;
|
||||
// Use repr name for text
|
||||
const name = QueueNameRepr(job.queueName);
|
||||
const text = `${name} => ${job.id}`;
|
||||
// Use original queue name in the URL
|
||||
const queue = encodeURIComponent(job.queueName);
|
||||
const id = encodeURIComponent(job.id);
|
||||
const url = `${base}/jobs/queue/${queue}/${id}`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,14 +3,7 @@ import { QueueManager } from '@waha/apps/chatwoot/services/QueueManager';
|
||||
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
import { Conversation } from '@waha/apps/chatwoot/client/Conversation';
|
||||
import { QueueRegistry } from '@waha/apps/chatwoot/services/QueueRegistry';
|
||||
|
||||
function repr(name: string): string {
|
||||
// No chatwoot in the name always (it start with chatwoot)
|
||||
name = name.replace('chatwoot.', '');
|
||||
// No waha in the name
|
||||
name = name.replace('waha |', 'whatsapp |');
|
||||
return name;
|
||||
}
|
||||
import { QueueNameRepr } from '@waha/apps/app_sdk/JobUtils';
|
||||
|
||||
export interface QueueCommandContext {
|
||||
queues: {
|
||||
@@ -25,7 +18,7 @@ export async function QueueStatus(ctx: QueueCommandContext, name: string) {
|
||||
const names = manager.resolve(name);
|
||||
let result = await manager.status(names);
|
||||
for (const status of result) {
|
||||
status.name = repr(status.name);
|
||||
status.name = QueueNameRepr(status.name);
|
||||
}
|
||||
// locked: true - last
|
||||
result = lodash.sortBy(result, [(x) => !!x.locked, 'name']);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { Conversation } from '@waha/apps/chatwoot/client/Conversation';
|
||||
import {
|
||||
ContactIds,
|
||||
ConversationResult,
|
||||
ConversationService,
|
||||
} from '@waha/apps/chatwoot/client/ConversationService';
|
||||
import { ChatWootAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
|
||||
@@ -43,9 +44,10 @@ export class ContactConversationService {
|
||||
this.cache = CacheForConfig(config);
|
||||
}
|
||||
|
||||
public async upsertByContactInfo(
|
||||
private async getByContactInfo(
|
||||
contactInfo: ContactInfo,
|
||||
): Promise<ContactIds> {
|
||||
upsert: boolean,
|
||||
): Promise<ContactIds | null> {
|
||||
const chatId = contactInfo.ChatId();
|
||||
|
||||
// Check cache for chat id
|
||||
@@ -77,10 +79,21 @@ export class ContactConversationService {
|
||||
//
|
||||
// Get or create a conversation for this inbox
|
||||
//
|
||||
const conversation = await this.conversationService.upsert({
|
||||
const contactIds = {
|
||||
id: cwContact.data.id,
|
||||
sourceId: cwContact.sourceId,
|
||||
});
|
||||
};
|
||||
let conversation: ConversationResult;
|
||||
if (upsert) {
|
||||
conversation = await this.conversationService.upsert(contactIds);
|
||||
} else {
|
||||
conversation = await this.conversationService.find(contactIds);
|
||||
if (!conversation) {
|
||||
// Nothing found, do not save it in cache
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
`Using conversation for chat.id: ${chatId}, conversation.id: ${conversation.id}, contact.id: ${cwContact.sourceId}`,
|
||||
);
|
||||
@@ -94,11 +107,15 @@ export class ContactConversationService {
|
||||
return ids;
|
||||
}
|
||||
|
||||
public async ConversationByContact(
|
||||
private async conversationByContact(
|
||||
contactInfo: ContactInfo,
|
||||
): Promise<Conversation> {
|
||||
upsert: boolean,
|
||||
): Promise<Conversation | null> {
|
||||
const chatId = contactInfo.ChatId();
|
||||
const ids = await this.upsertByContactInfo(contactInfo);
|
||||
const ids = await this.getByContactInfo(contactInfo, upsert);
|
||||
if (!ids) {
|
||||
return null;
|
||||
}
|
||||
const conversation = new Conversation(
|
||||
this.accountAPI,
|
||||
this.config.accountId,
|
||||
@@ -118,6 +135,26 @@ export class ContactConversationService {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find or create if not exists conversation for the contact
|
||||
*/
|
||||
public async ConversationByContact(contactInfo: ContactInfo) {
|
||||
const conversation = await this.conversationByContact(contactInfo, true);
|
||||
if (!conversation) {
|
||||
// Shouldn't happen, but for type safety
|
||||
throw new Error('Conversation could not be created or found');
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find suitable conversation for the contact (based on filters)
|
||||
* If nothing found - it doesn't create a new one
|
||||
*/
|
||||
public async FindConversationByContact(contactInfo: ContactInfo) {
|
||||
return this.conversationByContact(contactInfo, false);
|
||||
}
|
||||
|
||||
public ConversationById(conversationId: number): Conversation {
|
||||
return new Conversation(
|
||||
this.accountAPI,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { sanitizeName } from '@waha/apps/chatwoot/client/ContactService';
|
||||
|
||||
describe('sanitizeName', () => {
|
||||
it('should return the same name', () => {
|
||||
const name = 'John Doe';
|
||||
expect(sanitizeName(name)).toBe(name);
|
||||
});
|
||||
it('should return 254 symbols max', () => {
|
||||
const name = 'a'.repeat(300);
|
||||
expect(sanitizeName(name)).toBe('a'.repeat(255));
|
||||
});
|
||||
|
||||
it('should remove bidi characters and trim the name', () => {
|
||||
const name =
|
||||
'+123123';
|
||||
expect(sanitizeName(name)).toBe('+123123');
|
||||
});
|
||||
|
||||
it('should not remove bidi characters if within limit', () => {
|
||||
const name = 'محمد +123';
|
||||
expect(sanitizeName(name)).toBe(name);
|
||||
});
|
||||
});
|
||||
@@ -26,6 +26,20 @@ export enum AvatarUpdateMode {
|
||||
ALWAYS,
|
||||
}
|
||||
|
||||
export function sanitizeName(name: string) {
|
||||
// 255 chars max
|
||||
const limit = 255;
|
||||
if (!name) {
|
||||
return name;
|
||||
}
|
||||
if (name.length < limit) {
|
||||
return name;
|
||||
}
|
||||
// remove only bidi controls
|
||||
const clean = name.replace(/[\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, '');
|
||||
return clean.slice(0, 255).trim();
|
||||
}
|
||||
|
||||
export class ContactService {
|
||||
constructor(
|
||||
private config: ChatWootAPIConfig,
|
||||
@@ -139,6 +153,7 @@ export class ContactService {
|
||||
chatId: string,
|
||||
payload: public_contact_create_update_payload,
|
||||
): Promise<ContactResponse> {
|
||||
payload.name = sanitizeName(payload.name);
|
||||
const contact = await this.inboxAPI.contacts.create({
|
||||
inboxIdentifier: this.config.inboxIdentifier,
|
||||
data: payload,
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
} from '@waha/apps/chatwoot/client/interfaces';
|
||||
import type { conversation } from '@figuro/chatwoot-sdk/dist/models/conversation';
|
||||
import { ConversationSelector } from '@waha/apps/chatwoot/services/ConversationSelector';
|
||||
import axios from 'axios';
|
||||
import { ChatWootInboxNewAPI } from '@waha/apps/chatwoot/client/ChatWootInboxNewAPI';
|
||||
|
||||
export type ConversationResult = Pick<conversation, 'id' | 'account_id'>;
|
||||
@@ -25,14 +24,20 @@ export class ConversationService {
|
||||
private logger: ILogger,
|
||||
) {}
|
||||
|
||||
private async find(contact: ContactIds): Promise<ConversationResult | null> {
|
||||
public async find(contact: ContactIds): Promise<ConversationResult | null> {
|
||||
const result: { payload: contact_conversations } =
|
||||
(await this.accountAPI.contacts.listConversations({
|
||||
accountId: this.config.accountId,
|
||||
id: contact.id,
|
||||
})) as any;
|
||||
const conversations = result.payload;
|
||||
return this.selector.select(conversations);
|
||||
const conversation = this.selector.select(conversations);
|
||||
if (conversation) {
|
||||
this.logger.debug(
|
||||
`Found existing conversation.id: ${conversation.id} for contact.id: ${contact.id}, contact.sourceId: ${contact.sourceId}`,
|
||||
);
|
||||
}
|
||||
return conversation;
|
||||
}
|
||||
|
||||
private async create(contact: ContactIds): Promise<ConversationResult> {
|
||||
@@ -51,9 +56,6 @@ export class ConversationService {
|
||||
if (!conversation) {
|
||||
conversation = await this.create(contact);
|
||||
}
|
||||
this.logger.debug(
|
||||
`Using conversation.id: ${conversation.id} for contact.id: ${contact.id}, contact.sourceId: ${contact.sourceId}`,
|
||||
);
|
||||
return conversation;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import { WAHAWebhookMessageAck } from '@waha/structures/webhooks.dto';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import { ShouldMarkAsReadInChatWoot } from '@waha/apps/chatwoot/consumers/waha/message.ack.utils';
|
||||
import { toCusFormat } from '@waha/core/utils/jids';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { MessageMappingService } from '@waha/apps/chatwoot/storage';
|
||||
|
||||
@@ -95,7 +94,16 @@ class MessageAckHandler {
|
||||
this.locale,
|
||||
);
|
||||
const conversation =
|
||||
await this.contactConversationService.ConversationByContact(contactInfo);
|
||||
await this.contactConversationService.FindConversationByContact(
|
||||
contactInfo,
|
||||
);
|
||||
|
||||
if (!conversation) {
|
||||
this.logger.debug(
|
||||
`No suitable conversation found to mark as read for chat.id: ${payload.from}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.info.onConversationId(conversation.conversationId);
|
||||
|
||||
const sourceId = conversation.sourceId;
|
||||
|
||||
@@ -77,7 +77,7 @@ export class MessageAnyHandler extends MessageBaseHandler<WAMessage> {
|
||||
): Promise<ChatWootMessagePartial> {
|
||||
let converter: MessageToChatWootConverter;
|
||||
let msg: ChatWootMessagePartial;
|
||||
const protoMessage = resolveProtoMessage(payload);
|
||||
const protoMessage = resolveProtoMessage(payload._data);
|
||||
|
||||
// Check for Facebook Ad first - but let it use the normal flow later
|
||||
converter = new FacebookAdMessage(this.l, this.logger);
|
||||
|
||||
@@ -65,7 +65,7 @@ class MessageEditedHandler extends MessageBaseHandler<WAMessageEditedBody> {
|
||||
protected async getMessage(
|
||||
payload: WAMessageEditedBody,
|
||||
): Promise<ChatWootMessagePartial> {
|
||||
const protoMessage = resolveProtoMessage(payload);
|
||||
const protoMessage = resolveProtoMessage(payload._data);
|
||||
const converter: MessageToChatWootConverter = new MessageEdited(this.l);
|
||||
return converter.convert(payload, protoMessage);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
import { isEmptyString } from './utils/proto';
|
||||
import { isEmptyString } from './utils/text';
|
||||
import * as lodash from 'lodash';
|
||||
import type { proto } from '@adiwajshing/baileys';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base'
|
||||
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
|
||||
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/proto';
|
||||
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/text';
|
||||
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
|
||||
import type { proto } from '@adiwajshing/baileys';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base'
|
||||
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
|
||||
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/proto';
|
||||
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/text';
|
||||
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
|
||||
import * as lodash from 'lodash';
|
||||
import type { proto } from '@adiwajshing/baileys';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
import { isEmptyString } from './utils/proto';
|
||||
import { isEmptyString } from './utils/text';
|
||||
import * as lodash from 'lodash';
|
||||
import type { proto } from '@adiwajshing/baileys';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
|
||||
@@ -103,20 +103,20 @@ describe('PixMessage - pt-BR', () => {
|
||||
|
||||
// Test
|
||||
const logger = JestLogger();
|
||||
const protoMessage = resolveProtoMessage(payload);
|
||||
const protoMessage = resolveProtoMessage(payload._data);
|
||||
const converter = new PixMessage(locale, logger);
|
||||
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();
|
||||
@@ -222,21 +222,21 @@ describe('PixMessage - pt-BR', () => {
|
||||
|
||||
// Test
|
||||
const logger = JestLogger();
|
||||
const protoMessage = resolveProtoMessage(payload);
|
||||
const protoMessage = resolveProtoMessage(payload._data);
|
||||
const converter = new PixMessage(locale, logger);
|
||||
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();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base'
|
||||
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
import { PixTemplatePayload, TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
|
||||
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/proto';
|
||||
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/text';
|
||||
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
|
||||
import * as lodash from 'lodash';
|
||||
import type { proto } from '@adiwajshing/baileys';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base'
|
||||
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
import { PollCreationMessage, TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
|
||||
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/proto';
|
||||
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/text';
|
||||
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
|
||||
import type { proto } from '@adiwajshing/baileys';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base'
|
||||
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { WAHASelf } from '@waha/apps/app_sdk/waha/WAHASelf';
|
||||
import { isEmptyString } from './utils/proto';
|
||||
import { isEmptyString } from './utils/text';
|
||||
import type { proto } from '@adiwajshing/baileys';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
|
||||
|
||||
@@ -2,7 +2,6 @@ import { WAMessage } from '@waha/structures/responses.dto';
|
||||
import type { proto } from '@adiwajshing/baileys';
|
||||
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
|
||||
export { resolveProtoMessage } from './utils/proto';
|
||||
export { TextMessage } from './TextMessage';
|
||||
export { LocationMessage } from './LocationMessage';
|
||||
export { ShareContactMessage } from './ShareContactMessage';
|
||||
@@ -23,3 +22,4 @@ export interface MessageToChatWootConverter {
|
||||
protoMessage: proto.Message | null,
|
||||
): Awaitable<ChatWootMessagePartial | null>;
|
||||
}
|
||||
export { resolveProtoMessage } from '@waha/core/engines/gows/waproto';
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export function isEmptyString(content: string) {
|
||||
if (!content) {
|
||||
return true;
|
||||
}
|
||||
return content === '' || content === '\n';
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -148,7 +148,12 @@ export class WhatsappConfigService implements OnApplicationBootstrap {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(',');
|
||||
return value.split(',').filter(Boolean);
|
||||
}
|
||||
|
||||
getExcludedFullPaths(): string[] {
|
||||
const paths = this.getExcludedPaths();
|
||||
return paths.map((path) => (path.startsWith('/') ? path : `/${path}`));
|
||||
}
|
||||
|
||||
getHealthMediaFilesThreshold(): number {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as lodash from 'lodash';
|
||||
import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
|
||||
import { DECORATORS } from '@nestjs/swagger/dist/constants';
|
||||
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
||||
@@ -78,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`)
|
||||
@@ -185,13 +187,17 @@ export class SwaggerConfiguratorCore {
|
||||
setUpAuth(credentials: [string, string]): void {
|
||||
const [username, password] = credentials;
|
||||
const dashboardConfig = this.app.get(DashboardConfigServiceCore);
|
||||
const exclude = [
|
||||
const config = this.app.get(WhatsappConfigService);
|
||||
const exclude = lodash.uniq([
|
||||
'/api/',
|
||||
dashboardConfig.dashboardUri,
|
||||
'/health',
|
||||
'/ping',
|
||||
'/ws',
|
||||
'/webhooks/',
|
||||
];
|
||||
...config.getExcludedFullPaths(),
|
||||
]);
|
||||
|
||||
const authFunction = BasicAuthFunction(username, password, exclude);
|
||||
this.app.use(authFunction);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
|
||||
/**
|
||||
* Decorator to mark a method as an activity that
|
||||
* keeps the WhatsApp session online.
|
||||
* @constructor
|
||||
*/
|
||||
export function Activity() {
|
||||
return function <T extends (...args: any[]) => Promise<any>>(
|
||||
target: WhatsappSession,
|
||||
propertyKey: string,
|
||||
descriptor: TypedPropertyDescriptor<T>,
|
||||
) {
|
||||
const original = descriptor.value!;
|
||||
descriptor.value = async function (
|
||||
this: WhatsappSession,
|
||||
...args: Parameters<T>
|
||||
): Promise<ReturnType<T>> {
|
||||
await this.maintainPresenceOnline();
|
||||
return await original.apply(this, args);
|
||||
} as T;
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
+135
-4
@@ -6,6 +6,7 @@ import { MessagesForRead } from '@waha/core/utils/convertors';
|
||||
import {
|
||||
IgnoreJidConfig,
|
||||
isJidBroadcast,
|
||||
isJidGroup,
|
||||
isJidNewsletter,
|
||||
isNullJid,
|
||||
JidFilter,
|
||||
@@ -101,6 +102,7 @@ import { EventMessageRequest } from '../../structures/events.dto';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
GroupField,
|
||||
GroupParticipant,
|
||||
GroupsListFields,
|
||||
ParticipantsRequest,
|
||||
SettingsSecurityChangeInfo,
|
||||
@@ -130,6 +132,11 @@ import { IMediaManager } from '../media/IMediaManager';
|
||||
import { QR } from '../QR';
|
||||
import { DataStore } from './DataStore';
|
||||
import { fetchBuffer } from '@waha/utils/fetch';
|
||||
import {
|
||||
PRESENCE_AUTO_ONLINE,
|
||||
PRESENCE_AUTO_ONLINE_DURATION_SECONDS,
|
||||
} from '@waha/core/env';
|
||||
import { Activity } from '@waha/core/abc/activity';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const qrcode = require('qrcode-terminal');
|
||||
@@ -174,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;
|
||||
@@ -183,6 +190,16 @@ export abstract class WhatsappSession {
|
||||
protected jids: JidFilter;
|
||||
|
||||
private _status: WAHASessionStatus;
|
||||
private _presence:
|
||||
| WAHAPresenceStatus.ONLINE
|
||||
| WAHAPresenceStatus.OFFLINE
|
||||
| null = null;
|
||||
private lastActivityTimestamp?: number;
|
||||
protected presenceAutoOnlineConfig = {
|
||||
enabled: PRESENCE_AUTO_ONLINE,
|
||||
duration: PRESENCE_AUTO_ONLINE_DURATION_SECONDS * 1000,
|
||||
};
|
||||
|
||||
private shouldPrintQR: boolean;
|
||||
protected events2: DefaultMap<WAHAEvents, SwitchObservable<any>>;
|
||||
private status$: Subject<WAHASessionStatus>;
|
||||
@@ -196,6 +213,8 @@ export abstract class WhatsappSession {
|
||||
stdTTL: 10 * 60, // 10 minutes
|
||||
});
|
||||
|
||||
private presenceOfflineTimeout?: ReturnType<typeof setTimeout>;
|
||||
|
||||
public mediaConverter: IMediaConverter = new CoreMediaConverter();
|
||||
|
||||
public constructor({
|
||||
@@ -302,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
|
||||
@@ -316,6 +335,30 @@ export abstract class WhatsappSession {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
protected set presence(value: WAHAPresenceStatus) {
|
||||
switch (value) {
|
||||
case null:
|
||||
this._presence = null;
|
||||
break;
|
||||
case WAHAPresenceStatus.ONLINE:
|
||||
this._presence = WAHAPresenceStatus.ONLINE;
|
||||
break;
|
||||
case WAHAPresenceStatus.OFFLINE:
|
||||
this._presence = WAHAPresenceStatus.OFFLINE;
|
||||
break;
|
||||
default:
|
||||
// Ignore chat relates presence
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public get presence():
|
||||
| WAHAPresenceStatus.ONLINE
|
||||
| WAHAPresenceStatus.OFFLINE
|
||||
| null {
|
||||
return this._presence;
|
||||
}
|
||||
|
||||
getBrowserExecutablePath() {
|
||||
return getBrowserExecutablePath();
|
||||
}
|
||||
@@ -534,10 +577,79 @@ export abstract class WhatsappSession {
|
||||
|
||||
abstract sendSeen(chat: SendSeenRequest);
|
||||
|
||||
abstract startTyping(chat: ChatRequest);
|
||||
abstract startTyping(chat: ChatRequest): Promise<void>;
|
||||
|
||||
abstract stopTyping(chat: ChatRequest);
|
||||
|
||||
/**
|
||||
* Activity tracking and presence management
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the timestamp of the last "activity" in the session
|
||||
* @returns Timestamp in milliseconds or undefined if there was never any activity
|
||||
*/
|
||||
public getLastActivityTimestamp(): number | undefined {
|
||||
return this.lastActivityTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintains ONLINE presence active while there is activity
|
||||
* Resets the timer on each activity, only goes OFFLINE after Xs without activity
|
||||
*/
|
||||
async maintainPresenceOnline(): Promise<void> {
|
||||
if (!this.presenceAutoOnlineConfig.enabled) {
|
||||
return;
|
||||
}
|
||||
if (this.status !== WAHASessionStatus.WORKING) {
|
||||
return;
|
||||
}
|
||||
this.lastActivityTimestamp = Date.now();
|
||||
// If not ONLINE yet, send ONLINE
|
||||
if (this._presence !== WAHAPresenceStatus.ONLINE) {
|
||||
try {
|
||||
// Force set ONLINE in case of many requests comes at the same time
|
||||
// So we'll set ONLINE exactly once
|
||||
this.presence = WAHAPresenceStatus.ONLINE;
|
||||
await this.setPresence(WAHAPresenceStatus.ONLINE);
|
||||
this.logger.debug('Set presence to ONLINE due to activity');
|
||||
} catch (error) {
|
||||
this.logger.debug('Failed to set presence ONLINE', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Cancel the previous timeout (if exists)
|
||||
this.cleanupPresenceTimeout();
|
||||
|
||||
// Schedule to go back OFFLINE after timeout without activity
|
||||
this.presenceOfflineTimeout = setTimeout(async () => {
|
||||
try {
|
||||
const working = this.status === WAHASessionStatus.WORKING;
|
||||
const online = this.presence === WAHAPresenceStatus.ONLINE;
|
||||
if (!working || !online) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
await this.setPresence(WAHAPresenceStatus.OFFLINE);
|
||||
this.logger.debug(
|
||||
'Auto-set presence to OFFLINE after time without activity',
|
||||
);
|
||||
} catch (error) {
|
||||
this.presence = WAHAPresenceStatus.OFFLINE;
|
||||
this.logger.debug('Failed to set presence OFFLINE', error);
|
||||
}
|
||||
this.cleanupPresenceTimeout();
|
||||
}, this.presenceAutoOnlineConfig.duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the timeout when the session stops
|
||||
*/
|
||||
protected cleanupPresenceTimeout() {
|
||||
clearTimeout(this.presenceOfflineTimeout);
|
||||
this.presenceOfflineTimeout = null;
|
||||
}
|
||||
|
||||
abstract setReaction(request: MessageReactionRequest);
|
||||
|
||||
setStar(request: MessageStarRequest): Promise<void> {
|
||||
@@ -552,6 +664,10 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public rejectCall(from: string, id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Chats methods
|
||||
*/
|
||||
@@ -821,6 +937,10 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getGroupParticipants(id: string): Promise<GroupParticipant[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getInfoAdminsOnly(id): Promise<SettingsSecurityChangeInfo> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -918,7 +1038,10 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public setPresence(presence: WAHAPresenceStatus, chatId?: string) {
|
||||
public setPresence(
|
||||
presence: WAHAPresenceStatus,
|
||||
chatId?: string,
|
||||
): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
@@ -1080,6 +1203,14 @@ export abstract class WhatsappSession {
|
||||
public fetch(url: string): Promise<Buffer> {
|
||||
return fetchBuffer(url);
|
||||
}
|
||||
|
||||
public async resolveMentionsAll(chatId: string): Promise<string[]> {
|
||||
const participants = await this.getGroupParticipants(chatId);
|
||||
let mentions = participants.map((p) => p.id);
|
||||
// Exclude my ids
|
||||
const me = this.getSessionMeInfo();
|
||||
return mentions.filter((id) => id !== me.id && id !== me.lid);
|
||||
}
|
||||
}
|
||||
|
||||
export function getGroupInviteLink(code: string) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface SValue {
|
||||
generated: boolean;
|
||||
}
|
||||
|
||||
function rand() {
|
||||
export function rand() {
|
||||
return crypto.randomUUID().toString().replace(/-/g, '');
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { EnginePayload } from '@waha/structures/webhooks.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { Logger } from 'pino';
|
||||
import { Observable } from 'rxjs';
|
||||
import { rand } from '@waha/core/auth/config';
|
||||
|
||||
/**
|
||||
* Observable that listens to a gRPC stream and emits EnginePayload objects.
|
||||
@@ -14,17 +15,35 @@ export class GowsEventStreamObservable extends Observable<EnginePayload> {
|
||||
CLIENT_CLOSE_TIMEOUT = 1_000;
|
||||
|
||||
constructor(
|
||||
private logger: Logger,
|
||||
logger: Logger,
|
||||
factory: () => {
|
||||
client: grpc.Client;
|
||||
stream: grpc.ClientReadableStream<messages.EventJson>;
|
||||
},
|
||||
) {
|
||||
super((subscriber) => {
|
||||
this.logger.debug('Creating grpc client and stream...');
|
||||
logger.debug('Creating grpc stream...');
|
||||
logger.setBindings({ id: rand() });
|
||||
const { client, stream } = factory();
|
||||
this._client = client;
|
||||
|
||||
let closed = false;
|
||||
const cleanup = async (reason: string) => {
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
closed = true;
|
||||
|
||||
logger.debug({ reason }, 'Cancelling gRPC stream...');
|
||||
try {
|
||||
stream.cancel();
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'Failed to cancel gRPC stream');
|
||||
}
|
||||
|
||||
await sleep(this.CLIENT_CLOSE_TIMEOUT);
|
||||
};
|
||||
|
||||
stream.on('data', (raw) => {
|
||||
const obj = raw.toObject();
|
||||
obj.data = JSON.parse(obj.data);
|
||||
@@ -32,18 +51,21 @@ export class GowsEventStreamObservable extends Observable<EnginePayload> {
|
||||
});
|
||||
|
||||
stream.on('end', (...args) => {
|
||||
this.logger.debug('Stream ended', args);
|
||||
logger.debug('Stream ended', args);
|
||||
subscriber?.complete();
|
||||
subscriber = null;
|
||||
void cleanup('end');
|
||||
});
|
||||
|
||||
stream.on('error', async (err: any) => {
|
||||
const CLIENT_CANCELLED_CODE = grpc.status.CANCELLED;
|
||||
if (err.code === CLIENT_CANCELLED_CODE) {
|
||||
this.logger.debug('Stream cancelled by client');
|
||||
logger.debug('Stream cancelled by client');
|
||||
await cleanup('cancelled');
|
||||
return;
|
||||
}
|
||||
this.logger.error(err, 'Stream error');
|
||||
logger.error(err, 'Stream error');
|
||||
await cleanup('error');
|
||||
// Give some time to node event loop to process the error
|
||||
await sleep(100);
|
||||
subscriber?.error(err);
|
||||
@@ -51,14 +73,7 @@ export class GowsEventStreamObservable extends Observable<EnginePayload> {
|
||||
});
|
||||
|
||||
return async () => {
|
||||
this.logger.debug('Closing stream client...');
|
||||
client.close();
|
||||
await sleep(this.CLIENT_CLOSE_TIMEOUT);
|
||||
this.logger.debug('Stream client closed');
|
||||
|
||||
this.logger.debug('Cancelling stream...');
|
||||
stream.cancel();
|
||||
this.logger.debug('Stream cancelled');
|
||||
await cleanup('teardown');
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { messages } from '@waha/core/engines/gows/grpc/gows';
|
||||
|
||||
let messageServiceClient: messages.MessageServiceClient | null = null;
|
||||
let eventStreamClient: messages.EventStreamClient | null = null;
|
||||
|
||||
export const MessageServiceClientSingleton = (
|
||||
address: string,
|
||||
credentials: grpc.ChannelCredentials,
|
||||
): messages.MessageServiceClient => {
|
||||
if (!messageServiceClient) {
|
||||
messageServiceClient = new messages.MessageServiceClient(
|
||||
address,
|
||||
credentials,
|
||||
{
|
||||
'grpc.max_send_message_length': 128 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 128 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
}
|
||||
return messageServiceClient;
|
||||
};
|
||||
|
||||
export const EventStreamClientSingleton = (
|
||||
address: string,
|
||||
credentials: grpc.ChannelCredentials,
|
||||
): messages.EventStreamClient => {
|
||||
if (!eventStreamClient) {
|
||||
eventStreamClient = new messages.EventStreamClient(address, credentials, {
|
||||
'grpc.max_send_message_length': 128 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 128 * 1024 * 1024,
|
||||
});
|
||||
}
|
||||
return eventStreamClient;
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
GroupInfoFull,
|
||||
JoinedGroupEvent,
|
||||
} from '@waha/core/engines/gows/types.group';
|
||||
import { toJID } from '@waha/core/utils/jids';
|
||||
import { toCusFormat, toJID } from '@waha/core/utils/jids';
|
||||
import {
|
||||
GroupId,
|
||||
GroupInfo,
|
||||
@@ -116,7 +116,7 @@ function getParticipants(jids: string[] | null, type: GroupParticipantType) {
|
||||
const participants: GroupParticipant[] = [];
|
||||
for (const jid of jids) {
|
||||
participants.push({
|
||||
id: jid,
|
||||
id: toCusFormat(jid),
|
||||
role: role,
|
||||
});
|
||||
}
|
||||
@@ -124,7 +124,7 @@ function getParticipants(jids: string[] | null, type: GroupParticipantType) {
|
||||
}
|
||||
|
||||
function ToGroupInfo(group: GroupInfoFull): GroupInfo {
|
||||
const participants: GroupParticipant[] = extractParticipants(
|
||||
const participants: GroupParticipant[] = ToGroupParticipants(
|
||||
group.Participants,
|
||||
);
|
||||
return {
|
||||
@@ -169,7 +169,7 @@ function ToGroupInfoPartial(group: GroupInfoEvent): GroupInfo | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractParticipants(
|
||||
export function ToGroupParticipants(
|
||||
participants: GOWSGroupParticipant[],
|
||||
): GroupParticipant[] {
|
||||
const result: GroupParticipant[] = [];
|
||||
@@ -183,7 +183,8 @@ function extractParticipants(
|
||||
role = GroupParticipantRole.PARTICIPANT;
|
||||
}
|
||||
result.push({
|
||||
id: participant.JID,
|
||||
id: toCusFormat(participant.JID),
|
||||
pn: toCusFormat(participant.PhoneNumber),
|
||||
role: role,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -3564,9 +3587,10 @@ export namespace messages {
|
||||
list?: ListMessage;
|
||||
location?: Location;
|
||||
pollVote?: PollVoteMessage;
|
||||
mentions?: string[];
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [11, 13], this.#one_of_decls);
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [11, 13, 19], this.#one_of_decls);
|
||||
if (!Array.isArray(data) && typeof data == "object") {
|
||||
if ("session" in data && data.session != undefined) {
|
||||
this.session = data.session;
|
||||
@@ -3622,6 +3646,9 @@ export namespace messages {
|
||||
if ("pollVote" in data && data.pollVote != undefined) {
|
||||
this.pollVote = data.pollVote;
|
||||
}
|
||||
if ("mentions" in data && data.mentions != undefined) {
|
||||
this.mentions = data.mentions;
|
||||
}
|
||||
}
|
||||
}
|
||||
get session() {
|
||||
@@ -3762,6 +3789,12 @@ export namespace messages {
|
||||
get has_pollVote() {
|
||||
return pb_1.Message.getField(this, 18) != null;
|
||||
}
|
||||
get mentions() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 19, []) as string[];
|
||||
}
|
||||
set mentions(value: string[]) {
|
||||
pb_1.Message.setField(this, 19, value);
|
||||
}
|
||||
static fromObject(data: {
|
||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||
jid?: string;
|
||||
@@ -3781,6 +3814,7 @@ export namespace messages {
|
||||
list?: ReturnType<typeof ListMessage.prototype.toObject>;
|
||||
location?: ReturnType<typeof Location.prototype.toObject>;
|
||||
pollVote?: ReturnType<typeof PollVoteMessage.prototype.toObject>;
|
||||
mentions?: string[];
|
||||
}): MessageRequest {
|
||||
const message = new MessageRequest({});
|
||||
if (data.session != null) {
|
||||
@@ -3837,6 +3871,9 @@ export namespace messages {
|
||||
if (data.pollVote != null) {
|
||||
message.pollVote = PollVoteMessage.fromObject(data.pollVote);
|
||||
}
|
||||
if (data.mentions != null) {
|
||||
message.mentions = data.mentions;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
@@ -3859,6 +3896,7 @@ export namespace messages {
|
||||
list?: ReturnType<typeof ListMessage.prototype.toObject>;
|
||||
location?: ReturnType<typeof Location.prototype.toObject>;
|
||||
pollVote?: ReturnType<typeof PollVoteMessage.prototype.toObject>;
|
||||
mentions?: string[];
|
||||
} = {};
|
||||
if (this.session != null) {
|
||||
data.session = this.session.toObject();
|
||||
@@ -3914,6 +3952,9 @@ export namespace messages {
|
||||
if (this.pollVote != null) {
|
||||
data.pollVote = this.pollVote.toObject();
|
||||
}
|
||||
if (this.mentions != null) {
|
||||
data.mentions = this.mentions;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
@@ -3956,6 +3997,8 @@ export namespace messages {
|
||||
writer.writeMessage(17, this.location, () => this.location.serialize(writer));
|
||||
if (this.has_pollVote)
|
||||
writer.writeMessage(18, this.pollVote, () => this.pollVote.serialize(writer));
|
||||
if (this.mentions.length)
|
||||
writer.writeRepeatedString(19, this.mentions);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
@@ -4019,6 +4062,9 @@ export namespace messages {
|
||||
case 18:
|
||||
reader.readMessage(message.pollVote, () => message.pollVote = PollVoteMessage.deserialize(reader));
|
||||
break;
|
||||
case 19:
|
||||
pb_1.Message.addToRepeatedField(message, 19, reader.readString());
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -7926,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);
|
||||
@@ -7942,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() {
|
||||
@@ -7971,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) {
|
||||
@@ -7990,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() {
|
||||
@@ -7998,6 +8058,7 @@ export namespace messages {
|
||||
message?: string;
|
||||
jid?: string;
|
||||
messageId?: string;
|
||||
contentPath?: string;
|
||||
} = {};
|
||||
if (this.session != null) {
|
||||
data.session = this.session.toObject();
|
||||
@@ -8011,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;
|
||||
@@ -8025,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();
|
||||
}
|
||||
@@ -8046,6 +8112,9 @@ export namespace messages {
|
||||
case 4:
|
||||
message.messageId = reader.readString();
|
||||
break;
|
||||
case 5:
|
||||
message.contentPath = reader.readString();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -8062,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);
|
||||
@@ -8069,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() {
|
||||
@@ -8077,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;
|
||||
@@ -8101,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();
|
||||
}
|
||||
@@ -8113,6 +8202,9 @@ export namespace messages {
|
||||
case 1:
|
||||
message.content = reader.readBytes();
|
||||
break;
|
||||
case 5:
|
||||
message.contentPath = reader.readString();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -10013,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[] | {
|
||||
@@ -10841,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,
|
||||
@@ -10930,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;
|
||||
@@ -11118,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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8555,7 +8607,7 @@ proto.messages.EventMessage.prototype.hasLocation = function() {
|
||||
* @private {!Array<number>}
|
||||
* @const
|
||||
*/
|
||||
proto.messages.MessageRequest.repeatedFields_ = [11,13];
|
||||
proto.messages.MessageRequest.repeatedFields_ = [11,13,19];
|
||||
|
||||
|
||||
|
||||
@@ -8606,7 +8658,8 @@ proto.messages.MessageRequest.toObject = function(includeInstance, msg) {
|
||||
poll: (f = msg.getPoll()) && proto.messages.PollMessage.toObject(includeInstance, f),
|
||||
list: (f = msg.getList()) && proto.messages.ListMessage.toObject(includeInstance, f),
|
||||
location: (f = msg.getLocation()) && proto.messages.Location.toObject(includeInstance, f),
|
||||
pollvote: (f = msg.getPollvote()) && proto.messages.PollVoteMessage.toObject(includeInstance, f)
|
||||
pollvote: (f = msg.getPollvote()) && proto.messages.PollVoteMessage.toObject(includeInstance, f),
|
||||
mentionsList: (f = jspb.Message.getRepeatedField(msg, 19)) == null ? undefined : f
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -8726,6 +8779,10 @@ proto.messages.MessageRequest.deserializeBinaryFromReader = function(msg, reader
|
||||
reader.readMessage(value,proto.messages.PollVoteMessage.deserializeBinaryFromReader);
|
||||
msg.setPollvote(value);
|
||||
break;
|
||||
case 19:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.addMentions(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
@@ -8892,6 +8949,13 @@ proto.messages.MessageRequest.serializeBinaryToWriter = function(message, writer
|
||||
proto.messages.PollVoteMessage.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
f = message.getMentionsList();
|
||||
if (f.length > 0) {
|
||||
writer.writeRepeatedString(
|
||||
19,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -9448,6 +9512,43 @@ proto.messages.MessageRequest.prototype.hasPollvote = function() {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* repeated string mentions = 19;
|
||||
* @return {!Array<string>}
|
||||
*/
|
||||
proto.messages.MessageRequest.prototype.getMentionsList = function() {
|
||||
return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 19));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {!Array<string>} value
|
||||
* @return {!proto.messages.MessageRequest} returns this
|
||||
*/
|
||||
proto.messages.MessageRequest.prototype.setMentionsList = function(value) {
|
||||
return jspb.Message.setField(this, 19, value || []);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {number=} opt_index
|
||||
* @return {!proto.messages.MessageRequest} returns this
|
||||
*/
|
||||
proto.messages.MessageRequest.prototype.addMentions = function(value, opt_index) {
|
||||
return jspb.Message.addToRepeatedField(this, 19, value, opt_index);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the list making it empty but non-null.
|
||||
* @return {!proto.messages.MessageRequest} returns this
|
||||
*/
|
||||
proto.messages.MessageRequest.prototype.clearMentionsList = function() {
|
||||
return this.setMentionsList([]);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16624,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) {
|
||||
@@ -16678,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;
|
||||
@@ -16736,6 +16842,13 @@ proto.messages.DownloadMediaRequest.serializeBinaryToWriter = function(message,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getContentpath();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
5,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16830,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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16862,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) {
|
||||
@@ -16903,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;
|
||||
@@ -16939,6 +17075,13 @@ proto.messages.DownloadMediaResponse.serializeBinaryToWriter = function(message,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getContentpath();
|
||||
if (f.length > 0) {
|
||||
writer.writeString(
|
||||
5,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16984,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);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20715,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>}
|
||||
|
||||
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user