Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f17164a3d9 | ||
|
|
0b9f9b2335 | ||
|
|
71d3e24241 | ||
|
|
ccd4512e8f | ||
|
|
fe6b77d67d | ||
|
|
58d12f4e70 | ||
|
|
17303039b8 | ||
|
|
f70d5203a3 | ||
|
|
efd6b1db65 | ||
|
|
dcca0fa921 | ||
|
|
e5bc447807 | ||
|
|
cd6b4b5ab1 | ||
|
|
fb79ae6138 | ||
|
|
17801b0b13 | ||
|
|
9010388de0 | ||
|
|
2da901f994 | ||
|
|
7832cc7a5d | ||
|
|
13ab2e8d0e | ||
|
|
868d23a278 | ||
|
|
eaf2bca44e | ||
|
|
af8f537f58 | ||
|
|
bc7ff472be | ||
|
|
9b9a2b5203 | ||
|
|
347259394b | ||
|
|
ead61b5e37 | ||
|
|
c9f7814f5d | ||
|
|
3ee3355e81 | ||
|
|
ce8f8b0ce9 | ||
|
|
cae35cf3b9 | ||
|
|
900e3c23bc | ||
|
|
aaeffeb983 | ||
|
|
15600324be | ||
|
|
da4913edb5 | ||
|
|
e8cc1e4906 | ||
|
|
801fc3f9fd | ||
|
|
703b0b4cde | ||
|
|
d55895389a | ||
|
|
56009e482b | ||
|
|
767d1bb3e3 | ||
|
|
05132ac0f5 | ||
|
|
fb372f8c28 | ||
|
|
a2dc863b7a | ||
|
|
15671c6781 | ||
|
|
6296115adb | ||
|
|
0a099a91d0 | ||
|
|
5f060fe022 | ||
|
|
a4bbe1cf7b | ||
|
|
dce74ae7d5 | ||
|
|
871c509493 | ||
|
|
9333470e3b | ||
|
|
0c108f5232 | ||
|
|
39d298c867 | ||
|
|
376e804a28 | ||
|
|
79e5c0589d | ||
|
|
f6679b7368 | ||
|
|
a3c0b86b67 | ||
|
|
c8f48dd62b | ||
|
|
aed157697a | ||
|
|
913b8606d9 | ||
|
|
7cadbad36e | ||
|
|
abb5bdffcc | ||
|
|
6a44c15dd3 | ||
|
|
e669dfe533 | ||
|
|
be888f195d | ||
|
|
d87cdef311 | ||
|
|
ecf07cb520 | ||
|
|
10829d53b9 | ||
|
|
71093f2f31 | ||
|
|
e88192002a | ||
|
|
3e14282603 | ||
|
|
5f16f4ca77 | ||
|
|
7b4fa4331f | ||
|
|
3ad0a463b5 | ||
|
|
d28d640f77 | ||
|
|
594c0a6122 | ||
|
|
843a826437 |
No files matched your search
@@ -1,4 +1,5 @@
|
||||
Dockerfile
|
||||
src/core/engines/gows/grpc
|
||||
.*sessions
|
||||
sessions
|
||||
files
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
src/core/engines/gows/grpc/*
|
||||
@@ -8,7 +8,9 @@ on:
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
name: ${{ matrix.platform }} - ${{ matrix.browser }} - ${{ matrix.tag }}
|
||||
name:
|
||||
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
|
||||
${{ matrix.tag }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -17,6 +19,7 @@ jobs:
|
||||
tag: 'latest'
|
||||
platform: 'amd64'
|
||||
browser: 'chromium'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chromium - ARM
|
||||
@@ -24,6 +27,7 @@ jobs:
|
||||
tag: 'arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'chromium'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-arm'
|
||||
|
||||
# Chrome - x86
|
||||
@@ -31,6 +35,7 @@ jobs:
|
||||
tag: 'chrome'
|
||||
platform: 'amd64'
|
||||
browser: 'chrome'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chrome - ARM (Chrome is not available for ARM)
|
||||
@@ -40,18 +45,36 @@ jobs:
|
||||
# browser: "chrome"
|
||||
# goss: 'goss-linux-arm'
|
||||
|
||||
# No browser - x86
|
||||
# NOWEB - x86
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
tag: 'noweb'
|
||||
platform: 'amd64'
|
||||
browser: 'none'
|
||||
engine: 'NOWEB'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# No browser - ARM
|
||||
# NOWEB - ARM
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
tag: 'noweb-arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'none'
|
||||
engine: 'NOWEB'
|
||||
goss: 'goss-linux-arm'
|
||||
|
||||
# GOWS - x86
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
tag: 'gows'
|
||||
platform: 'amd64'
|
||||
browser: 'none'
|
||||
engine: 'GOWS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# No browser - ARM
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
tag: 'gows-arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'none'
|
||||
engine: 'GOWS'
|
||||
goss: 'goss-linux-arm'
|
||||
|
||||
steps:
|
||||
@@ -85,6 +108,7 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -116,6 +140,7 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,3 +1,4 @@
|
||||
src/core/engines/gows/grpc
|
||||
.env
|
||||
*.heapsnapshot
|
||||
.yarn
|
||||
|
||||
@@ -2,6 +2,7 @@ exclude: |
|
||||
(?x)^(
|
||||
^src/dashboard/.*|
|
||||
^src/core/engines/webjs/.*html|
|
||||
^src/core/engines/gows/grpc/.*|
|
||||
^src/plus/engines/webjs/.*html|
|
||||
^.yarn/.*
|
||||
)$
|
||||
|
||||
+67
-2
@@ -1,3 +1,9 @@
|
||||
# GOWS
|
||||
ARG GOWS_GITHUB_REPO=devlikeapro/gows
|
||||
ARG GOWS_SHA=95ca63a1dee5faec75232a9f59a548dfd4e414ff
|
||||
ARG WAHA_DASHBOARD_GITHUB_REPO=devlikeapro/dashboard
|
||||
ARG WAHA_DASHBOARD_SHA=a1c839d8da94d1cade182addffdfc42fc0cf7ed3
|
||||
|
||||
#
|
||||
# Build
|
||||
#
|
||||
@@ -5,6 +11,10 @@ ARG NODE_VERSION=22.8-bullseye
|
||||
FROM node:${NODE_VERSION} AS build
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
|
||||
# install protoc
|
||||
RUN apt-get update && \
|
||||
apt-get install protobuf-compiler -y
|
||||
|
||||
# npm packages
|
||||
WORKDIR /src
|
||||
COPY package.json .
|
||||
@@ -18,6 +28,16 @@ RUN yarn install
|
||||
WORKDIR /src
|
||||
ADD . /src
|
||||
RUN yarn install
|
||||
|
||||
# Build node grpc client for GOWS engine
|
||||
WORKDIR /gows/proto
|
||||
ARG GOWS_GITHUB_REPO
|
||||
ARG GOWS_SHA
|
||||
RUN wget https://raw.githubusercontent.com/${GOWS_GITHUB_REPO}/${GOWS_SHA}/proto/gows.proto
|
||||
WORKDIR /src
|
||||
|
||||
RUN make proto-gows
|
||||
|
||||
RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
|
||||
#
|
||||
@@ -26,15 +46,41 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
FROM node:${NODE_VERSION} AS dashboard
|
||||
|
||||
# Download WAHA Dashboard
|
||||
ENV WAHA_DASHBOARD_SHA e6a72c5de2effc97d83e997cb1f361851604bae8
|
||||
ARG WAHA_DASHBOARD_GITHUB_REPO
|
||||
ARG WAHA_DASHBOARD_SHA
|
||||
RUN \
|
||||
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
|
||||
wget https://github.com/${WAHA_DASHBOARD_GITHUB_REPO}/archive/${WAHA_DASHBOARD_SHA}.zip \
|
||||
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
|
||||
&& mkdir -p /dashboard \
|
||||
&& mv /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}/* /dashboard/ \
|
||||
&& rm -rf ${WAHA_DASHBOARD_SHA}.zip \
|
||||
&& rm -rf /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}
|
||||
|
||||
#
|
||||
# GOWS
|
||||
#
|
||||
FROM golang:1.23-bullseye AS gows
|
||||
# install protoc
|
||||
RUN apt-get update && \
|
||||
apt-get install protobuf-compiler -y
|
||||
|
||||
# Image processing for thumbnails
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libvips-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ARG GOWS_GITHUB_REPO
|
||||
ARG GOWS_SHA
|
||||
RUN git clone https://github.com/${GOWS_GITHUB_REPO} gows && \
|
||||
cd gows && \
|
||||
git checkout ${GOWS_SHA}
|
||||
|
||||
WORKDIR /go/gows
|
||||
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||
RUN make all
|
||||
|
||||
|
||||
#
|
||||
# Final
|
||||
#
|
||||
@@ -44,12 +90,18 @@ ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
# https://github.com/devlikeapro/waha/issues/347
|
||||
ENV NODE_OPTIONS="--max-old-space-size=16384"
|
||||
ARG USE_BROWSER=chromium
|
||||
ARG WHATSAPP_DEFAULT_ENGINE
|
||||
|
||||
RUN echo "USE_BROWSER=$USE_BROWSER"
|
||||
|
||||
# Install ffmpeg to generate previews for videos
|
||||
RUN apt-get update && apt-get install -y ffmpeg --no-install-recommends && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Image processing for thumbnails
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libvips \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install zip and unzip - either for chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update \
|
||||
@@ -99,12 +151,25 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# GOWS requirements
|
||||
# libc6
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libc6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the ENV for docker image
|
||||
ENV WHATSAPP_DEFAULT_ENGINE=$WHATSAPP_DEFAULT_ENGINE
|
||||
|
||||
# Attach sources, install packages
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
COPY --from=build /src/node_modules ./node_modules
|
||||
COPY --from=build /src/dist ./dist
|
||||
COPY --from=dashboard /dashboard ./dist/dashboard
|
||||
COPY --from=gows /go/gows/bin/gows /app/gows
|
||||
ENV WAHA_GOWS_PATH /app/gows
|
||||
ENV WAHA_GOWS_SOCKET /tmp/gows.sock
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
# Chokidar options to monitor file changes
|
||||
|
||||
@@ -5,7 +5,10 @@ build-chrome:
|
||||
docker build . -t devlikeapro/waha-plus:chrome --build-arg USE_BROWSER=chrome
|
||||
|
||||
build-noweb:
|
||||
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none
|
||||
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
|
||||
build-plus-gows:
|
||||
docker build . -t devlikeapro/waha-plus:gows --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=GOWS
|
||||
|
||||
build-all: build build-chrome build-noweb
|
||||
|
||||
@@ -34,8 +37,27 @@ for-swagger:
|
||||
up-noweb:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-678
|
||||
|
||||
up-noweb-libsignal:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
|
||||
|
||||
up-webjs:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-channels
|
||||
|
||||
start-proxy:
|
||||
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
|
||||
|
||||
proto-gows:
|
||||
rm -rf src/core/engines/gows/grpc
|
||||
mkdir -p src/core/engines/gows/grpc
|
||||
protoc \
|
||||
--plugin=protoc-gen-ts=node_modules/.bin/protoc-gen-ts \
|
||||
--plugin=protoc-gen-grpc=node_modules/.bin/grpc_tools_node_protoc_plugin \
|
||||
--js_out=import_style=commonjs,binary:src/core/engines/gows/grpc \
|
||||
--grpc_out=grpc_js:src/core/engines/gows/grpc \
|
||||
--ts_out=grpc_js:src/core/engines/gows/grpc \
|
||||
-I ../gows/proto ../gows/proto/gows.proto
|
||||
|
||||
gows:
|
||||
cd ../gows && \
|
||||
export PATH=${HOME}/go/bin:${PATH} && \
|
||||
make all
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
|
||||
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
|
||||
uv_threadpool_size=$(($cpus * 2))
|
||||
uv_threadpool_size=$(($cpus * 1))
|
||||
|
||||
# Set UV_THREADPOOL_SIZE as an environment variable
|
||||
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
|
||||
|
||||
+12
-3
@@ -55,10 +55,12 @@
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"google-protobuf": "^3.21.4",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"joi": "^17.13.3",
|
||||
"knex": "^3.1.0",
|
||||
"libphonenumber-js": "^1.10.36",
|
||||
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
|
||||
"link-preview-js": "^3.0.4",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.27",
|
||||
@@ -75,10 +77,12 @@
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.1.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.4",
|
||||
"swagger-ui-express": "^4.1.4",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-channels"
|
||||
"ulid": "^2.3.0",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-channels",
|
||||
"write-file-atomic": "^6.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.8"
|
||||
@@ -87,9 +91,12 @@
|
||||
"bufferutil": "^4.0.8",
|
||||
"ws": "^8.18.0",
|
||||
"puppeteer": "^23.6.0",
|
||||
"whatwg-url": "13.0.0"
|
||||
"whatwg-url": "13.0.0",
|
||||
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grpc/grpc-js": "^1.12.4",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.1",
|
||||
"@nestjs/testing": "^9.0.9",
|
||||
@@ -103,8 +110,10 @@
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"grpc-tools": "^1.12.4",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^1.19.1",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
"supertest": "^4.0.2",
|
||||
"ts-jest": "^29.1.3",
|
||||
"ts-loader": "^6.2.1",
|
||||
|
||||
@@ -21,12 +21,16 @@ import {
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { parseBool } from '../helpers';
|
||||
import {
|
||||
ChatPictureQuery,
|
||||
ChatPictureResponse,
|
||||
ChatsPaginationParams,
|
||||
ChatSummary,
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewPaginationParams,
|
||||
PinMessageRequest,
|
||||
} from '../structures/chats.dto';
|
||||
import { EditMessageRequest } from '../structures/chatting.dto';
|
||||
|
||||
@@ -47,6 +51,20 @@ class ChatsController {
|
||||
return session.getChats(pagination);
|
||||
}
|
||||
|
||||
@Get('overview')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Get chats overview. Includes all necessary things to build UI "your chats overview" page - chat id, name, picture, last message. Sorting by last message timestamp',
|
||||
})
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
getChatsOverview(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() pagination: OverviewPaginationParams,
|
||||
): Promise<ChatSummary[]> {
|
||||
return session.getChatsOverview(pagination);
|
||||
}
|
||||
|
||||
@Delete(':chatId')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Deletes the chat' })
|
||||
@@ -58,6 +76,19 @@ class ChatsController {
|
||||
return session.deleteChat(chatId);
|
||||
}
|
||||
|
||||
@Get(':chatId/picture')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets chat picture' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getChatPicture(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Query() query: ChatPictureQuery,
|
||||
): Promise<ChatPictureResponse> {
|
||||
const url = await session.getContactProfilePicture(chatId, query.refresh);
|
||||
return { url: url };
|
||||
}
|
||||
|
||||
@Get(':chatId/messages')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets messages in the chat' })
|
||||
@@ -89,6 +120,33 @@ class ChatsController {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Post(':chatId/messages/:messageId/pin')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Pins a message in the chat' })
|
||||
@ChatIdApiParam
|
||||
async pinMessage(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Param('messageId') messageId: string,
|
||||
@Body() body: PinMessageRequest,
|
||||
) {
|
||||
const result = await session.pinMessage(chatId, messageId, body.duration);
|
||||
return { success: result };
|
||||
}
|
||||
|
||||
@Post(':chatId/messages/:messageId/unpin')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Unpins a message in the chat' })
|
||||
@ChatIdApiParam
|
||||
async unpinMessage(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Param('messageId') messageId: string,
|
||||
) {
|
||||
const result = await session.unpinMessage(chatId, messageId);
|
||||
return { success: result };
|
||||
}
|
||||
|
||||
@Delete(':chatId/messages')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Clears all messages from the chat' })
|
||||
|
||||
@@ -171,15 +171,10 @@ export class ChattingController {
|
||||
return whatsapp.sendLinkPreview(request);
|
||||
}
|
||||
|
||||
@Get('/messages')
|
||||
@ApiOperation({ summary: 'Get messages in a chat' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getMessages(
|
||||
@Query() query: GetMessageQuery,
|
||||
@Query() filter: GetChatMessagesFilter,
|
||||
) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getChatMessages(query.chatId, query, filter);
|
||||
@Post('/sendContactVcard')
|
||||
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendContactVCard(request);
|
||||
}
|
||||
|
||||
@Get('/sendText')
|
||||
@@ -192,10 +187,19 @@ export class ChattingController {
|
||||
return whatsapp.sendText(msg);
|
||||
}
|
||||
|
||||
@Post('/sendContactVcard')
|
||||
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendContactVCard(request);
|
||||
@Get('/messages')
|
||||
@ApiOperation({
|
||||
summary: 'Get messages in a chat',
|
||||
description: 'DEPRECATED. Use "GET /api/chats/{id}/messages" instead',
|
||||
deprecated: true,
|
||||
})
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getMessages(
|
||||
@Query() query: GetMessageQuery,
|
||||
@Query() filter: GetChatMessagesFilter,
|
||||
) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getChatMessages(query.chatId, query, filter);
|
||||
}
|
||||
|
||||
@Get('/checkNumberStatus')
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
WANumberExistResult,
|
||||
} from '../structures/chatting.dto';
|
||||
import {
|
||||
ContactProfilePictureQuery,
|
||||
ContactQuery,
|
||||
ContactRequest,
|
||||
ContactsPaginationParams,
|
||||
@@ -75,9 +76,14 @@ export class ContactsController {
|
||||
description:
|
||||
'If privacy settings do not allow to get the picture, the method will return null.',
|
||||
})
|
||||
async getProfilePicture(@Query() query: ContactQuery) {
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getProfilePicture(@Query() query: ContactProfilePictureQuery) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getContactProfilePicture(query);
|
||||
const url = await whatsapp.getContactProfilePicture(
|
||||
query.contactId,
|
||||
query.refresh,
|
||||
);
|
||||
return { profilePictureURL: url };
|
||||
}
|
||||
|
||||
@Post('/block')
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { GroupIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
@@ -19,6 +22,9 @@ import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
DescriptionRequest,
|
||||
GroupsPaginationParams,
|
||||
JoinGroupRequest,
|
||||
JoinGroupResponse,
|
||||
ParticipantsRequest,
|
||||
SettingsSecurityChangeInfo,
|
||||
SubjectRequest,
|
||||
@@ -40,11 +46,48 @@ export class GroupsController {
|
||||
return session.createGroup(request);
|
||||
}
|
||||
|
||||
@Get('join-info')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get info about the group before joining.' })
|
||||
async joinInfoGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() query: JoinGroupRequest,
|
||||
): Promise<any> {
|
||||
// https://chat.whatsapp.com/123 => 123
|
||||
const code = query.code.split('/').pop();
|
||||
return session.joinInfoGroup(code);
|
||||
}
|
||||
|
||||
@Post('join')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Join group via code' })
|
||||
async joinGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: JoinGroupRequest,
|
||||
): Promise<JoinGroupResponse> {
|
||||
// https://chat.whatsapp.com/123 => 123
|
||||
const code = request.code.split('/').pop();
|
||||
const id = await session.joinGroup(code);
|
||||
return { id: id };
|
||||
}
|
||||
|
||||
@Get('')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get all groups.' })
|
||||
getGroups(@WorkingSessionParam session: WhatsappSession) {
|
||||
return session.getGroups();
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
getGroups(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() pagination: GroupsPaginationParams,
|
||||
) {
|
||||
return session.getGroups(pagination);
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Refresh groups from the server.' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async refreshGroups(@WorkingSessionParam session: WhatsappSession) {
|
||||
return { success: await session.refreshGroups() };
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
||||
+8
-5
@@ -1,13 +1,16 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const QRCode = require('qrcode');
|
||||
|
||||
export class QR {
|
||||
private base64: string;
|
||||
public raw?: string;
|
||||
|
||||
save(base64: string, raw?: string) {
|
||||
this.base64 = base64.replace(/^data:image\/png;base64,/, '');
|
||||
save(raw?: string) {
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
get(): Buffer {
|
||||
return Buffer.from(this.base64, 'base64');
|
||||
async get(): Promise<Buffer> {
|
||||
const url = await QRCode.toDataURL(this.raw);
|
||||
const base64 = url.replace(/^data:image\/png;base64,/, '');
|
||||
return Buffer.from(base64, 'base64');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface EngineBootstrap {
|
||||
bootstrap(): Promise<void>;
|
||||
|
||||
shutdown(): Promise<void>;
|
||||
}
|
||||
|
||||
export class NoopEngineBootstrap implements EngineBootstrap {
|
||||
async bootstrap(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
+67
-25
@@ -1,15 +1,22 @@
|
||||
import {
|
||||
BeforeApplicationShutdown,
|
||||
OnApplicationBootstrap,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import {
|
||||
EngineBootstrap,
|
||||
NoopEngineBootstrap,
|
||||
} from '@waha/core/abc/EngineBootstrap';
|
||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||
import { GowsBootstrap } from '@waha/core/engines/gows/GowsBootstrap';
|
||||
import { ISessionMeRepository } from '@waha/core/storage/ISessionMeRepository';
|
||||
import { ISessionWorkerRepository } from '@waha/core/storage/ISessionWorkerRepository';
|
||||
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
|
||||
import { waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { VERSION } from '@waha/version';
|
||||
import { EventEmitter } from 'events';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import { merge, Observable, of } from 'rxjs';
|
||||
|
||||
import {
|
||||
WAHAEngine,
|
||||
@@ -25,28 +32,33 @@ import {
|
||||
import { ISessionAuthRepository } from '../storage/ISessionAuthRepository';
|
||||
import { ISessionConfigRepository } from '../storage/ISessionConfigRepository';
|
||||
import { WhatsappSession } from './session.abc';
|
||||
import { WebhookConductor } from './webhooks.abc';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
|
||||
export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
export abstract class SessionManager
|
||||
implements BeforeApplicationShutdown, OnApplicationBootstrap
|
||||
{
|
||||
public store: any;
|
||||
public sessionAuthRepository: ISessionAuthRepository;
|
||||
public sessionConfigRepository: ISessionConfigRepository;
|
||||
protected sessionMeRepository: ISessionMeRepository;
|
||||
protected sessionWorkerRepository: ISessionWorkerRepository;
|
||||
public events: EventEmitter;
|
||||
private lock: any;
|
||||
|
||||
WAIT_STATUS_INTERVAL = 500;
|
||||
WAIT_STATUS_TIMEOUT = 5_000;
|
||||
LOCK_TIMEOUT = 10_000;
|
||||
|
||||
protected constructor(
|
||||
protected config: WhatsappConfigService,
|
||||
protected log: PinoLogger,
|
||||
protected config: WhatsappConfigService,
|
||||
protected gowsConfigService: GowsEngineConfigService,
|
||||
) {
|
||||
this.lock = new AsyncLock({ maxPending: Infinity });
|
||||
this.lock = new AsyncLock({
|
||||
maxPending: Infinity,
|
||||
timeout: this.LOCK_TIMEOUT,
|
||||
});
|
||||
this.log.setContext(SessionManager.name);
|
||||
}
|
||||
|
||||
@@ -72,7 +84,18 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
|
||||
protected abstract get EngineClass(): typeof WhatsappSession;
|
||||
|
||||
protected abstract get WebhookConductorClass(): typeof WebhookConductor;
|
||||
public getSessionEvent(session: string, event: WAHAEvents): Observable<any> {
|
||||
return of();
|
||||
}
|
||||
|
||||
public getSessionEvents(
|
||||
session: string,
|
||||
events: WAHAEvents[],
|
||||
): Observable<any> {
|
||||
return merge(
|
||||
...events.map((event) => this.getSessionEvent(session, event)),
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// API Methods
|
||||
@@ -114,22 +137,6 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
await this.sessionWorkerRepository?.unassign(name, this.workerId);
|
||||
}
|
||||
|
||||
protected handleSessionEvent(event: WAHAEvents, session: WhatsappSession) {
|
||||
return (payload: any) => {
|
||||
const me = session.getSessionMeInfo();
|
||||
const data: WAHAWebhook = {
|
||||
event: event,
|
||||
session: session.name,
|
||||
metadata: session.sessionConfig?.metadata,
|
||||
me: me,
|
||||
payload: payload,
|
||||
engine: session.engine,
|
||||
environment: VERSION,
|
||||
};
|
||||
this.events.emit(event, data);
|
||||
};
|
||||
}
|
||||
|
||||
async getWorkingSession(sessionName: string): Promise<WhatsappSession> {
|
||||
return this.waitUntilStatus(sessionName, [WAHASessionStatus.WORKING]);
|
||||
}
|
||||
@@ -160,7 +167,42 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
return session;
|
||||
}
|
||||
|
||||
async beforeApplicationShutdown(signal?: string) {
|
||||
this.events.removeAllListeners();
|
||||
beforeApplicationShutdown(signal?: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
onApplicationBootstrap(): any {
|
||||
return;
|
||||
}
|
||||
|
||||
protected getEngineBootstrap(engine: WAHAEngine): EngineBootstrap {
|
||||
const logger = this.log.logger.child({ engine: engine.toLowerCase() });
|
||||
if (engine === WAHAEngine.GOWS) {
|
||||
const config = this.gowsConfigService.getBootstrapConfig();
|
||||
return new GowsBootstrap(logger, config);
|
||||
}
|
||||
return new NoopEngineBootstrap();
|
||||
}
|
||||
}
|
||||
|
||||
export function populateSessionInfo(
|
||||
event: WAHAEvents,
|
||||
session: WhatsappSession,
|
||||
) {
|
||||
return (payload: any): WAHAWebhook => {
|
||||
const id = payload._eventId;
|
||||
const data = { ...payload };
|
||||
delete data._eventId;
|
||||
const me = session.getSessionMeInfo();
|
||||
return {
|
||||
id: id,
|
||||
event: event,
|
||||
session: session.name,
|
||||
metadata: session.sessionConfig?.metadata,
|
||||
me: me,
|
||||
payload: data,
|
||||
engine: session.engine,
|
||||
environment: VERSION,
|
||||
};
|
||||
};
|
||||
}
|
||||
+150
-34
@@ -4,6 +4,7 @@ import {
|
||||
ListChannelsQuery,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import {
|
||||
ChatSummary,
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
@@ -12,11 +13,28 @@ import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
|
||||
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
import { DefaultMap } from '@waha/utils/DefaultMap';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { EventEmitter } from 'events';
|
||||
import { complete } from '@waha/utils/reactive/complete';
|
||||
import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable';
|
||||
import * as fs from 'fs';
|
||||
import * as lodash from 'lodash';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import * as NodeCache from 'node-cache';
|
||||
import { Logger } from 'pino';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
delay,
|
||||
filter,
|
||||
of,
|
||||
retry,
|
||||
share,
|
||||
Subject,
|
||||
switchMap,
|
||||
} from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
import { MessageId } from 'whatsapp-web.js';
|
||||
|
||||
import {
|
||||
@@ -36,6 +54,7 @@ import {
|
||||
MessageTextRequest,
|
||||
MessageVideoRequest,
|
||||
MessageVoiceRequest,
|
||||
SendSeenRequest,
|
||||
} from '../../structures/chatting.dto';
|
||||
import { ContactQuery, ContactRequest } from '../../structures/contacts.dto';
|
||||
import {
|
||||
@@ -46,6 +65,7 @@ import {
|
||||
} from '../../structures/enums.dto';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
GroupsPaginationParams,
|
||||
ParticipantsRequest,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '../../structures/groups.dto';
|
||||
@@ -88,10 +108,6 @@ export function ensureSuffix(phone) {
|
||||
return phone + suffix;
|
||||
}
|
||||
|
||||
export enum WAHAInternalEvent {
|
||||
ENGINE_START = 'engine.start',
|
||||
}
|
||||
|
||||
export interface SessionParams {
|
||||
name: string;
|
||||
printQR: boolean;
|
||||
@@ -104,7 +120,6 @@ export interface SessionParams {
|
||||
}
|
||||
|
||||
export abstract class WhatsappSession {
|
||||
public events: EventEmitter;
|
||||
public engine: WAHAEngine;
|
||||
|
||||
public name: string;
|
||||
@@ -119,6 +134,11 @@ export abstract class WhatsappSession {
|
||||
|
||||
private _status: WAHASessionStatus;
|
||||
private shouldPrintQR: boolean;
|
||||
protected events2: DefaultMap<WAHAEvents, SwitchObservable<any>>;
|
||||
private status$: Subject<WAHASessionStatus>;
|
||||
protected profilePictures: NodeCache = new NodeCache({
|
||||
stdTTL: 24 * 60 * 60, // 1 day
|
||||
});
|
||||
|
||||
public constructor({
|
||||
name,
|
||||
@@ -130,11 +150,64 @@ export abstract class WhatsappSession {
|
||||
sessionConfig,
|
||||
engineConfig,
|
||||
}: SessionParams) {
|
||||
this.events = new EventEmitter();
|
||||
this.status$ = new BehaviorSubject(null);
|
||||
|
||||
this.name = name;
|
||||
this.proxyConfig = proxyConfig;
|
||||
this.loggerBuilder = loggerBuilder;
|
||||
this.logger = loggerBuilder.child({ name: 'WhatsappSession' });
|
||||
this.events2 = new DefaultMap<WAHAEvents, SwitchObservable<any>>(
|
||||
(key) =>
|
||||
new SwitchObservable((obs$) => {
|
||||
return obs$.pipe(
|
||||
catchError((err) => {
|
||||
this.logger.error(
|
||||
`Caught error, dropping value from, event: '${key}'`,
|
||||
);
|
||||
this.logger.error(err, err.stack);
|
||||
throw err;
|
||||
}),
|
||||
filter(Boolean),
|
||||
map((data) => {
|
||||
data._eventId = generatePrefixedId('evt');
|
||||
return data;
|
||||
}),
|
||||
retry(),
|
||||
share(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
this.events2.get(WAHAEvents.SESSION_STATUS).switch(
|
||||
this.status$
|
||||
// initial value is null
|
||||
.pipe(filter(Boolean))
|
||||
// Wait for WORKING status to get all the info
|
||||
// https://github.com/devlikeapro/waha/issues/409
|
||||
.pipe(
|
||||
switchMap((status: WAHASessionStatus) => {
|
||||
const me = this.getSessionMeInfo();
|
||||
const hasMe = !!me?.pushName && !!me?.id;
|
||||
// Delay WORKING by 1 second if condition is met
|
||||
// Usually we get WORKING with all the info after
|
||||
if (status === WAHASessionStatus.WORKING && !hasMe) {
|
||||
return of(status).pipe(delay(2000));
|
||||
}
|
||||
return of(status);
|
||||
}),
|
||||
// Remove consecutive duplicate WORKING statuses
|
||||
distinctUntilChanged(
|
||||
(prev, curr) => prev === curr && curr === WAHASessionStatus.WORKING,
|
||||
),
|
||||
)
|
||||
// Populate the session info
|
||||
.pipe(
|
||||
map<WAHASessionStatus, WASessionStatusBody>((status) => {
|
||||
return { name: this.name, status: status };
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
this.sessionStore = sessionStore;
|
||||
this.mediaManager = mediaManager;
|
||||
this.sessionConfig = sessionConfig;
|
||||
@@ -142,6 +215,10 @@ export abstract class WhatsappSession {
|
||||
this.shouldPrintQR = printQR;
|
||||
}
|
||||
|
||||
public getEventObservable(event: WAHAEvents) {
|
||||
return this.events2.get(event);
|
||||
}
|
||||
|
||||
protected set status(value: WAHASessionStatus) {
|
||||
if (this.unpairing && value !== WAHASessionStatus.STOPPED) {
|
||||
// In case of unpairing
|
||||
@@ -149,8 +226,7 @@ export abstract class WhatsappSession {
|
||||
return;
|
||||
}
|
||||
this._status = value;
|
||||
const body: WASessionStatusBody = { name: this.name, status: value };
|
||||
this.events.emit(WAHAEvents.SESSION_STATUS, body);
|
||||
this.status$.next(value);
|
||||
}
|
||||
|
||||
public get status() {
|
||||
@@ -196,7 +272,8 @@ export abstract class WhatsappSession {
|
||||
'--hide-scrollbars',
|
||||
'--ignore-certificate-errors',
|
||||
'--ignore-ssl-errors',
|
||||
'--in-process-gpu', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
|
||||
// https://github.com/devlikeapro/waha/issues/725
|
||||
// '--in-process-gpu', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
|
||||
'--metrics-recording-only',
|
||||
'--mute-audio',
|
||||
'--no-default-browser-check',
|
||||
@@ -223,30 +300,15 @@ export abstract class WhatsappSession {
|
||||
/** Stop the session */
|
||||
abstract stop(): Promise<void>;
|
||||
|
||||
protected stopEvents() {
|
||||
complete(this.events2);
|
||||
}
|
||||
|
||||
/* Unpair the account */
|
||||
async unpair(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
/** Subscribe the handler to specific hook */
|
||||
subscribeSessionEvent(
|
||||
hook: WAHAEvents | string,
|
||||
handler: (message) => void,
|
||||
): boolean {
|
||||
switch (hook) {
|
||||
case WAHAEvents.SESSION_STATUS:
|
||||
this.events.on(WAHAEvents.SESSION_STATUS, handler);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
abstract subscribeEngineEvent(
|
||||
hook: WAHAEvents | string,
|
||||
handler: (message) => void,
|
||||
): boolean;
|
||||
|
||||
/**
|
||||
* START - Methods for API
|
||||
*/
|
||||
@@ -320,7 +382,7 @@ export abstract class WhatsappSession {
|
||||
|
||||
abstract reply(request: MessageReplyRequest);
|
||||
|
||||
abstract sendSeen(chat: ChatRequest);
|
||||
abstract sendSeen(chat: SendSeenRequest);
|
||||
|
||||
abstract startTyping(chat: ChatRequest);
|
||||
|
||||
@@ -339,6 +401,12 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getChatsOverview(
|
||||
pagination: PaginationParams,
|
||||
): Promise<ChatSummary[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public deleteChat(chatId) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -359,6 +427,18 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public pinMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
duration: number,
|
||||
): Promise<boolean> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public unpinMessage(chatId: string, messageId: string): Promise<boolean> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public deleteMessage(chatId: string, messageId: string) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -438,8 +518,32 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getContactProfilePicture(query: ContactQuery) {
|
||||
throw new NotImplementedByEngineError();
|
||||
/**
|
||||
* Fetch the latest profile picture of the contact (group, newsletter, etc.)
|
||||
* @param id
|
||||
*/
|
||||
abstract fetchContactProfilePicture(id: string): Promise<string | null>;
|
||||
|
||||
public async getContactProfilePicture(
|
||||
id: string,
|
||||
refresh: boolean,
|
||||
): Promise<string | null> {
|
||||
const has: boolean = this.profilePictures.has(id);
|
||||
if (!has || refresh) {
|
||||
await this.refreshProfilePicture(id);
|
||||
}
|
||||
return this.profilePictures.get(id) || null;
|
||||
}
|
||||
|
||||
protected async refreshProfilePicture(id: string) {
|
||||
this.profilePictures.del(id);
|
||||
const url = await this.fetchContactProfilePicture(id).catch((err) => {
|
||||
this.logger.error('Error fetching profile picture');
|
||||
this.logger.error(err, err.stack);
|
||||
return null;
|
||||
});
|
||||
this.profilePictures.set(id, url);
|
||||
return url;
|
||||
}
|
||||
|
||||
public blockContact(request: ContactRequest) {
|
||||
@@ -457,7 +561,19 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getGroups() {
|
||||
public joinGroup(code: string): Promise<string> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public joinInfoGroup(code: string): Promise<any> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getGroups(pagination: PaginationParams) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public refreshGroups(): Promise<boolean> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
@@ -537,7 +653,7 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public subscribePresence(id: string): Promise<void> {
|
||||
public subscribePresence(id: string): Promise<any> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
import { WebhookConfig } from '../../structures/webhooks.config.dto';
|
||||
import { WhatsappSession } from './session.abc';
|
||||
|
||||
export abstract class WebhookSender {
|
||||
protected url: string;
|
||||
protected logger: Logger;
|
||||
protected readonly config: WebhookConfig;
|
||||
|
||||
constructor(
|
||||
loggerBuilder: LoggerBuilder,
|
||||
protected webhookConfig: WebhookConfig,
|
||||
) {
|
||||
this.url = webhookConfig.url;
|
||||
this.logger = loggerBuilder.child({ name: WebhookSender.name });
|
||||
this.config = webhookConfig;
|
||||
}
|
||||
|
||||
abstract send(json: any): void;
|
||||
}
|
||||
|
||||
export abstract class WebhookConductor {
|
||||
abstract configure(session: WhatsappSession, webhooks: WebhookConfig[]);
|
||||
}
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WebsocketHeartbeatJob } from '@waha/nestjs/ws/WebsocketHeartbeatJob';
|
||||
import { WebSocket } from '@waha/nestjs/ws/ws';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { WAHAEvents, WAHAEventsWild } from '@waha/structures/enums.dto';
|
||||
import { EventWildUnmask } from '@waha/utils/events';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { IncomingMessage } from 'http';
|
||||
import * as lodash from 'lodash';
|
||||
import * as url from 'url';
|
||||
import { Server } from 'ws';
|
||||
|
||||
@@ -37,11 +37,9 @@ export class WebsocketGatewayCore
|
||||
@WebSocketServer()
|
||||
server: Server;
|
||||
|
||||
private listeners: Map<WebSocket, { session: string; events: string[] }> =
|
||||
new Map();
|
||||
|
||||
private readonly logger: LoggerService;
|
||||
private heartbeat: WebsocketHeartbeatJob;
|
||||
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
|
||||
|
||||
constructor(private manager: SessionManager) {
|
||||
this.logger = new Logger('WebsocketGateway');
|
||||
@@ -56,49 +54,44 @@ export class WebsocketGatewayCore
|
||||
const id = generatePrefixedId('wsc');
|
||||
socket.id = id;
|
||||
this.logger.debug(`New client connected: ${request.url}`);
|
||||
const params = this.getParams(id, request, socket);
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
const { session, events } = params;
|
||||
const params = this.getParams(request);
|
||||
const session: string = params.session;
|
||||
const events: WAHAEvents[] = params.events;
|
||||
this.logger.debug(
|
||||
`Client connected to session: '${session}', events: ${events}, ${id}`,
|
||||
);
|
||||
this.listeners.set(socket, { session, events });
|
||||
|
||||
const sub = this.manager
|
||||
.getSessionEvents(session, events)
|
||||
.subscribe((data) => {
|
||||
this.logger.debug(`Sending data to client, event.id: ${data.id}`, data);
|
||||
socket.send(JSON.stringify(data), (err) => {
|
||||
if (!err) {
|
||||
return;
|
||||
}
|
||||
this.logger.error(`Error sending data to client: ${err}`);
|
||||
});
|
||||
});
|
||||
socket.on('close', () => {
|
||||
this.logger.debug(`Client disconnected - ${socket.id}`);
|
||||
sub.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
private getParams(id: string, request: IncomingMessage, socket: WebSocket) {
|
||||
private getParams(request: IncomingMessage) {
|
||||
const query = url.parse(request.url, true).query;
|
||||
const session = (query.session as string) || '*';
|
||||
if (session !== '*') {
|
||||
this.logger.warn(
|
||||
`Only connecting to all sessions is allowed for now, use session=*, ${id}`,
|
||||
);
|
||||
const error =
|
||||
'Only connecting to all sessions is allowed for now, use session=*';
|
||||
socket.close(4001, JSON.stringify({ error }));
|
||||
return null;
|
||||
}
|
||||
|
||||
const events = ((query.events as string) || '*').split(',');
|
||||
if (
|
||||
!lodash.isEqual(events, ['*']) &&
|
||||
!lodash.isEqual(events, [WAHAEvents.SESSION_STATUS])
|
||||
) {
|
||||
this.logger.warn(
|
||||
`Only \'session.status\' event is allowed for now, use events=session.status or events=*, ${id}`,
|
||||
);
|
||||
const error =
|
||||
"Only 'session.status' event is allowed for now, use events=session.status or events=*";
|
||||
socket.close(4001, JSON.stringify({ error }));
|
||||
return null;
|
||||
let paramsEvents = (query.events as string[]) || '*';
|
||||
// if params events string - split by ","
|
||||
if (typeof paramsEvents === 'string') {
|
||||
paramsEvents = paramsEvents.split(',');
|
||||
}
|
||||
const events = this.eventUnmask.unmask(paramsEvents);
|
||||
return { session, events };
|
||||
}
|
||||
|
||||
handleDisconnect(socket: WebSocket): any {
|
||||
this.logger.debug(`Client disconnected - ${socket.id}`);
|
||||
this.listeners.delete(socket);
|
||||
}
|
||||
|
||||
async beforeApplicationShutdown(signal?: string) {
|
||||
@@ -118,24 +111,9 @@ export class WebsocketGatewayCore
|
||||
|
||||
afterInit(server: Server) {
|
||||
this.logger.debug('Websocket server initialized');
|
||||
this.manager.events.on(
|
||||
WAHAEvents.SESSION_STATUS,
|
||||
this.sendToAll.bind(this),
|
||||
);
|
||||
this.logger.debug('Subscribed to manager events');
|
||||
|
||||
this.logger.debug('Starting heartbeat service...');
|
||||
this.heartbeat.start(server);
|
||||
this.logger.debug('Heartbeat service started');
|
||||
}
|
||||
|
||||
sendToAll(data: any) {
|
||||
this.listeners.forEach((options, client) => {
|
||||
if (options.events.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.logger.debug('Sending data to client', data);
|
||||
client.send(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
ServerDebugController,
|
||||
} from '@waha/api/server.controller';
|
||||
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
|
||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
||||
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor';
|
||||
@@ -146,6 +148,8 @@ const PROVIDERS = [
|
||||
},
|
||||
DashboardConfigServiceCore,
|
||||
SwaggerConfigServiceCore,
|
||||
WebJSEngineConfigService,
|
||||
GowsEngineConfigService,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
WebsocketGatewayCore,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { BootstrapConfig } from '@waha/core/engines/gows/GowsBootstrap';
|
||||
import { GowsConfig } from '@waha/core/engines/gows/session.gows.core';
|
||||
|
||||
@Injectable()
|
||||
export class GowsEngineConfigService {
|
||||
constructor(protected configService: ConfigService) {}
|
||||
|
||||
getBootstrapConfig(): BootstrapConfig {
|
||||
return {
|
||||
path: this.configService.get('WAHA_GOWS_PATH'),
|
||||
socket: this.getSocket(),
|
||||
};
|
||||
}
|
||||
|
||||
getSocket() {
|
||||
return this.configService.get('WAHA_GOWS_SOCKET', '/tmp/gows.sock');
|
||||
}
|
||||
|
||||
getConfig(): GowsConfig {
|
||||
return {
|
||||
connection: 'unix:' + this.getSocket(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { WebJSConfig } from '../../core/engines/webjs/session.webjs.core';
|
||||
|
||||
@Injectable()
|
||||
export class WebJSEngineConfigService {
|
||||
constructor(protected configService: ConfigService) {}
|
||||
|
||||
getConfig(): WebJSConfig {
|
||||
let webVersion = this.configService.get<string>(
|
||||
'WAHA_WEBJS_WEB_VERSION',
|
||||
undefined,
|
||||
);
|
||||
if (webVersion === '2.2412.54-videofix') {
|
||||
// Deprecated version
|
||||
webVersion = undefined;
|
||||
}
|
||||
return {
|
||||
webVersion: webVersion,
|
||||
cacheType: this.getCacheType(),
|
||||
};
|
||||
}
|
||||
|
||||
getCacheType(): 'local' | 'none' {
|
||||
const cacheType = this.configService
|
||||
.get<string>('WAHA_WEBJS_CACHE_TYPE', 'none')
|
||||
.toLowerCase();
|
||||
if (cacheType != 'local' && cacheType != 'none') {
|
||||
throw new Error(
|
||||
'Invalid cache type, only "local" and "none" are allowed',
|
||||
);
|
||||
}
|
||||
|
||||
return cacheType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
|
||||
interface EventValue<T> {
|
||||
event: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export class EventsFromObservable<Events extends string> extends EventEmitter {
|
||||
private subscription: Subscription;
|
||||
|
||||
constructor(private observable: Observable<EventValue<any>>) {
|
||||
super();
|
||||
}
|
||||
|
||||
on(event: Events, listener: (...args: any[]) => void): this {
|
||||
return super.on(event as string, listener);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.subscription = this.observable.subscribe({
|
||||
next: (data) => this.emit(data.event, data.data),
|
||||
complete: () => this.removeAllListeners(),
|
||||
error: (err) => null,
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { EngineBootstrap } from '@waha/core/abc/EngineBootstrap';
|
||||
import { GowsSubprocess } from '@waha/core/engines/gows/GowsSubprocess';
|
||||
import { promises as fs } from 'fs';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
export async function isUnixSocket(socketPath: string): Promise<boolean> {
|
||||
try {
|
||||
// Get the file stats
|
||||
const stats = await fs.lstat(socketPath);
|
||||
// Check if the path is a socket
|
||||
return stats.isSocket();
|
||||
} catch (error) {
|
||||
// Handle errors (e.g., file does not exist)
|
||||
if (error.code === 'ENOENT') {
|
||||
return false; // Path does not exist
|
||||
}
|
||||
// Re-throw other errors for debugging purposes
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BootstrapConfig {
|
||||
path: string;
|
||||
socket: string;
|
||||
}
|
||||
|
||||
export class GowsBootstrap implements EngineBootstrap {
|
||||
private gows: GowsSubprocess;
|
||||
|
||||
constructor(
|
||||
private logger: Logger,
|
||||
private config: BootstrapConfig,
|
||||
) {}
|
||||
|
||||
async bootstrap(): Promise<void> {
|
||||
if (!this.config.path) {
|
||||
this.logger.warn('GOWS path is not set, skipping GOWS initialization.');
|
||||
this.logger.warn('Make sure to run GOWS manually.');
|
||||
this.checkSocket(this.config.socket);
|
||||
return;
|
||||
}
|
||||
|
||||
this.gows = new GowsSubprocess(
|
||||
this.logger,
|
||||
this.config.path,
|
||||
this.config.socket,
|
||||
);
|
||||
this.gows.start(() => {
|
||||
this.logger.info(`GOWS stopped, exiting...`);
|
||||
process.kill(process.pid, 'SIGTERM');
|
||||
});
|
||||
await this.gows.waitWhenReady(10_000);
|
||||
await this.checkSocket(this.config.socket);
|
||||
return;
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
if (this.gows) {
|
||||
await this.gows.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the provided path exists and is a socket
|
||||
* @param path
|
||||
*/
|
||||
async checkSocket(path: string) {
|
||||
if (!(await isUnixSocket(path))) {
|
||||
throw new Error(`Invalid socket path: ${path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { messages } from '@waha/core/engines/gows/grpc/gows';
|
||||
import { EnginePayload } from '@waha/structures/webhooks.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { Logger } from 'pino';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Observable that listens to a gRPC stream and emits EnginePayload objects.
|
||||
* Pass a factory function that returns a client and a stream.
|
||||
*/
|
||||
export class GowsEventStreamObservable extends Observable<EnginePayload> {
|
||||
_client: grpc.Client;
|
||||
CLIENT_CLOSE_TIMEOUT = 1_000;
|
||||
|
||||
constructor(
|
||||
private logger: Logger,
|
||||
factory: () => {
|
||||
client: grpc.Client;
|
||||
stream: grpc.ClientReadableStream<messages.EventJson>;
|
||||
},
|
||||
) {
|
||||
super((subscriber) => {
|
||||
this.logger.debug('Creating grpc client and stream...');
|
||||
const { client, stream } = factory();
|
||||
this._client = client;
|
||||
|
||||
stream.on('data', (raw) => {
|
||||
const obj = raw.toObject();
|
||||
obj.data = JSON.parse(obj.data);
|
||||
subscriber?.next(obj);
|
||||
});
|
||||
|
||||
stream.on('end', (...args) => {
|
||||
this.logger.debug('Stream ended', args);
|
||||
subscriber?.complete();
|
||||
subscriber = null;
|
||||
});
|
||||
|
||||
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');
|
||||
return;
|
||||
}
|
||||
this.logger.error(err, 'Stream error');
|
||||
// Give some time to node event loop to process the error
|
||||
await sleep(100);
|
||||
subscriber?.error(err);
|
||||
subscriber = null;
|
||||
});
|
||||
|
||||
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');
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get client(): Omit<grpc.Client, 'close'> {
|
||||
return this._client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { spawn } from 'child_process';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
export class GowsSubprocess {
|
||||
private checkIntervalMs: number = 100;
|
||||
private readyDelayMs: number = 1_000;
|
||||
private readyText = 'gRPC server started!';
|
||||
|
||||
private child: any;
|
||||
private ready: boolean = false;
|
||||
|
||||
constructor(
|
||||
private logger: Logger,
|
||||
readonly path: string,
|
||||
readonly socket: string,
|
||||
) {}
|
||||
|
||||
start(onExit: (code: number) => void) {
|
||||
this.logger.info('Starting GOWS subprocess...');
|
||||
this.logger.debug(`GOWS path '${this.path}', socket: '${this.socket}'...`);
|
||||
|
||||
this.child = spawn(this.path, ['--socket', this.socket]);
|
||||
this.logger.debug(`GOWS started with PID: ${this.child.pid}`);
|
||||
this.child.on('close', async (code, singal) => {
|
||||
const msg = code
|
||||
? `GOWS subprocess closed with code ${code}`
|
||||
: `GOWS subprocess closed by signal ${singal}`;
|
||||
this.logger.debug(msg);
|
||||
onExit(code);
|
||||
});
|
||||
this.child.on('error', (err) => {
|
||||
this.logger.error(`GOWS subprocess error: ${err}`);
|
||||
});
|
||||
|
||||
this.child.stderr.setEncoding('utf8');
|
||||
this.child.stderr.on('data', (data) => {
|
||||
this.logger.error(data);
|
||||
});
|
||||
|
||||
this.child.stdout.setEncoding('utf8');
|
||||
this.child.stdout.on('data', async (data) => {
|
||||
// remove empty line at the end, split by \n
|
||||
const lines = data.trim().split('\n');
|
||||
lines.forEach((line) => this.log(line));
|
||||
});
|
||||
this.listenReady();
|
||||
}
|
||||
|
||||
listenReady() {
|
||||
this.child.stdout.on('data', async (data) => {
|
||||
if (this.ready) {
|
||||
return;
|
||||
}
|
||||
if (!data.includes(this.readyText)) {
|
||||
return;
|
||||
}
|
||||
await sleep(this.readyDelayMs);
|
||||
this.ready = true;
|
||||
this.logger.info('GOWS is ready');
|
||||
});
|
||||
}
|
||||
|
||||
async waitWhenReady(timeout: number) {
|
||||
const started = await waitUntil(
|
||||
async () => this.ready,
|
||||
this.checkIntervalMs,
|
||||
timeout,
|
||||
);
|
||||
if (!started) {
|
||||
const msg = 'GOWS did not start after 10 seconds';
|
||||
this.logger.error(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.logger.info('Stopping GOWS subprocess...');
|
||||
this.child?.kill();
|
||||
this.logger.info('GOWS subprocess stopped');
|
||||
}
|
||||
|
||||
private log(msg) {
|
||||
if (msg.startsWith('ERROR | ')) {
|
||||
this.logger.error(msg.slice(8));
|
||||
} else if (msg.startsWith('WARN | ')) {
|
||||
this.logger.warn(msg.slice(7));
|
||||
} else if (msg.startsWith('INFO | ')) {
|
||||
this.logger.info(msg.slice(7));
|
||||
} else if (msg.startsWith('DEBUG | ')) {
|
||||
this.logger.debug(msg.slice(8));
|
||||
} else if (msg.startsWith('TRACE | ')) {
|
||||
this.logger.trace(msg.slice(8));
|
||||
} else {
|
||||
this.logger.info(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,890 @@
|
||||
import { isJidGroup, jidNormalizedUser } from '@adiwajshing/baileys';
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { connectivityState } from '@grpc/grpc-js';
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { EventsFromObservable } from '@waha/core/engines/gows/EventsFromObservable';
|
||||
import { GowsEventStreamObservable } from '@waha/core/engines/gows/GowsEventStreamObservable';
|
||||
import { messages } from '@waha/core/engines/gows/grpc/gows';
|
||||
import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFactoryCore';
|
||||
import {
|
||||
parseMessageIdSerialized,
|
||||
toCusFormat,
|
||||
toJID,
|
||||
} from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { AvailableInPlusVersion } from '@waha/core/exceptions';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
MessageFileRequest,
|
||||
MessageForwardRequest,
|
||||
MessageImageRequest,
|
||||
MessageLocationRequest,
|
||||
MessageReactionRequest,
|
||||
MessageReplyRequest,
|
||||
MessageTextRequest,
|
||||
MessageVoiceRequest,
|
||||
SendSeenRequest,
|
||||
} from '@waha/structures/chatting.dto';
|
||||
import {
|
||||
ACK_UNKNOWN,
|
||||
WAHAEvents,
|
||||
WAHAPresenceStatus,
|
||||
WAHASessionStatus,
|
||||
WAMessageAck,
|
||||
} from '@waha/structures/enums.dto';
|
||||
import {
|
||||
WAHAChatPresences,
|
||||
WAHAPresenceData,
|
||||
} from '@waha/structures/presence.dto';
|
||||
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
|
||||
import { MeInfo, ProxyConfig } from '@waha/structures/sessions.dto';
|
||||
import { EnginePayload, WAMessageAckBody } from '@waha/structures/webhooks.dto';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { onlyEvent } from '@waha/utils/reactive/ops/onlyEvent';
|
||||
import * as NodeCache from 'node-cache';
|
||||
import {
|
||||
debounceTime,
|
||||
filter,
|
||||
groupBy,
|
||||
merge,
|
||||
mergeMap,
|
||||
Observable,
|
||||
partition,
|
||||
retry,
|
||||
share,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import * as gows from './types';
|
||||
import MessageServiceClient = messages.MessageServiceClient;
|
||||
|
||||
enum WhatsMeowEvent {
|
||||
CONNECTED = 'gows.ConnectedEventData',
|
||||
DISCONNECTED = 'events.Disconnected',
|
||||
KEEP_ALIVE_TIMEOUT = 'events.KeepAliveTimeout',
|
||||
QR_CHANNEL_ITEM = 'whatsmeow.QRChannelItem',
|
||||
MESSAGE = 'events.Message',
|
||||
RECEIPT = 'events.Receipt',
|
||||
PRESENCE = 'events.Presence',
|
||||
CHAT_PRESENCE = 'events.ChatPresence',
|
||||
PUSH_NAME_SETTING = 'events.PushNameSetting',
|
||||
LOGGED_OUT = 'events.LoggedOut',
|
||||
}
|
||||
|
||||
export interface GowsConfig {
|
||||
connection: string;
|
||||
}
|
||||
|
||||
export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
protected authFactory = new GowsAuthFactoryCore();
|
||||
|
||||
protected qr: QR;
|
||||
public client: MessageServiceClient;
|
||||
protected stream$: GowsEventStreamObservable;
|
||||
protected all$: Observable<EnginePayload>;
|
||||
protected events: EventsFromObservable<WhatsMeowEvent>;
|
||||
|
||||
protected me: MeInfo | null;
|
||||
public session: messages.Session;
|
||||
protected presences: any;
|
||||
|
||||
public constructor(config) {
|
||||
super(config);
|
||||
this.qr = new QR();
|
||||
this.session = new messages.Session({ id: this.name });
|
||||
this.presences = new NodeCache({
|
||||
stdTTL: 60 * 60, // 1 hour
|
||||
useClones: false,
|
||||
});
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.status = WAHASessionStatus.STARTING;
|
||||
this.buildStreams();
|
||||
this.subscribeEvents();
|
||||
this.subscribeEngineEvents2();
|
||||
if (this.isDebugEnabled()) {
|
||||
this.listenEngineEventsInDebugMode();
|
||||
}
|
||||
|
||||
// start session
|
||||
const auth = await this.authFactory.buildAuth(this.sessionStore, this.name);
|
||||
|
||||
const level = messages.LogLevel[this.logger.level.toUpperCase()];
|
||||
const request = new messages.StartSessionRequest({
|
||||
id: this.name,
|
||||
config: new messages.SessionConfig({
|
||||
store: new messages.SessionStoreConfig({
|
||||
address: auth.address(),
|
||||
dialect: auth.dialect(),
|
||||
}),
|
||||
log: new messages.SessionLogConfig({
|
||||
level: level ?? messages.LogLevel.INFO,
|
||||
}),
|
||||
proxy: new messages.SessionProxyConfig({
|
||||
url: this.getProxyUrl(this.proxyConfig),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
this.client = new MessageServiceClient(
|
||||
this.engineConfig.connection,
|
||||
grpc.credentials.createInsecure(),
|
||||
);
|
||||
|
||||
try {
|
||||
await promisify(this.client.StartSession)(request);
|
||||
} catch (err) {
|
||||
this.logger.error('Failed to start the client');
|
||||
this.logger.error(err, err.stack);
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
protected getProxyUrl(config: ProxyConfig): string {
|
||||
if (!config || !config.server) {
|
||||
return '';
|
||||
}
|
||||
const { server, username, password } = config;
|
||||
const auth = username && password ? `${username}:${password}@` : '';
|
||||
const schema = 'http';
|
||||
return `${schema}://${auth}${server}`;
|
||||
}
|
||||
|
||||
buildStreams() {
|
||||
this.stream$ = new GowsEventStreamObservable(
|
||||
this.loggerBuilder.child({ grpc: 'stream' }),
|
||||
() => {
|
||||
const client = new messages.EventStreamClient(
|
||||
this.engineConfig.connection,
|
||||
grpc.credentials.createInsecure(),
|
||||
);
|
||||
const stream = client.StreamEvents(this.session);
|
||||
return { client, stream };
|
||||
},
|
||||
);
|
||||
|
||||
// Retry on error with delay
|
||||
this.all$ = this.stream$.pipe(retry({ delay: 2_000 }), share());
|
||||
}
|
||||
|
||||
subscribeEvents() {
|
||||
// Handle connection status
|
||||
this.events = new EventsFromObservable<WhatsMeowEvent>(this.all$);
|
||||
const events = this.events;
|
||||
events.on(WhatsMeowEvent.CONNECTED, (data) => {
|
||||
this.status = WAHASessionStatus.WORKING;
|
||||
this.me = {
|
||||
id: toCusFormat(jidNormalizedUser(data.ID)),
|
||||
pushName: data.PushName,
|
||||
};
|
||||
});
|
||||
|
||||
events.on(WhatsMeowEvent.DISCONNECTED, () => {
|
||||
if (this.status != WAHASessionStatus.STARTING) {
|
||||
this.status = WAHASessionStatus.STARTING;
|
||||
}
|
||||
});
|
||||
events.on(WhatsMeowEvent.KEEP_ALIVE_TIMEOUT, () => {
|
||||
if (this.status != WAHASessionStatus.STARTING) {
|
||||
this.status = WAHASessionStatus.STARTING;
|
||||
}
|
||||
});
|
||||
events.on(WhatsMeowEvent.QR_CHANNEL_ITEM, async (data) => {
|
||||
if (!data.Event) {
|
||||
return;
|
||||
}
|
||||
if (data.Event == 'success') {
|
||||
return;
|
||||
}
|
||||
if (data.Event != 'code') {
|
||||
this.logger.warn(data, 'Failed QR item event');
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
return;
|
||||
}
|
||||
const qr = data.Code;
|
||||
if (!qr) {
|
||||
return;
|
||||
}
|
||||
this.qr.save(qr);
|
||||
this.printQR(this.qr);
|
||||
this.status = WAHASessionStatus.SCAN_QR_CODE;
|
||||
});
|
||||
events.on(WhatsMeowEvent.PUSH_NAME_SETTING, (data) => {
|
||||
this.me = { ...this.me, pushName: data.Action.name };
|
||||
});
|
||||
events.on(WhatsMeowEvent.LOGGED_OUT, () => {
|
||||
this.logger.error('Logged out');
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
});
|
||||
events.on(WhatsMeowEvent.PRESENCE, (event: gows.Presence) => {
|
||||
if (isJidGroup(event.From)) {
|
||||
// So group is not "online"
|
||||
return;
|
||||
}
|
||||
const Chat = event.From;
|
||||
const Sender = event.From;
|
||||
const stored = this.presences.get(Chat) || [];
|
||||
// remove values by event.Sender
|
||||
const filtered = stored.filter(
|
||||
(p) => p.Sender !== Sender && p.From !== Sender,
|
||||
);
|
||||
// add new value
|
||||
this.presences.set(Chat, [...filtered, event]);
|
||||
});
|
||||
events.on(WhatsMeowEvent.CHAT_PRESENCE, (event: gows.ChatPresence) => {
|
||||
const Chat = event.Chat;
|
||||
const Sender = event.Sender;
|
||||
const stored = this.presences.get(Chat) || [];
|
||||
// remove values by event.Sender
|
||||
const filtered = stored.filter(
|
||||
(p) => p.Sender !== Sender && p.From !== Sender,
|
||||
);
|
||||
// add new value
|
||||
this.presences.set(Chat, [...filtered, event]);
|
||||
});
|
||||
events.start();
|
||||
}
|
||||
|
||||
subscribeEngineEvents2() {
|
||||
const all$ = this.all$;
|
||||
this.events2.get(WAHAEvents.ENGINE_EVENT).switch(all$);
|
||||
|
||||
const messages$ = all$.pipe(onlyEvent(WhatsMeowEvent.MESSAGE));
|
||||
|
||||
let [messagesFromMe$, messagesFromOthers$] = partition(messages$, isMine);
|
||||
messagesFromMe$ = messagesFromMe$.pipe(
|
||||
mergeMap((msg) => this.processIncomingMessage(msg, true)),
|
||||
share(), // share it so we don't process twice in message.any
|
||||
);
|
||||
messagesFromOthers$ = messagesFromOthers$.pipe(
|
||||
mergeMap((msg) => this.processIncomingMessage(msg, true)),
|
||||
share(), // share it so we don't process twice in message.any
|
||||
);
|
||||
const messagesFromAll$ = merge(messagesFromMe$, messagesFromOthers$);
|
||||
this.events2.get(WAHAEvents.MESSAGE).switch(messagesFromOthers$);
|
||||
this.events2.get(WAHAEvents.MESSAGE_ANY).switch(messagesFromAll$);
|
||||
|
||||
const receipt$ = all$.pipe(onlyEvent(WhatsMeowEvent.RECEIPT));
|
||||
const messageAck$ = receipt$.pipe(
|
||||
mergeMap(this.receiptToMessageAck.bind(this)),
|
||||
// Create a composite key for deduplication
|
||||
groupBy((message) => `${message.id}-${message.ack}`),
|
||||
mergeMap((group$) =>
|
||||
group$.pipe(
|
||||
debounceTime(1000), // Wait 1 second for deduplication
|
||||
map((message) => message), // Pass the latest message after debounce
|
||||
),
|
||||
),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_ACK).switch(messageAck$);
|
||||
|
||||
const messageReactions$ = messages$.pipe(
|
||||
filter((msg) => !!msg?.Message?.reactionMessage),
|
||||
map(this.processMessageReaction.bind(this)),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_REACTION).switch(messageReactions$);
|
||||
|
||||
const presence$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.PRESENCE),
|
||||
filter((event) => !isJidGroup(event.From)),
|
||||
);
|
||||
const chatPresence$ = all$.pipe(onlyEvent(WhatsMeowEvent.CHAT_PRESENCE));
|
||||
const presenceUpdates$ = merge(presence$, chatPresence$).pipe(
|
||||
map((event) => this.toWahaPresences(event.From || event.Chat, [event])),
|
||||
);
|
||||
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(presenceUpdates$);
|
||||
}
|
||||
|
||||
async fetchContactProfilePicture(id: string): Promise<string> {
|
||||
const jid = toJID(this.ensureSuffix(id));
|
||||
const request = new messages.ProfilePictureRequest({
|
||||
jid: jid,
|
||||
session: this.session,
|
||||
});
|
||||
const response = await promisify(this.client.GetProfilePicture)(request);
|
||||
const url = response.toObject().url;
|
||||
return url;
|
||||
}
|
||||
|
||||
protected listenEngineEventsInDebugMode() {
|
||||
this.events2.get(WAHAEvents.ENGINE_EVENT).subscribe((data) => {
|
||||
this.logger.debug({ events: data }, `GOWS event`);
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.client) {
|
||||
const response = await promisify(this.client.StopSession)(this.session);
|
||||
response.toObject();
|
||||
}
|
||||
this.status = WAHASessionStatus.STOPPED;
|
||||
this.events?.stop();
|
||||
this.stopEvents();
|
||||
this.client?.close();
|
||||
}
|
||||
|
||||
public async requestCode(phoneNumber: string, method: string, params?: any) {
|
||||
if (this.status == WAHASessionStatus.STARTING) {
|
||||
this.logger.debug('Waiting for connection update...');
|
||||
await waitUntil(
|
||||
async () => this.status === WAHASessionStatus.SCAN_QR_CODE,
|
||||
100,
|
||||
2000,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.status != WAHASessionStatus.SCAN_QR_CODE) {
|
||||
const err = `Can request code only in SCAN_QR_CODE status. The current status is ${this.status}`;
|
||||
throw new UnprocessableEntityException(err);
|
||||
}
|
||||
|
||||
const request = new messages.PairCodeRequest({
|
||||
session: this.session,
|
||||
phone: phoneNumber,
|
||||
});
|
||||
const response = await promisify(this.client.RequestCode)(request);
|
||||
const code: string = response.toObject().code;
|
||||
this.logger.info(`Your code: ${code}`);
|
||||
return { code: code };
|
||||
}
|
||||
|
||||
async unpair() {
|
||||
await promisify(this.client.Logout)(this.session);
|
||||
}
|
||||
|
||||
public getSessionMeInfo(): MeInfo | null {
|
||||
return this.me;
|
||||
}
|
||||
|
||||
/**
|
||||
* START - Methods for API
|
||||
*/
|
||||
|
||||
/**
|
||||
* Auth methods
|
||||
*/
|
||||
public getQR(): QR {
|
||||
return this.qr;
|
||||
}
|
||||
|
||||
async getScreenshot(): Promise<Buffer> {
|
||||
if (this.status === WAHASessionStatus.STARTING) {
|
||||
throw new UnprocessableEntityException(
|
||||
`The session is starting, please try again after few seconds`,
|
||||
);
|
||||
} else if (this.status === WAHASessionStatus.SCAN_QR_CODE) {
|
||||
return this.qr.get();
|
||||
} else if (this.status === WAHASessionStatus.WORKING) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Can not get screenshot for non chrome based engine.`,
|
||||
);
|
||||
} else {
|
||||
throw new UnprocessableEntityException(`Unknown status - ${this.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
async sendText(request: MessageTextRequest) {
|
||||
const jid = toJID(this.ensureSuffix(request.chatId));
|
||||
const message = new messages.MessageRequest({
|
||||
jid: jid,
|
||||
text: request.text,
|
||||
session: this.session,
|
||||
});
|
||||
const response = await promisify(this.client.SendMessage)(message);
|
||||
const data = response.toObject();
|
||||
return this.messageResponse(jid, data);
|
||||
}
|
||||
|
||||
protected messageResponse(jid, data) {
|
||||
const id = buildMessageId({
|
||||
ID: data.id,
|
||||
IsFromMe: true,
|
||||
IsGroup: isJidGroup(jid),
|
||||
Chat: jid,
|
||||
Sender: this.me.id,
|
||||
});
|
||||
return {
|
||||
id: id,
|
||||
_data: data,
|
||||
};
|
||||
}
|
||||
|
||||
checkNumberStatus(request: CheckNumberStatusQuery) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
sendLocation(request: MessageLocationRequest) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
forwardMessage(request: MessageForwardRequest): Promise<WAMessage> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
sendImage(request: MessageImageRequest) {
|
||||
throw new AvailableInPlusVersion();
|
||||
}
|
||||
|
||||
sendFile(request: MessageFileRequest) {
|
||||
throw new AvailableInPlusVersion();
|
||||
}
|
||||
|
||||
sendVoice(request: MessageVoiceRequest) {
|
||||
throw new AvailableInPlusVersion();
|
||||
}
|
||||
|
||||
reply(request: MessageReplyRequest) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async sendSeen(request: SendSeenRequest) {
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
const req = new messages.MarkReadRequest({
|
||||
session: this.session,
|
||||
jid: key.remoteJid,
|
||||
messageId: key.id,
|
||||
sender: key.fromMe ? this.me.id : key.participant,
|
||||
});
|
||||
const response = await promisify(this.client.MarkRead)(req);
|
||||
response.toObject();
|
||||
return;
|
||||
}
|
||||
|
||||
startTyping(chat: ChatRequest) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
stopTyping(chat: ChatRequest) {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async setReaction(request: MessageReactionRequest) {
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
const message = new messages.MessageReaction({
|
||||
session: this.session,
|
||||
jid: key.remoteJid,
|
||||
messageId: key.id,
|
||||
reaction: request.reaction,
|
||||
sender: key.fromMe ? this.me.id : key.participant,
|
||||
});
|
||||
const response = await promisify(this.client.SendReaction)(message);
|
||||
const data = response.toObject();
|
||||
return this.messageResponse(key.remoteJid, data);
|
||||
}
|
||||
|
||||
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) {
|
||||
let request: any;
|
||||
let method: any;
|
||||
const jid = chatId ? toJID(this.ensureSuffix(chatId)) : null;
|
||||
switch (presence) {
|
||||
case WAHAPresenceStatus.ONLINE:
|
||||
request = new messages.PresenceRequest({
|
||||
session: this.session,
|
||||
status: messages.Presence.AVAILABLE,
|
||||
});
|
||||
method = this.client.SendPresence;
|
||||
break;
|
||||
case WAHAPresenceStatus.OFFLINE:
|
||||
request = new messages.PresenceRequest({
|
||||
session: this.session,
|
||||
status: messages.Presence.UNAVAILABLE,
|
||||
});
|
||||
method = this.client.SendPresence;
|
||||
break;
|
||||
case WAHAPresenceStatus.TYPING:
|
||||
request = new messages.ChatPresenceRequest({
|
||||
session: this.session,
|
||||
jid: jid,
|
||||
status: messages.ChatPresence.TYPING,
|
||||
});
|
||||
method = this.client.SendChatPresence;
|
||||
break;
|
||||
case WAHAPresenceStatus.RECORDING:
|
||||
request = new messages.ChatPresenceRequest({
|
||||
session: this.session,
|
||||
jid: jid,
|
||||
status: messages.ChatPresence.RECORDING,
|
||||
});
|
||||
method = this.client.SendChatPresence;
|
||||
break;
|
||||
case WAHAPresenceStatus.PAUSED:
|
||||
request = new messages.ChatPresenceRequest({
|
||||
session: this.session,
|
||||
jid: jid,
|
||||
status: messages.ChatPresence.PAUSED,
|
||||
});
|
||||
method = this.client.SendChatPresence;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Invalid presence status');
|
||||
}
|
||||
await promisify(method)(request);
|
||||
}
|
||||
|
||||
public async getPresences(): Promise<WAHAChatPresences[]> {
|
||||
const result: WAHAChatPresences[] = [];
|
||||
for (const remoteJid in this.presences.keys()) {
|
||||
const storedPresences = this.presences.get(remoteJid);
|
||||
result.push(this.toWahaPresences(remoteJid, storedPresences));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
|
||||
const remoteJid = toJID(chatId);
|
||||
if (!(remoteJid in this.presences.keys())) {
|
||||
await this.subscribePresence(remoteJid);
|
||||
await sleep(1000);
|
||||
}
|
||||
const result = this.presences.get(remoteJid) || [];
|
||||
return this.toWahaPresences(remoteJid, result);
|
||||
}
|
||||
|
||||
async subscribePresence(chatId: string) {
|
||||
const jid = toJID(chatId);
|
||||
const req = new messages.SubscribePresenceRequest({
|
||||
session: this.session,
|
||||
jid: jid,
|
||||
});
|
||||
const response = await promisify(this.client.SubscribePresence)(req);
|
||||
return response.toObject();
|
||||
}
|
||||
|
||||
protected toWahaPresenceData(
|
||||
data: gows.Presence | gows.ChatPresence,
|
||||
): WAHAPresenceData {
|
||||
if ('From' in data) {
|
||||
data = data as gows.Presence;
|
||||
const lastKnownPresence = data.Unavailable
|
||||
? WAHAPresenceStatus.OFFLINE
|
||||
: WAHAPresenceStatus.ONLINE;
|
||||
return {
|
||||
participant: toCusFormat(data.From),
|
||||
lastKnownPresence: lastKnownPresence,
|
||||
lastSeen: parseTimestamp(data.LastSeen),
|
||||
};
|
||||
}
|
||||
|
||||
data = data as gows.ChatPresence;
|
||||
let lastKnownPresence: WAHAPresenceStatus;
|
||||
if (data.State === gows.ChatPresenceState.PAUSED) {
|
||||
lastKnownPresence = WAHAPresenceStatus.PAUSED;
|
||||
} else if (
|
||||
data.State === gows.ChatPresenceState.COMPOSING &&
|
||||
data.Media === gows.ChatPresenceMedia.TEXT
|
||||
) {
|
||||
lastKnownPresence = WAHAPresenceStatus.TYPING;
|
||||
} else if (
|
||||
data.State === gows.ChatPresenceState.COMPOSING &&
|
||||
data.Media === gows.ChatPresenceMedia.AUDIO
|
||||
) {
|
||||
lastKnownPresence = WAHAPresenceStatus.RECORDING;
|
||||
}
|
||||
return {
|
||||
participant: toCusFormat(data.Sender),
|
||||
lastKnownPresence: lastKnownPresence,
|
||||
lastSeen: null,
|
||||
};
|
||||
}
|
||||
|
||||
protected toWahaPresences(
|
||||
jid,
|
||||
result: null | gows.Presence[] | gows.ChatPresence[],
|
||||
): WAHAChatPresences {
|
||||
const chatId = toCusFormat(jid);
|
||||
return {
|
||||
id: chatId,
|
||||
presences: result?.map(this.toWahaPresenceData.bind(this)),
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// END - Methods for API
|
||||
//
|
||||
|
||||
private async processIncomingMessage(message, downloadMedia = true) {
|
||||
// if there is no text or media message
|
||||
if (!message) return;
|
||||
if (!message.Message) return;
|
||||
// Ignore reactions, we have dedicated handler for that
|
||||
if (message.Message.reactionMessage) return;
|
||||
// Ignore poll votes, we have dedicated handler for that
|
||||
if (message.Message.pollUpdateMessage) return;
|
||||
// Ignore protocol messages
|
||||
if (message.Message.protocolMessage) return;
|
||||
|
||||
if (downloadMedia) {
|
||||
try {
|
||||
message = await this.downloadMedia(message);
|
||||
} catch (e) {
|
||||
this.logger.error('Failed when tried to download media for a message');
|
||||
this.logger.error(e, e.stack);
|
||||
}
|
||||
}
|
||||
return this.toWAMessage(message);
|
||||
}
|
||||
|
||||
protected downloadMedia(message) {
|
||||
const processor = new GOWSEngineMediaProcessor(this);
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
protected toWAMessage(message): WAMessage {
|
||||
const fromToParticipant = getFromToParticipant(message);
|
||||
const id = buildMessageId(message);
|
||||
const body = this.extractBody(message.Message);
|
||||
const replyTo = null; // TODO: this.extractReplyTo(message.message);
|
||||
const ack = null; // TODO: Extract
|
||||
|
||||
return {
|
||||
id: id,
|
||||
timestamp: parseTimestamp(message.Info.Timestamp),
|
||||
from: toCusFormat(fromToParticipant.from),
|
||||
fromMe: message.Info.IsFromMe,
|
||||
body: body,
|
||||
to: toCusFormat(fromToParticipant.to),
|
||||
participant: toCusFormat(fromToParticipant.participant),
|
||||
// Media
|
||||
hasMedia: Boolean(message.media),
|
||||
media: message.media,
|
||||
mediaUrl: message.media?.url,
|
||||
// @ts-ignore
|
||||
ack: ack,
|
||||
// @ts-ignore
|
||||
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
|
||||
replyTo: replyTo,
|
||||
_data: message,
|
||||
};
|
||||
}
|
||||
|
||||
private extractBody(message) {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
let body = message.Conversation || message.conversation;
|
||||
if (!body) {
|
||||
body = message.extendedTextMessage?.text;
|
||||
}
|
||||
if (!body) {
|
||||
const media = extractMediaContent(message);
|
||||
body = media?.caption;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
public async getEngineInfo() {
|
||||
const clientState = this.client?.getChannel().getConnectivityState(false);
|
||||
const streamState = this.stream$?.client
|
||||
?.getChannel()
|
||||
.getConnectivityState(false);
|
||||
const grpc = {
|
||||
client: connectivityState[clientState] || 'NO_CLIENT',
|
||||
stream: connectivityState[streamState] || 'NO_STREAM',
|
||||
};
|
||||
if (!this.client) {
|
||||
return {
|
||||
grpc: grpc,
|
||||
};
|
||||
}
|
||||
|
||||
let gows;
|
||||
try {
|
||||
const response = await promisify(this.client.GetSessionState)(
|
||||
this.session,
|
||||
);
|
||||
const info = response.toObject();
|
||||
gows = { ...info };
|
||||
} catch (err) {
|
||||
gows = { error: err };
|
||||
}
|
||||
return {
|
||||
grpc: grpc,
|
||||
gows: gows,
|
||||
};
|
||||
}
|
||||
|
||||
receiptToMessageAck(receipt: any): WAMessageAckBody[] {
|
||||
const fromToParticipant = getFromToParticipant(receipt);
|
||||
|
||||
let ack;
|
||||
switch (receipt.Type) {
|
||||
case '':
|
||||
ack = WAMessageAck.DEVICE;
|
||||
break;
|
||||
case 'server-error':
|
||||
ack = WAMessageAck.ERROR;
|
||||
break;
|
||||
case 'inactive':
|
||||
ack = WAMessageAck.DEVICE;
|
||||
break;
|
||||
case 'active':
|
||||
ack = WAMessageAck.DEVICE;
|
||||
break;
|
||||
case 'read':
|
||||
ack = WAMessageAck.READ;
|
||||
break;
|
||||
case 'played':
|
||||
ack = WAMessageAck.PLAYED;
|
||||
break;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
const acks = [];
|
||||
for (const messageId of receipt.MessageIDs) {
|
||||
const msg = {
|
||||
...receipt,
|
||||
ID: messageId,
|
||||
// Reverse the IsFromMe flag
|
||||
IsFromMe: !receipt.IsFromMe,
|
||||
Sender: receipt.MessageSender || this.me?.id,
|
||||
};
|
||||
const id = buildMessageId(msg);
|
||||
const body: WAMessageAckBody = {
|
||||
id: id,
|
||||
from: toCusFormat(fromToParticipant.from),
|
||||
to: toCusFormat(fromToParticipant.to),
|
||||
participant: toCusFormat(fromToParticipant.participant),
|
||||
fromMe: msg.IsFromMe,
|
||||
ack: ack,
|
||||
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
|
||||
_data: receipt,
|
||||
};
|
||||
acks.push(body);
|
||||
}
|
||||
return acks;
|
||||
}
|
||||
|
||||
private processMessageReaction(message): WAMessageReaction | null {
|
||||
if (!message) return null;
|
||||
if (!message.Message) return null;
|
||||
if (!message.Message.reactionMessage) return null;
|
||||
|
||||
const id = buildMessageId(message);
|
||||
const fromToParticipant = getFromToParticipant(message);
|
||||
const reactionMessage = message.Message.reactionMessage;
|
||||
const messageId = this.buildMessageIdFromKey(reactionMessage.key);
|
||||
const reaction: WAMessageReaction = {
|
||||
id: id,
|
||||
timestamp: parseTimestamp(message.Info.Timestamp),
|
||||
from: toCusFormat(fromToParticipant.from),
|
||||
fromMe: message.Info.IsFromMe,
|
||||
to: toCusFormat(fromToParticipant.to),
|
||||
participant: toCusFormat(fromToParticipant.participant),
|
||||
reaction: {
|
||||
text: reactionMessage.text,
|
||||
messageId: messageId,
|
||||
},
|
||||
};
|
||||
return reaction;
|
||||
}
|
||||
|
||||
buildMessageIdFromKey(key: WARawKey) {
|
||||
const sender = key.fromMe ? this.me.id : key.participant || key.remoteJID;
|
||||
const info: MessageIdData = {
|
||||
Chat: key.remoteJID,
|
||||
Sender: sender,
|
||||
ID: key.ID,
|
||||
IsFromMe: key.fromMe,
|
||||
IsGroup: isJidGroup(key.remoteJID),
|
||||
};
|
||||
return buildMessageId(info);
|
||||
}
|
||||
}
|
||||
|
||||
export class GOWSEngineMediaProcessor implements IMediaEngineProcessor<any> {
|
||||
constructor(public session: WhatsappSessionGoWSCore) {}
|
||||
|
||||
hasMedia(message: any): boolean {
|
||||
return Boolean(extractMediaContent(message.Message));
|
||||
}
|
||||
|
||||
getMessageId(message: any): string {
|
||||
return message.Info.ID;
|
||||
}
|
||||
|
||||
getMimetype(message: any): string {
|
||||
const content = extractMediaContent(message.Message);
|
||||
return content.mimetype;
|
||||
}
|
||||
|
||||
async getMediaBuffer(message: any): Promise<Buffer | null> {
|
||||
const data = JSON.stringify(message.Message);
|
||||
const request = new messages.DownloadMediaRequest({
|
||||
session: this.session.session,
|
||||
message: data,
|
||||
});
|
||||
const response = await promisify(this.session.client.DownloadMedia)(
|
||||
request,
|
||||
);
|
||||
const obj = response.toObject();
|
||||
return Buffer.from(obj.content);
|
||||
}
|
||||
|
||||
getFilename(message: any): string | null {
|
||||
const content = extractMediaContent(message.Message);
|
||||
return content?.fileName;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractMediaContent(message: any) {
|
||||
const mediaContent =
|
||||
message?.documentMessage ||
|
||||
message?.imageMessage ||
|
||||
message?.videoMessage ||
|
||||
message?.audioMessage ||
|
||||
message?.stickerMessage;
|
||||
return mediaContent;
|
||||
}
|
||||
|
||||
function isMine(message) {
|
||||
return !!message.Info.IsFromMe;
|
||||
}
|
||||
|
||||
function getFromToParticipant(message) {
|
||||
const info = message.Info || message;
|
||||
return {
|
||||
from: info.Chat,
|
||||
to: info.IsGroup ? info.Sender : null,
|
||||
participant: info.IsGroup ? info.Sender : null,
|
||||
};
|
||||
}
|
||||
|
||||
interface MessageIdData {
|
||||
Info?: MessageIdData;
|
||||
Chat: string;
|
||||
Sender: string;
|
||||
ID: string;
|
||||
IsFromMe: boolean;
|
||||
IsGroup: boolean;
|
||||
}
|
||||
|
||||
function buildMessageId(message: MessageIdData) {
|
||||
const info = message.Info || message;
|
||||
const chatId = toCusFormat(info.Chat);
|
||||
const participant = toCusFormat(info.Sender);
|
||||
if (info.IsGroup) {
|
||||
return `${info.IsFromMe}_${chatId}_${info.ID}_${participant}`;
|
||||
}
|
||||
return `${info.IsFromMe}_${chatId}_${info.ID}`;
|
||||
}
|
||||
|
||||
interface WARawKey {
|
||||
remoteJID: string;
|
||||
fromMe: boolean;
|
||||
ID: string;
|
||||
participant?: string;
|
||||
}
|
||||
|
||||
function parseTimestamp(timestamp: string): number {
|
||||
if (timestamp.startsWith('0001')) {
|
||||
return null;
|
||||
}
|
||||
// "2024-12-25T14:28:42+03:00" => 1234567890
|
||||
return new Date(timestamp).getTime();
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface GowsAuth {
|
||||
address(): string;
|
||||
dialect(): string;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { DataStore } from '@waha/core/abc/DataStore';
|
||||
import { GowsAuthSimple } from '@waha/core/engines/gows/store/GowsAuthSimple';
|
||||
import { LocalStore } from '@waha/core/storage/LocalStore';
|
||||
|
||||
import { GowsAuth } from './GowsAuth';
|
||||
|
||||
export class GowsAuthFactoryCore {
|
||||
buildAuth(store: DataStore, name: string): Promise<GowsAuth> {
|
||||
if (store instanceof LocalStore) return this.buildSqlite3(store, name);
|
||||
throw new Error(`Unsupported store type '${store.constructor.name}'`);
|
||||
}
|
||||
|
||||
protected async buildSqlite3(
|
||||
store: LocalStore,
|
||||
name: string,
|
||||
): Promise<GowsAuth> {
|
||||
await store.init(name);
|
||||
const authFolder = store.getSessionDirectory(name);
|
||||
// resolve path
|
||||
const authFolderFullPath = path.resolve(authFolder);
|
||||
const connection = `file:${authFolderFullPath}/gows.db`;
|
||||
return new GowsAuthSimple(connection, 'sqlite3');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { GowsAuth } from '@waha/core/engines/gows/store/GowsAuth';
|
||||
|
||||
export class GowsAuthSimple implements GowsAuth {
|
||||
private _address: string;
|
||||
private _dialect: string;
|
||||
|
||||
constructor(address: string, dialect: string) {
|
||||
this._address = address;
|
||||
this._dialect = dialect;
|
||||
}
|
||||
|
||||
address() {
|
||||
return this._address;
|
||||
}
|
||||
|
||||
dialect(): string {
|
||||
return this._dialect;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface Presence {
|
||||
From: string;
|
||||
Unavailable: boolean;
|
||||
LastSeen: string;
|
||||
}
|
||||
|
||||
export enum ChatPresenceState {
|
||||
COMPOSING = 'composing',
|
||||
PAUSED = 'paused',
|
||||
}
|
||||
|
||||
export enum ChatPresenceMedia {
|
||||
TEXT = '',
|
||||
AUDIO = 'audio',
|
||||
}
|
||||
|
||||
export interface ChatPresence {
|
||||
Chat: string;
|
||||
Sender: string;
|
||||
IsFromMe: boolean;
|
||||
IsGroup: boolean;
|
||||
State: ChatPresenceState;
|
||||
Media: ChatPresenceMedia;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,10 @@ export interface IChatRepository {
|
||||
|
||||
getAllByIds(ids: string[]): Promise<Chat[]>;
|
||||
|
||||
getAllWithMessages(pagination: PaginationParams): Promise<Chat[]>;
|
||||
getAllWithMessages(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
): Promise<Chat[]>;
|
||||
|
||||
getById(id: string): Promise<Chat | null>;
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { GroupMetadata } from '@adiwajshing/baileys';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface IGroupRepository {
|
||||
getAll(pagination?: PaginationParams): Promise<GroupMetadata[]>;
|
||||
|
||||
getById(id: string): Promise<GroupMetadata | null>;
|
||||
|
||||
deleteAll(): Promise<void>;
|
||||
|
||||
deleteById(id: string): Promise<void>;
|
||||
|
||||
save(group: GroupMetadata): Promise<void>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { WAMessage } from '@adiwajshing/baileys';
|
||||
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
|
||||
@@ -16,6 +17,8 @@ export abstract class INowebStorage {
|
||||
|
||||
abstract getChatRepository(): IChatRepository;
|
||||
|
||||
abstract getGroupRepository(): IGroupRepository;
|
||||
|
||||
abstract getMessagesRepository(): IMessagesRepository;
|
||||
|
||||
abstract getLabelsRepository(): ILabelsRepository;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Contact,
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
@@ -27,7 +28,7 @@ export interface INowebStore {
|
||||
|
||||
getMessageById(chatId: string, messageId: string): Promise<any>;
|
||||
|
||||
getChats(pagination: PaginationParams): Promise<Chat[]>;
|
||||
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]>;
|
||||
|
||||
getContacts(pagination: PaginationParams): Promise<Contact[]>;
|
||||
|
||||
@@ -40,4 +41,8 @@ export interface INowebStore {
|
||||
getChatsByLabelId(labelId: string): Promise<Chat[]>;
|
||||
|
||||
getChatLabels(chatId: string): Promise<Label[]>;
|
||||
|
||||
getGroups(pagination: PaginationParams): Promise<GroupMetadata[]>;
|
||||
|
||||
resetGroupsCache(): void;
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
|
||||
import makeWASocket, {
|
||||
Chat,
|
||||
Contact,
|
||||
GroupMetadata,
|
||||
makeInMemoryStore,
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
|
||||
import { INowebStore } from './INowebStore';
|
||||
|
||||
@@ -10,6 +17,8 @@ import { INowebStore } from './INowebStore';
|
||||
const logger = require('pino')();
|
||||
|
||||
export class NowebInMemoryStore implements INowebStore {
|
||||
private socket: ReturnType<typeof makeWASocket>;
|
||||
|
||||
private store: ReturnType<typeof makeInMemoryStore>;
|
||||
errorMessage =
|
||||
'Enable NOWEB store "config.noweb.store.enabled=True" and "config.noweb.store.full_sync=True" when starting a new session. ' +
|
||||
@@ -33,6 +42,7 @@ export class NowebInMemoryStore implements INowebStore {
|
||||
|
||||
bind(ev: any, socket: any) {
|
||||
this.store.bind(ev);
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
loadMessage(jid: string, id: string): Promise<proto.IWebMessageInfo> {
|
||||
@@ -51,7 +61,7 @@ export class NowebInMemoryStore implements INowebStore {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getChats(pagination: PaginationParams): Promise<Chat[]> {
|
||||
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
@@ -78,4 +88,15 @@ export class NowebInMemoryStore implements INowebStore {
|
||||
getChatLabels(chatId: string): Promise<Label[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
async getGroups(pagination: PaginationParams): Promise<GroupMetadata[]> {
|
||||
const response = await this.socket?.groupFetchAllParticipating();
|
||||
const groups = Object.values(response);
|
||||
const paginator = new PaginatorInMemory(pagination);
|
||||
return paginator.apply(groups);
|
||||
}
|
||||
|
||||
resetGroupsCache() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,34 @@
|
||||
import {
|
||||
import makeWASocket, {
|
||||
areJidsSameUser,
|
||||
BaileysEventEmitter,
|
||||
Chat,
|
||||
ChatUpdate,
|
||||
Contact,
|
||||
GroupParticipant,
|
||||
isRealMessage,
|
||||
jidNormalizedUser,
|
||||
ParticipantAction,
|
||||
proto,
|
||||
updateMessageWithReaction,
|
||||
updateMessageWithReceipt,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import {
|
||||
LabelAssociation,
|
||||
LabelAssociationType,
|
||||
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
import { DefaultMap } from '@waha/utils/DefaultMap';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import * as lodash from 'lodash';
|
||||
import { toNumber } from 'lodash';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
import { toJID } from '../session.noweb.core';
|
||||
import { IChatRepository } from './IChatRepository';
|
||||
import { IContactRepository } from './IContactRepository';
|
||||
import { IMessagesRepository } from './IMessagesRepository';
|
||||
@@ -31,15 +38,27 @@ import { INowebStore } from './INowebStore';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
|
||||
type ms = number;
|
||||
const HOUR: ms = 60 * 60 * 1000;
|
||||
|
||||
export class NowebPersistentStore implements INowebStore {
|
||||
private socket: any;
|
||||
private socket: ReturnType<typeof makeWASocket>;
|
||||
private chatRepo: IChatRepository;
|
||||
private groupRepo: IGroupRepository;
|
||||
private contactRepo: IContactRepository;
|
||||
private messagesRepo: IMessagesRepository;
|
||||
private labelsRepo: ILabelsRepository;
|
||||
private labelAssociationsRepo: ILabelAssociationRepository;
|
||||
public presences: any;
|
||||
private lock: any;
|
||||
private groupsFetchLock: any = new AsyncLock({
|
||||
maxPending: Infinity,
|
||||
maxExecutionTime: 60_000,
|
||||
});
|
||||
|
||||
private lastTimeGroupUpdate: Date = new Date(0);
|
||||
private lastTimeGroupFetch: Date = new Date(0);
|
||||
private GROUP_METADATA_CACHE_TIME = 24 * HOUR;
|
||||
|
||||
constructor(
|
||||
private logger: Logger,
|
||||
@@ -47,6 +66,7 @@ export class NowebPersistentStore implements INowebStore {
|
||||
) {
|
||||
this.socket = null;
|
||||
this.chatRepo = storage.getChatRepository();
|
||||
this.groupRepo = storage.getGroupRepository();
|
||||
this.contactRepo = storage.getContactsRepository();
|
||||
this.messagesRepo = storage.getMessagesRepository();
|
||||
this.labelsRepo = storage.getLabelsRepository();
|
||||
@@ -88,6 +108,19 @@ export class NowebPersistentStore implements INowebStore {
|
||||
ev.on('chats.delete', (data) =>
|
||||
this.withLock('chats', () => this.onChatDelete(data)),
|
||||
);
|
||||
// Groups
|
||||
ev.on('groups.upsert', (data) =>
|
||||
this.withLock('groups', () => this.onGroupUpsert(data)),
|
||||
);
|
||||
ev.on('groups.update', (data) =>
|
||||
this.withLock('groups', () => this.onGroupUpdate(data)),
|
||||
);
|
||||
ev.on('group-participants.update', (data) =>
|
||||
this.withLock(`group-${data.id}`, () =>
|
||||
this.onGroupParticipantsUpdate(data),
|
||||
),
|
||||
);
|
||||
|
||||
// Contacts
|
||||
ev.on('contacts.upsert', (data) =>
|
||||
this.withLock('contacts', () => this.onContactsUpsert(data)),
|
||||
@@ -195,7 +228,84 @@ export class NowebPersistentStore implements INowebStore {
|
||||
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
|
||||
await this.chatRepo.save(chat);
|
||||
}
|
||||
this.logger.info(`history sync - '${chats.length}' synced chats`);
|
||||
this.logger.info(`store sync - '${chats.length}' synced chats`);
|
||||
}
|
||||
|
||||
private async onGroupUpsert(groups: GroupMetadata[]) {
|
||||
for (const group of groups) {
|
||||
await this.groupRepo.save(group);
|
||||
}
|
||||
this.logger.info(`store sync - '${groups.length}' synced groups`);
|
||||
}
|
||||
|
||||
private async onGroupUpdate(groups: Partial<GroupMetadata>[]) {
|
||||
for (const update of groups) {
|
||||
let group = await this.groupRepo.getById(update.id);
|
||||
group = Object.assign(group || {}, update) as GroupMetadata;
|
||||
await this.groupRepo.save(group);
|
||||
}
|
||||
this.logger.info(`store sync - '${groups.length}' updated groups`);
|
||||
this.lastTimeGroupUpdate = new Date();
|
||||
}
|
||||
|
||||
private async onGroupParticipantsUpdate(data) {
|
||||
const id: string = data.id;
|
||||
const participants: string[] = data.participants;
|
||||
const action: ParticipantAction = data.action;
|
||||
|
||||
if (action == 'remove') {
|
||||
// Remove the group if the current user is removed
|
||||
const myJid = this.socket?.authState?.creds?.me?.id;
|
||||
const participantsIncludesMe = lodash.find(participants, (p) =>
|
||||
areJidsSameUser(p, myJid),
|
||||
);
|
||||
if (participantsIncludesMe) {
|
||||
await this.groupRepo.deleteById(id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let group = await this.groupRepo.getById(id);
|
||||
if (!group) {
|
||||
group = { id: id, participants: [] } as GroupMetadata;
|
||||
}
|
||||
|
||||
const participantsById = new DefaultMap<string, GroupParticipant>((key) => {
|
||||
return { id: key, admin: null } as GroupParticipant;
|
||||
});
|
||||
for (const participant of group.participants) {
|
||||
participantsById.set(participant.id, participant);
|
||||
}
|
||||
|
||||
for (const participant of participants) {
|
||||
this.participantUpdate(participantsById, participant, action);
|
||||
}
|
||||
group.participants = Array.from(participantsById.values());
|
||||
await this.groupRepo.save(group);
|
||||
}
|
||||
|
||||
private participantUpdate(
|
||||
participantsById: DefaultMap<string, GroupParticipant>,
|
||||
participant: string,
|
||||
action: ParticipantAction,
|
||||
) {
|
||||
switch (action) {
|
||||
case 'add':
|
||||
// if there's no participant - add it (by id)
|
||||
participantsById.get(participant);
|
||||
break;
|
||||
case 'remove':
|
||||
// remove the participant (by id)
|
||||
participantsById.delete(participant);
|
||||
break;
|
||||
case 'promote':
|
||||
// set admin: admin
|
||||
participantsById.get(participant).admin = 'admin';
|
||||
break;
|
||||
case 'demote':
|
||||
participantsById.get(participant).admin = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async onChatUpdate(updates: ChatUpdate[]) {
|
||||
@@ -338,10 +448,45 @@ export class NowebPersistentStore implements INowebStore {
|
||||
return this.messagesRepo.getByJidById(chatId, messageId);
|
||||
}
|
||||
|
||||
getChats(pagination: PaginationParams): Promise<Chat[]> {
|
||||
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
|
||||
pagination.sortBy ||= 'conversationTimestamp';
|
||||
pagination.sortOrder ||= SortOrder.DESC;
|
||||
return this.chatRepo.getAllWithMessages(pagination);
|
||||
return this.chatRepo.getAllWithMessages(pagination, broadcast);
|
||||
}
|
||||
|
||||
private shouldFetchGroup(): boolean {
|
||||
const timePassed = new Date().getTime() - this.lastTimeGroupFetch.getTime();
|
||||
return timePassed > this.GROUP_METADATA_CACHE_TIME;
|
||||
}
|
||||
|
||||
private async fetchGroups() {
|
||||
await this.groupsFetchLock.acquire('groups-fetch', async () => {
|
||||
if (!this.shouldFetchGroup()) {
|
||||
// Update has been done by another request
|
||||
return;
|
||||
}
|
||||
const lastTimeGroupUpdate = this.lastTimeGroupUpdate;
|
||||
await this.groupRepo.deleteAll();
|
||||
await this.socket?.groupFetchAllParticipating();
|
||||
// Wait until the groups update is done
|
||||
await waitUntil(
|
||||
async () => this.lastTimeGroupUpdate > lastTimeGroupUpdate,
|
||||
100,
|
||||
5_000,
|
||||
);
|
||||
this.lastTimeGroupFetch = new Date();
|
||||
});
|
||||
}
|
||||
|
||||
resetGroupsCache() {
|
||||
this.lastTimeGroupFetch = new Date(0);
|
||||
}
|
||||
|
||||
async getGroups(pagination: PaginationParams): Promise<GroupMetadata[]> {
|
||||
if (this.shouldFetchGroup()) {
|
||||
await this.fetchGroups();
|
||||
}
|
||||
return this.groupRepo.getAll(pagination);
|
||||
}
|
||||
|
||||
getContactById(jid) {
|
||||
|
||||
@@ -18,6 +18,11 @@ export const NOWEB_STORE_SCHEMA = [
|
||||
new Index('chats_conversationTimestamp_index', ['conversationTimestamp']),
|
||||
],
|
||||
),
|
||||
new Schema(
|
||||
'groups',
|
||||
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
|
||||
[new Index('groups_id_index', ['id'])],
|
||||
),
|
||||
new Schema(
|
||||
'messages',
|
||||
[
|
||||
|
||||
@@ -15,9 +15,18 @@ export class Sqlite3ChatRepository
|
||||
{
|
||||
protected Paginator = ChatPaginator;
|
||||
|
||||
async getAllWithMessages(pagination: PaginationParams): Promise<Chat[]> {
|
||||
async getAllWithMessages(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
): Promise<Chat[]> {
|
||||
// Get chats with conversationTimestamp is not Null
|
||||
let query = this.select().whereNotNull('conversationTimestamp');
|
||||
if (!broadcast) {
|
||||
// filter out chat by id if it ends at @newsletter or @broadcast
|
||||
query = query
|
||||
.andWhereNot('id', 'like', '%@broadcast')
|
||||
.andWhereNot('id', 'like', '%@newsletter');
|
||||
}
|
||||
query = this.pagination(query, pagination);
|
||||
return await this.all(query);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
|
||||
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
|
||||
import { KnexPaginator } from '@waha/utils/Paginator';
|
||||
|
||||
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
|
||||
|
||||
class Paginator extends KnexPaginator {
|
||||
indexes = ['id'];
|
||||
}
|
||||
|
||||
export class Sqlite3GroupRepository
|
||||
extends NOWEBSqlite3KVRepository<GroupMetadata>
|
||||
implements IGroupRepository
|
||||
{
|
||||
protected Paginator = Paginator;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { WAMessage } from '@adiwajshing/baileys';
|
||||
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
import { Sqlite3GroupRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3GroupRepository';
|
||||
import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelAssociationsRepository';
|
||||
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
|
||||
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
|
||||
@@ -62,6 +63,14 @@ export class Sqlite3Storage extends INowebStorage {
|
||||
'CREATE INDEX IF NOT EXISTS chats_conversationTimestamp_index ON chats (conversationTimestamp)',
|
||||
);
|
||||
|
||||
// Groups
|
||||
this.db.exec(
|
||||
'CREATE TABLE IF NOT EXISTS groups (id TEXT PRIMARY KEY, data TEXT)',
|
||||
);
|
||||
this.db.exec(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS groups_id_index ON groups (id)',
|
||||
);
|
||||
|
||||
// Messages
|
||||
this.db.exec(
|
||||
'CREATE TABLE IF NOT EXISTS messages (jid TEXT, id TEXT, messageTimestamp INTEGER, data TEXT)',
|
||||
@@ -119,6 +128,10 @@ export class Sqlite3Storage extends INowebStorage {
|
||||
return new Sqlite3ChatRepository(this.db, this.getSchema('chats'));
|
||||
}
|
||||
|
||||
getGroupRepository() {
|
||||
return new Sqlite3GroupRepository(this.db, this.getSchema('groups'));
|
||||
}
|
||||
|
||||
getLabelsRepository(): ILabelsRepository {
|
||||
return new Sqlite3LabelsRepository(this.db, this.getSchema('labels'));
|
||||
}
|
||||
|
||||
@@ -4,9 +4,12 @@ import {
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { BufferJSON, initAuthCreds } from '@adiwajshing/baileys/lib/Utils';
|
||||
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
|
||||
import { mkdir, readFile, stat, unlink } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const writeFileAtomic = require('write-file-atomic');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
|
||||
@@ -33,7 +36,10 @@ export const useMultiFileAuthState = async (
|
||||
const writeData = (data: any, file: string) => {
|
||||
const filePath = join(folder, fixFileName(file));
|
||||
return fileLock.acquire(filePath, () =>
|
||||
writeFile(join(filePath), JSON.stringify(data, BufferJSON.replacer)),
|
||||
writeFileAtomic(
|
||||
join(filePath),
|
||||
JSON.stringify(data, BufferJSON.replacer),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -78,6 +78,14 @@ const isObjectALong = (value: any): value is Long => {
|
||||
);
|
||||
};
|
||||
|
||||
export function ensureNumber(value: number | Long): number {
|
||||
if (!value) {
|
||||
// @ts-ignore
|
||||
return value;
|
||||
}
|
||||
return typeof value === 'number' ? value : toNumber(value);
|
||||
}
|
||||
|
||||
const toNumber = (longValue: Long): number => {
|
||||
const { low, high, unsigned } = longValue;
|
||||
const result = unsigned ? low >>> 0 : low + high * 0x100000000;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import {
|
||||
getChannelInviteLink,
|
||||
WAHAInternalEvent,
|
||||
WhatsappSession,
|
||||
} from '@waha/core/abc/session.abc';
|
||||
import { toJID } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { LocalAuth } from '@waha/core/engines/webjs/LocalAuth';
|
||||
import { WebjsClient } from '@waha/core/engines/webjs/WebjsClient';
|
||||
import {
|
||||
@@ -15,7 +13,6 @@ import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { splitAt } from '@waha/helpers';
|
||||
import { PairingCodeResponse } from '@waha/structures/auth.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import {
|
||||
Channel,
|
||||
ChannelRole,
|
||||
@@ -23,8 +20,8 @@ import {
|
||||
ListChannelsQuery,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import {
|
||||
ChatArchiveEvent,
|
||||
ChatSortField,
|
||||
ChatSummary,
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
@@ -57,23 +54,26 @@ import {
|
||||
} from '@waha/structures/enums.dto';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
GroupSortField,
|
||||
ParticipantsRequest,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import { StatusRequest, TextStatus } from '@waha/structures/status.dto';
|
||||
import {
|
||||
BROADCAST_ID,
|
||||
StatusRequest,
|
||||
TextStatus,
|
||||
} from '@waha/structures/status.dto';
|
||||
import { WAMessageRevokedBody } from '@waha/structures/webhooks.dto';
|
||||
EnginePayload,
|
||||
WAMessageRevokedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import * as lodash from 'lodash';
|
||||
import { fromEvent, merge, mergeMap, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import {
|
||||
Call,
|
||||
Channel as WEBJSChannel,
|
||||
@@ -91,11 +91,9 @@ import {
|
||||
} from 'whatsapp-web.js';
|
||||
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const QRCode = require('qrcode');
|
||||
|
||||
export interface WebJSConfig {
|
||||
webVersion?: string;
|
||||
cacheType: 'local' | 'none';
|
||||
}
|
||||
|
||||
export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
@@ -140,7 +138,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
const path = this.getClassDirName();
|
||||
const webVersion =
|
||||
this.engineConfig?.webVersion || '2.3000.1018072227-alpha';
|
||||
this.logger.info(`Using web version: '${webVersion}'`);
|
||||
const cacheType = this.engineConfig?.cacheType || 'none';
|
||||
this.logger.info(`Using cache type: '${cacheType}'`);
|
||||
if (cacheType === 'local') {
|
||||
this.logger.info(`Using web version: '${webVersion}'`);
|
||||
}
|
||||
return {
|
||||
puppeteer: {
|
||||
headless: true,
|
||||
@@ -150,8 +152,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
},
|
||||
webVersion: webVersion,
|
||||
webVersionCache: {
|
||||
// type: 'none',
|
||||
type: 'local',
|
||||
type: cacheType,
|
||||
path: path,
|
||||
strict: true,
|
||||
},
|
||||
@@ -244,7 +245,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
this.listenEngineEventsInDebugMode();
|
||||
}
|
||||
this.listenConnectionEvents();
|
||||
this.events.emit(WAHAInternalEvent.ENGINE_START);
|
||||
this.subscribeEngineEvents2();
|
||||
}
|
||||
|
||||
async start() {
|
||||
@@ -260,7 +261,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
async stop() {
|
||||
this.shouldRestart = false;
|
||||
this.status = WAHASessionStatus.STOPPED;
|
||||
this.events.removeAllListeners();
|
||||
this.stopEvents();
|
||||
this.startDelayedJob.cancel();
|
||||
await this.end();
|
||||
}
|
||||
@@ -331,8 +332,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
this.whatsapp.on(Events.QR_RECEIVED, async (qr) => {
|
||||
this.logger.debug('QR received');
|
||||
// Convert to image and save
|
||||
const url = await QRCode.toDataURL(qr);
|
||||
this.qr.save(url, qr);
|
||||
this.qr.save(qr);
|
||||
this.printQR(this.qr);
|
||||
this.status = WAHASessionStatus.SCAN_QR_CODE;
|
||||
});
|
||||
@@ -564,6 +564,41 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return this.whatsapp.getChats(pagination);
|
||||
}
|
||||
|
||||
public async getChatsOverview(
|
||||
pagination: PaginationParams,
|
||||
): Promise<ChatSummary[]> {
|
||||
pagination = {
|
||||
...pagination,
|
||||
sortBy: ChatSortField.CONVERSATION_TIMESTAMP,
|
||||
sortOrder: SortOrder.DESC,
|
||||
};
|
||||
const chats = await this.getChats(pagination);
|
||||
|
||||
const promises = [];
|
||||
for (const chat of chats) {
|
||||
promises.push(this.fetchChatSummary(chat));
|
||||
}
|
||||
const result = await Promise.all(promises);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async fetchChatSummary(chat: Chat): Promise<ChatSummary> {
|
||||
const picture = await this.getContactProfilePicture(
|
||||
chat.id._serialized,
|
||||
false,
|
||||
);
|
||||
const lastMessage = !!chat.lastMessage
|
||||
? this.toWAMessage(chat.lastMessage)
|
||||
: null;
|
||||
return {
|
||||
id: chat.id._serialized,
|
||||
name: chat.name || null,
|
||||
picture: picture,
|
||||
lastMessage: lastMessage,
|
||||
_chat: chat,
|
||||
};
|
||||
}
|
||||
|
||||
public async getChatMessages(
|
||||
chatId: string,
|
||||
query: GetChatMessagesQuery,
|
||||
@@ -575,7 +610,6 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
);
|
||||
}
|
||||
|
||||
const limit = query.limit;
|
||||
const downloadMedia = query.downloadMedia;
|
||||
// Test there's chat with id
|
||||
await this.whatsapp.getChatById(this.ensureSuffix(chatId));
|
||||
@@ -604,6 +638,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return await this.processIncomingMessage(message, query.downloadMedia);
|
||||
}
|
||||
|
||||
public async pinMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
duration: number,
|
||||
): Promise<boolean> {
|
||||
const message = await this.whatsapp.getMessageById(messageId);
|
||||
return message.pin(duration);
|
||||
}
|
||||
|
||||
public async unpinMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
): Promise<boolean> {
|
||||
const message = await this.whatsapp.getMessageById(messageId);
|
||||
return message.unpin();
|
||||
}
|
||||
|
||||
async deleteChat(chatId) {
|
||||
const chat = await this.whatsapp.getChatById(chatId);
|
||||
return chat.delete();
|
||||
@@ -706,11 +757,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return { about: await contact.getAbout() };
|
||||
}
|
||||
|
||||
public async getContactProfilePicture(query: ContactQuery) {
|
||||
const contact = await this.whatsapp.getContactById(
|
||||
this.ensureSuffix(query.contactId),
|
||||
);
|
||||
return { profilePictureURL: await contact.getProfilePicUrl() };
|
||||
public async fetchContactProfilePicture(id: string) {
|
||||
const contact = await this.whatsapp.getContactById(this.ensureSuffix(id));
|
||||
const url = await contact.getProfilePicUrl();
|
||||
return url;
|
||||
}
|
||||
|
||||
public async blockContact(request: ContactRequest) {
|
||||
@@ -737,6 +787,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return this.whatsapp.createGroup(request.name, participantIds);
|
||||
}
|
||||
|
||||
public joinGroup(code: string) {
|
||||
return this.whatsapp.acceptInvite(code);
|
||||
}
|
||||
|
||||
public joinInfoGroup(code: string) {
|
||||
return this.whatsapp.getInviteInfo(code);
|
||||
}
|
||||
|
||||
public async getInfoAdminsOnly(id): Promise<SettingsSecurityChangeInfo> {
|
||||
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
|
||||
return {
|
||||
@@ -766,10 +824,25 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return groupChat.setMessagesAdminsOnly(value);
|
||||
}
|
||||
|
||||
public getGroups() {
|
||||
return this.whatsapp
|
||||
.getChats()
|
||||
.then((chats) => chats.filter((chat) => chat.isGroup));
|
||||
public async getGroups(pagination: PaginationParams) {
|
||||
const chats = await this.whatsapp.getChats();
|
||||
const groups = lodash.filter(chats, (chat) => chat.isGroup);
|
||||
|
||||
switch (pagination.sortBy) {
|
||||
case GroupSortField.ID:
|
||||
pagination.sortBy = 'id._serialized';
|
||||
break;
|
||||
case GroupSortField.SUBJECT:
|
||||
pagination.sortBy = 'groupMetadata.subject';
|
||||
break;
|
||||
}
|
||||
|
||||
const paginator = new PaginatorInMemory(pagination);
|
||||
return paginator.apply(groups);
|
||||
}
|
||||
|
||||
public async refreshGroups(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
public getGroup(id) {
|
||||
@@ -994,85 +1067,146 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
/**
|
||||
* END - Methods for API
|
||||
*/
|
||||
|
||||
subscribeEngineEvent(event, handler): boolean {
|
||||
switch (event) {
|
||||
case WAHAEvents.MESSAGE:
|
||||
this.whatsapp.on(Events.MESSAGE_RECEIVED, (message) =>
|
||||
this.processIncomingMessage(message).then(handler),
|
||||
);
|
||||
return true;
|
||||
case WAHAEvents.MESSAGE_WAITING:
|
||||
this.whatsapp.on(Events.MESSAGE_CIPHERTEXT, (message) =>
|
||||
this.processIncomingMessage(message).then(handler),
|
||||
);
|
||||
return true;
|
||||
case WAHAEvents.MESSAGE_REVOKED:
|
||||
this.whatsapp.on(
|
||||
Events.MESSAGE_REVOKED_EVERYONE,
|
||||
async (after, before) => {
|
||||
const afterMessage = after ? await this.toWAMessage(after) : null;
|
||||
const beforeMessage = before
|
||||
? await this.toWAMessage(before)
|
||||
: null;
|
||||
const body: WAMessageRevokedBody = {
|
||||
after: afterMessage,
|
||||
before: beforeMessage,
|
||||
subscribeEngineEvents2() {
|
||||
//
|
||||
// All
|
||||
//
|
||||
const events: Observable<EnginePayload>[] = [];
|
||||
for (const key in Events) {
|
||||
const event = Events[key];
|
||||
const event$ = fromEvent(this.whatsapp, event);
|
||||
events.push(
|
||||
event$.pipe(
|
||||
map((data) => {
|
||||
return {
|
||||
event: event,
|
||||
data: data,
|
||||
};
|
||||
handler(body);
|
||||
},
|
||||
);
|
||||
return true;
|
||||
case WAHAEvents.MESSAGE_REACTION:
|
||||
this.whatsapp.on('message_reaction', (message) =>
|
||||
handler(this.processMessageReaction(message)),
|
||||
);
|
||||
return true;
|
||||
case WAHAEvents.MESSAGE_ANY:
|
||||
this.whatsapp.on(Events.MESSAGE_CREATE, (message) =>
|
||||
this.processIncomingMessage(message).then(handler),
|
||||
);
|
||||
return true;
|
||||
case WAHAEvents.STATE_CHANGE:
|
||||
this.whatsapp.on(Events.STATE_CHANGED, handler);
|
||||
return true;
|
||||
case WAHAEvents.MESSAGE_ACK:
|
||||
// We do not download media here
|
||||
this.whatsapp.on(Events.MESSAGE_ACK, (message) =>
|
||||
this.toWAMessage(message).then(handler),
|
||||
);
|
||||
return true;
|
||||
case WAHAEvents.GROUP_JOIN:
|
||||
this.whatsapp.on(Events.GROUP_JOIN, handler);
|
||||
return true;
|
||||
case WAHAEvents.GROUP_LEAVE:
|
||||
this.whatsapp.on(Events.GROUP_LEAVE, handler);
|
||||
return true;
|
||||
case WAHAEvents.CHAT_ARCHIVE:
|
||||
this.whatsapp.on('chat_archived', (chat, archived, _) => {
|
||||
const body: ChatArchiveEvent = {
|
||||
id: chat.id._serialized,
|
||||
archived: archived,
|
||||
timestamp: chat.timestamp,
|
||||
};
|
||||
handler(body);
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.CALL_RECEIVED:
|
||||
this.whatsapp.on('call', (call: Call) => {
|
||||
const body: CallData = {
|
||||
id: call.id,
|
||||
from: call.from,
|
||||
timestamp: call.timestamp,
|
||||
isVideo: call.isVideo,
|
||||
isGroup: call.isGroup,
|
||||
};
|
||||
handler(body);
|
||||
});
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
const all$ = merge(...events);
|
||||
this.events2.get(WAHAEvents.ENGINE_EVENT).switch(all$);
|
||||
|
||||
//
|
||||
// Messages
|
||||
//
|
||||
const messageReceived$ = fromEvent(this.whatsapp, Events.MESSAGE_RECEIVED);
|
||||
const messagesFromOthers$ = messageReceived$.pipe(
|
||||
mergeMap((msg: any) => this.processIncomingMessage(msg, true)),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE).switch(messagesFromOthers$);
|
||||
|
||||
const messageCreate$ = fromEvent(this.whatsapp, Events.MESSAGE_CREATE);
|
||||
const messagesFromAll$ = messageCreate$.pipe(
|
||||
mergeMap((msg: any) => this.processIncomingMessage(msg, true)),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_ANY).switch(messagesFromAll$);
|
||||
|
||||
const messageCiphertext$ = fromEvent(
|
||||
this.whatsapp,
|
||||
Events.MESSAGE_CIPHERTEXT,
|
||||
);
|
||||
const messagesWaiting$ = messageCiphertext$.pipe(
|
||||
mergeMap((msg: any) => this.processIncomingMessage(msg, true)),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_WAITING).switch(messagesWaiting$);
|
||||
|
||||
const messageRevoked$ = fromEvent(
|
||||
this.whatsapp,
|
||||
Events.MESSAGE_REVOKED_EVERYONE,
|
||||
(after, before) => {
|
||||
return { after, before };
|
||||
},
|
||||
);
|
||||
const messagesRevoked$ = messageRevoked$.pipe(
|
||||
map((event): WAMessageRevokedBody => {
|
||||
const afterMessage = event.after ? this.toWAMessage(event.after) : null;
|
||||
const beforeMessage = event.before
|
||||
? this.toWAMessage(event.before)
|
||||
: null;
|
||||
return {
|
||||
after: afterMessage,
|
||||
before: beforeMessage,
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_REVOKED).switch(messagesRevoked$);
|
||||
|
||||
const messageReaction$ = fromEvent(this.whatsapp, 'message_reaction');
|
||||
const messagesReaction$ = messageReaction$.pipe(
|
||||
map(this.processMessageReaction.bind(this)),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_REACTION).switch(messagesReaction$);
|
||||
|
||||
const messageAck$ = fromEvent(
|
||||
this.whatsapp,
|
||||
Events.MESSAGE_ACK,
|
||||
(message, ack) => {
|
||||
return { message, ack };
|
||||
},
|
||||
);
|
||||
const messagesAck$ = messageAck$.pipe(
|
||||
map((event) => event.message),
|
||||
map(this.toWAMessage.bind(this)),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_ACK).switch(messagesAck$);
|
||||
|
||||
//
|
||||
// Others
|
||||
//
|
||||
const stateChanged$ = fromEvent(this.whatsapp, Events.STATE_CHANGED);
|
||||
this.events2.get(WAHAEvents.STATE_CHANGE).switch(stateChanged$);
|
||||
|
||||
//
|
||||
// Groups
|
||||
//
|
||||
const groupJoin$ = fromEvent(this.whatsapp, Events.GROUP_JOIN);
|
||||
this.events2.get(WAHAEvents.GROUP_JOIN).switch(groupJoin$);
|
||||
const groupLeave$ = fromEvent(this.whatsapp, Events.GROUP_LEAVE);
|
||||
this.events2.get(WAHAEvents.GROUP_LEAVE).switch(groupLeave$);
|
||||
|
||||
//
|
||||
// Chats
|
||||
//
|
||||
const chatArchived$ = fromEvent(
|
||||
this.whatsapp,
|
||||
'chat_archived',
|
||||
(chat, archived, _) => {
|
||||
return {
|
||||
chat: chat,
|
||||
archived: archived,
|
||||
};
|
||||
},
|
||||
);
|
||||
const chatsArchived$ = chatArchived$.pipe(
|
||||
map((event) => {
|
||||
return {
|
||||
id: event.chat.id._serialized,
|
||||
archived: event.archived,
|
||||
timestamp: event.chat.timestamp,
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.events2.get(WAHAEvents.CHAT_ARCHIVE).switch(chatsArchived$);
|
||||
|
||||
//
|
||||
// Calls
|
||||
//
|
||||
const call$ = fromEvent(this.whatsapp, 'call');
|
||||
const calls$ = call$.pipe(
|
||||
map((call: Call) => {
|
||||
return {
|
||||
id: call.id,
|
||||
from: call.from,
|
||||
timestamp: call.timestamp,
|
||||
isVideo: call.isVideo,
|
||||
isGroup: call.isGroup,
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.events2.get(WAHAEvents.CALL_RECEIVED).switch(calls$);
|
||||
}
|
||||
|
||||
private async processIncomingMessage(message: Message, downloadMedia = true) {
|
||||
@@ -1084,7 +1218,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
this.logger.error(e, e.stack);
|
||||
}
|
||||
}
|
||||
return await this.toWAMessage(message);
|
||||
return this.toWAMessage(message);
|
||||
}
|
||||
|
||||
private processMessageReaction(reaction: Reaction): WAMessageReaction {
|
||||
@@ -1102,10 +1236,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
};
|
||||
}
|
||||
|
||||
protected toWAMessage(message: Message): Promise<WAMessage> {
|
||||
protected toWAMessage(message: Message): WAMessage {
|
||||
const replyTo = this.extractReplyTo(message);
|
||||
// @ts-ignore
|
||||
return Promise.resolve({
|
||||
return {
|
||||
id: message.id._serialized,
|
||||
timestamp: message.timestamp,
|
||||
from: message.from,
|
||||
@@ -1126,7 +1260,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
vCards: message.vCards,
|
||||
replyTo: replyTo,
|
||||
_data: message.rawData,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
protected extractReplyTo(message: Message): ReplyToMessage | null {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { populateSessionInfo } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { WebhookSender } from '@waha/core/integrations/webhooks/WebhookSender';
|
||||
import { WAHAEvents, WAHAEventsWild } from '@waha/structures/enums.dto';
|
||||
import { WebhookConfig } from '@waha/structures/webhooks.config.dto';
|
||||
import { EventWildUnmask } from '@waha/utils/events';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
export class WebhookConductor {
|
||||
private logger: Logger;
|
||||
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
|
||||
|
||||
constructor(protected loggerBuilder: LoggerBuilder) {
|
||||
this.logger = loggerBuilder.child({ name: WebhookConductor.name });
|
||||
}
|
||||
|
||||
protected buildSender(webhookConfig: WebhookConfig): WebhookSender {
|
||||
return new WebhookSender(this.loggerBuilder, webhookConfig);
|
||||
}
|
||||
|
||||
private getSuitableEvents(events: WAHAEvents[] | string[]): WAHAEvents[] {
|
||||
return this.eventUnmask.unmask(events);
|
||||
}
|
||||
|
||||
public configure(session: WhatsappSession, webhooks: WebhookConfig[]) {
|
||||
for (const webhookConfig of webhooks) {
|
||||
this.configureSingleWebhook(session, webhookConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private configureSingleWebhook(
|
||||
session: WhatsappSession,
|
||||
webhook: WebhookConfig,
|
||||
) {
|
||||
if (!webhook || !webhook.url || webhook.events.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = webhook.url;
|
||||
this.logger.info(`Configuring webhooks for ${url}...`);
|
||||
const events = this.getSuitableEvents(webhook.events);
|
||||
const sender = this.buildSender(webhook);
|
||||
for (const event of events) {
|
||||
const obs$ = session.getEventObservable(event);
|
||||
obs$.subscribe((payload) => {
|
||||
const data = populateSessionInfo(event, session)(payload);
|
||||
sender.send(data);
|
||||
});
|
||||
this.logger.debug(`Event '${event}' is enabled for url: ${url}`);
|
||||
}
|
||||
this.logger.info(`Webhooks were configured for ${url}.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { SECOND } from '@waha/structures/enums.dto';
|
||||
import {
|
||||
RetryPolicy,
|
||||
WebhookConfig,
|
||||
} from '@waha/structures/webhooks.config.dto';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { VERSION } from '@waha/version';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import axiosRetry, { retryAfter } from 'axios-retry';
|
||||
import * as crypto from 'crypto';
|
||||
import { Logger } from 'pino';
|
||||
import { ulid } from 'ulid';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const HttpAgent = require('agentkeepalive');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const HttpsAgent = require('agentkeepalive').HttpsAgent;
|
||||
|
||||
const DEFAULT_RETRY_DELAY_SECONDS = 2;
|
||||
const DEFAULT_RETRY_ATTEMPTS = 15;
|
||||
const DEFAULT_HMAC_ALGORITHM = 'sha512';
|
||||
|
||||
function noDelay(_retryNumber = 0, error: any) {
|
||||
return Math.max(0, retryAfter(error));
|
||||
}
|
||||
|
||||
function constantDelay(delayFactor: number) {
|
||||
return (_retryNumber = 0, error = undefined) => {
|
||||
return Math.max(delayFactor, retryAfter(error));
|
||||
};
|
||||
}
|
||||
|
||||
export function exponentialDelay(delayFactor: number) {
|
||||
return (retryNumber = 0, error = undefined) => {
|
||||
const calculatedDelay = 2 ** retryNumber * delayFactor;
|
||||
const delay = Math.max(calculatedDelay, retryAfter(error));
|
||||
const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay
|
||||
return delay + randomSum;
|
||||
};
|
||||
}
|
||||
|
||||
export class WebhookSender {
|
||||
protected static AGENTS = {
|
||||
http: new HttpAgent({}),
|
||||
https: new HttpsAgent({}),
|
||||
};
|
||||
|
||||
protected url: string;
|
||||
protected logger: Logger;
|
||||
protected readonly config: WebhookConfig;
|
||||
|
||||
protected axios: AxiosInstance;
|
||||
|
||||
constructor(
|
||||
loggerBuilder: LoggerBuilder,
|
||||
protected webhookConfig: WebhookConfig,
|
||||
) {
|
||||
this.url = webhookConfig.url;
|
||||
this.logger = loggerBuilder.child({ name: WebhookSender.name });
|
||||
this.config = webhookConfig;
|
||||
this.axios = this.buildAxiosInstance();
|
||||
}
|
||||
|
||||
send(json: any) {
|
||||
const body = JSON.stringify(json);
|
||||
const headers = {
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
Object.assign(headers, this.getWebhookHeader());
|
||||
Object.assign(headers, this.getHMACHeaders(body));
|
||||
const ctx = {
|
||||
id: headers['X-Webhook-Request-Id'],
|
||||
['event.id']: json.id,
|
||||
url: this.url,
|
||||
};
|
||||
this.logger.info(ctx, `Sending POST...`);
|
||||
this.logger.debug(ctx, `POST DATA`);
|
||||
|
||||
this.axios
|
||||
.post(this.url, body, { headers: headers })
|
||||
.then((response) => {
|
||||
this.logger.info(
|
||||
ctx,
|
||||
`POST request was sent with status code: ${response.status}`,
|
||||
);
|
||||
this.logger.debug(
|
||||
{
|
||||
...ctx,
|
||||
body: response.data,
|
||||
},
|
||||
`Response`,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error(
|
||||
{
|
||||
...ctx,
|
||||
error: error.message,
|
||||
data: error.response?.data,
|
||||
},
|
||||
`POST request failed: ${error.message}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
protected buildAxiosInstance(): AxiosInstance {
|
||||
// configure headers
|
||||
const customHeaders = this.config.customHeaders || [];
|
||||
const headers = {
|
||||
'content-type': 'application/json',
|
||||
'User-Agent': `WAHA/${VERSION.version}`,
|
||||
};
|
||||
customHeaders.forEach((header) => {
|
||||
headers[header.name] = header.value;
|
||||
});
|
||||
|
||||
// configure retry
|
||||
const attempts = this.config.retries?.attempts ?? DEFAULT_RETRY_ATTEMPTS;
|
||||
const delaySeconds =
|
||||
this.config.retries?.delaySeconds ?? DEFAULT_RETRY_DELAY_SECONDS;
|
||||
const delayMs = delaySeconds * SECOND;
|
||||
const policy = this.config.retries?.policy;
|
||||
const retryDelay = this.buildRetryDelay(policy, delayMs);
|
||||
|
||||
const instance = axios.create({
|
||||
headers: headers,
|
||||
httpAgent: WebhookSender.AGENTS.http,
|
||||
httpsAgent: WebhookSender.AGENTS.https,
|
||||
});
|
||||
axiosRetry(instance, {
|
||||
retries: attempts,
|
||||
retryDelay: retryDelay,
|
||||
retryCondition: (error) => true,
|
||||
onRetry: (retryCount, error, requestConfig) => {
|
||||
this.logger.warn(
|
||||
{ id: requestConfig.headers['X-Webhook-Request-Id'] },
|
||||
`Error sending POST request: '${error.message}'. Retrying ${retryCount}/${attempts}...`,
|
||||
);
|
||||
},
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
|
||||
protected getHMACHeaders(body: string) {
|
||||
// HMAC
|
||||
const hmac = this.calculateHmac(body, DEFAULT_HMAC_ALGORITHM);
|
||||
if (!hmac) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
'X-Webhook-Hmac': hmac,
|
||||
'X-Webhook-Hmac-Algorithm': DEFAULT_HMAC_ALGORITHM,
|
||||
};
|
||||
}
|
||||
|
||||
protected getWebhookHeader() {
|
||||
return {
|
||||
// UUID, no '-' in it
|
||||
'X-Webhook-Request-Id': ulid(),
|
||||
// unix timestamp with ms
|
||||
'X-Webhook-Timestamp': Date.now().toString(),
|
||||
};
|
||||
}
|
||||
|
||||
private calculateHmac(body, algorithm) {
|
||||
if (!this.config.hmac || !this.config.hmac.key) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return crypto
|
||||
.createHmac(algorithm, this.config.hmac.key)
|
||||
.update(body)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
private buildRetryDelay(
|
||||
policy: RetryPolicy | null,
|
||||
ms: number,
|
||||
): (retryNumber: number, error: any) => number {
|
||||
if (!ms) {
|
||||
this.logger.debug(`Using no delay, because delaySeconds set to 0`);
|
||||
return noDelay;
|
||||
}
|
||||
|
||||
switch (policy) {
|
||||
case RetryPolicy.CONSTANT:
|
||||
this.logger.debug(`Using constant delay with '${ms}' ms factor`);
|
||||
return constantDelay(ms);
|
||||
|
||||
case RetryPolicy.LINEAR:
|
||||
this.logger.debug(`Using linear delay with '${ms}' ms factor`);
|
||||
return axiosRetry.linearDelay(ms);
|
||||
|
||||
case RetryPolicy.EXPONENTIAL:
|
||||
this.logger.debug(`Using exponential delay with '${ms}' ms factor`);
|
||||
return exponentialDelay(ms);
|
||||
|
||||
default:
|
||||
this.logger.debug('No delay policy specified, using constant delay');
|
||||
return constantDelay(ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
-20
@@ -3,11 +3,20 @@ import {
|
||||
NotFoundException,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { EngineBootstrap } from '@waha/core/abc/EngineBootstrap';
|
||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
||||
import { WhatsappSessionGoWSCore } from '@waha/core/engines/gows/session.gows.core';
|
||||
import { WebhookConductor } from '@waha/core/integrations/webhooks/WebhookConductor';
|
||||
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
|
||||
import { DefaultMap } from '@waha/utils/DefaultMap';
|
||||
import { getPinoLogLevel, LoggerBuilder } from '@waha/utils/logging';
|
||||
import { promiseTimeout, sleep } from '@waha/utils/promiseTimeout';
|
||||
import { EventEmitter } from 'events';
|
||||
import { complete } from '@waha/utils/reactive/complete';
|
||||
import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import { Observable, retry, share } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { WhatsappConfigService } from '../config.service';
|
||||
import {
|
||||
@@ -23,7 +32,7 @@ import {
|
||||
SessionInfo,
|
||||
} from '../structures/sessions.dto';
|
||||
import { WebhookConfig } from '../structures/webhooks.config.dto';
|
||||
import { SessionManager } from './abc/manager.abc';
|
||||
import { populateSessionInfo, SessionManager } from './abc/manager.abc';
|
||||
import { SessionParams, WhatsappSession } from './abc/session.abc';
|
||||
import { EngineConfigService } from './config/EngineConfigService';
|
||||
import { WhatsappSessionNoWebCore } from './engines/noweb/session.noweb.core';
|
||||
@@ -33,7 +42,6 @@ import { getProxyConfig } from './helpers.proxy';
|
||||
import { MediaManager } from './media/MediaManager';
|
||||
import { LocalSessionAuthRepository } from './storage/LocalSessionAuthRepository';
|
||||
import { LocalStoreCore } from './storage/LocalStoreCore';
|
||||
import { WebhookConductorCore } from './webhooks.core';
|
||||
|
||||
export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
|
||||
constructor() {
|
||||
@@ -59,25 +67,34 @@ export class SessionManagerCore extends SessionManager {
|
||||
private sessionConfig?: SessionConfig;
|
||||
DEFAULT = 'default';
|
||||
|
||||
// @ts-ignore
|
||||
protected WebhookConductorClass = WebhookConductorCore;
|
||||
protected readonly EngineClass: typeof WhatsappSession;
|
||||
protected events2: DefaultMap<WAHAEvents, SwitchObservable<any>>;
|
||||
protected readonly engineBootstrap: EngineBootstrap;
|
||||
|
||||
constructor(
|
||||
config: WhatsappConfigService,
|
||||
private engineConfigService: EngineConfigService,
|
||||
private webjsEngineConfigService: WebJSEngineConfigService,
|
||||
gowsConfigService: GowsEngineConfigService,
|
||||
log: PinoLogger,
|
||||
private mediaStorageFactory: MediaStorageFactory,
|
||||
) {
|
||||
super(config, log);
|
||||
this.events = new EventEmitter();
|
||||
super(log, config, gowsConfigService);
|
||||
this.session = DefaultSessionStatus.STOPPED;
|
||||
this.sessionConfig = null;
|
||||
const engineName = this.engineConfigService.getDefaultEngineName();
|
||||
this.EngineClass = this.getEngine(engineName);
|
||||
this.engineBootstrap = this.getEngineBootstrap(engineName);
|
||||
|
||||
this.events2 = new DefaultMap<WAHAEvents, SwitchObservable<any>>(
|
||||
(key) =>
|
||||
new SwitchObservable((obs$) => {
|
||||
return obs$.pipe(retry(), share());
|
||||
}),
|
||||
);
|
||||
|
||||
this.store = new LocalStoreCore(engineName.toLowerCase());
|
||||
this.sessionAuthRepository = new LocalSessionAuthRepository(this.store);
|
||||
this.startPredefinedSessions();
|
||||
this.clearStorage().catch((error) => {
|
||||
this.log.error({ error }, 'Error while clearing storage');
|
||||
});
|
||||
@@ -88,6 +105,8 @@ export class SessionManagerCore extends SessionManager {
|
||||
return WhatsappSessionWebJSCore;
|
||||
} else if (engine === WAHAEngine.NOWEB) {
|
||||
return WhatsappSessionNoWebCore;
|
||||
} else if (engine === WAHAEngine.GOWS) {
|
||||
return WhatsappSessionGoWSCore;
|
||||
} else {
|
||||
throw new NotFoundException(`Unknown whatsapp engine '${engine}'.`);
|
||||
}
|
||||
@@ -100,10 +119,17 @@ export class SessionManagerCore extends SessionManager {
|
||||
}
|
||||
|
||||
async beforeApplicationShutdown(signal?: string) {
|
||||
this.stopEvents();
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
await this.stop(this.DEFAULT, true);
|
||||
await this.engineBootstrap.shutdown();
|
||||
}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
await this.engineBootstrap.bootstrap();
|
||||
this.startPredefinedSessions();
|
||||
}
|
||||
|
||||
private async clearStorage() {
|
||||
@@ -138,7 +164,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
`Session '${this.DEFAULT}' is already started.`,
|
||||
);
|
||||
}
|
||||
this.log.info(`'${name}' - starting session...`);
|
||||
this.log.info({ session: name }, `Starting session...`);
|
||||
const logger = this.log.logger.child({ session: name });
|
||||
logger.level = getPinoLogLevel(this.sessionConfig?.debug);
|
||||
const loggerBuilder: LoggerBuilder = logger;
|
||||
@@ -153,7 +179,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
loggerBuilder.child({ name: 'MediaManager' }),
|
||||
);
|
||||
|
||||
const webhook = new this.WebhookConductorClass(loggerBuilder);
|
||||
const webhook = new WebhookConductor(loggerBuilder);
|
||||
const proxyConfig = this.getProxyConfig();
|
||||
const sessionConfig: SessionParams = {
|
||||
name,
|
||||
@@ -164,21 +190,21 @@ export class SessionManagerCore extends SessionManager {
|
||||
proxyConfig: proxyConfig,
|
||||
sessionConfig: this.sessionConfig,
|
||||
};
|
||||
if (this.EngineClass === WhatsappSessionWebJSCore) {
|
||||
sessionConfig.engineConfig = this.webjsEngineConfigService.getConfig();
|
||||
} else if (this.EngineClass === WhatsappSessionGoWSCore) {
|
||||
sessionConfig.engineConfig = this.gowsConfigService.getConfig();
|
||||
}
|
||||
await this.sessionAuthRepository.init(name);
|
||||
// @ts-ignore
|
||||
const session = new this.EngineClass(sessionConfig);
|
||||
this.session = session;
|
||||
this.updateSession();
|
||||
|
||||
// configure webhooks
|
||||
const webhooks = this.getWebhooks();
|
||||
webhook.configure(session, webhooks);
|
||||
|
||||
// configure events
|
||||
session.events.on(
|
||||
WAHAEvents.SESSION_STATUS,
|
||||
this.handleSessionEvent(WAHAEvents.SESSION_STATUS, session),
|
||||
);
|
||||
|
||||
// start session
|
||||
await session.start();
|
||||
logger.info('Session has been started.');
|
||||
@@ -189,14 +215,32 @@ export class SessionManagerCore extends SessionManager {
|
||||
};
|
||||
}
|
||||
|
||||
private updateSession() {
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
const session: WhatsappSession = this.session as WhatsappSession;
|
||||
for (const eventName in WAHAEvents) {
|
||||
const event = WAHAEvents[eventName];
|
||||
const stream$ = session
|
||||
.getEventObservable(event)
|
||||
.pipe(map(populateSessionInfo(event, session)));
|
||||
this.events2.get(event).switch(stream$);
|
||||
}
|
||||
}
|
||||
|
||||
getSessionEvent(session: string, event: WAHAEvents): Observable<any> {
|
||||
return this.events2.get(event);
|
||||
}
|
||||
|
||||
async stop(name: string, silent: boolean): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
if (!this.isRunning(name)) {
|
||||
this.log.debug(`Session is not running.`, { session: name });
|
||||
this.log.debug({ session: name }, `Session is not running.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.info(`Stopping session...`, { session: name });
|
||||
this.log.info({ session: name }, `Stopping session...`);
|
||||
try {
|
||||
const session = this.getSession(name);
|
||||
await session.stop();
|
||||
@@ -206,8 +250,9 @@ export class SessionManagerCore extends SessionManager {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
this.log.info(`Session has been stopped.`, { session: name });
|
||||
this.log.info({ session: name }, `Session has been stopped.`);
|
||||
this.session = DefaultSessionStatus.STOPPED;
|
||||
this.updateSession();
|
||||
await sleep(this.SESSION_STOP_TIMEOUT);
|
||||
}
|
||||
|
||||
@@ -217,7 +262,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
}
|
||||
const session = this.session as WhatsappSession;
|
||||
|
||||
this.log.info('Unpairing the device from account...', { session: name });
|
||||
this.log.info({ session: name }, 'Unpairing the device from account...');
|
||||
await session.unpair().catch((err) => {
|
||||
this.log.warn(`Error while unpairing from device: ${err}`);
|
||||
});
|
||||
@@ -232,6 +277,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
async delete(name: string): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
this.session = DefaultSessionStatus.REMOVED;
|
||||
this.updateSession();
|
||||
this.sessionConfig = undefined;
|
||||
}
|
||||
|
||||
@@ -339,4 +385,8 @@ export class SessionManagerCore extends SessionManager {
|
||||
const engine = await this.fetchEngineInfo();
|
||||
return { ...session, engine: engine };
|
||||
}
|
||||
|
||||
protected stopEvents() {
|
||||
complete(this.events2);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ import { SECOND } from '@waha/structures/enums.dto';
|
||||
import * as fsp from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { Logger } from 'pino';
|
||||
import { promisify } from 'util';
|
||||
import fs = require('fs');
|
||||
import del = require('del');
|
||||
import { fileExists } from '@waha/utils/files';
|
||||
|
||||
const writeFileAsync = promisify(fs.writeFile);
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const writeFileAtomic = require('write-file-atomic');
|
||||
|
||||
/**
|
||||
* Save files locally using the filesystem
|
||||
@@ -41,7 +41,7 @@ export class MediaLocalStorage implements IMediaStorage {
|
||||
const filepath = this.getFullPath(data);
|
||||
const folder = path.dirname(filepath);
|
||||
await fsp.mkdir(folder, { recursive: true });
|
||||
await writeFileAsync(filepath, buffer);
|
||||
await writeFileAtomic(filepath, buffer);
|
||||
this.postponeRemoval(filepath);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import { SessionConfig } from '../../structures/sessions.dto';
|
||||
import { ISessionConfigRepository } from './ISessionConfigRepository';
|
||||
import { LocalStore } from './LocalStore';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const writeFileAtomic = require('write-file-atomic');
|
||||
|
||||
export class LocalSessionConfigRepository extends ISessionConfigRepository {
|
||||
FILENAME = '.waha.session.config.json';
|
||||
private store: LocalStore;
|
||||
@@ -52,7 +55,7 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
|
||||
// Save config
|
||||
const filepath = this.getFilePath(sessionName);
|
||||
const content = JSON.stringify(config || null);
|
||||
await fs.writeFile(filepath, content);
|
||||
await writeFileAtomic(filepath, content);
|
||||
}
|
||||
|
||||
private getFilePath(sessionName): string {
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import Agent from 'agentkeepalive';
|
||||
import axios from 'axios';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
import { WAHAEvents } from '../structures/enums.dto';
|
||||
import { WebhookConfig } from '../structures/webhooks.config.dto';
|
||||
import { WAHAWebhook } from '../structures/webhooks.dto';
|
||||
import { VERSION } from '../version';
|
||||
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
|
||||
import { WebhookConductor, WebhookSender } from './abc/webhooks.abc';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const HttpAgent = require('agentkeepalive');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const HttpsAgent = require('agentkeepalive').HttpsAgent;
|
||||
|
||||
export class WebhookSenderCore extends WebhookSender {
|
||||
protected static AGENTS = {
|
||||
http: new HttpAgent({}),
|
||||
https: new HttpsAgent({}),
|
||||
};
|
||||
|
||||
protected axios: AxiosInstance;
|
||||
|
||||
constructor(loggerBuilder: LoggerBuilder, webhookConfig: WebhookConfig) {
|
||||
super(loggerBuilder, webhookConfig);
|
||||
this.axios = this.buildAxiosInstance();
|
||||
}
|
||||
|
||||
protected buildAxiosInstance() {
|
||||
const headers = {
|
||||
'content-type': 'application/json',
|
||||
'User-Agent': `WAHA/${VERSION.version}`,
|
||||
};
|
||||
return axios.create({
|
||||
headers: headers,
|
||||
httpAgent: WebhookSenderCore.AGENTS.http,
|
||||
httpsAgent: WebhookSenderCore.AGENTS.https,
|
||||
});
|
||||
}
|
||||
|
||||
send(json: any, headers?: Record<string, string>) {
|
||||
headers = headers || {};
|
||||
this.logger.info(
|
||||
{ id: headers['X-Webhook-Request-Id'], url: this.url },
|
||||
`Sending POST...`,
|
||||
);
|
||||
this.logger.debug(
|
||||
{ id: headers['X-Webhook-Request-Id'], data: json },
|
||||
`POST DATA`,
|
||||
);
|
||||
|
||||
this.axios
|
||||
.post(this.url, json, { headers: headers })
|
||||
.then((response) => {
|
||||
this.logger.info(
|
||||
{ id: headers['X-Webhook-Request-Id'] },
|
||||
`POST request was sent with status code: ${response.status}`,
|
||||
);
|
||||
this.logger.debug(
|
||||
{ id: headers['X-Webhook-Request-Id'], body: response.data },
|
||||
`Response`,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error(
|
||||
{
|
||||
id: headers['X-Webhook-Request-Id'],
|
||||
error: error.message,
|
||||
data: error.response?.data,
|
||||
},
|
||||
`POST request failed: ${error.message}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class WebhookConductorCore implements WebhookConductor {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(protected loggerBuilder: LoggerBuilder) {
|
||||
this.logger = loggerBuilder.child({ name: WebhookConductor.name });
|
||||
}
|
||||
|
||||
protected buildSender(webhookConfig: WebhookConfig): WebhookSender {
|
||||
return new WebhookSenderCore(this.loggerBuilder, webhookConfig);
|
||||
}
|
||||
|
||||
private getSuitableEvents(events: WAHAEvents[] | string[]): WAHAEvents[] {
|
||||
const allEvents = Object.values(WAHAEvents);
|
||||
|
||||
// Enable all events if * in the events
|
||||
// @ts-ignore
|
||||
if (events.includes('*')) {
|
||||
return allEvents;
|
||||
}
|
||||
|
||||
// Get only known events, log and ignore others
|
||||
const rightEvents = [];
|
||||
for (const event of events) {
|
||||
// @ts-ignore
|
||||
if (!allEvents.includes(event)) {
|
||||
this.logger.error(`Unknown event for webhook: '${event}'`);
|
||||
continue;
|
||||
}
|
||||
rightEvents.push(event);
|
||||
}
|
||||
return rightEvents;
|
||||
}
|
||||
|
||||
public configure(session: WhatsappSession, webhooks: WebhookConfig[]) {
|
||||
for (const webhookConfig of webhooks) {
|
||||
this.configureSingleWebhook(session, webhookConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private configureSingleWebhook(
|
||||
session: WhatsappSession,
|
||||
webhook: WebhookConfig,
|
||||
) {
|
||||
if (!webhook || !webhook.url || webhook.events.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = webhook.url;
|
||||
this.logger.info(`Configuring webhooks for ${url}...`);
|
||||
const events = this.getSuitableEvents(webhook.events);
|
||||
const sender = this.buildSender(webhook);
|
||||
for (const event of events) {
|
||||
const found = this.configureSingleEvent(
|
||||
session.subscribeSessionEvent,
|
||||
session,
|
||||
event,
|
||||
sender,
|
||||
url,
|
||||
);
|
||||
|
||||
if (!found) {
|
||||
// Postpone for ENGINE_START event and configure engine events
|
||||
session.events.on(WAHAInternalEvent.ENGINE_START, () => {
|
||||
const found = this.configureSingleEvent(
|
||||
session.subscribeEngineEvent,
|
||||
session,
|
||||
event,
|
||||
sender,
|
||||
url,
|
||||
);
|
||||
if (!found) {
|
||||
this.logger.error(
|
||||
`Engine does not support webhook event: '${event}' for url '${url}'`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this.logger.info(`Webhooks were configured for ${url}.`);
|
||||
}
|
||||
|
||||
private configureSingleEvent(
|
||||
subscribeMethod,
|
||||
session: WhatsappSession,
|
||||
event: WAHAEvents,
|
||||
sender: WebhookSender,
|
||||
url: string,
|
||||
) {
|
||||
const found = subscribeMethod.apply(session, [
|
||||
event,
|
||||
(data: any) => this.callWebhook(event, session, data, sender),
|
||||
]);
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
this.logger.debug(`Event '${event}' is enabled for url: ${url}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async callWebhook(
|
||||
event,
|
||||
session: WhatsappSession,
|
||||
data: any,
|
||||
sender: WebhookSender,
|
||||
) {
|
||||
const me = session.getSessionMeInfo();
|
||||
const json: WAHAWebhook = {
|
||||
event: event,
|
||||
session: session.name,
|
||||
metadata: session.sessionConfig?.metadata,
|
||||
me: me,
|
||||
payload: data,
|
||||
engine: session.engine,
|
||||
environment: VERSION,
|
||||
};
|
||||
sender.send(json);
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,7 @@ async function bootstrap() {
|
||||
logger: getNestJSLogLevels(),
|
||||
httpsOptions: httpsOptions,
|
||||
bufferLogs: true,
|
||||
forceCloseConnections: true,
|
||||
});
|
||||
app.useLogger(app.get(NestJSPinoLogger));
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export class WebsocketHeartbeatJob {
|
||||
}
|
||||
|
||||
client.isAlive = false;
|
||||
this.logger.debug(`Sending heartbeat (ping) to ${client.id}`);
|
||||
this.logger.verbose(`Sending heartbeat (ping) to ${client.id}`);
|
||||
client.ping();
|
||||
});
|
||||
}, this.intervalTime);
|
||||
@@ -40,7 +40,7 @@ export class WebsocketHeartbeatJob {
|
||||
private onPong(ws: WebSocket) {
|
||||
return (event: any) => {
|
||||
ws.isAlive = true;
|
||||
this.logger.debug(`Heartbeat (pong) received from ${ws.id}`);
|
||||
this.logger.verbose(`Heartbeat (pong) received from ${ws.id}`);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export const WHATSAPP_DEFAULT_SESSION_NAME = 'default';
|
||||
|
||||
@@ -7,6 +7,7 @@ export const WHATSAPP_DEFAULT_SESSION_NAME = 'default';
|
||||
*/
|
||||
export class SessionQuery {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
session: string = WHATSAPP_DEFAULT_SESSION_NAME;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import {
|
||||
LimitOffsetParams,
|
||||
PaginationParams,
|
||||
} from '@waha/structures/pagination.dto';
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { IsBoolean, IsEnum, IsNumber, IsOptional } from 'class-validator';
|
||||
@@ -38,6 +41,23 @@ export class GetChatMessagesFilter {
|
||||
'filter.fromMe'?: boolean;
|
||||
}
|
||||
|
||||
export class ChatPictureQuery {
|
||||
@Transform(BooleanString)
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
example: false,
|
||||
required: false,
|
||||
description:
|
||||
'Refresh the picture from the server (24h cache by default). Do not refresh if not needed, you can get rate limit error',
|
||||
})
|
||||
refresh?: boolean = false;
|
||||
}
|
||||
|
||||
export class ChatPictureResponse {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export class GetChatMessagesQuery {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@@ -50,7 +70,7 @@ export class GetChatMessagesQuery {
|
||||
offset?: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
example: false,
|
||||
required: false,
|
||||
description: 'Download media for messages',
|
||||
})
|
||||
@@ -88,6 +108,38 @@ export class ChatsPaginationParams extends PaginationParams {
|
||||
sortBy?: string;
|
||||
}
|
||||
|
||||
export enum PinDuration {
|
||||
DAY = 86400,
|
||||
WEEK = 604800,
|
||||
MONTH = 2592000,
|
||||
}
|
||||
|
||||
export class PinMessageRequest {
|
||||
@IsNumber()
|
||||
@IsEnum(PinDuration)
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Duration in seconds. 24 hours (86400), 7 days (604800), 30 days (2592000)',
|
||||
example: 86400,
|
||||
})
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export class OverviewPaginationParams extends LimitOffsetParams {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit?: number = 20;
|
||||
}
|
||||
|
||||
export class ChatSummary {
|
||||
id: string;
|
||||
name: string | null;
|
||||
picture: string | null;
|
||||
lastMessage: any;
|
||||
_chat: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Events
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
import { IsEnum, IsOptional, IsString } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
import { SessionBaseRequest, SessionQuery } from './base.dto';
|
||||
|
||||
@@ -15,6 +17,19 @@ export class ContactQuery extends SessionQuery {
|
||||
contactId: string;
|
||||
}
|
||||
|
||||
export class ContactProfilePictureQuery extends ContactQuery {
|
||||
@Transform(BooleanString)
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
example: false,
|
||||
required: false,
|
||||
description:
|
||||
'Refresh the picture from the server (24h cache by default). Do not refresh if not needed, you can get rate limit error',
|
||||
})
|
||||
refresh?: boolean = false;
|
||||
}
|
||||
|
||||
enum ContactSortField {
|
||||
ID = 'id',
|
||||
NAME = 'name',
|
||||
|
||||
@@ -22,8 +22,14 @@ export enum WAHAEvents {
|
||||
LABEL_DELETED = 'label.deleted',
|
||||
LABEL_CHAT_ADDED = 'label.chat.added',
|
||||
LABEL_CHAT_DELETED = 'label.chat.deleted',
|
||||
ENGINE_EVENT = 'engine.event',
|
||||
}
|
||||
|
||||
// All but no state.change, it's internal one
|
||||
export const WAHAEventsWild = Object.values(WAHAEvents).filter(
|
||||
(e) => e !== WAHAEvents.STATE_CHANGE && e !== WAHAEvents.ENGINE_EVENT,
|
||||
);
|
||||
|
||||
export enum WAHASessionStatus {
|
||||
STOPPED = 'STOPPED',
|
||||
STARTING = 'STARTING',
|
||||
@@ -35,6 +41,7 @@ export enum WAHASessionStatus {
|
||||
export enum WAHAEngine {
|
||||
WEBJS = 'WEBJS',
|
||||
NOWEB = 'NOWEB',
|
||||
GOWS = 'GOWS',
|
||||
}
|
||||
|
||||
export enum WAHAPresenceStatus {
|
||||
@@ -53,4 +60,5 @@ export enum WAMessageAck {
|
||||
READ = 3,
|
||||
PLAYED = 4,
|
||||
}
|
||||
|
||||
export const ACK_UNKNOWN = 'UNKNOWN';
|
||||
@@ -1,5 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsString } from 'class-validator';
|
||||
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
|
||||
import { Transform } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
import { PaginationParams } from './pagination.dto';
|
||||
|
||||
/**
|
||||
* Structures
|
||||
@@ -46,3 +56,35 @@ export class CreateGroupRequest {
|
||||
@IsArray()
|
||||
participants: Array<Participant>;
|
||||
}
|
||||
|
||||
export class JoinGroupRequest {
|
||||
@ApiProperty({
|
||||
description: 'Group code (123) or url (https://chat.whatsapp.com/123)',
|
||||
example: 'https://chat.whatsapp.com/1234567890abcdef',
|
||||
})
|
||||
@IsString()
|
||||
code: string;
|
||||
}
|
||||
|
||||
export class JoinGroupResponse {
|
||||
@ApiProperty({
|
||||
description: 'Group ID',
|
||||
example: '123@g.us',
|
||||
})
|
||||
id: string;
|
||||
}
|
||||
|
||||
export enum GroupSortField {
|
||||
ID = 'id',
|
||||
SUBJECT = 'subject',
|
||||
}
|
||||
|
||||
export class GroupsPaginationParams extends PaginationParams {
|
||||
@ApiProperty({
|
||||
description: 'Sort by field',
|
||||
enum: GroupSortField,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(GroupSortField)
|
||||
sortBy?: string;
|
||||
}
|
||||
@@ -6,8 +6,7 @@ export enum SortOrder {
|
||||
DESC = 'desc',
|
||||
ASC = 'asc',
|
||||
}
|
||||
|
||||
export class PaginationParams {
|
||||
export class LimitOffsetParams {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@@ -17,7 +16,9 @@ export class PaginationParams {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export class PaginationParams extends LimitOffsetParams {
|
||||
@ApiProperty({
|
||||
description: 'Sort by field',
|
||||
})
|
||||
|
||||
@@ -32,6 +32,8 @@ export class WAMessageAckBody {
|
||||
fromMe: boolean;
|
||||
ack: WAMessageAck;
|
||||
ackName: string;
|
||||
|
||||
_data?: any;
|
||||
}
|
||||
|
||||
export class WAGroupPayload {
|
||||
@@ -101,6 +103,11 @@ export class WASessionStatusBody {
|
||||
}
|
||||
|
||||
export class WAHAWebhook {
|
||||
@ApiProperty({
|
||||
example: 'evt_01jcn4pjwwg47bwy2gsey6q5sx',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'default',
|
||||
})
|
||||
@@ -320,6 +327,20 @@ class WAHAWebhookLabelChatDeleted extends WAHAWebhook {
|
||||
payload: LabelChatAssociation;
|
||||
}
|
||||
|
||||
export class EnginePayload {
|
||||
event: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
class WAHAWebhookEngineEvent extends WAHAWebhook {
|
||||
@ApiProperty({
|
||||
description: 'Internal engine event.',
|
||||
})
|
||||
event = WAHAEvents.ENGINE_EVENT;
|
||||
|
||||
payload: EnginePayload;
|
||||
}
|
||||
|
||||
const WAHA_WEBHOOKS = [
|
||||
WAHAWebhookSessionStatus,
|
||||
WAHAWebhookMessage,
|
||||
@@ -341,5 +362,6 @@ const WAHA_WEBHOOKS = [
|
||||
WAHAWebhookLabelDeleted,
|
||||
WAHAWebhookLabelChatAdded,
|
||||
WAHAWebhookLabelChatDeleted,
|
||||
WAHAWebhookEngineEvent,
|
||||
];
|
||||
export { WAHA_WEBHOOKS };
|
||||
@@ -0,0 +1,15 @@
|
||||
export class DefaultMap<K, T> extends Map<K, T> {
|
||||
private readonly factory: (key: K) => T;
|
||||
|
||||
constructor(factory: (key: K) => T) {
|
||||
super();
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
get(key: K): T {
|
||||
if (!this.has(key)) {
|
||||
this.set(key, this.factory(key));
|
||||
}
|
||||
return super.get(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Unmask "*" in events list to exact values
|
||||
* Remove duplicates if any
|
||||
*/
|
||||
export class EventWildUnmask {
|
||||
constructor(
|
||||
private readonly events: string[] | any,
|
||||
private readonly all: string[] | any | null = null,
|
||||
) {
|
||||
// in case of enum - convert to array
|
||||
this.events = Object.values(events);
|
||||
this.all = all ? Object.values(all) : this.events;
|
||||
}
|
||||
|
||||
unmask(events: string[]) {
|
||||
const rightEvents = [];
|
||||
if (events.includes('*')) {
|
||||
rightEvents.push(...this.all);
|
||||
}
|
||||
|
||||
// Get only known events, log and ignore others
|
||||
for (const event of events) {
|
||||
if (!this.events.includes(event)) {
|
||||
continue;
|
||||
}
|
||||
rightEvents.push(event);
|
||||
}
|
||||
// return unique values
|
||||
return [...new Set(rightEvents)];
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -1,10 +1,9 @@
|
||||
import { v4 as uuid4 } from 'uuid';
|
||||
import { ulid } from 'ulid';
|
||||
|
||||
/**
|
||||
* Generate prefix uuid (but remove -)
|
||||
* @param prefix
|
||||
*/
|
||||
export function generatePrefixedId(prefix: string) {
|
||||
const id = uuid4().replace(/-/g, '');
|
||||
return `${prefix}_${id}`;
|
||||
return `${prefix}_${ulid().toLowerCase()}`;
|
||||
}
|
||||
@@ -25,10 +25,14 @@ function getNestJSLogLevels(): LogLevel[] {
|
||||
|
||||
export function getPinoLogLevel(debug: boolean = false): Level {
|
||||
const enableDebug = process.env.DEBUG != undefined || debug;
|
||||
const level = getDefaultPinoLogLevel();
|
||||
if (level === 'trace') {
|
||||
return 'trace';
|
||||
}
|
||||
if (enableDebug) {
|
||||
return 'debug';
|
||||
}
|
||||
return getDefaultPinoLogLevel();
|
||||
return level;
|
||||
}
|
||||
|
||||
export function isDebugEnabled() {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BehaviorSubject, EMPTY, Observable, share, switchMap } from 'rxjs';
|
||||
|
||||
const noop = (x: any) => x;
|
||||
|
||||
/**
|
||||
* Observable that can easily be switched to new observable
|
||||
* So you can change "source" of the observable,
|
||||
* but clients will use the same observable (but consume NEW data from NEW source)
|
||||
*/
|
||||
export class SwitchObservable<T> extends Observable<T> {
|
||||
private subject$: BehaviorSubject<Observable<T>>;
|
||||
|
||||
/**
|
||||
* fn - Modify inner observable
|
||||
*
|
||||
*/
|
||||
constructor(fn: (source: Observable<T>) => Observable<T> = noop) {
|
||||
const subject$ = new BehaviorSubject<Observable<any>>(EMPTY);
|
||||
let observable$ = subject$.pipe(switchMap((stream$) => stream$));
|
||||
observable$ = fn(observable$);
|
||||
|
||||
super((subscriber) => observable$.subscribe(subscriber));
|
||||
this.subject$ = subject$;
|
||||
}
|
||||
|
||||
switch(newObservable$: Observable<T>) {
|
||||
this.subject$.next(newObservable$);
|
||||
}
|
||||
|
||||
complete() {
|
||||
this.switch(EMPTY);
|
||||
this.subject$.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
interface WithValues<T> {
|
||||
values(): IterableIterator<T>;
|
||||
}
|
||||
|
||||
interface Completable {
|
||||
complete(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go over collection and complete each item
|
||||
* @param collection
|
||||
*/
|
||||
export function complete(collection: WithValues<Completable>) {
|
||||
for (const item of collection.values()) {
|
||||
item.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { filter } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Just as filter, but opposite
|
||||
*/
|
||||
export function exclude<T>(predicate: (...args) => boolean) {
|
||||
return filter((...args) => !predicate(...args));
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { filter, pipe } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
interface EventValue<T> {
|
||||
event: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export function onlyEvent<T>(event: any) {
|
||||
return pipe(
|
||||
filter<EventValue<T>>((obj) => obj.event === event),
|
||||
map((event) => event.data),
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -33,7 +33,7 @@ export function getEngineName(): string {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2024.11.4',
|
||||
version: '2025.1.3',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser: getBrowserExecutablePath(),
|
||||
|
||||
@@ -7,7 +7,7 @@ __metadata:
|
||||
|
||||
"@adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-678":
|
||||
version: 6.7.8
|
||||
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=e2d6246da89a4775a8482fbe8de2a42b8421516d"
|
||||
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=04738f528321857efe837e3f93c73059878ecdcb"
|
||||
dependencies:
|
||||
"@adiwajshing/keyed-db": ^0.2.4
|
||||
"@hapi/boom": ^9.1.3
|
||||
@@ -39,7 +39,7 @@ __metadata:
|
||||
optional: true
|
||||
sharp:
|
||||
optional: true
|
||||
checksum: 43ac116fcc3eca176ff036a296c99d8be39bdcaafb788fb65305ee06d59060f0b8b8d5dee8596d9dca9794843dd76e90769e5acfa6d14ff55ad2a0f2616686e3
|
||||
checksum: 0f7b2031eeb25137c65aee3151028f7a8637cfed0763c7426817415ec818ca71f55267b9de88511905bd266f4006301a26206dac9bdb1821d13f1a0cbf075e32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1242,6 +1242,30 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grpc/grpc-js@npm:^1.12.4":
|
||||
version: 1.12.4
|
||||
resolution: "@grpc/grpc-js@npm:1.12.4"
|
||||
dependencies:
|
||||
"@grpc/proto-loader": ^0.7.13
|
||||
"@js-sdsl/ordered-map": ^4.4.2
|
||||
checksum: 6dab69c70173735b775717361662ed5a931d2ca1bc9f3ed5f7095a05f26eb59489b1805b0a8f78b77f34d48a896f358ca951f0ce38d8053d18b0d5d0fd92837f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grpc/proto-loader@npm:^0.7.13":
|
||||
version: 0.7.13
|
||||
resolution: "@grpc/proto-loader@npm:0.7.13"
|
||||
dependencies:
|
||||
lodash.camelcase: ^4.3.0
|
||||
long: ^5.0.0
|
||||
protobufjs: ^7.2.5
|
||||
yargs: ^17.7.2
|
||||
bin:
|
||||
proto-loader-gen-types: build/bin/proto-loader-gen-types.js
|
||||
checksum: 399c1b8a4627f93dc31660d9636ea6bf58be5675cc7581e3df56a249369e5be02c6cd0d642c5332b0d5673bc8621619bc06fb045aa3e8f57383737b5d35930dc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@hapi/boom@npm:^9.1.3":
|
||||
version: 9.1.4
|
||||
resolution: "@hapi/boom@npm:9.1.4"
|
||||
@@ -1780,6 +1804,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@js-sdsl/ordered-map@npm:^4.4.2":
|
||||
version: 4.4.2
|
||||
resolution: "@js-sdsl/ordered-map@npm:4.4.2"
|
||||
checksum: a927ae4ff8565ecb75355cc6886a4f8fadbf2af1268143c96c0cce3ba01261d241c3f4ba77f21f3f017a00f91dfe9e0673e95f830255945c80a0e96c6d30508a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lukeed/csprng@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "@lukeed/csprng@npm:1.1.0"
|
||||
@@ -1787,6 +1818,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mapbox/node-pre-gyp@npm:^1.0.5":
|
||||
version: 1.0.11
|
||||
resolution: "@mapbox/node-pre-gyp@npm:1.0.11"
|
||||
dependencies:
|
||||
detect-libc: ^2.0.0
|
||||
https-proxy-agent: ^5.0.0
|
||||
make-dir: ^3.1.0
|
||||
node-fetch: ^2.6.7
|
||||
nopt: ^5.0.0
|
||||
npmlog: ^5.0.1
|
||||
rimraf: ^3.0.2
|
||||
semver: ^7.3.5
|
||||
tar: ^6.1.11
|
||||
bin:
|
||||
node-pre-gyp: bin/node-pre-gyp
|
||||
checksum: b848f6abc531a11961d780db813cc510ca5a5b6bf3184d72134089c6875a91c44d571ba6c1879470020803f7803609e7b2e6e429651c026fe202facd11d444b8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@microsoft/tsdoc@npm:^0.14.2":
|
||||
version: 0.14.2
|
||||
resolution: "@microsoft/tsdoc@npm:0.14.2"
|
||||
@@ -3675,6 +3725,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"abbrev@npm:1":
|
||||
version: 1.1.1
|
||||
resolution: "abbrev@npm:1.1.1"
|
||||
checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"abbrev@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "abbrev@npm:2.0.0"
|
||||
@@ -3753,6 +3810,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"agent-base@npm:6":
|
||||
version: 6.0.2
|
||||
resolution: "agent-base@npm:6.0.2"
|
||||
dependencies:
|
||||
debug: 4
|
||||
checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1":
|
||||
version: 7.1.1
|
||||
resolution: "agent-base@npm:7.1.1"
|
||||
@@ -3935,6 +4001,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"aproba@npm:^1.0.3 || ^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "aproba@npm:2.0.0"
|
||||
checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"archiver-utils@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "archiver-utils@npm:2.1.0"
|
||||
@@ -3986,6 +4059,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"are-we-there-yet@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "are-we-there-yet@npm:2.0.0"
|
||||
dependencies:
|
||||
delegates: ^1.0.0
|
||||
readable-stream: ^3.6.0
|
||||
checksum: 6c80b4fd04ecee6ba6e737e0b72a4b41bdc64b7d279edfc998678567ff583c8df27e27523bc789f2c99be603ffa9eaa612803da1d886962d2086e7ff6fa90c7c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"arg@npm:^4.1.0":
|
||||
version: 4.1.3
|
||||
resolution: "arg@npm:4.1.3"
|
||||
@@ -5077,6 +5160,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"color-support@npm:^1.1.2":
|
||||
version: 1.1.3
|
||||
resolution: "color-support@npm:1.1.3"
|
||||
bin:
|
||||
color-support: bin.js
|
||||
checksum: 9b7356817670b9a13a26ca5af1c21615463b500783b739b7634a0c2047c16cef4b2865d7576875c31c3cddf9dd621fa19285e628f20198b233a5cfdda6d0793b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"color@npm:^4.2.3":
|
||||
version: 4.2.3
|
||||
resolution: "color@npm:4.2.3"
|
||||
@@ -5176,6 +5268,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "console-control-strings@npm:1.1.0"
|
||||
checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"content-disposition@npm:0.5.4":
|
||||
version: 0.5.4
|
||||
resolution: "content-disposition@npm:0.5.4"
|
||||
@@ -5572,6 +5671,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"delegates@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "delegates@npm:1.0.0"
|
||||
checksum: a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"depd@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "depd@npm:2.0.0"
|
||||
@@ -6873,6 +6979,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gauge@npm:^3.0.0":
|
||||
version: 3.0.2
|
||||
resolution: "gauge@npm:3.0.2"
|
||||
dependencies:
|
||||
aproba: ^1.0.3 || ^2.0.0
|
||||
color-support: ^1.1.2
|
||||
console-control-strings: ^1.0.0
|
||||
has-unicode: ^2.0.1
|
||||
object-assign: ^4.1.1
|
||||
signal-exit: ^3.0.0
|
||||
string-width: ^4.2.3
|
||||
strip-ansi: ^6.0.1
|
||||
wide-align: ^1.1.2
|
||||
checksum: 81296c00c7410cdd48f997800155fbead4f32e4f82109be0719c63edc8560e6579946cc8abd04205297640691ec26d21b578837fd13a4e96288ab4b40b1dc3e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gensync@npm:^1.0.0-beta.2":
|
||||
version: 1.0.0-beta.2
|
||||
resolution: "gensync@npm:1.0.0-beta.2"
|
||||
@@ -7065,6 +7188,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"google-protobuf@npm:^3.21.4":
|
||||
version: 3.21.4
|
||||
resolution: "google-protobuf@npm:3.21.4"
|
||||
checksum: 048fa2cb579f5f88c977774b2ae36851807379d9329a6895fe3685df69ba6c927e2ff463d08d5eecd56becd9c65bca406f34f90e27984a3077e8629bb3a2a766
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gopd@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "gopd@npm:1.0.1"
|
||||
@@ -7081,6 +7211,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"grpc-tools@npm:^1.12.4":
|
||||
version: 1.12.4
|
||||
resolution: "grpc-tools@npm:1.12.4"
|
||||
dependencies:
|
||||
"@mapbox/node-pre-gyp": ^1.0.5
|
||||
bin:
|
||||
grpc_tools_node_protoc: bin/protoc.js
|
||||
grpc_tools_node_protoc_plugin: bin/protoc_plugin.js
|
||||
checksum: 5ea946c2d231e2b58be48b14e12f2982b44845f62d05327cf84f67fae30355c7a781d8443b0988f9db77d4eb219f15e5f98e869f6a14c4276084726e48dc8ec0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "has-bigints@npm:1.0.2"
|
||||
@@ -7134,6 +7276,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-unicode@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "has-unicode@npm:2.0.1"
|
||||
checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hasown@npm:^2.0.0, hasown@npm:^2.0.1, hasown@npm:^2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "hasown@npm:2.0.2"
|
||||
@@ -7199,6 +7348,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"https-proxy-agent@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "https-proxy-agent@npm:5.0.1"
|
||||
dependencies:
|
||||
agent-base: 6
|
||||
debug: 4
|
||||
checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2":
|
||||
version: 7.0.4
|
||||
resolution: "https-proxy-agent@npm:7.0.4"
|
||||
@@ -8474,13 +8633,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"libsignal@github:WhiskeySockets/libsignal-node":
|
||||
"libsignal@github:devlikeapro/libsignal-node#fork-master":
|
||||
version: 2.0.1
|
||||
resolution: "libsignal@https://github.com/WhiskeySockets/libsignal-node.git#commit=83a3e3a3864511cb74df1b796373f0d49d071134"
|
||||
resolution: "libsignal@https://github.com/devlikeapro/libsignal-node.git#commit=83bc9236ee3d96ae1747cf3ca2e505b10cf97250"
|
||||
dependencies:
|
||||
curve25519-js: ^0.0.4
|
||||
protobufjs: 6.8.8
|
||||
checksum: bc8a8c4d54395e6b3f7224e72fe0c447859eed688cc28da3a56d76dcd5bf1ea71b25b3ff0da96646b636f2bb679898ca38eff909cb83ef3e3b017fe81e06ed50
|
||||
checksum: 3ee39bc7cbd123fa2c1f84ff53ab4e3dacfb5a00d10944960fd72156b24a5eb5af1fa5ca31b9f25b26aab107c712d8c295c85b0bed26ad6eba6d0e3f26f38927
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8537,6 +8696,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.camelcase@npm:^4.3.0":
|
||||
version: 4.3.0
|
||||
resolution: "lodash.camelcase@npm:4.3.0"
|
||||
checksum: cb9227612f71b83e42de93eccf1232feeb25e705bdb19ba26c04f91e885bfd3dd5c517c4a97137658190581d3493ea3973072ca010aab7e301046d90740393d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.clonedeep@npm:^4.5.0":
|
||||
version: 4.5.0
|
||||
resolution: "lodash.clonedeep@npm:4.5.0"
|
||||
@@ -8663,6 +8829,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-dir@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "make-dir@npm:3.1.0"
|
||||
dependencies:
|
||||
semver: ^6.0.0
|
||||
checksum: 484200020ab5a1fdf12f393fe5f385fc8e4378824c940fba1729dcd198ae4ff24867bc7a5646331e50cead8abff5d9270c456314386e629acec6dff4b8016b78
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-dir@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "make-dir@npm:4.0.0"
|
||||
@@ -9206,7 +9381,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.9":
|
||||
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9":
|
||||
version: 2.7.0
|
||||
resolution: "node-fetch@npm:2.7.0"
|
||||
dependencies:
|
||||
@@ -9279,6 +9454,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nopt@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "nopt@npm:5.0.0"
|
||||
dependencies:
|
||||
abbrev: 1
|
||||
bin:
|
||||
nopt: bin/nopt.js
|
||||
checksum: d35fdec187269503843924e0114c0c6533fb54bbf1620d0f28b4b60ba01712d6687f62565c55cc20a504eff0fbe5c63e22340c3fad549ad40469ffb611b04f2f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nopt@npm:^7.0.0":
|
||||
version: 7.2.1
|
||||
resolution: "nopt@npm:7.2.1"
|
||||
@@ -9306,6 +9492,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"npmlog@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "npmlog@npm:5.0.1"
|
||||
dependencies:
|
||||
are-we-there-yet: ^2.0.0
|
||||
console-control-strings: ^1.1.0
|
||||
gauge: ^3.0.0
|
||||
set-blocking: ^2.0.0
|
||||
checksum: 516b2663028761f062d13e8beb3f00069c5664925871a9b57989642ebe09f23ab02145bf3ab88da7866c4e112cafff72401f61a672c7c8a20edc585a7016ef5f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nth-check@npm:^2.0.1":
|
||||
version: 2.1.1
|
||||
resolution: "nth-check@npm:2.1.1"
|
||||
@@ -10087,6 +10285,35 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"protobufjs@npm:^7.2.5":
|
||||
version: 7.4.0
|
||||
resolution: "protobufjs@npm:7.4.0"
|
||||
dependencies:
|
||||
"@protobufjs/aspromise": ^1.1.2
|
||||
"@protobufjs/base64": ^1.1.2
|
||||
"@protobufjs/codegen": ^2.0.4
|
||||
"@protobufjs/eventemitter": ^1.1.0
|
||||
"@protobufjs/fetch": ^1.1.0
|
||||
"@protobufjs/float": ^1.0.2
|
||||
"@protobufjs/inquire": ^1.1.0
|
||||
"@protobufjs/path": ^1.1.2
|
||||
"@protobufjs/pool": ^1.1.0
|
||||
"@protobufjs/utf8": ^1.1.0
|
||||
"@types/node": ">=13.7.0"
|
||||
long: ^5.0.0
|
||||
checksum: ba0e6b60541bbf818bb148e90f5eb68bd99004e29a6034ad9895a381cbd352be8dce5376e47ae21b2e05559f2505b4a5f4a3c8fa62402822c6ab4dcdfb89ffb3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"protoc-gen-ts@npm:^0.8.7":
|
||||
version: 0.8.7
|
||||
resolution: "protoc-gen-ts@npm:0.8.7"
|
||||
bin:
|
||||
protoc-gen-ts: protoc-gen-ts.js
|
||||
checksum: 556d89ef43b152b9f457dab79d51f649f5108fb1ef447da41f0c3b6f33df1a5bd8ef3392c0ce9b25730e68ccb0c972a52ca37d50775ce4602a94b502aeab1e88
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"proxy-addr@npm:~2.0.7":
|
||||
version: 2.0.7
|
||||
resolution: "proxy-addr@npm:2.0.7"
|
||||
@@ -10619,7 +10846,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rxjs@npm:7.8.1, rxjs@npm:^7.1.0, rxjs@npm:^7.5.5":
|
||||
"rxjs@npm:7.8.1, rxjs@npm:^7.5.5, rxjs@npm:^7.8.1":
|
||||
version: 7.8.1
|
||||
resolution: "rxjs@npm:7.8.1"
|
||||
dependencies:
|
||||
@@ -10923,7 +11150,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7":
|
||||
"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7":
|
||||
version: 3.0.7
|
||||
resolution: "signal-exit@npm:3.0.7"
|
||||
checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318
|
||||
@@ -11185,7 +11412,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3":
|
||||
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3":
|
||||
version: 4.2.3
|
||||
resolution: "string-width@npm:4.2.3"
|
||||
dependencies:
|
||||
@@ -12018,6 +12245,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ulid@npm:^2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "ulid@npm:2.3.0"
|
||||
bin:
|
||||
ulid: ./bin/cli.js
|
||||
checksum: d6dbf253fdc189f60fe2829d934ee5447b3dab62d05449a2e0fe89670d77087dd6eba4f844a69f9ffdb01384ec6fd97bdd9be638fc67d593569a45e8969f1e69
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unbox-primitive@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "unbox-primitive@npm:1.0.2"
|
||||
@@ -12216,6 +12452,8 @@ __metadata:
|
||||
"@adiwajshing/keyed-db": ^0.2.4
|
||||
"@aws-sdk/client-s3": ^3.633.0
|
||||
"@aws-sdk/s3-request-presigner": ^3.633.0
|
||||
"@grpc/grpc-js": ^1.12.4
|
||||
"@grpc/proto-loader": ^0.7.13
|
||||
"@nestjs/axios": ^3.0.2
|
||||
"@nestjs/cli": ^9.0.0
|
||||
"@nestjs/common": ^9.0.9
|
||||
@@ -12258,11 +12496,14 @@ __metadata:
|
||||
express-basic-auth: ^1.2.1
|
||||
file-type: 16.5.4
|
||||
fs-extra: ^11.2.0
|
||||
google-protobuf: ^3.21.4
|
||||
grpc-tools: ^1.12.4
|
||||
https-proxy-agent: ^7.0.0
|
||||
jest: ^29.7.0
|
||||
joi: ^17.13.3
|
||||
knex: ^3.1.0
|
||||
libphonenumber-js: ^1.10.36
|
||||
libsignal: "github:devlikeapro/libsignal-node#fork-master"
|
||||
link-preview-js: ^3.0.4
|
||||
lodash: ^4.17.21
|
||||
mime-types: ^2.1.27
|
||||
@@ -12275,12 +12516,13 @@ __metadata:
|
||||
prettier: ^1.19.1
|
||||
pretty-bytes: 5.6.0
|
||||
promise-retry: ^2.0.1
|
||||
protoc-gen-ts: ^0.8.7
|
||||
puppeteer: ^23.6.0
|
||||
qrcode: ^1.5.1
|
||||
qrcode-terminal: ^0.12.0
|
||||
reflect-metadata: ^0.1.13
|
||||
rimraf: ^3.0.2
|
||||
rxjs: ^7.1.0
|
||||
rxjs: ^7.8.1
|
||||
sharp: ^0.33.4
|
||||
supertest: ^4.0.2
|
||||
swagger-ui-express: ^4.1.4
|
||||
@@ -12289,7 +12531,9 @@ __metadata:
|
||||
ts-node: ^10.9.1
|
||||
tsconfig-paths: ^4.1.0
|
||||
typescript: ^4.8.4
|
||||
ulid: ^2.3.0
|
||||
whatsapp-web.js: "github:devlikeapro/whatsapp-web.js#fork-main-channels"
|
||||
write-file-atomic: ^6.0.0
|
||||
dependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
@@ -12384,7 +12628,7 @@ __metadata:
|
||||
|
||||
"whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-channels":
|
||||
version: 1.26.0
|
||||
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=aa90f7378659a237f30cda6d368359a1f81904dc"
|
||||
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=a8683b0d1a5b8025ac1e7deb3476042ceadb4c07"
|
||||
dependencies:
|
||||
"@pedroslopez/moduleraid": ^5.0.2
|
||||
archiver: ^5.3.1
|
||||
@@ -12402,7 +12646,7 @@ __metadata:
|
||||
optional: true
|
||||
unzipper:
|
||||
optional: true
|
||||
checksum: 7c914e9cdd4a25c967e057d0cb5019973f0556f6220320e4bcbc0666669801b92abf97c2cc783762ef462d0006123953e233136f457689ebac395260fe69a5fc
|
||||
checksum: 637e53dffdaa80813a5cdd72e519f8f68978e00db17b39b471f39c8e2b98bc85643fe74168255a1dfdc655c71803e3d01b4ca6635bc6159c429dd59232ac07d4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -12482,6 +12726,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"wide-align@npm:^1.1.2":
|
||||
version: 1.1.5
|
||||
resolution: "wide-align@npm:1.1.5"
|
||||
dependencies:
|
||||
string-width: ^1.0.2 || 2 || 3 || 4
|
||||
checksum: d5fc37cd561f9daee3c80e03b92ed3e84d80dde3365a8767263d03dacfc8fa06b065ffe1df00d8c2a09f731482fcacae745abfbb478d4af36d0a891fad4834d3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"widest-line@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "widest-line@npm:3.1.0"
|
||||
@@ -12557,6 +12810,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"write-file-atomic@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "write-file-atomic@npm:6.0.0"
|
||||
dependencies:
|
||||
imurmurhash: ^0.1.4
|
||||
signal-exit: ^4.0.1
|
||||
checksum: 35f1303b0229c89c36d0817de9912b43a242f775cb0f386fecf97bac735013e1fde5f464c2ce9f63288d2c91b1ec5bc18d55347b0e37c0e4dbc64b60dc220629
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"write@npm:1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "write@npm:1.0.3"
|
||||
|
||||
Reference in new issue
Block a user