Compare commits

..
78 Commits
Author SHA1 Message Date
devlikepro 96da96281d [core] 2025.12.1
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-12-05 00:15:53 -08:00
devlikepro 50e09c3680 [core] Up dashboard 2025-12-05 00:15:53 -08:00
devlikepro 1da6a16640 [core] for-swagger 2025-12-05 00:15:52 -08:00
devlikepro 10f80132f8 [core] NOWEB - poll.vote iterate over creators/votes
fix #1688
2025-12-05 00:15:52 -08:00
devlikepro 750b2b86b5 [core] WEBJS - WORKING as AUTHENTICATED
fix #1690
close #1695
2025-12-05 00:15:51 -08:00
devlikepro 2fc2caed85 [core] ChatWoot - mark as read flag
fix #1650
2025-12-05 00:15:50 -08:00
devlikepro 1431e8c0f1 [core] Apps allow to remove app if not enabled 2025-12-05 00:15:50 -08:00
devlikepro 01c9c2ca56 [core] session FAILED if apps not started 2025-12-05 00:15:49 -08:00
devlikepro f5d216df6f [core] apps in sessions 2025-12-05 00:15:49 -08:00
devlikepro 7614822d2b [core] Apps - upsert to service 2025-12-05 00:15:49 -08:00
devlikepro d2eb463d32 [core] Apps on and off 2025-12-05 00:15:49 -08:00
devlikepro 07b1e50534 [core] Remove apps by session 2025-12-05 00:15:49 -08:00
devlikepro 41b6b3459e [core] Calls App
fix #1694
2025-12-05 00:13:15 -08:00
devlikepro 0f5fcf60f6 [core] ChatWoot - markdown fix for symbols in the middle of text
fix #1643
2025-12-05 00:13:14 -08:00
devlikepro 18d8330ab3 [core] Up GOWS 2025-12-05 00:13:14 -08:00
devlikepro 7de5f461aa [core] ChatWoot - call messages 2025-12-05 00:13:13 -08:00
devlikepro caa055abed [core] ChatWoot - weaken the type for handler 2025-12-05 00:13:13 -08:00
devlikepro 49af88dea3 [core] ChatWoot - type based on private flag or fromMe 2025-12-05 00:13:13 -08:00
devlikepro 05d856d8ae [core] WEBJS - call.rejected for API rejection 2025-12-05 00:13:12 -08:00
devlikepro c5c99136fa [core] Reject Calls - WEBJS, NOWEB, GOWS
fix #608
fix #1258
2025-12-05 00:13:12 -08:00
devlikepro a8b520012f [core] GOWS - add call events
fix #1673
2025-12-05 00:13:12 -08:00
devlikepro 449df53c19 [core] Add _data to calls event - WEBJS, NOWEB 2025-12-05 00:13:12 -08:00
devlikepro 69ac19b5c0 [core] AGENTS.md 2025-12-05 00:13:12 -08:00
devlikepro 163b11aa36 [core] 2025.11.4
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-12-01 11:46:58 +07:00
devlikepro f4171a9086 [core] Up WEBJS
fix #1678
2025-12-01 11:46:57 +07:00
devlikepro dfbae9c71e [core] GOWS - 128Mb max back in grpc (shouldn't happen tho) 2025-12-01 11:46:57 +07:00
devlikepro 91caa7ba25 [core] nodejs 24.11 2025-12-01 11:46:56 +07:00
devlikepro 2ac57bb6c3 [core] GOWS - send media via file 2025-12-01 11:46:56 +07:00
devlikepro e01647eb48 [core] GOWS - fetch media via file 2025-12-01 11:46:55 +07:00
devlikepro 90bc05ecf3 [core] Use uniqid in webhook for request id 2025-12-01 11:46:54 +07:00
devlikepro 21998cbfae [core] log event 2025-12-01 11:46:53 +07:00
devlikepro 87c49a8766 [core] downgrade google-protobug 2025-12-01 11:46:53 +07:00
devlikepro 4e78d559a8 [core] Disable cpu-prof when WAHA_DEBUG_MODE=true
fix #1681
2025-12-01 11:46:53 +07:00
devlikepro 18ce902c60 [core] Up GOWS to 1.0.24 2025-12-01 11:46:53 +07:00
devlikepro 5784db8055 [core] 2025.11.3
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-11-28 14:02:15 +07:00
devlikepro 6862063654 [core] Up GOWS 2025-11-28 14:02:14 +07:00
devlikepro 4aea282356 [core] ChatWoot - do not log binary 2025-11-28 14:02:13 +07:00
devlikepro 82ff15ae56 [core] GOWS - 10 mins deadline for media fetch 2025-11-28 14:02:12 +07:00
devlikepro 6cbf8cded2 [core] Up Dashboard - app id is uuid now 2025-11-28 14:02:11 +07:00
devlikepro 35e9418988 [core] add cpu profiling for troubleshooting 2025-11-28 14:02:10 +07:00
devlikepro a58be84e56 [core] GOWS - Use one grpc client to handle all sessions (but per session stream) 2025-11-28 14:02:09 +07:00
devlikepro 2994e0a54d [core] ignore tmp 2025-11-28 14:02:09 +07:00
devlikepro f16100e1d9 [core] GOWS - logs for grpc client closing 2025-11-28 14:02:07 +07:00
devlikepro e57be58d48 [core] GOWS - up grpc 2025-11-28 14:02:06 +07:00
devlikepro 7909910dc6 [core] ChatWoot - limit names to 255
fix #1556
2025-11-28 14:02:05 +07:00
devlikepro 7f1778ceb0 [core] ChatWoot - do not create new conversation when closed on message.ack
close devlikeapro/waha-plus#28
fix #1646
2025-11-28 14:02:05 +07:00
devlikepro aba90574d8 [core] ChatWoot - remove unused imports 2025-11-28 14:02:05 +07:00
devlikepro 33c945bf95 [core] NOWEB - Up engine 2025-11-28 14:02:04 +07:00
devlikepro d40e82eb02 [core] WEBJS - up engine 2025-11-28 14:02:04 +07:00
devlikepro 6e82d6f40b [core] GOWS - force to use cgo dns
fix #1649
2025-11-28 14:02:03 +07:00
devlikepro e193eb5696 [core] Up go 1.24
Tho we don't built anything here
2025-11-28 14:02:03 +07:00
devlikepro 8c63c4b097 [core] WEBJS - fix code pairing
fix #1651
2025-11-28 14:02:03 +07:00
devlikepro 4127d609cf [core] for-swagger 2025-11-28 14:02:03 +07:00
devlikepro 57f6db4252 [core] 2025.11.2
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-11-14 10:23:33 +07:00
devlikepro 68a91cb55c [core] up GOWS v1.0.21 2025-11-14 10:23:32 +07:00
devlikepro 869a343e43 [core] API - mentions for send image, file, video 2025-11-14 10:23:32 +07:00
devlikepro fd2b22b339 [core] Up better-sqlite3 to 12.4.1 2025-11-14 10:23:31 +07:00
devlikepro 90d3a5b1ad [core] NOWEB - add pool for sqlite3 2025-11-14 10:23:31 +07:00
devlikepro b4a2ad6b96 [core] WAHA_SQLITE_ENGINE - Add a way to use better-sqlite3 (sync sqlite3 engine) 2025-11-14 10:23:31 +07:00
devlikepro 27ff7e7a21 [core] Reset presence on engine restart (end) 2025-11-14 10:23:30 +07:00
devlikepro 94ae624662 [core] Set to OFFLINE always 2025-11-14 10:23:30 +07:00
devlikepro 52e018aa93 [core] Use timestamps.activity 2025-11-14 10:23:29 +07:00
Berg Pinheiro 8bd13a233e [core] Add automatic presence management and activity tracking
fix #1584
closes #1586

This prevents WhatsApp from disconnecting sessions due to inactivity by
signaling activity through presence updates while avoiding constant online
status that could generate unnecessary notifications.
2025-11-14 10:23:28 +07:00
Emir Muhammadzadeh 87f79c1dc5 [core] NOWEB, GOWS - add location and vcards
closes #1609
2025-11-14 10:23:28 +07:00
devlikepro 31533b5d19 [core] GOWS - move waproto helper to engine 2025-11-14 10:23:27 +07:00
devlikepro 27cc9f4764 [core] Add opustags 2025-11-14 10:23:27 +07:00
devlikepro 4fdfce3944 [core] 2025.11.1
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
2025-11-07 13:55:03 +07:00
devlikepro 1e12a83cdf [core] Up GOWS
Fixes for poll.vote using @lid
fix #1561
fix #1570

Ignore status@broadcast for ack updates
fix #1557

Increase gRPC limit to 512MB
fix #1365
2025-11-07 13:55:03 +07:00
devlikepro 1c41b8be65 [core] Add mentions: ["all"] to /api/sendText in groups - WEBJS, GOWS, NOWEB
fix #1377
2025-11-07 13:55:03 +07:00
devlikepro f72ee7c1f8 [core] Add /api/:session/groups/:id/participants/v2 - WEBJS, GOWS, NOWEB 2025-11-07 13:55:03 +07:00
devlikepro e75912435b [core] GOWS - support mentions
fix #1372
2025-11-07 13:55:02 +07:00
devlikepro da294a58a6 [core] GOWS - increase gRPC limits to 512MB
fix #1365
2025-11-07 13:55:01 +07:00
devlikepro 7689199e11 [core] ChatWoot - no chatwoot/waha mention in links for jobs 2025-11-07 13:55:01 +07:00
devlikepro 324ab11f09 [core] Fix Chat UI on dashboard (sortBy: messageTimestamp in /messages)
fix #1602
2025-11-07 13:55:00 +07:00
devlikepro b1f2e17e3d [core] Up NOWEB
fix #1590
fix #1605
2025-11-07 13:55:00 +07:00
devlikepro 90822cd582 [core] Up WEBJS
fix #1583
fix #1546
2025-11-07 13:54:59 +07:00
devlikepro 9eec72b641 [core] dotenv 2025-11-07 13:54:59 +07:00
devlikepro 050c652480 [core] Exclude /ping and /health using WAHA_API_KEY_EXCLUDE_PATH from all auth
fix #1562
2025-11-07 13:54:59 +07:00
128 changed files with 3750 additions and 437 deletions

No files matched your search

+3
View File
@@ -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
+6
View File
@@ -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 =====
+2
View File
@@ -1,5 +1,7 @@
tmp/*
src/core/engines/gows/proto
.env
.envs/*
*.heapsnapshot
.yarn
example.ts
+1 -1
View File
@@ -1 +1 @@
v22.16
v24.11
+6
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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",
+34
View File
@@ -0,0 +1,34 @@
import {
Body,
Controller,
Post,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { RejectCallRequest } from '../structures/calls.dto';
@ApiSecurity('api_key')
@Controller('api/:session/calls')
@ApiTags('📞 Calls')
export class CallsController {
constructor(private manager: SessionManager) {}
@Post('reject')
@SessionApiParam
@ApiOperation({ summary: 'Reject incoming call' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
rejectCall(
@WorkingSessionParam session: WhatsappSession,
@Body() request: RejectCallRequest,
) {
return session.rejectCall(request.from, request.id);
}
}
+3
View File
@@ -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);
+20
View File
@@ -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);
}
+12
View File
@@ -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
-1
View File
@@ -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 {
+56 -1
View File
@@ -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',
+55 -9
View File
@@ -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')
+102
View File
@@ -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,
},
},
},
],
},
},
};
+14 -5
View File
@@ -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;
+15 -1
View File
@@ -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}`;
+17 -3
View File
@@ -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);
}
}
}
+57
View File
@@ -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);
+28
View File
@@ -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,
},
};
+4
View File
@@ -0,0 +1,4 @@
export enum AppName {
chatwoot = 'chatwoot',
calls = 'calls',
}
+12 -7
View File
@@ -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;
+14
View File
@@ -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),
};
+4 -1
View File
@@ -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();
}
}
+106 -24
View File
@@ -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 -1
View File
@@ -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();
}
}
+15
View File
@@ -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],
};
+127
View File
@@ -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
View File
@@ -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;
+7
View File
@@ -0,0 +1,7 @@
import { CallsAppService } from '@waha/apps/calls/services/CallsAppService';
export const CallsAppExports = {
providers: [CallsAppService],
imports: [],
controllers: [],
};
+44
View File
@@ -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;
}
}
+151
View File
@@ -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',
);
}
}
}
+18
View File
@@ -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,
+2 -9
View File
@@ -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;
}
+3
View File
@@ -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
//
+29 -8
View File
@@ -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);
}
+23 -13
View File
@@ -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);
}
+11 -4
View File
@@ -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 {
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+11
View File
@@ -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**
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+12
View File
@@ -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}}}`
+7
View File
@@ -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]:
+15
View File
@@ -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
*/
+6 -1
View File
@@ -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 {
+8 -2
View File
@@ -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);
}
+25
View File
@@ -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
View File
@@ -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) {
+2
View File
@@ -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,
+1 -1
View File
@@ -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');
};
});
}
+35
View File
@@ -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;
};
+6 -5
View File
@@ -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,
});
}
+222 -1
View File
@@ -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: {
+377 -5
View File
@@ -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