Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faf3c77508 | ||
|
|
e6fdfb88ca | ||
|
|
99746df626 | ||
|
|
43aeeb55e8 | ||
|
|
4f89f49d44 | ||
|
|
be583e7e40 | ||
|
|
e8c8f89733 | ||
|
|
13b6d3ddf7 | ||
|
|
5e97fb74b1 | ||
|
|
cc47321cad | ||
|
|
607dff8f7a | ||
|
|
0a74f8dbcd | ||
|
|
796befcf87 | ||
|
|
2c5610dd01 | ||
|
|
43d110fa31 | ||
|
|
bf52dd4874 | ||
|
|
f8268180bf | ||
|
|
04cf9bc8c0 | ||
|
|
ff38cf7660 | ||
|
|
4c0b44c4b5 | ||
|
|
e75c29d57f | ||
|
|
f8329c29b1 | ||
|
|
52222a6b98 | ||
|
|
99a0a19d8c | ||
|
|
48ed33bc94 | ||
|
|
b234e134bf | ||
|
|
f6004fd294 | ||
|
|
91d541387b | ||
|
|
9dbd45da6e | ||
|
|
adc1c3960c | ||
|
|
ff742d2e84 | ||
|
|
10bd4d6271 | ||
|
|
874cafe114 | ||
|
|
0ce2c4b9fa | ||
|
|
a8a7988bfa | ||
|
|
2940d93b81 | ||
|
|
88707cf72b | ||
|
|
b0f883c6a8 | ||
|
|
4cddb766c2 | ||
|
|
6675492901 | ||
|
|
a9d030af2a | ||
|
|
2782a188a9 | ||
|
|
256688497e | ||
|
|
4def193c57 | ||
|
|
7f3a2e6aa5 | ||
|
|
60e4f13b67 | ||
|
|
2d6cfeb0fa | ||
|
|
03e85434df | ||
|
|
0ce4c01c48 | ||
|
|
0e95890ca4 | ||
|
|
072f4b2ab0 | ||
|
|
47a91ef669 | ||
|
|
5143bd1ef8 | ||
|
|
0efc0751c9 | ||
|
|
51dda739cb | ||
|
|
3b28826679 | ||
|
|
81646bbaa2 | ||
|
|
d8d84107fb | ||
|
|
58d917d374 | ||
|
|
f8f0063975 | ||
|
|
73faf330e3 | ||
|
|
5a1ebb186d | ||
|
|
54d6d715b0 | ||
|
|
5d9d6e29e2 | ||
|
|
cf3385f624 | ||
|
|
0346c5e7b9 | ||
|
|
443652b0a7 | ||
|
|
1e73c3dc6a | ||
|
|
afa9d4208f | ||
|
|
64eda25b5d | ||
|
|
cfc6c931a3 | ||
|
|
35bc56c948 | ||
|
|
371bde4c3e | ||
|
|
9497e2900e | ||
|
|
51bbc9998a | ||
|
|
057f7bce56 | ||
|
|
52412fb4cc | ||
|
|
a57f3443de | ||
|
|
9b2537e7c7 | ||
|
|
a378ad4c3d | ||
|
|
8f867e0b5d | ||
|
|
d4f6ec18fd | ||
|
|
ac05dd83e9 | ||
|
|
17c22f0cd4 | ||
|
|
971979c13f | ||
|
|
845e203637 | ||
|
|
7a2e1cd731 | ||
|
|
84ba011a4c | ||
|
|
50399f1192 | ||
|
|
a2517b248d | ||
|
|
f02d95331a | ||
|
|
cb1faff88c | ||
|
|
3ff1c06eea | ||
|
|
c91d2c59eb | ||
|
|
4cf5a74d23 | ||
|
|
13de42dbe0 | ||
|
|
f99b86ed18 | ||
|
|
07f6c78372 | ||
|
|
83678553e3 | ||
|
|
9d1bbd780e | ||
|
|
899f9f2e84 | ||
|
|
1d1a083464 | ||
|
|
fabc6753e6 | ||
|
|
a16f3b67fe | ||
|
|
c0c3381a36 | ||
|
|
212a27d9df | ||
|
|
a18c1d82c1 | ||
|
|
67349e4509 | ||
|
|
293267536b |
No files matched your search
@@ -22,6 +22,9 @@ WHATSAPP_SWAGGER_ENABLED=True
|
||||
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
|
||||
WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
|
||||
# "Firefox (YourApp)" in Linked Devices
|
||||
# WAHA_CLIENT_DEVICE_NAME=YourApp
|
||||
|
||||
# Base URL for the API (used for webhooks, file URLs, etc.)
|
||||
WAHA_BASE_URL=http://localhost:3000
|
||||
# WAHA_PUBLIC_URL=http://localhost:3000
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
# Chromium - x86
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'latest'
|
||||
platform: 'amd64'
|
||||
browser: 'chromium'
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chromium - ARM
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
- runner: 'ubuntu-24.04-arm'
|
||||
tag: 'arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'chromium'
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
goss: 'goss-linux-arm'
|
||||
|
||||
# Chrome - x86
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'chrome'
|
||||
platform: 'amd64'
|
||||
browser: 'chrome'
|
||||
@@ -39,14 +39,14 @@ jobs:
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chrome - ARM (Chrome is not available for ARM)
|
||||
#- runner: "buildjet-4vcpu-ubuntu-2204-arm"
|
||||
#- runner: "ubuntu-24.04-arm"
|
||||
# tag: "chrome-arm"
|
||||
# platform: "linux/arm64"
|
||||
# browser: "chrome"
|
||||
# goss: 'goss-linux-arm'
|
||||
|
||||
# NOWEB - x86
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'noweb'
|
||||
platform: 'amd64'
|
||||
browser: 'none'
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# NOWEB - ARM
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
- runner: 'ubuntu-24.04-arm'
|
||||
tag: 'noweb-arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'none'
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
goss: 'goss-linux-arm'
|
||||
|
||||
# GOWS - x86
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'gows'
|
||||
platform: 'amd64'
|
||||
browser: 'none'
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# No browser - ARM
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
- runner: 'ubuntu-24.04-arm'
|
||||
tag: 'gows-arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'none'
|
||||
@@ -100,7 +100,51 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
- name: Build (attempt 1)
|
||||
id: build1
|
||||
continue-on-error: true
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Wait to retry build
|
||||
if: steps.build1.outcome == 'failure'
|
||||
run: |
|
||||
sleep 10
|
||||
|
||||
- name: Build (attempt 2)
|
||||
id: build2
|
||||
if: steps.build1.outcome == 'failure'
|
||||
continue-on-error: true
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Wait to retry build
|
||||
if: steps.build2.outcome == 'failure'
|
||||
run: |
|
||||
sleep 10
|
||||
|
||||
- name: Build (attempt 3)
|
||||
if: steps.build2.outcome == 'failure'
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
|
||||
@@ -6,8 +6,37 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# unit-tests:
|
||||
# if: github.repository == 'devlikeapro/waha-plus'
|
||||
# runs-on: ubuntu-22.04
|
||||
# name: Unit tests
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
#
|
||||
# - name: Set up Node
|
||||
# uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: 22
|
||||
#
|
||||
# - name: Enable Corepack
|
||||
# run: |
|
||||
# corepack enable
|
||||
# corepack prepare yarn@4.9.2 --activate
|
||||
#
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# unset GIT_CONFIG_PARAMETERS
|
||||
# yarn install --immutable
|
||||
#
|
||||
# - name: Run unit tests
|
||||
# run: yarn test:unit
|
||||
|
||||
docker-build:
|
||||
if: github.repository == 'devlikeapro/waha-plus'
|
||||
# needs: unit-tests
|
||||
runs-on: ${{ matrix.runner }}
|
||||
name:
|
||||
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
|
||||
@@ -15,13 +44,24 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
# Chromium - x86
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'dev'
|
||||
platform: 'amd64'
|
||||
browser: 'chromium'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-amd64'
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
|
||||
# Chrome - x86
|
||||
- runner: 'ubuntu-latest'
|
||||
tag: 'dev-chrome'
|
||||
platform: 'amd64'
|
||||
browser: 'chrome'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chromium - ARM
|
||||
- runner: 'ubuntu-24.04-arm'
|
||||
tag: 'dev-arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'chromium'
|
||||
@@ -71,8 +111,60 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
- name: Build (attempt 1)
|
||||
id: build1
|
||||
if: ${{ steps.recent.outputs.has_recent == 'true' }}
|
||||
continue-on-error: true
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Wait to retry build
|
||||
if:
|
||||
${{ steps.recent.outputs.has_recent == 'true' && steps.build1.outcome
|
||||
== 'failure' }}
|
||||
run: |
|
||||
sleep 10
|
||||
|
||||
- name: Build (attempt 2)
|
||||
id: build2
|
||||
if:
|
||||
${{ steps.recent.outputs.has_recent == 'true' && steps.build1.outcome
|
||||
== 'failure' }}
|
||||
continue-on-error: true
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Wait to retry build
|
||||
if:
|
||||
${{ steps.recent.outputs.has_recent == 'true' && steps.build2.outcome
|
||||
== 'failure' }}
|
||||
run: |
|
||||
sleep 10
|
||||
|
||||
- name: Build (attempt 3)
|
||||
if:
|
||||
${{ steps.recent.outputs.has_recent == 'true' && steps.build2.outcome
|
||||
== 'failure' }}
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
This guide summarizes how to explore, modify, and validate the WhatsApp HTTP API
|
||||
(WAHA) codebase when assisting as an automation or coding agent.
|
||||
|
||||
- When you asked to refactor or "apply new lib" instead of current one - do not
|
||||
change the behavior, make a minimum amount of changes possible. If you see
|
||||
some edge case during that process that haven't been covered - tell it,
|
||||
suggest fix, but don't change the code.
|
||||
|
||||
## Product & Variants
|
||||
|
||||
- WAHA ships in **Core** and **Plus** editions. Core lives under `src/core` and
|
||||
@@ -70,6 +75,7 @@ This guide summarizes how to explore, modify, and validate the WhatsApp HTTP API
|
||||
`src/utils/logging.ts`. `console.log` is blocked by pre-commit.
|
||||
- Respect path aliases (`@waha/...`) defined in `tsconfig.json`; keep imports
|
||||
consistent (use absolute aliases, not relative `../../../`).
|
||||
- Prefer named function declarations over `const` arrow functions when possible.
|
||||
- Avoid naming unused variables with a leading underscore; if a parameter is
|
||||
required by a signature, explicitly `void` it instead.
|
||||
- For configs, prefer runtime configurability over constants. Environment keys
|
||||
@@ -126,7 +132,9 @@ You can find related source code in the following paths:
|
||||
|
||||
- WEBJS: `../whatsapp-web.js`
|
||||
- NOWEB: `../WhiskeySockets-Baileys`
|
||||
- GOWS: `../gows` + `../whatsmeow`
|
||||
- whatsapp-rust-bridge - `../whatsapp-rust-bridge`
|
||||
- GOWS: `../gows`
|
||||
- whatsmeow - `../whatsmeow`
|
||||
- ChatWoot: `../chatwoot`
|
||||
|
||||
Following this playbook keeps contributions aligned with WAHA’s structure,
|
||||
|
||||
+20
-1
@@ -6,9 +6,28 @@ ARG GOLANG_IMAGE_TAG=1.24-bookworm
|
||||
#
|
||||
FROM node:${NODE_IMAGE_TAG} AS build
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
ENV RUST_BUN_VERSION=1.3.9
|
||||
ENV RUSTUP_TOOLCHAIN=nightly-2026-01-30
|
||||
ENV RUST_WASM_PACK_VERSION=0.14.0
|
||||
ENV RUST_BUN_INSTALL=/root/.bun
|
||||
ENV PATH=${RUST_BUN_INSTALL}/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
# git + build toolchain for git deps
|
||||
RUN apt-get update && apt-get install -y git python3 build-essential && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends git python3 build-essential curl ca-certificates unzip && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# bun + rust toolchains for whatsapp-rust-bridge prepare scripts
|
||||
RUN set -eux; \
|
||||
mkdir -p "${RUST_BUN_INSTALL}"; \
|
||||
curl -fsSL https://bun.sh/install | bash -s -- bun-v${RUST_BUN_VERSION}; \
|
||||
curl -fsSL https://sh.rustup.rs | bash -s -- -y --default-toolchain ${RUSTUP_TOOLCHAIN}; \
|
||||
/root/.cargo/bin/rustup target add wasm32-unknown-unknown; \
|
||||
/root/.cargo/bin/cargo install wasm-pack --vers ${RUST_WASM_PACK_VERSION} --locked; \
|
||||
"${RUST_BUN_INSTALL}/bin/bun" --version; \
|
||||
/root/.cargo/bin/cargo --version; \
|
||||
/root/.cargo/bin/rustc --version; \
|
||||
/root/.cargo/bin/wasm-pack --version
|
||||
|
||||
# npm packages
|
||||
WORKDIR /git
|
||||
|
||||
@@ -32,16 +32,19 @@ push:
|
||||
docker push devlikeapro/waha
|
||||
|
||||
for-swagger:
|
||||
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true WHATSAPP_SWAGGER_PASSWORD=666 nvm exec yarn start
|
||||
export WHATSAPP_SWAGGER_CONFIG_ADVANCED=true && export WHATSAPP_SWAGGER_PASSWORD=666 && yarn start
|
||||
|
||||
up-noweb:
|
||||
yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-12-17
|
||||
yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2026-02-11
|
||||
|
||||
up-noweb-libsignal:
|
||||
yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
|
||||
|
||||
up-webjs:
|
||||
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-12-17
|
||||
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-02-01
|
||||
|
||||
up-rust-bridge:
|
||||
yarn up -R whatsapp-rust-bridge
|
||||
|
||||
start-proxy:
|
||||
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
|
||||
@@ -57,4 +60,3 @@ gows:
|
||||
copy-dashboard:
|
||||
cd ../waha-hub/ui && \
|
||||
make copy-waha
|
||||
|
||||
@@ -147,7 +147,18 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
|
||||
## Start the project
|
||||
1. Clone the repository
|
||||
2. Make sure you're using node>=22 (check [.nvmrc](/.nvmrc) to get the version)
|
||||
3. Run the following commands:
|
||||
3. Install the **whatsapp-rust-bridge prerequisites**:
|
||||
```bash
|
||||
# Bun runtime used by whatsapp-rust-bridge prepare scripts
|
||||
curl -fsSL https://bun.sh/install | bash -s -- bun-v1.3.9
|
||||
|
||||
# Rust nightly toolchain used for whatsapp-rust-bridge
|
||||
curl -fsSL https://sh.rustup.rs | bash -s -- -y --default-toolchain nightly-2026-01-30
|
||||
rustup target add wasm32-unknown-unknown
|
||||
cargo install wasm-pack --vers 0.14.0 --locked
|
||||
```
|
||||
|
||||
4. Run the following commands:
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
|
||||
@@ -32,6 +32,8 @@ WAHA_PUBLIC_URL=http://localhost:3000
|
||||
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
|
||||
WHATSAPP_DEFAULT_ENGINE=GOWS
|
||||
|
||||
# WAHA_CLIENT_DEVICE_NAME=YourApp
|
||||
|
||||
# =================
|
||||
# ===== APPS ======
|
||||
# =================
|
||||
|
||||
@@ -75,7 +75,6 @@ services:
|
||||
- POSTGRES_USER=postgres
|
||||
# Please provide your own password.
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
|
||||
+54
-23
@@ -16,23 +16,27 @@
|
||||
"start:prod-exit": "node dist/main",
|
||||
"lint": "oxlint --deny-warnings",
|
||||
"lint-fix": "oxlint --fix --deny-warnings",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"test": "yarn test:unit",
|
||||
"test:watch": "jest --selectProjects unit --watch",
|
||||
"test:cov": "jest --selectProjects unit --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --selectProjects unit --runInBand",
|
||||
"test:unit": "jest --selectProjects unit",
|
||||
"test:e2e": "jest --selectProjects e2e",
|
||||
"test:e8e": "yarn test:e2e",
|
||||
"test:all": "yarn test:unit && yarn test:e8e",
|
||||
"gows:proto:fetch": "node scripts/gows-proto.js fetch",
|
||||
"gows:proto:build": "node scripts/gows-proto.js build",
|
||||
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-12-17",
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2026-02-11",
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@aws-sdk/client-s3": "^3.633.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.633.0",
|
||||
"@bull-board/api": "^6.9.1",
|
||||
"@bull-board/express": "^6.9.1",
|
||||
"@bull-board/nestjs": "^6.9.1",
|
||||
"@casl/ability": "^6.8.0",
|
||||
"@figuro/chatwoot-sdk": "^1.1.17",
|
||||
"@liaoliaots/nestjs-redis": "^9",
|
||||
"@nestjs/axios": "^3.0.2",
|
||||
@@ -107,7 +111,7 @@
|
||||
"ulid": "^2.3.0",
|
||||
"undici": "^7.16.0",
|
||||
"uniqid": "^5.4.0",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-12-17",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2026-02-01",
|
||||
"write-file-atomic": "^6.0.0",
|
||||
"yaml": "^2.7.1"
|
||||
},
|
||||
@@ -120,7 +124,8 @@
|
||||
"ws": "^8.18.0",
|
||||
"puppeteer": "^24.31.0",
|
||||
"whatwg-url": "13.0.0",
|
||||
"axios": "^1.9.0"
|
||||
"axios": "^1.9.0",
|
||||
"whatsapp-rust-bridge": "github:devlikeapro/whatsapp-rust-bridge.git#fork-master-2026-02-04"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grpc/grpc-js": "^1.14.1",
|
||||
@@ -148,21 +153,47 @@
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"moduleNameMapper": {
|
||||
"^@waha/(.*)$": "<rootDir>/$1"
|
||||
},
|
||||
"testRegex": ".test.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
"projects": [
|
||||
{
|
||||
"displayName": "unit",
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"moduleNameMapper": {
|
||||
"^@waha/(.*)$": "<rootDir>/$1"
|
||||
},
|
||||
"testRegex": ".test.ts$",
|
||||
"testPathIgnorePatterns": [
|
||||
"/__tests__/e2e/"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
{
|
||||
"displayName": "e2e",
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"moduleNameMapper": {
|
||||
"^@waha/(.*)$": "<rootDir>/$1"
|
||||
},
|
||||
"testRegex": "__tests__/e2e/.*\\.test\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.9.2"
|
||||
}
|
||||
+17
-3
@@ -76,17 +76,31 @@ const updatedContent = exampleContent
|
||||
|
||||
fs.writeFileSync(targetPath, updatedContent, { encoding: 'utf8' });
|
||||
|
||||
const generatedEnvValues = [
|
||||
`WAHA_API_KEY=${apiKey}`,
|
||||
`WAHA_API_KEY_PLAIN=${apiKey}`,
|
||||
'WAHA_DASHBOARD_USERNAME=admin',
|
||||
`WAHA_DASHBOARD_PASSWORD=${adminPassword}`,
|
||||
'WHATSAPP_SWAGGER_USERNAME=admin',
|
||||
`WHATSAPP_SWAGGER_PASSWORD=${adminPassword}`,
|
||||
];
|
||||
|
||||
const lines = [
|
||||
'Credentials generated.',
|
||||
'',
|
||||
'Dashboard and Swagger:',
|
||||
'Generated env values:',
|
||||
...generatedEnvValues.map((line) => ` - ${line}`),
|
||||
'',
|
||||
'Use these credentials to login in Dashboard or Swagger:',
|
||||
' - Username: admin',
|
||||
` - Password: ${adminPassword}`,
|
||||
'',
|
||||
'API key: ',
|
||||
'Use this API key in the x-api-key header:',
|
||||
` - ${apiKey}`,
|
||||
'',
|
||||
'Use it in the Dashboard connection flow: https://waha.devlike.pro/docs/how-to/dashboard/#api-key',
|
||||
'Read more:',
|
||||
' - https://waha.devlike.pro/docs/how-to/dashboard/#api-key',
|
||||
' - https://waha.devlike.pro/docs/how-to/security/',
|
||||
];
|
||||
|
||||
console.log(lines.join('\n'));
|
||||
@@ -0,0 +1,391 @@
|
||||
import axios from 'axios';
|
||||
import WebSocket = require('ws');
|
||||
|
||||
const VALID_API_KEY = '666';
|
||||
const VALID_USERNAME = 'admin';
|
||||
const VALID_PASSWORD = '666';
|
||||
const VALID_SESSION_API_KEY = 'key_0fQfI3n3rFVsczBbBfknLXKUreY2my6J';
|
||||
|
||||
const BASE_URL = (process.env.WAHA_BASE_URL ?? 'http://localhost:3000').replace(
|
||||
/\/$/,
|
||||
'',
|
||||
);
|
||||
const WS_BASE_URL = BASE_URL.replace(/^http/, 'ws');
|
||||
|
||||
const HTTP_OK = 200;
|
||||
const HTTP_CREATED = 201;
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
const HTTP_FORBIDDEN = 403;
|
||||
const HTTP_NOT_FOUND = 404;
|
||||
const HTTP_UNPROCESSABLE = 422;
|
||||
const WS_POLICY_VIOLATION = 1008;
|
||||
const WS_OK_CODES = [1000, 1005];
|
||||
|
||||
describe('admin - GET /api/sessions/{name}', () => {
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/default`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('valid api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/default`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('invalid api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/default`, {
|
||||
headers: { 'X-Api-Key': '123' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin - POST /api/sessions', () => {
|
||||
test('admin key can create a session without name and remove it', async () => {
|
||||
const createResponse = await axios.post(
|
||||
`${BASE_URL}/api/sessions`,
|
||||
{},
|
||||
{
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
expect(createResponse.status).toBe(HTTP_CREATED);
|
||||
expect(createResponse.data?.name).toBeTruthy();
|
||||
|
||||
const deleteResponse = await axios.delete(
|
||||
`${BASE_URL}/api/sessions/${createResponse.data.name}`,
|
||||
{
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
expect(deleteResponse.status).toBe(HTTP_OK);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/sessions?all=true', () => {
|
||||
beforeAll(async () => {
|
||||
const createResponse = await axios.post(
|
||||
`${BASE_URL}/api/sessions`,
|
||||
{ name: 'another' },
|
||||
{
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
if (![HTTP_CREATED, HTTP_UNPROCESSABLE].includes(createResponse.status)) {
|
||||
throw new Error(
|
||||
`Unexpected status for creating "another" session: ${createResponse.status}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const deleteResponse = await axios.delete(
|
||||
`${BASE_URL}/api/sessions/another`,
|
||||
{
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
if (![HTTP_OK, HTTP_NOT_FOUND].includes(deleteResponse.status)) {
|
||||
throw new Error(
|
||||
`Unexpected status for deleting "another" session: ${deleteResponse.status}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('admin key returns two sessions', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions?all=true`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
expect(response.data).toHaveLength(2);
|
||||
expect(response.data.map((session) => session.name).sort()).toEqual([
|
||||
'another',
|
||||
'default',
|
||||
]);
|
||||
});
|
||||
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions?all=true`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('session key returns one session', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions?all=true`, {
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
expect(response.data).toHaveLength(1);
|
||||
expect(response.data[0]?.name).toBe('default');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/server/version', () => {
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/version`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('admin api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/version`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('session api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/version`, {
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/server/environment', () => {
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/environment`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('admin api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/environment`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('session api key is forbidden', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/server/environment`, {
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_FORBIDDEN);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin - GET /health', () => {
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/health`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('valid api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/health`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('invalid api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/health`, {
|
||||
headers: { 'X-Api-Key': '123' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('/ws', () => {
|
||||
const buildWsUrl = (apiKey?: string) => {
|
||||
const wsUrl = new URL('/ws', WS_BASE_URL);
|
||||
wsUrl.searchParams.set('session', '*');
|
||||
wsUrl.searchParams.set('events', '*');
|
||||
if (apiKey) {
|
||||
wsUrl.searchParams.set('x-api-key', apiKey);
|
||||
}
|
||||
return wsUrl.toString();
|
||||
};
|
||||
|
||||
const waitForOpen = (socket: WebSocket, timeoutMs = 5_000) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('WebSocket open timeout'));
|
||||
}, timeoutMs);
|
||||
socket.once('open', () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
socket.once('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
const waitForClose = (socket: WebSocket, timeoutMs = 5_000) =>
|
||||
new Promise<{ code: number; reason: string }>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('WebSocket close timeout'));
|
||||
}, timeoutMs);
|
||||
socket.once('close', (code, reason) => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ code, reason: reason.toString() });
|
||||
});
|
||||
socket.once('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
test('no api key is unauthorized', async () => {
|
||||
const socket = new WebSocket(buildWsUrl());
|
||||
const { code } = await waitForClose(socket);
|
||||
expect(code).toBe(WS_POLICY_VIOLATION);
|
||||
});
|
||||
|
||||
test('admin api key is ok', async () => {
|
||||
const socket = new WebSocket(buildWsUrl(VALID_API_KEY));
|
||||
await waitForOpen(socket);
|
||||
socket.close();
|
||||
const { code } = await waitForClose(socket);
|
||||
expect(WS_OK_CODES).toContain(code);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/files/test.txt', () => {
|
||||
// create "test.txt" in current dir/.media/test.txt
|
||||
let file = null;
|
||||
beforeAll(() => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mediaDir = path.resolve('./.media');
|
||||
if (!fs.existsSync(mediaDir)) {
|
||||
fs.mkdirSync(mediaDir);
|
||||
}
|
||||
const filePath = path.join(mediaDir, 'test.txt');
|
||||
fs.writeFileSync(filePath, 'This is a test file.');
|
||||
file = filePath;
|
||||
});
|
||||
test('no api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/files/test.txt`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('valid api key is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/files/test.txt`, {
|
||||
headers: { 'X-Api-Key': VALID_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('invalid api key is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/files/test.txt`, {
|
||||
headers: { 'X-Api-Key': '123' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /', () => {
|
||||
test('no auth is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('valid auth is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/`, {
|
||||
auth: { username: VALID_USERNAME, password: VALID_PASSWORD },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('wrong auth is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/`, {
|
||||
auth: { username: VALID_USERNAME, password: 'password-another' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /dashboard', () => {
|
||||
test('no auth is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/dashboard/`, {
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('valid auth is ok', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/dashboard/`, {
|
||||
auth: { username: VALID_USERNAME, password: VALID_PASSWORD },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('wrong auth is unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/dashboard/`, {
|
||||
auth: { username: VALID_USERNAME, password: 'password-another' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('session key - GET /api/sessions/{name}', () => {
|
||||
test('default key - default session - is authorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/default`, {
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_OK);
|
||||
});
|
||||
|
||||
test('default key - create a new sessions - forbidden', async () => {
|
||||
const response = await axios.post(
|
||||
`${BASE_URL}/api/sessions`,
|
||||
{ name: 'newone' },
|
||||
{
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
},
|
||||
);
|
||||
expect(response.status).toBe(HTTP_FORBIDDEN);
|
||||
});
|
||||
|
||||
test('another key - default session - unauthorized', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/default`, {
|
||||
headers: { 'X-Api-Key': 'key_another' },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_UNAUTHORIZED);
|
||||
});
|
||||
|
||||
test('default key - another session - forbidden', async () => {
|
||||
const response = await axios.get(`${BASE_URL}/api/sessions/another`, {
|
||||
headers: { 'X-Api-Key': VALID_SESSION_API_KEY },
|
||||
validateStatus: () => true,
|
||||
});
|
||||
expect(response.status).toBe(HTTP_FORBIDDEN);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
UnprocessableEntityException,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { ApiKey, CheckInvariant } from '@waha/core/storage/IApiKeyRepository';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { ApiKeyDTO, ApiKeyRequest } from '@waha/structures/apikeys.dto';
|
||||
import { generatePrefixedId, generateSecret } from '@waha/utils/ids';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/keys')
|
||||
@ApiTags('🔑 Api Keys')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanServer(Action.Manage))
|
||||
export class ApiKeysController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post('/')
|
||||
@ApiOperation({ summary: 'Create a new API key' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() body: ApiKeyRequest): Promise<ApiKeyDTO> {
|
||||
CheckInvariant(body);
|
||||
if (body.session) {
|
||||
const exists = await this.manager.exists(body.session);
|
||||
if (!exists) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Session "${body.session}" does not exist`,
|
||||
);
|
||||
}
|
||||
}
|
||||
let apikey: ApiKey | null = null;
|
||||
// Try 5 times to check there's no conflict on id and key
|
||||
for (let i = 0; i < 5; i++) {
|
||||
apikey = {
|
||||
id: generatePrefixedId('key_id'),
|
||||
key: `key_${generateSecret(32)}`,
|
||||
isActive: body.isActive,
|
||||
isAdmin: body.isAdmin,
|
||||
session: body.session,
|
||||
rules: null,
|
||||
};
|
||||
const idExists = await this.manager.apiKeyRepository.getById(apikey.id);
|
||||
if (idExists) {
|
||||
continue;
|
||||
}
|
||||
const keyExists = await this.manager.apiKeyRepository.getByKey(
|
||||
apikey.key,
|
||||
);
|
||||
if (keyExists) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!apikey) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Failed to generate API key, try again`,
|
||||
);
|
||||
}
|
||||
CheckInvariant(apikey);
|
||||
await this.manager.apiKeyRepository.upsert(apikey);
|
||||
return ApiKeyToDTO(apikey);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({ summary: 'Get all API keys' })
|
||||
async list(): Promise<ApiKeyDTO[]> {
|
||||
const keys = await this.manager.apiKeyRepository.list();
|
||||
return keys.map((key) => ApiKeyToDTO(key));
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@ApiOperation({ summary: 'Update an API key' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() body: ApiKeyRequest,
|
||||
): Promise<ApiKeyDTO> {
|
||||
const existing = await this.manager.apiKeyRepository.getById(id);
|
||||
if (!existing) {
|
||||
throw new NotFoundException('API key not found');
|
||||
}
|
||||
|
||||
const apikey: ApiKey = {
|
||||
...existing,
|
||||
isActive: body.isActive,
|
||||
isAdmin: body.isAdmin,
|
||||
session: body.session,
|
||||
};
|
||||
CheckInvariant(apikey);
|
||||
if (apikey.session) {
|
||||
const exists = await this.manager.exists(apikey.session);
|
||||
if (!exists) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Session "${apikey.session}" does not exist`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CheckInvariant(apikey);
|
||||
await this.manager.apiKeyRepository.upsert(apikey);
|
||||
return ApiKeyToDTO(apikey);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@ApiOperation({ summary: 'Delete an API key' })
|
||||
async delete(@Param('id') id: string): Promise<{ result: true }> {
|
||||
const existing = await this.manager.apiKeyRepository.getById(id);
|
||||
if (!existing) {
|
||||
throw new NotFoundException('API key not found');
|
||||
}
|
||||
await this.manager.apiKeyRepository.deleteById(id);
|
||||
return { result: true };
|
||||
}
|
||||
}
|
||||
|
||||
function ApiKeyToDTO(apikey: ApiKey): ApiKeyDTO {
|
||||
return {
|
||||
id: apikey.id,
|
||||
key: apikey.key,
|
||||
isActive: apikey.isActive,
|
||||
isAdmin: apikey.isAdmin,
|
||||
session: apikey.session,
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
UseInterceptors,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
@@ -24,10 +25,17 @@ import {
|
||||
RequestCodeRequest,
|
||||
} from '../structures/auth.dto';
|
||||
import { Base64File } from '../structures/files.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/auth')
|
||||
@ApiTags('🔑 Auth')
|
||||
@ApiTags('📱 Pairing')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
class AuthController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Post,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -14,10 +15,17 @@ import {
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { RejectCallRequest } from '../structures/calls.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/calls')
|
||||
@ApiTags('📞 Calls')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class CallsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiParam, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
@@ -39,10 +40,17 @@ import {
|
||||
parseChannelInviteLink,
|
||||
WhatsappSession,
|
||||
} from '../core/abc/session.abc';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/channels')
|
||||
@ApiTags('📢 Channels')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class ChannelsController {
|
||||
constructor(
|
||||
private manager: SessionManager,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -40,11 +41,18 @@ import {
|
||||
} from '../structures/chats.dto';
|
||||
import { EditMessageRequest } from '../structures/chatting.dto';
|
||||
import { SortOrder } from '@waha/structures/pagination.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/chats')
|
||||
@ApiTags('💬 Chats')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
class ChatsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -48,15 +49,22 @@ import {
|
||||
mentionsAll,
|
||||
validateRequestMentions,
|
||||
} from '@waha/core/utils/mentions.all';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromBody, FromQuery } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api')
|
||||
@ApiTags('📤 Chatting')
|
||||
@UseGuards(PoliciesGuard)
|
||||
export class ChattingController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendText(@Body() request: MessageTextRequest): Promise<WAMessage> {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
@@ -72,6 +80,7 @@ export class ChattingController {
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendImage(@Body() request: MessageImageRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
@@ -87,6 +96,7 @@ export class ChattingController {
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendFile(@Body() request: MessageFileRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
@@ -102,6 +112,7 @@ export class ChattingController {
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendVoice(@Body() request: MessageVoiceRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendVoice(request);
|
||||
@@ -113,6 +124,7 @@ export class ChattingController {
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendVideo(@Body() request: MessageVideoRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (mentionsAll(request)) {
|
||||
@@ -128,6 +140,7 @@ export class ChattingController {
|
||||
description:
|
||||
'You can use regular /api/sendText if you wanna send auto-generated link preview.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async sendLinkCustomPreview(
|
||||
@Body() request: MessageLinkCustomPreviewRequest,
|
||||
@@ -147,6 +160,7 @@ export class ChattingController {
|
||||
description: 'Send Buttons',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async sendButtons(@Body() request: SendButtonsRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
@@ -158,6 +172,7 @@ export class ChattingController {
|
||||
summary: 'Send a list message (interactive)',
|
||||
description: 'Send a List message with sections and rows',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async sendList(@Body() request: SendListRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
@@ -165,6 +180,7 @@ export class ChattingController {
|
||||
}
|
||||
|
||||
@Post('/forwardMessage')
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async forwardMessage(
|
||||
@Body() request: MessageForwardRequest,
|
||||
): Promise<WAMessage> {
|
||||
@@ -173,6 +189,7 @@ export class ChattingController {
|
||||
}
|
||||
|
||||
@Post('/sendSeen')
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendSeen(@Body() chat: SendSeenRequest) {
|
||||
const hasMessageId = chat.messageIds?.length > 0 || Boolean(chat.messageId);
|
||||
if (!hasMessageId) {
|
||||
@@ -188,6 +205,7 @@ export class ChattingController {
|
||||
}
|
||||
|
||||
@Post('/startTyping')
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async startTyping(@Body() chat: ChatRequest) {
|
||||
// It's infinitive action
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
@@ -196,6 +214,7 @@ export class ChattingController {
|
||||
}
|
||||
|
||||
@Post('/stopTyping')
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async stopTyping(@Body() chat: ChatRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
await whatsapp.stopTyping(chat);
|
||||
@@ -204,6 +223,7 @@ export class ChattingController {
|
||||
|
||||
@Put('/reaction')
|
||||
@ApiOperation({ summary: 'React to a message with an emoji' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async setReaction(@Body() request: MessageReactionRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.setReaction(request);
|
||||
@@ -211,6 +231,7 @@ export class ChattingController {
|
||||
|
||||
@Put('/star')
|
||||
@ApiOperation({ summary: 'Star or unstar a message' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async setStar(@Body() request: MessageStarRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
await whatsapp.setStar(request);
|
||||
@@ -222,6 +243,7 @@ export class ChattingController {
|
||||
summary: 'Send a poll with options',
|
||||
description: 'You can use it as buttons or list replacement',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendPoll(@Body() request: MessagePollRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendPoll(request);
|
||||
@@ -232,6 +254,7 @@ export class ChattingController {
|
||||
summary: 'Vote on a poll',
|
||||
description: 'Cast vote(s) on an existing poll message',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async sendPollVote(@Body() request: MessagePollVoteRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
@@ -239,12 +262,14 @@ export class ChattingController {
|
||||
}
|
||||
|
||||
@Post('/sendLocation')
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendLocation(@Body() request: MessageLocationRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendLocation(request);
|
||||
}
|
||||
|
||||
@Post('/sendContactVcard')
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendContactVCard(request);
|
||||
@@ -254,6 +279,7 @@ export class ChattingController {
|
||||
@ApiOperation({
|
||||
summary: 'Reply on a button message',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async sendButtonsReply(@Body() request: MessageButtonReply) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
@@ -262,6 +288,7 @@ export class ChattingController {
|
||||
|
||||
@Get('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message', deprecated: true })
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
async sendTextGet(@Query() query: MessageTextQuery) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
const msg = new MessageTextRequest();
|
||||
@@ -276,6 +303,7 @@ export class ChattingController {
|
||||
description: 'DEPRECATED. Use "GET /api/chats/{id}/messages" instead',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getMessages(
|
||||
@Query() query: GetMessageQuery,
|
||||
@@ -292,6 +320,7 @@ export class ChattingController {
|
||||
description: 'DEPRECATED. Use "POST /contacts/check-exists" instead',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
async DEPRECATED_checkNumberStatus(
|
||||
@Query() request: CheckNumberStatusQuery,
|
||||
): Promise<WANumberExistResult> {
|
||||
@@ -305,6 +334,7 @@ export class ChattingController {
|
||||
'DEPRECATED - you can set "reply_to" field when sending text, image, etc',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async reply(@Body() request: MessageReplyRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.reply(request);
|
||||
@@ -312,6 +342,7 @@ export class ChattingController {
|
||||
|
||||
@Post('/sendLinkPreview')
|
||||
@ApiOperation({ deprecated: true })
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async sendLinkPreview_DEPRECATED(@Body() request: MessageLinkPreviewRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendLinkPreview(request);
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Get,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -21,15 +22,22 @@ import {
|
||||
ContactRequest,
|
||||
ContactsPaginationParams,
|
||||
} from '../structures/contacts.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromBody, FromQuery } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/contacts')
|
||||
@ApiTags('👤 Contacts')
|
||||
@UseGuards(PoliciesGuard)
|
||||
export class ContactsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/all')
|
||||
@ApiOperation({ summary: 'Get all contacts' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getAll(
|
||||
@Query() query: SessionQuery,
|
||||
@@ -45,6 +53,7 @@ export class ContactsController {
|
||||
description:
|
||||
'The method always return result, even if the phone number is not registered in WhatsApp. For that - use /contacts/check-exists endpoint below.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
async get(@Query() query: ContactQuery) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getContact(query);
|
||||
@@ -52,6 +61,7 @@ export class ContactsController {
|
||||
|
||||
@Get('/check-exists')
|
||||
@ApiOperation({ summary: 'Check phone number is registered in WhatsApp.' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
async checkExists(
|
||||
@Query() request: CheckNumberStatusQuery,
|
||||
): Promise<WANumberExistResult> {
|
||||
@@ -65,6 +75,7 @@ export class ContactsController {
|
||||
description:
|
||||
'Returns null if you do not have permission to read their status.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
async getAbout(@Query() query: ContactQuery) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getContactAbout(query);
|
||||
@@ -76,6 +87,7 @@ export class ContactsController {
|
||||
description:
|
||||
'If privacy settings do not allow to get the picture, the method will return null.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getProfilePicture(@Query() query: ContactProfilePictureQuery) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
@@ -88,6 +100,7 @@ export class ContactsController {
|
||||
|
||||
@Post('/block')
|
||||
@ApiOperation({ summary: 'Block contact' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async block(@Body() request: ContactRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.blockContact(request);
|
||||
@@ -95,6 +108,7 @@ export class ContactsController {
|
||||
|
||||
@Post('/unblock')
|
||||
@ApiOperation({ summary: 'Unblock contact' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
async unblock(@Body() request: ContactRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.unblockContact(request);
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { Body, Controller, Delete, Param, Put, UsePipes } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Param,
|
||||
Put,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import {
|
||||
@@ -8,13 +18,16 @@ import {
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
|
||||
import { SessionManager } from './core/abc/manager.abc';
|
||||
import { Result } from './structures/base.dto';
|
||||
import { ContactUpdateBody } from './structures/contacts.dto';
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { Result } from '../structures/base.dto';
|
||||
import { ContactUpdateBody } from '../structures/contacts.dto';
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/contacts')
|
||||
@ApiTags('👤 Contacts')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class ContactsSessionController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Controller,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -19,10 +20,17 @@ import {
|
||||
EventMessageRequest,
|
||||
} from '../structures/events.dto';
|
||||
import { WAMessage } from '../structures/responses.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/events')
|
||||
@ApiTags('📅 Events')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class EventsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -40,10 +41,17 @@ import {
|
||||
SettingsSecurityChangeInfo,
|
||||
SubjectRequest,
|
||||
} from '../structures/groups.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/groups')
|
||||
@ApiTags('👥 Groups')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class GroupsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { HealthCheck } from '@nestjs/terminus';
|
||||
|
||||
import { WAHAHealthCheckService } from '../core/abc/WAHAHealthCheckService';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('health')
|
||||
@ApiTags('🔍 Observability')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanServer(Action.Manage))
|
||||
export class HealthController {
|
||||
constructor(private wahaHealth: WAHAHealthCheckService) {}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
UnprocessableEntityException,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -26,10 +27,17 @@ import {
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/labels')
|
||||
@ApiTags('🏷️ Labels')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class LabelsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Param,
|
||||
Query,
|
||||
UnprocessableEntityException,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -22,10 +23,17 @@ import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { isLidUser } from '@waha/core/utils/jids';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/lids')
|
||||
@ApiTags('👤 Contacts')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class LidsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Controller,
|
||||
Post,
|
||||
UnprocessableEntityException,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
@@ -21,10 +22,17 @@ import {
|
||||
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/media')
|
||||
@ApiTags('🖼️ Media')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
class MediaController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
@@ -20,10 +21,17 @@ import {
|
||||
WAHAChatPresences,
|
||||
WAHASessionPresence,
|
||||
} from '../structures/presence.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/presence')
|
||||
@ApiTags('✅ Presence')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class PresenceController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Get,
|
||||
Put,
|
||||
UnprocessableEntityException,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
@@ -22,10 +23,17 @@ import {
|
||||
ProfilePictureRequest,
|
||||
ProfileStatusRequest,
|
||||
} from '@waha/structures/profile.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/profile')
|
||||
@ApiTags('🆔 Profile')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
export class ProfileController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -4,23 +4,35 @@ import {
|
||||
Query,
|
||||
Res,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import { SessionQuery } from '../structures/base.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromQuery } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api')
|
||||
@ApiTags('🖼️ Screenshot')
|
||||
@ApiTags('📱 Pairing')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
export class ScreenshotController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/screenshot')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Get a screenshot of the current WhatsApp session (**WEBJS** only)',
|
||||
})
|
||||
@UseInterceptors(new BufferResponseInterceptor('image/jpeg'))
|
||||
@ApiFileAcceptHeader('image/jpeg')
|
||||
async screenshot(
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Logger,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
@@ -22,10 +23,16 @@ import {
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { VERSION } from '@waha/version';
|
||||
import * as lodash from 'lodash';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/server')
|
||||
@ApiTags('🔍 Observability')
|
||||
@UseGuards(PoliciesGuard)
|
||||
export class ServerController {
|
||||
private logger: Logger;
|
||||
|
||||
@@ -35,12 +42,14 @@ export class ServerController {
|
||||
|
||||
@Get('version')
|
||||
@ApiOperation({ summary: 'Get the version of the server' })
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
get(): WAHAEnvironment {
|
||||
return VERSION;
|
||||
}
|
||||
|
||||
@Get('environment')
|
||||
@ApiOperation({ summary: 'Get the server environment' })
|
||||
@CheckPolicies(CanServer(Action.Manage))
|
||||
environment(
|
||||
@Query(new WAHAValidationPipe()) query: EnvironmentQuery,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
@@ -67,6 +76,7 @@ export class ServerController {
|
||||
|
||||
@Get('status')
|
||||
@ApiOperation({ summary: 'Get the server status' })
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
async status(): Promise<ServerStatusResponse> {
|
||||
const now = Date.now();
|
||||
const uptime = Math.floor(process.uptime() * 1000);
|
||||
@@ -87,6 +97,7 @@ export class ServerController {
|
||||
"If you're using docker, after calling this endpoint Docker will start a new container, " +
|
||||
'so you can use this endpoint to restart the server',
|
||||
})
|
||||
@CheckPolicies(CanServer(Action.Manage))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async stop(@Body() request: StopRequest): Promise<StopResponse> {
|
||||
const timeout = 1_000;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
NotFoundException,
|
||||
Query,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
@@ -25,10 +26,17 @@ import {
|
||||
CpuProfileQuery,
|
||||
} from '@waha/structures/server.debug.dto';
|
||||
import { createReadStream } from 'fs';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/server/debug')
|
||||
@ApiTags('🔍 Observability')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanServer(Action.Manage))
|
||||
export class ServerDebugController {
|
||||
private logger: Logger;
|
||||
private readonly enabled: boolean;
|
||||
|
||||
@@ -9,10 +9,18 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { ApiBody, ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiBody,
|
||||
ApiOAuth2,
|
||||
ApiOperation,
|
||||
ApiSecurity,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
@@ -34,18 +42,24 @@ import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import {
|
||||
ListSessionsQuery,
|
||||
MeInfo,
|
||||
SessionExpand,
|
||||
SessionInfoQuery,
|
||||
SessionCreateRequest,
|
||||
SessionDTO,
|
||||
SessionExpand,
|
||||
SessionInfo,
|
||||
SessionInfoQuery,
|
||||
SessionUpdateRequest,
|
||||
} from '../structures/sessions.dto';
|
||||
import { SessionExamples } from './sessions.examples';
|
||||
import { FilterSessions } from '../core/auth/casl.ability';
|
||||
import { CheckPolicies } from '../core/auth/policies.decorator';
|
||||
import { PoliciesGuard } from '../core/auth/policies.guard';
|
||||
import { CanSession, FromBody, FromParam } from '../core/auth/policies';
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/sessions')
|
||||
@ApiTags('🖥️ Sessions')
|
||||
@UseGuards(PoliciesGuard)
|
||||
class SessionsController {
|
||||
constructor(
|
||||
private manager: SessionManager,
|
||||
@@ -57,11 +71,19 @@ class SessionsController {
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({ summary: 'List all sessions' })
|
||||
@ApiOperation({
|
||||
summary: 'List all sessions',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.List))
|
||||
@ApiOAuth2(['read:items'])
|
||||
async list(
|
||||
@Query(new WAHAValidationPipe()) query: ListSessionsQuery,
|
||||
@Req() req,
|
||||
): Promise<SessionInfo[]> {
|
||||
const sessions = await this.manager.getSessions(query.all);
|
||||
let sessions = await this.manager.getSessions(query.all);
|
||||
if (!req.user.isAdmin) {
|
||||
sessions = FilterSessions(req.ability, Action.Read, sessions);
|
||||
}
|
||||
if (query.expand?.includes(SessionExpand.apps)) {
|
||||
for (const session of sessions) {
|
||||
session.apps = await this.appsService.list(this.manager, session.name);
|
||||
@@ -73,6 +95,7 @@ class SessionsController {
|
||||
@Get('/:session')
|
||||
@ApiOperation({ summary: 'Get session information' })
|
||||
@SessionApiParam
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async get(
|
||||
@Param('session') name: string,
|
||||
@@ -91,6 +114,7 @@ class SessionsController {
|
||||
@Get(':session/me')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get information about the authenticated account' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
getMe(@SessionParam session: WhatsappSession): MeInfo | null {
|
||||
return session.getSessionMeInfo();
|
||||
}
|
||||
@@ -102,6 +126,7 @@ class SessionsController {
|
||||
'Create session a new session (and start it at the same time if required).',
|
||||
})
|
||||
@ApiBody({ type: SessionCreateRequest, examples: SessionExamples })
|
||||
@CheckPolicies(CanSession(Action.Create))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() request: SessionCreateRequest): Promise<SessionDTO> {
|
||||
const name = request.name || generatePrefixedId('session');
|
||||
@@ -139,6 +164,7 @@ class SessionsController {
|
||||
})
|
||||
@SessionApiParam
|
||||
@ApiBody({ type: SessionUpdateRequest, examples: SessionExamples })
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe({ forbidNonWhitelisted: false }))
|
||||
async update(
|
||||
@Param('session') name: string,
|
||||
@@ -177,6 +203,7 @@ class SessionsController {
|
||||
description:
|
||||
'Delete the session with the given name. Stop and logout as well. Idempotent operation.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Delete, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async delete(@Param('session') name: string): Promise<void> {
|
||||
await this.withLock(name, async () => {
|
||||
@@ -195,6 +222,7 @@ class SessionsController {
|
||||
description:
|
||||
'Start the session with the given name. The session must exist. Idempotent operation.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async start(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
@@ -214,6 +242,7 @@ class SessionsController {
|
||||
summary: 'Stop the session',
|
||||
description: 'Stop the session with the given name. Idempotent operation.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async stop(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
@@ -229,6 +258,7 @@ class SessionsController {
|
||||
summary: 'Logout from the session',
|
||||
description: 'Logout the session, restart a session if it was not STOPPED',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async logout(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
@@ -253,6 +283,7 @@ class SessionsController {
|
||||
summary: 'Restart the session',
|
||||
description: 'Restart the session with the given name.',
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async restart(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.manager.restart(name);
|
||||
@@ -266,6 +297,7 @@ class SessionsController {
|
||||
'Create session (if not exists) or update a config (if exists) and start it.',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('name')))
|
||||
async DEPRACATED_start(
|
||||
@Body() request: SessionStartDeprecatedRequest,
|
||||
): Promise<SessionDTO> {
|
||||
@@ -292,6 +324,7 @@ class SessionsController {
|
||||
description: 'Stop session and Logout by default.',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('name')))
|
||||
async DEPRECATED_stop(
|
||||
@Body() request: SessionStopDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
@@ -323,6 +356,7 @@ class SessionsController {
|
||||
description: 'Stop, Logout and Delete session.',
|
||||
deprecated: true,
|
||||
})
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('name')))
|
||||
async DEPRECATED_logout(
|
||||
@Body() request: SessionLogoutDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
@@ -15,10 +15,17 @@ import {
|
||||
VideoStatus,
|
||||
VoiceStatus,
|
||||
} from '../structures/status.dto';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/status')
|
||||
@ApiTags('🟢 Status')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanSession(Action.Use, FromParam('session')))
|
||||
class StatusController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
ApiSecurity,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { WAHAEnvironment } from '../structures/environment.dto';
|
||||
import { VERSION } from '../version';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { CanServer } from '@waha/core/auth/policies';
|
||||
|
||||
import { Action } from '@waha/core/auth/casl.types';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/version')
|
||||
@ApiTags('🔍 Observability')
|
||||
@UseGuards(PoliciesGuard)
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
export class VersionController {
|
||||
@Get('')
|
||||
@ApiOperation({
|
||||
|
||||
@@ -19,8 +19,10 @@ 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 url from 'url';
|
||||
import { URL } from 'url';
|
||||
import { Server } from 'ws';
|
||||
import { CaslAbilityFactory } from '@waha/core/auth/casl.ability';
|
||||
import { Action, session as SessionName } from '@waha/core/auth/casl.types';
|
||||
|
||||
export enum WebSocketCloseCode {
|
||||
NORMAL = 1000,
|
||||
@@ -54,6 +56,7 @@ export class WebsocketGatewayCore
|
||||
constructor(
|
||||
private manager: SessionManager,
|
||||
private auth: WebSocketAuth,
|
||||
private readonly casl: CaslAbilityFactory,
|
||||
) {
|
||||
this.logger = new Logger('WebsocketGateway');
|
||||
this.heartbeat = new WebsocketHeartbeatJob(
|
||||
@@ -62,11 +65,16 @@ export class WebsocketGatewayCore
|
||||
);
|
||||
}
|
||||
|
||||
handleConnection(socket: WebSocket, request: IncomingMessage, ...args): any {
|
||||
async handleConnection(
|
||||
socket: WebSocket,
|
||||
request: IncomingMessage,
|
||||
...args
|
||||
): Promise<any> {
|
||||
// wsc - websocket client
|
||||
socket.id = generatePrefixedId('wsc');
|
||||
|
||||
if (!this.auth.validateRequest(request)) {
|
||||
const user = await this.auth.validateRequest(request);
|
||||
if (!user) {
|
||||
// Not authorized - close connection
|
||||
socket.close(WebSocketCloseCode.POLICY_VIOLATION, 'Unauthorized');
|
||||
this.logger.debug(
|
||||
@@ -75,9 +83,20 @@ export class WebsocketGatewayCore
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(`New client connected: ${request.url} - ${socket.id}`);
|
||||
const params = this.getParams(request);
|
||||
const session: string = params.session;
|
||||
let session: string = params.session;
|
||||
const ability = this.casl.createForUser(user);
|
||||
if (session == '*' && !ability.can(Action.Use, 'all')) {
|
||||
// Limit user to listen only the session events
|
||||
session = user.session;
|
||||
}
|
||||
|
||||
if (!ability.can(Action.Use, new SessionName(session))) {
|
||||
socket.close(WebSocketCloseCode.POLICY_VIOLATION, 'Forbidden');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(`New client connected: ${request.url} - ${socket.id}`);
|
||||
const events: WAHAEvents[] = params.events;
|
||||
this.logger.debug(
|
||||
`Client connected to session: '${session}', events: ${events}, ${socket.id}`,
|
||||
@@ -86,12 +105,17 @@ export class WebsocketGatewayCore
|
||||
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}`);
|
||||
setImmediate(() => {
|
||||
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', () => {
|
||||
@@ -101,14 +125,13 @@ export class WebsocketGatewayCore
|
||||
}
|
||||
|
||||
private getParams(request: IncomingMessage) {
|
||||
const query = url.parse(request.url, true).query;
|
||||
const session = (query.session as string) || '*';
|
||||
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);
|
||||
// We need only search params, so localhost is fine here
|
||||
const query = new URL(request.url, 'http://localhost').searchParams;
|
||||
const session = query.get('session') || '*';
|
||||
const paramsEvents = query.getAll('events');
|
||||
const eventsRaw = paramsEvents.length > 0 ? paramsEvents : ['*'];
|
||||
const eventsList = eventsRaw.flatMap((value) => value.split(','));
|
||||
const events = this.eventUnmask.unmask(eventsList);
|
||||
return { session, events };
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export class AxiosLogging {
|
||||
|
||||
// Log debug for 2xx/3xx responses
|
||||
this.logger.debug(`${methodStr} ${status}:OK ${url}`);
|
||||
if (type.startsWith('application/json')) {
|
||||
if (type?.startsWith('application/json')) {
|
||||
const data = JSON.stringify(response.data);
|
||||
this.logger.trace(`${methodStr} ${status}:OK ${url} ${data}`);
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
@@ -17,6 +20,10 @@ import {
|
||||
IAppsService,
|
||||
} from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { CanSession, FromBody, FromQuery } from '@waha/core/auth/policies';
|
||||
import { Action, session as SessionName } from '@waha/core/auth/casl.types';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
|
||||
import { App } from '../dto/app.dto';
|
||||
@@ -25,6 +32,7 @@ import { ListAppsQuery } from '../dto/query.dto';
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/apps')
|
||||
@ApiTags('🧩 Apps')
|
||||
@UseGuards(PoliciesGuard)
|
||||
export class AppsController {
|
||||
constructor(
|
||||
@Inject(AppsService)
|
||||
@@ -34,6 +42,7 @@ export class AppsController {
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({ summary: 'List all apps for a session' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromQuery('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async list(
|
||||
@Query(new WAHAValidationPipe()) query: ListAppsQuery,
|
||||
@@ -43,6 +52,7 @@ export class AppsController {
|
||||
|
||||
@Post('/')
|
||||
@ApiOperation({ summary: 'Create a new app' })
|
||||
@CheckPolicies(CanSession(Action.Use, FromBody('session')))
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() app: App): Promise<App> {
|
||||
const result = await this.appsService.create(this.manager, app);
|
||||
@@ -56,14 +66,36 @@ export class AppsController {
|
||||
@Get('/:id')
|
||||
@ApiOperation({ summary: 'Get app by ID' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async get(@Param('id') id: string): Promise<App> {
|
||||
return await this.appsService.get(this.manager, id);
|
||||
async get(@Param('id') id: string, @Req() req: any): Promise<App> {
|
||||
const app = await this.appsService.get(this.manager, id);
|
||||
if (!app) {
|
||||
throw new NotFoundException(`App '${id}' not found`);
|
||||
}
|
||||
if (!req.ability?.can(Action.Use, new SessionName(app.session))) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return app;
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@ApiOperation({ summary: 'Update an existing app' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async update(@Param('id') id: string, @Body() app: App): Promise<App> {
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() app: App,
|
||||
@Req() req: any,
|
||||
): Promise<App> {
|
||||
const existing = await this.appsService.get(this.manager, id);
|
||||
if (existing) {
|
||||
if (!req.ability?.can(Action.Use, new SessionName(existing.session))) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
} else {
|
||||
if (!req.ability?.can(Action.Use, new SessionName(app.session))) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
|
||||
if (!app.id) {
|
||||
app.id = id;
|
||||
} else if (app.id !== id) {
|
||||
@@ -83,7 +115,14 @@ export class AppsController {
|
||||
@Delete('/:id')
|
||||
@ApiOperation({ summary: 'Delete an app' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async delete(@Param('id') id: string): Promise<void> {
|
||||
async delete(@Param('id') id: string, @Req() req: any): Promise<void> {
|
||||
const existing = await this.appsService.get(this.manager, id);
|
||||
if (!existing) {
|
||||
throw new NotFoundException(`App '${id}' not found`);
|
||||
}
|
||||
if (!req.ability?.can(Action.Use, new SessionName(existing.session))) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
const app = await this.appsService.delete(this.manager, id);
|
||||
const isRunning = this.manager.isRunning(app.session);
|
||||
if (isRunning) {
|
||||
|
||||
@@ -25,13 +25,18 @@ function jobRemoveOptions() {
|
||||
|
||||
export const JobRemoveOptions = jobRemoveOptions();
|
||||
|
||||
export const ExponentialRetriesJobOptions: DefaultJobOptions = {
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 1000,
|
||||
},
|
||||
};
|
||||
function exponentialRetriesJobOptions(): DefaultJobOptions {
|
||||
return {
|
||||
attempts: parseInt(process.env.WAHA_APPS_JOBS_ATTEMPTS) || 3,
|
||||
delay: parseInt(process.env.WAHA_APPS_JOBS_DELAY) || 0,
|
||||
backoff: {
|
||||
type: process.env.WAHA_APPS_JOBS_BACKOFF_TYPE || 'exponential',
|
||||
delay: parseInt(process.env.WAHA_APPS_JOBS_BACKOFF_DELAY) || 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const ExponentialRetriesJobOptions = exponentialRetriesJobOptions();
|
||||
|
||||
export const NoRetriesJobOptions: DefaultJobOptions = {
|
||||
attempts: 1,
|
||||
|
||||
@@ -21,7 +21,7 @@ export class AppsDisabledService implements IAppsService {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async get(manager: SessionManager, appId: string): Promise<App> {
|
||||
async get(manager: SessionManager, appId: string): Promise<App | null> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
|
||||
@@ -104,12 +104,12 @@ export class AppsEnabledService implements IAppsService {
|
||||
return result;
|
||||
}
|
||||
|
||||
async get(manager: SessionManager, appId: string): Promise<App> {
|
||||
async get(manager: SessionManager, appId: string): Promise<App | null> {
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const app = await repo.getById(appId);
|
||||
if (!app) {
|
||||
throw new NotFoundException(`App '${appId}' not found`);
|
||||
return null;
|
||||
}
|
||||
delete (app as any).pk;
|
||||
return app;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Knex } from 'knex';
|
||||
export interface IAppsService {
|
||||
list(manager: SessionManager, session: string): Promise<App[]>;
|
||||
|
||||
get(manager: SessionManager, appId: string): Promise<App>;
|
||||
get(manager: SessionManager, appId: string): Promise<App | null>;
|
||||
|
||||
create(manager: SessionManager, app: App): Promise<App>;
|
||||
|
||||
|
||||
@@ -146,6 +146,17 @@ export class WAHASelf {
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getGroupParticipants(
|
||||
session: string,
|
||||
groupId: string,
|
||||
opts?: RequestOptions,
|
||||
) {
|
||||
const url = `/api/${session}/groups/${groupId}/participants/v2`;
|
||||
return await this.client
|
||||
.get(url, { signal: opts?.signal })
|
||||
.then((response) => response.data);
|
||||
}
|
||||
|
||||
async getChannel(
|
||||
session: string,
|
||||
channelId: string,
|
||||
@@ -371,6 +382,10 @@ export class WAHASessionAPI {
|
||||
return this.api.getGroup(this.session, groupId, opts);
|
||||
}
|
||||
|
||||
getGroupParticipants(groupId: string, opts?: RequestOptions): Promise<any> {
|
||||
return this.api.getGroupParticipants(this.session, groupId, opts);
|
||||
}
|
||||
|
||||
getChannel(channelId: string, opts?: RequestOptions): Promise<Channel> {
|
||||
return this.api.getChannel(this.session, channelId, opts);
|
||||
}
|
||||
@@ -459,4 +474,8 @@ export class WAHASessionAPI {
|
||||
findLIDByPN(pn: string, opts?: RequestOptions) {
|
||||
return this.api.findLIDByPN(this.session, pn, opts);
|
||||
}
|
||||
|
||||
getSessionInfo(opts?: RequestOptions) {
|
||||
return this.api.get(this.session, opts);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
|
||||
import { CacheForConfig } from '../cache/ConversationCache';
|
||||
import { IConversationCache } from '../cache/IConversationCache';
|
||||
import { AttributeKey } from '@waha/apps/chatwoot/const';
|
||||
|
||||
export interface ContactInfo {
|
||||
ChatId(): string;
|
||||
@@ -63,6 +64,8 @@ export class ContactConversationService {
|
||||
`Updating if required contact custom attributes for chat.id: ${chatId}, contact.id: ${cwContact.data.id}`,
|
||||
);
|
||||
const attributes = await contactInfo.Attributes();
|
||||
// Keep the current chat id in sync to reply using the latest address.
|
||||
attributes[AttributeKey.WA_CHAT_ID] = chatId;
|
||||
await this.contactService.upsertCustomAttributes(
|
||||
cwContact.data,
|
||||
attributes,
|
||||
|
||||
@@ -27,15 +27,15 @@ export function GetAllChatIDs(contact: any): Array<string> {
|
||||
}
|
||||
|
||||
export function FindChatID(contact: any): string | null {
|
||||
if (GetChatID(contact)) {
|
||||
return GetChatID(contact);
|
||||
}
|
||||
if (GetJID(contact)) {
|
||||
return GetJID(contact);
|
||||
}
|
||||
if (GetLID(contact)) {
|
||||
return GetLID(contact);
|
||||
}
|
||||
if (GetChatID(contact)) {
|
||||
return GetChatID(contact);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { InboxData } from '@waha/apps/chatwoot/consumers/types';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import {
|
||||
ChatIDNotFoundForContactError,
|
||||
ChatWootAppNotFoundError,
|
||||
PhoneNumberNotFoundInWhatsAppError,
|
||||
} from '@waha/apps/chatwoot/errors';
|
||||
import { WAHASessionAPI } from '@waha/apps/app_sdk/waha/WAHASelf';
|
||||
@@ -43,7 +44,11 @@ export abstract class ChatWootInboxMessageConsumer extends AppConsumer {
|
||||
const knex = this.manager.store.getWAHADatabase();
|
||||
this.appRepository = new AppRepository(knex);
|
||||
const logger = new JobLoggerWrapper(job, this.logger);
|
||||
const app = await this.appRepository.getById(appId);
|
||||
const app = await this.appRepository.findEnabledAppById(appId);
|
||||
if (!app) {
|
||||
logger.warn(`Chatwoot app not found or disabled: ${appId}`);
|
||||
throw new ChatWootAppNotFoundError(appId);
|
||||
}
|
||||
return new DIContainer(app.pk, app.config, logger, knex);
|
||||
}
|
||||
|
||||
@@ -86,6 +91,10 @@ export abstract class ChatWootInboxMessageConsumer extends AppConsumer {
|
||||
await this.ReportErrorRecovered(job, body);
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err instanceof ChatWootAppNotFoundError) {
|
||||
this.logger.warn(err.message);
|
||||
throw err;
|
||||
}
|
||||
await this.ReportErrorForMessage(job, err, body);
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
MessageMappingService,
|
||||
} from '@waha/apps/chatwoot/storage';
|
||||
import { MarkdownToWhatsApp } from '@waha/apps/chatwoot/messages/to/whatsapp/markdown';
|
||||
import { parseMentionsFromText } from '@waha/apps/chatwoot/messages/to/whatsapp/mentions';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
LinkPreview,
|
||||
} from '@waha/apps/chatwoot/dto/config.dto';
|
||||
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
import { isJidGroup } from '@waha/core/utils/jids';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const mime = require('mime-types');
|
||||
@@ -90,7 +92,16 @@ export class MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = MarkdownToWhatsApp(message.content);
|
||||
let content: string = message.content;
|
||||
let mentions: null | string[] = null;
|
||||
if (isJidGroup(chatId)) {
|
||||
// Parse any mention in the text and get a new clean text if required
|
||||
const parsed = parseMentionsFromText(message.content);
|
||||
content = parsed.text;
|
||||
mentions = parsed.mentions;
|
||||
}
|
||||
|
||||
content = MarkdownToWhatsApp(content);
|
||||
const results = [];
|
||||
let part = 0; // Start from 0 and increment before each send/check
|
||||
const replyTo = await this.getReplyTo(message).catch((err) => {
|
||||
@@ -116,7 +127,7 @@ export class MessageHandler {
|
||||
content: content,
|
||||
chatwoot: body,
|
||||
});
|
||||
const msg = await this.sendTextMessage(chatId, text, replyTo);
|
||||
const msg = await this.sendTextMessage(chatId, text, replyTo, mentions);
|
||||
results.push(msg);
|
||||
await this.saveMapping(message, msg, part);
|
||||
this.logger.info(`Text message sent: ${msg.id}`);
|
||||
@@ -139,7 +150,7 @@ export class MessageHandler {
|
||||
chatwoot: body,
|
||||
singleAttachment: attachments.length == 1,
|
||||
});
|
||||
const msg = await this.sendFile(chatId, caption, file, replyTo);
|
||||
const msg = await this.sendFile(chatId, caption, replyTo, mentions, file);
|
||||
this.logger.info(
|
||||
`File message sent: ${msg.id} - ${file.data_url} - ${file.file_type}`,
|
||||
);
|
||||
@@ -196,6 +207,7 @@ export class MessageHandler {
|
||||
chatId: string,
|
||||
content: string,
|
||||
replyTo: string,
|
||||
mentions: string[] | null,
|
||||
) {
|
||||
const request: MessageTextRequest = {
|
||||
session: '',
|
||||
@@ -206,6 +218,7 @@ export class MessageHandler {
|
||||
this.config.linkPreview,
|
||||
),
|
||||
linkPreviewHighQuality: this.config.linkPreview == LinkPreview.HQ,
|
||||
mentions: mentions,
|
||||
};
|
||||
const session = this.session;
|
||||
await session.readMessages(chatId);
|
||||
@@ -219,8 +232,9 @@ export class MessageHandler {
|
||||
private async sendFile(
|
||||
chatId: string,
|
||||
content: string,
|
||||
file: MessageAttachment,
|
||||
replyTo: string,
|
||||
mentions: string[] | null,
|
||||
file: MessageAttachment,
|
||||
) {
|
||||
const url = file.data_url;
|
||||
let filename = url.split('/').pop();
|
||||
@@ -245,6 +259,7 @@ export class MessageHandler {
|
||||
url: url,
|
||||
mimetype: mimetype,
|
||||
},
|
||||
mentions: mentions,
|
||||
};
|
||||
return session.sendImage(imageRequest);
|
||||
case 'video':
|
||||
@@ -262,6 +277,7 @@ export class MessageHandler {
|
||||
filename: filename,
|
||||
},
|
||||
convert: true,
|
||||
mentions: mentions,
|
||||
};
|
||||
return session.sendVideo(videoRequest);
|
||||
case 'audio':
|
||||
@@ -288,6 +304,7 @@ export class MessageHandler {
|
||||
url: url,
|
||||
mimetype: mimetype,
|
||||
},
|
||||
mentions: mentions,
|
||||
};
|
||||
return session.sendFile(fileRequest);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { AppRepository } from '../../storage';
|
||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { ChatWootAppNotFoundError } from '@waha/apps/chatwoot/errors';
|
||||
|
||||
/**
|
||||
* Base class for ChatWoot scheduled consumers
|
||||
@@ -35,7 +36,11 @@ export abstract class ChatWootScheduledConsumer extends AppConsumer {
|
||||
const knex = this.manager.store.getWAHADatabase();
|
||||
this.appRepository = new AppRepository(knex);
|
||||
const logger = new JobLoggerWrapper(job, this.logger);
|
||||
const app = await this.appRepository.getById(appId);
|
||||
const app = await this.appRepository.findEnabledAppById(appId);
|
||||
if (!app) {
|
||||
logger.warn(`Chatwoot app not found or disabled: ${appId}`);
|
||||
throw new ChatWootAppNotFoundError(appId);
|
||||
}
|
||||
return new DIContainer(app.pk, app.config, logger, knex);
|
||||
}
|
||||
|
||||
@@ -62,6 +67,10 @@ export abstract class ChatWootScheduledConsumer extends AppConsumer {
|
||||
await this.ReportErrorRecovered(job);
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err instanceof ChatWootAppNotFoundError) {
|
||||
this.logger.warn(err.message);
|
||||
throw err;
|
||||
}
|
||||
await this.ReportErrorForJob(job, err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { AppRepository } from '../../storage';
|
||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||
import { SignalRace } from '@waha/utils/abortable';
|
||||
import { IsCommandsChat } from '@waha/apps/chatwoot/client/ids';
|
||||
import { ChatWootAppNotFoundError } from '@waha/apps/chatwoot/errors';
|
||||
|
||||
/**
|
||||
* Base class for ChatWoot background task consumers
|
||||
@@ -36,7 +37,11 @@ export abstract class ChatWootTaskConsumer extends AppConsumer {
|
||||
const knex = this.manager.store.getWAHADatabase();
|
||||
this.appRepository = new AppRepository(knex);
|
||||
const logger = new JobLoggerWrapper(job, this.logger);
|
||||
const app = await this.appRepository.getById(appId);
|
||||
const app = await this.appRepository.findEnabledAppById(appId);
|
||||
if (!app) {
|
||||
logger.warn(`Chatwoot app not found or disabled: ${appId}`);
|
||||
throw new ChatWootAppNotFoundError(appId);
|
||||
}
|
||||
return new DIContainer(app.pk, app.config, logger, knex);
|
||||
}
|
||||
|
||||
@@ -68,6 +73,10 @@ export abstract class ChatWootTaskConsumer extends AppConsumer {
|
||||
await this.ReportErrorRecovered(job);
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err instanceof ChatWootAppNotFoundError) {
|
||||
this.logger.warn(err.message);
|
||||
throw err;
|
||||
}
|
||||
this.ReportErrorForJob(job, err).catch((exc) =>
|
||||
this.logger.error(`Failed to report error for job ${job.id}: ${exc}`),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { WhatsAppChatIdKey } from '@waha/apps/chatwoot/consumers/mutex';
|
||||
import { EventData } from '@waha/apps/chatwoot/consumers/types';
|
||||
import { WhatsAppContactInfo } from '@waha/apps/chatwoot/contacts/WhatsAppContactInfo';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import { ChatWootAppNotFoundError } from '@waha/apps/chatwoot/errors';
|
||||
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
import { WAHASelf, WAHASessionAPI } from '@waha/apps/app_sdk/waha/WAHASelf';
|
||||
import {
|
||||
@@ -83,6 +84,10 @@ export abstract class ChatWootWAHABaseConsumer extends AppConsumer {
|
||||
await this.ReportErrorRecovered(errorReportInfo, job);
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err instanceof ChatWootAppNotFoundError) {
|
||||
this.logger.warn(err.message);
|
||||
throw err;
|
||||
}
|
||||
await this.ReportErrorForMessage(errorReportInfo, job, err);
|
||||
throw err;
|
||||
}
|
||||
@@ -95,7 +100,11 @@ export abstract class ChatWootWAHABaseConsumer extends AppConsumer {
|
||||
const knex = this.manager.store.getWAHADatabase();
|
||||
this.appRepository = new AppRepository(knex);
|
||||
const logger = new JobLoggerWrapper(job, this.logger);
|
||||
const app = await this.appRepository.getById(appId);
|
||||
const app = await this.appRepository.findEnabledAppById(appId);
|
||||
if (!app) {
|
||||
logger.warn(`Chatwoot app not found or disabled: ${appId}`);
|
||||
throw new ChatWootAppNotFoundError(appId);
|
||||
}
|
||||
return new DIContainer(app.pk, app.config, logger, knex);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ export class ChatWootErrorReporter {
|
||||
// There's retries more left - ignore it for now
|
||||
return;
|
||||
}
|
||||
if (!conversation) {
|
||||
this.logger.error(
|
||||
'Chatwoot error report skipped: conversation not ready',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const attempts = {
|
||||
current: this.job.attemptsMade + 1,
|
||||
max: this.job.opts?.attempts || 1,
|
||||
@@ -87,6 +93,12 @@ export class ChatWootErrorReporter {
|
||||
// https://github.com/devlikeapro/waha/issues/1395
|
||||
return null;
|
||||
|
||||
if (!conversation) {
|
||||
this.logger.warn(
|
||||
'Chatwoot success report skipped: conversation not ready',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const template = this.l.key(TKey.JOB_REPORT_SUCCEEDED);
|
||||
const attempts = {
|
||||
current: this.job.attemptsMade + 1,
|
||||
|
||||
@@ -15,3 +15,9 @@ export class UnknownJIDFormat extends Error {
|
||||
super(`WhatsApp Chat ID is not recognized: ${jid}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatWootAppNotFoundError extends Error {
|
||||
constructor(public appId: string) {
|
||||
super(`Chatwoot app not found or disabled: ${appId}`);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,14 @@ describe('MarkdownToWhatsApp', () => {
|
||||
expect(MarkdownToWhatsApp(input)).toBe(expected);
|
||||
});
|
||||
|
||||
// ChatWoot v4.10.1+ escapes newlines with trailing backslash
|
||||
it('strips trailing backslash before newlines', () => {
|
||||
const input = 'Hi \\\ntext message \\\nwith line \\\nbreak';
|
||||
expect(MarkdownToWhatsApp(input)).toBe(
|
||||
'Hi \ntext message \nwith line \nbreak',
|
||||
);
|
||||
});
|
||||
|
||||
// Tests for URL protection
|
||||
it('preserves URLs with multiple underscores', () => {
|
||||
const input = 'https://example.com/page__name__test';
|
||||
|
||||
@@ -4,6 +4,8 @@ export function MarkdownToWhatsApp(text: string): string {
|
||||
}
|
||||
return (
|
||||
text
|
||||
// Remove trailing backslash before newlines (ChatWoot v4.10.1+ escaping)
|
||||
.replace(/\\\n/g, '\n')
|
||||
// Triple-backtick blocks → WhatsApp monospace blocks (same syntax)
|
||||
.replace(/```([\s\S]*?)```/g, '```$1```')
|
||||
// Italic **first**, but only single‐star or single‐underscore
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { parseMentionsFromText } from './mentions';
|
||||
|
||||
describe('parseMentionsFromText', () => {
|
||||
it('returns null mentions when none exist', () => {
|
||||
const input = 'Hello there';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Hello there',
|
||||
mentions: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty content safely', () => {
|
||||
expect(parseMentionsFromText(null)).toEqual({
|
||||
text: null,
|
||||
mentions: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses @all and removes it from the text', () => {
|
||||
const input = 'Hello @all';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Hello',
|
||||
mentions: ['all'],
|
||||
});
|
||||
});
|
||||
|
||||
it('parses @ALL case-insensitively', () => {
|
||||
const input = 'Hello @ALL';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Hello',
|
||||
mentions: ['all'],
|
||||
});
|
||||
});
|
||||
|
||||
it('uses a fallback text when only @all is present', () => {
|
||||
const input = '@all';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: ' ',
|
||||
mentions: ['all'],
|
||||
});
|
||||
});
|
||||
|
||||
it('parses phone number mentions with a suffix', () => {
|
||||
const input = 'Ping @558591203123';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Ping @558591203123',
|
||||
mentions: ['558591203123@c.us'],
|
||||
});
|
||||
});
|
||||
|
||||
it('deduplicates phone number mentions', () => {
|
||||
const input = 'Hi @1111111 and @1111111';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Hi @1111111 and @1111111',
|
||||
mentions: ['1111111@c.us'],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles @all alongside phone mentions', () => {
|
||||
const input = '@all Hi @558591203123';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Hi @558591203123',
|
||||
mentions: ['all', '558591203123@c.us'],
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores quoted @all text', () => {
|
||||
const input = "You can send '@all' to chat";
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: "You can send '@all' to chat",
|
||||
mentions: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores email-like mentions', () => {
|
||||
const input = 'Reach us at email@12341domain.com';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Reach us at email@12341domain.com',
|
||||
mentions: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores @all when followed by letters', () => {
|
||||
const input = 'Please ping @allx';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Please ping @allx',
|
||||
mentions: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores phone mentions followed by letters', () => {
|
||||
const input = 'Call @1234567abc';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Call @1234567abc',
|
||||
mentions: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores @all inside email-like text', () => {
|
||||
const input = 'Contact email@alldomain.com for details';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Contact email@alldomain.com for details',
|
||||
mentions: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts phone mentions with trailing punctuation', () => {
|
||||
const input = 'Ping @1231234, then @5555555.';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Ping @1231234, then @5555555.',
|
||||
mentions: ['1231234@c.us', '5555555@c.us'],
|
||||
});
|
||||
});
|
||||
|
||||
it('parses @lid mentions without c.us suffix', () => {
|
||||
const input = 'Notify @123123@lid now';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: 'Notify @123123 now',
|
||||
mentions: ['123123@lid'],
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces @lid suffix with plain ids', () => {
|
||||
const input = '@123456@lid please ping @654321@lid.';
|
||||
expect(parseMentionsFromText(input)).toEqual({
|
||||
text: '@123456 please ping @654321.',
|
||||
mentions: ['123456@lid', '654321@lid'],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ensureSuffix } from '@waha/core/abc/session.abc';
|
||||
import { isJidGroup } from '@waha/core/utils/jids';
|
||||
|
||||
export interface ParsedMentions {
|
||||
text: string;
|
||||
mentions?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse mentions from text message
|
||||
* Supports:
|
||||
* - @all - mentions all group participants
|
||||
* - @558591203123 - mentions specific phone number
|
||||
*
|
||||
* @param text - The message text
|
||||
* @returns Parsed mentions with cleaned text
|
||||
*/
|
||||
export function parseMentionsFromText(
|
||||
text: string | null | undefined,
|
||||
): ParsedMentions {
|
||||
if (!text) {
|
||||
return {
|
||||
text: text,
|
||||
mentions: null,
|
||||
};
|
||||
}
|
||||
const mentions: string[] = [];
|
||||
let content = text;
|
||||
|
||||
// Regex to match @all (case-insensitive, word boundary)
|
||||
// Use separate regex instances to avoid lastIndex issues with global flag
|
||||
const allRegexTest = /(^|\s)@all\b/gi;
|
||||
if (allRegexTest.test(text)) {
|
||||
mentions.push('all');
|
||||
// Remove @all from text - WAHA endpoint doesn't display it, only mentions all participants
|
||||
// Create a new regex instance for replace to avoid lastIndex issues
|
||||
const allRegexReplace = /(^|\s)@all\b/gi;
|
||||
content = content.replace(allRegexReplace, '$1').trim();
|
||||
}
|
||||
|
||||
const lidRegex = /(^|\s)@(\d{6,15})@lid\b/g;
|
||||
const lidMatches = text.matchAll(lidRegex);
|
||||
|
||||
for (const match of lidMatches) {
|
||||
const lidNumber = match[2];
|
||||
const formattedLid = `${lidNumber}@lid`;
|
||||
if (!mentions.includes(formattedLid)) {
|
||||
mentions.push(formattedLid);
|
||||
}
|
||||
}
|
||||
if (mentions.length > 0) {
|
||||
const lidRegexReplace = /(^|\s)@(\d{6,15})@lid\b/g;
|
||||
content = content.replace(lidRegexReplace, '$1@$2').trim();
|
||||
}
|
||||
|
||||
// Regex to match @ followed by phone number
|
||||
// Matches: @558591203123, @5585991203123, etc.
|
||||
// Phone numbers: 7-15 digits (international format)
|
||||
const phoneRegex = /(^|\s)@(\d{7,15})(?!@lid)\b/g;
|
||||
const phoneMatches = text.matchAll(phoneRegex);
|
||||
|
||||
for (const match of phoneMatches) {
|
||||
const phoneNumber = match[2];
|
||||
// Format phone number to @c.us format
|
||||
const formattedJid = ensureSuffix(phoneNumber);
|
||||
if (!mentions.includes(formattedJid)) {
|
||||
mentions.push(formattedJid);
|
||||
}
|
||||
}
|
||||
|
||||
if (mentions.length == 0) {
|
||||
// Nothing found
|
||||
return {
|
||||
text: text,
|
||||
mentions: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure text is not empty when there are mentions
|
||||
// If text becomes empty after removing mentions, use a default message
|
||||
content = content.trim();
|
||||
if (content === '') {
|
||||
// When only mentions are present, use a minimal text to satisfy DTO requirements
|
||||
// The mentions array will handle the actual mentions
|
||||
content = ' ';
|
||||
}
|
||||
return {
|
||||
text: content,
|
||||
mentions: mentions,
|
||||
};
|
||||
}
|
||||
@@ -124,7 +124,7 @@ class WEBJSHelper implements IEngineHelper {
|
||||
return {
|
||||
timestamp: new Date(message.timestamp * 1000),
|
||||
from_me: message.fromMe,
|
||||
chat_id: message.from,
|
||||
chat_id: message.id.remote,
|
||||
message_id: message.id.id,
|
||||
participant: message.author || null,
|
||||
};
|
||||
|
||||
@@ -74,15 +74,14 @@ export class SwaggerConfiguratorCore {
|
||||
.setExternalDoc(this.title, this.externalDocUrl)
|
||||
.setVersion(VERSION.version)
|
||||
.addTag('🖥️ Sessions', 'Control WhatsApp sessions (accounts)')
|
||||
.addTag('🧩 Apps', 'Applications (built-in integrations)')
|
||||
.addTag('🔑 Auth', 'Authentication')
|
||||
.addTag('📱 Pairing', 'Pair a session with WhatsApp on your phone.')
|
||||
.addTag('🆔 Profile', 'Your profile information')
|
||||
.addTag('🖼️ Screenshot', 'Get screenshot of WhatsApp and show QR code')
|
||||
.addTag('📤 Chatting', 'Chatting methods')
|
||||
.addTag('📞 Calls', 'Call handling methods')
|
||||
.addTag('✅ Presence', `Presence information`)
|
||||
.addTag('📢 Channels', 'Channels (newsletters) methods')
|
||||
.addTag('🟢 Status', 'Status (aka stories) methods')
|
||||
.addTag('💬 Chats', `Chats methods`)
|
||||
.addTag('🔑 Api Keys', 'API Keys management')
|
||||
.addTag(
|
||||
'👤 Contacts',
|
||||
`Contacts methods.<br>
|
||||
@@ -90,13 +89,14 @@ export class SwaggerConfiguratorCore {
|
||||
'E.g: \`12312312310\` OR \`12312312310@c.us\`<br>`,
|
||||
)
|
||||
.addTag('👥 Groups', `Groups methods.<br>`)
|
||||
.addTag('✅ Presence', `Presence information`)
|
||||
.addTag('📞 Calls', 'Call handling methods')
|
||||
.addTag('📅 Events', `Event Message`)
|
||||
.addTag(
|
||||
'🏷️ Labels',
|
||||
'Labels - available only for WhatsApp Business accounts',
|
||||
)
|
||||
.addTag('🖼️ Media', 'Media methods')
|
||||
.addTag('🧩 Apps', 'Applications (built-in integrations)')
|
||||
.addTag('🔍 Observability', 'Other methods')
|
||||
.addTag('🗄️ Storage', 'Storage methods')
|
||||
.addApiKey({
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
import { ISessionAuthRepository } from '../storage/ISessionAuthRepository';
|
||||
import { ISessionConfigRepository } from '../storage/ISessionConfigRepository';
|
||||
import { WhatsappSession } from './session.abc';
|
||||
import { IApiKeyRepository } from '@waha/core/storage/IApiKeyRepository';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
@@ -48,6 +49,7 @@ export abstract class SessionManager
|
||||
public sessionConfigRepository: ISessionConfigRepository;
|
||||
protected sessionMeRepository: ISessionMeRepository;
|
||||
protected sessionWorkerRepository: ISessionWorkerRepository;
|
||||
public apiKeyRepository: IApiKeyRepository;
|
||||
private lock: any;
|
||||
|
||||
WAIT_SESSION_RUNNING_INTERVAL = 500;
|
||||
|
||||
@@ -50,7 +50,6 @@ import * as lodash from 'lodash';
|
||||
import * as NodeCache from 'node-cache';
|
||||
import { Logger } from 'pino';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
catchError,
|
||||
delay,
|
||||
filter,
|
||||
@@ -228,7 +227,8 @@ export abstract class WhatsappSession {
|
||||
engineConfig,
|
||||
ignore,
|
||||
}: SessionParams) {
|
||||
this.status$ = new BehaviorSubject(WAHASessionStatus.STOPPED);
|
||||
this._status = WAHASessionStatus.STOPPED;
|
||||
this.status$ = new Subject<WAHASessionStatus>();
|
||||
|
||||
this.name = name;
|
||||
this.proxyConfig = proxyConfig;
|
||||
@@ -381,6 +381,7 @@ export abstract class WhatsappSession {
|
||||
'--disable-default-apps',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-extensions',
|
||||
'--disable-metrics',
|
||||
// '--disable-features=site-per-process', // COMMENTED to test WEBJS stability
|
||||
'--disable-gpu', // COMMENTED to test WEBJS stability
|
||||
'--disable-offer-store-unmasked-wallet-cards',
|
||||
|
||||
@@ -9,15 +9,16 @@ import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { ChannelsController } from '@waha/api/channels.controller';
|
||||
import { LidsController } from '@waha/api/lids.controller';
|
||||
import { ApiKeysController } from '@waha/api/apikeys.controller';
|
||||
import { ProfileController } from '@waha/api/profile.controller';
|
||||
import { ServerController } from '@waha/api/server.controller';
|
||||
import { ServerDebugController } from '@waha/api/server.debug.controller';
|
||||
import { WebsocketGatewayCore } from '@waha/api/websocket.gateway.core';
|
||||
import { AppsModuleExports } from '@waha/apps/apps.module';
|
||||
import { ContactsSessionController } from '@waha/contacts.session.controller';
|
||||
import { ContactsSessionController } from '@waha/api/contacts.session.controller';
|
||||
import { ApiKeyStrategy } from '@waha/core/auth/apiKey.strategy';
|
||||
import { IApiKeyAuth } from '@waha/core/auth/auth';
|
||||
import { AuthMiddleware } from '@waha/core/auth/auth.middleware';
|
||||
import { ApiKeyAuthMiddleware } from '@waha/core/auth/api-key-auth.middleware';
|
||||
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
||||
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
|
||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||
@@ -64,6 +65,9 @@ import { EngineConfigService } from './config/EngineConfigService';
|
||||
import { SwaggerConfigServiceCore } from './config/SwaggerConfigServiceCore';
|
||||
import { WAHAHealthCheckServiceCore } from './health/WAHAHealthCheckServiceCore';
|
||||
import { SessionManagerCore } from './manager.core';
|
||||
import { CaslAbilityFactory } from '@waha/core/auth/casl.ability';
|
||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||
import { ApiKeyService } from '@waha/core/auth/ApiKeyService';
|
||||
|
||||
export const IMPORTS_CORE = [
|
||||
...AppsModuleExports.imports,
|
||||
@@ -140,6 +144,7 @@ const IMPORTS = [...IMPORTS_CORE, ...IMPORTS_MEDIA];
|
||||
|
||||
export const CONTROLLERS = [
|
||||
AuthController,
|
||||
ApiKeysController,
|
||||
SessionsController,
|
||||
ProfileController,
|
||||
ChattingController,
|
||||
@@ -178,6 +183,9 @@ export const PROVIDERS_BASE: Provider[] = [
|
||||
MediaLocalStorageConfig,
|
||||
WebSocketAuth,
|
||||
ApiKeyStrategy,
|
||||
ApiKeyService,
|
||||
CaslAbilityFactory,
|
||||
PoliciesGuard,
|
||||
{
|
||||
provide: IApiKeyAuth,
|
||||
useFactory: ApiKeyAuthFactory,
|
||||
@@ -234,11 +242,15 @@ export class AppModuleCore {
|
||||
}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
// Because we use ServeStaticModule, we need to inject a middleware
|
||||
// ServeStaticModule does not support @UseGuards
|
||||
const exclude = this.config.getExcludedPaths();
|
||||
consumer
|
||||
.apply(AuthMiddleware)
|
||||
.apply(ApiKeyAuthMiddleware)
|
||||
.exclude(...exclude)
|
||||
.forRoutes('api', 'health', 'ws');
|
||||
.forRoutes('api', 'health');
|
||||
|
||||
// Dashboard
|
||||
const dashboardCredentials = this.dashboardConfig.credentials;
|
||||
if (dashboardCredentials) {
|
||||
const username = dashboardCredentials[0];
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { User } from '@waha/core/auth/apiKey.strategy';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyService {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
async get(apikey: string): Promise<User | null> {
|
||||
if (!apikey) {
|
||||
return null;
|
||||
}
|
||||
const key = await this.manager.apiKeyRepository.getActiveByKey(apikey);
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
isAdmin: key.isAdmin,
|
||||
session: key.session,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { IncomingMessage } from 'http';
|
||||
import * as url from 'url';
|
||||
|
||||
import { IApiKeyAuth } from './auth';
|
||||
import { URL } from 'url';
|
||||
import { ApiKeyStrategy } from '@waha/core/auth/apiKey.strategy';
|
||||
|
||||
@Injectable()
|
||||
export class WebSocketAuth {
|
||||
constructor(private auth: IApiKeyAuth) {}
|
||||
constructor(private strategy: ApiKeyStrategy) {}
|
||||
|
||||
validateRequest(request: IncomingMessage) {
|
||||
if (this.auth.skipAuth()) {
|
||||
return true;
|
||||
}
|
||||
const provided = this.getKeyFromQueryParams(request);
|
||||
return this.auth.isValid(provided);
|
||||
async validateRequest(request: IncomingMessage) {
|
||||
const apikey = this.getKeyFromQueryParams('x-api-key', request);
|
||||
return await this.strategy.user(apikey);
|
||||
}
|
||||
|
||||
private getKeyFromQueryParams(request: IncomingMessage) {
|
||||
let query = url.parse(request.url, true).query;
|
||||
// case-insensitive query params
|
||||
query = Object.keys(query).reduce((acc, key) => {
|
||||
acc[key.toLowerCase()] = query[key];
|
||||
return acc;
|
||||
}, {});
|
||||
private getKeyFromQueryParams(name: string, request: IncomingMessage) {
|
||||
// Case-insensitive
|
||||
name = name.toLowerCase();
|
||||
const query = new URL(request.url || '', 'http://localhost').searchParams;
|
||||
const matches: string[] = [];
|
||||
|
||||
const provided = query['x-api-key'];
|
||||
// Check if it's array - return first
|
||||
if (Array.isArray(provided)) {
|
||||
return provided[0];
|
||||
for (const [key, value] of query.entries()) {
|
||||
if (key.toLowerCase() === name) {
|
||||
matches.push(value);
|
||||
}
|
||||
}
|
||||
return provided;
|
||||
|
||||
if (matches.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return matches[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyAuthGuard extends AuthGuard('headerapikey') {}
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import * as passport from 'passport';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyAuthMiddleware implements NestMiddleware {
|
||||
constructor() {}
|
||||
|
||||
use(req: any, res: any, next: () => void) {
|
||||
passport.authenticate('headerapikey', { session: false }, (err, user?) => {
|
||||
if (err) {
|
||||
const exception =
|
||||
err instanceof UnauthorizedException
|
||||
? err
|
||||
: new UnauthorizedException();
|
||||
res.status(exception.getStatus()).json(exception.getResponse());
|
||||
return;
|
||||
}
|
||||
if (!user) {
|
||||
const exception = new UnauthorizedException();
|
||||
res.status(exception.getStatus()).json(exception.getResponse());
|
||||
return;
|
||||
}
|
||||
req.user = user;
|
||||
next();
|
||||
})(req, res, next);
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,42 @@ import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { IApiKeyAuth } from '@waha/core/auth/auth';
|
||||
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
|
||||
import { ApiKeyService } from '@waha/core/auth/ApiKeyService';
|
||||
|
||||
export interface User {
|
||||
isAdmin: boolean;
|
||||
session?: string;
|
||||
}
|
||||
|
||||
function AdminUser(): User {
|
||||
return {
|
||||
isAdmin: true,
|
||||
session: null,
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) {
|
||||
constructor(private auth: IApiKeyAuth) {
|
||||
constructor(
|
||||
private auth: IApiKeyAuth,
|
||||
private apiKeyService: ApiKeyService,
|
||||
) {
|
||||
// @ts-ignore
|
||||
super({ header: 'X-Api-Key', prefix: '' }, true, (apikey, done) => {
|
||||
const isValid = this.auth.isValid(apikey);
|
||||
return done(isValid);
|
||||
return this.validate(apikey, done);
|
||||
});
|
||||
}
|
||||
|
||||
validate(apikey: string, done: (result: boolean) => void): void {
|
||||
const isValid = this.auth.isValid(apikey);
|
||||
return done(isValid);
|
||||
validate(apikey: string, done: (err?, user?: User) => void): void {
|
||||
this.user(apikey)
|
||||
.then((user) => done(null, user))
|
||||
.catch((err) => done(err, null));
|
||||
}
|
||||
|
||||
async user(apikey: string): Promise<User> {
|
||||
if (this.auth.isValid(apikey)) {
|
||||
return AdminUser();
|
||||
}
|
||||
return this.apiKeyService.get(apikey);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import * as passport from 'passport';
|
||||
|
||||
import { IApiKeyAuth } from './auth';
|
||||
|
||||
@Injectable()
|
||||
export class AuthMiddleware implements NestMiddleware {
|
||||
constructor(private auth: IApiKeyAuth) {}
|
||||
|
||||
use(req: any, res: any, next: () => void) {
|
||||
// Skip authentication if auth says so
|
||||
if (this.auth.skipAuth()) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
passport.authenticate('headerapikey', { session: false }, (value) => {
|
||||
if (!value) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
next();
|
||||
})(req, res, next);
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,12 @@ import * as crypto from 'crypto';
|
||||
|
||||
export abstract class IApiKeyAuth {
|
||||
abstract isValid(plain: string): boolean;
|
||||
|
||||
abstract skipAuth(): boolean;
|
||||
}
|
||||
|
||||
export class NoAuth implements IApiKeyAuth {
|
||||
isValid(plain: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
skipAuth(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class PlainApiKeyAuth implements IApiKeyAuth {
|
||||
@@ -22,10 +16,6 @@ export class PlainApiKeyAuth implements IApiKeyAuth {
|
||||
isValid(plain: string): boolean {
|
||||
return compare(plain, this.key);
|
||||
}
|
||||
|
||||
skipAuth(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class HashAuth implements IApiKeyAuth {
|
||||
@@ -41,10 +31,6 @@ export class HashAuth implements IApiKeyAuth {
|
||||
const hash = crypto.createHash(this.algorithm).update(plain).digest('hex');
|
||||
return compare(hash, this.hash);
|
||||
}
|
||||
|
||||
skipAuth(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { User } from './apiKey.strategy';
|
||||
import { createMongoAbility } from '@casl/ability';
|
||||
import { AdminRules, SessionRules } from './casl.rules';
|
||||
import { Action, AppAbility, session } from './casl.types';
|
||||
|
||||
@Injectable()
|
||||
export class CaslAbilityFactory {
|
||||
createForUser(user: User): AppAbility {
|
||||
// when we disable auth using WHATSAPP_API_KEY_EXCLUDE_PATH
|
||||
// req.use will be null, so we allow any request
|
||||
if (!user) {
|
||||
return createMongoAbility(AdminRules());
|
||||
}
|
||||
// Admin user
|
||||
if (user.isAdmin) {
|
||||
return createMongoAbility(AdminRules());
|
||||
}
|
||||
if (user.session) {
|
||||
return createMongoAbility(SessionRules(user.session));
|
||||
}
|
||||
return createMongoAbility([]);
|
||||
}
|
||||
}
|
||||
|
||||
export function FilterSessions<T extends { name: string }>(
|
||||
ability: AppAbility,
|
||||
action: Action,
|
||||
sessions: T[],
|
||||
) {
|
||||
return sessions.filter((s) => ability.can(action, new session(s.name)));
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { RawRuleOf } from '@casl/ability';
|
||||
import { Action, AppAbility } from './casl.types';
|
||||
|
||||
export function AdminRules(): RawRuleOf<AppAbility>[] {
|
||||
return [
|
||||
{
|
||||
action: Action.Manage,
|
||||
subject: 'all',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function SessionRules(name: string): RawRuleOf<AppAbility>[] {
|
||||
return [
|
||||
//
|
||||
// Server
|
||||
//
|
||||
{
|
||||
action: 'read',
|
||||
subject: 'server',
|
||||
},
|
||||
//
|
||||
// Session
|
||||
//
|
||||
{
|
||||
action: Action.List,
|
||||
subject: 'session',
|
||||
},
|
||||
{
|
||||
action: Action.Read,
|
||||
subject: 'session',
|
||||
conditions: { name: name },
|
||||
},
|
||||
// {
|
||||
// action: Action.Delete,
|
||||
// subject: 'session',
|
||||
// conditions: { name: name },
|
||||
// },
|
||||
{
|
||||
action: Action.Use,
|
||||
subject: 'session',
|
||||
conditions: { name: name },
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { InferSubjects, MongoAbility } from '@casl/ability';
|
||||
|
||||
export class session {
|
||||
constructor(public name: string) {}
|
||||
}
|
||||
|
||||
export class server {}
|
||||
|
||||
export enum Action {
|
||||
//
|
||||
// CASL
|
||||
//
|
||||
Manage = 'manage', // it's a special action in casl, meaning "any actions"
|
||||
//
|
||||
// Session
|
||||
//
|
||||
List = 'list', // list sessions
|
||||
Read = 'read', // read session info (not messages)
|
||||
Create = 'create', // create a new session
|
||||
Delete = 'delete', // delete a session
|
||||
Use = 'use', // all actions - send a message, retrieve, manage session status
|
||||
}
|
||||
|
||||
type Subjects =
|
||||
| InferSubjects<typeof session | typeof server | 'session' | 'server'>
|
||||
| 'all';
|
||||
type Actions = keyof typeof Action | Action | `${Action}`;
|
||||
export type AppAbility = MongoAbility<[Actions, Subjects]>;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export interface SValue {
|
||||
param: string;
|
||||
@@ -142,7 +143,7 @@ export function ReportGeneratedValue() {
|
||||
'Save these values to your environment (.env or WAHA_*) to reuse them; new keys are generated on every start otherwise.',
|
||||
);
|
||||
console.warn('');
|
||||
console.warn("cat <<'EOF' > .env");
|
||||
console.warn("cat <<'EOF' >> .env");
|
||||
console.warn('');
|
||||
for (const key of values) {
|
||||
console.warn(`${key.param}=${key.value}`);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { ExecutionContext } from '@nestjs/common';
|
||||
|
||||
import { AppAbility } from './casl.types';
|
||||
|
||||
export const CHECK_POLICIES_KEY = 'check_policies';
|
||||
|
||||
export type PolicyHandlerCallback = (
|
||||
ability: AppAbility,
|
||||
context: ExecutionContext,
|
||||
) => boolean;
|
||||
export type PolicyHandler = PolicyHandlerCallback;
|
||||
|
||||
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
|
||||
SetMetadata(CHECK_POLICIES_KEY, handlers);
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
import { CaslAbilityFactory } from '@waha/core/auth/casl.ability';
|
||||
import {
|
||||
CHECK_POLICIES_KEY,
|
||||
PolicyHandler,
|
||||
} from '@waha/core/auth/policies.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class PoliciesGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly reflector: Reflector,
|
||||
private readonly caslAbilityFactory: CaslAbilityFactory,
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const handlers =
|
||||
this.reflector.getAllAndOverride<PolicyHandler[]>(CHECK_POLICIES_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]) || [];
|
||||
|
||||
if (handlers.length === 0) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const user = req.user;
|
||||
const ability = this.caslAbilityFactory.createForUser(user);
|
||||
req.ability = ability;
|
||||
const ok = handlers.every((handler) => handler(ability, context));
|
||||
if (!ok) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { BadRequestException, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
Action,
|
||||
AppAbility,
|
||||
server,
|
||||
session,
|
||||
} from '@waha/core/auth/casl.types';
|
||||
|
||||
type GetSession = (req: any) => unknown;
|
||||
|
||||
function requireSessionName(value: unknown) {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
throw new BadRequestException('Session name is required');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function CanSession(action: Action, name?: GetSession) {
|
||||
return (ability: AppAbility, context: ExecutionContext) => {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
if (name) {
|
||||
const sessionName = requireSessionName(name(req));
|
||||
return ability.can(action, new session(sessionName));
|
||||
}
|
||||
return ability.can(action, 'session');
|
||||
};
|
||||
}
|
||||
|
||||
export function FromParam(key = 'session'): GetSession {
|
||||
return (req) => req.params?.[key];
|
||||
}
|
||||
|
||||
export function FromBody(key = 'session'): GetSession {
|
||||
return (req) => req.body?.[key];
|
||||
}
|
||||
|
||||
export function FromQuery(key = 'session'): GetSession {
|
||||
return (req) => req.query?.[key];
|
||||
}
|
||||
|
||||
export function CanServer(action: Action) {
|
||||
return (ability: AppAbility, context: ExecutionContext) => {
|
||||
return ability.can(action, 'server');
|
||||
};
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export class GowsEventStreamObservable extends Observable<EnginePayload> {
|
||||
},
|
||||
) {
|
||||
super((subscriber) => {
|
||||
logger.debug('Creating grpc stream...');
|
||||
logger.debug('Creating grpc client and stream...');
|
||||
logger.setBindings({ id: rand() });
|
||||
const { client, stream } = factory();
|
||||
this._client = client;
|
||||
@@ -41,13 +41,22 @@ export class GowsEventStreamObservable extends Observable<EnginePayload> {
|
||||
logger.warn({ err }, 'Failed to cancel gRPC stream');
|
||||
}
|
||||
|
||||
logger.debug({ reason }, 'Closing gRPC client...');
|
||||
try {
|
||||
client.close();
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'Failed to close gRPC client');
|
||||
}
|
||||
|
||||
await sleep(this.CLIENT_CLOSE_TIMEOUT);
|
||||
};
|
||||
|
||||
stream.on('data', (raw) => {
|
||||
const obj = raw.toObject();
|
||||
obj.data = JSON.parse(obj.data);
|
||||
subscriber?.next(obj);
|
||||
setImmediate(() => {
|
||||
const obj = raw.toObject();
|
||||
obj.data = JSON.parse(obj.data);
|
||||
subscriber?.next(obj);
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('end', (...args) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ export class GowsSubprocess {
|
||||
|
||||
private child: any;
|
||||
private ready: boolean = false;
|
||||
private stdoutBuffer: string = '';
|
||||
|
||||
constructor(
|
||||
private logger: Logger,
|
||||
@@ -17,7 +18,7 @@ export class GowsSubprocess {
|
||||
readonly pprof: boolean = false,
|
||||
) {}
|
||||
|
||||
start(onExit: (code: number) => void) {
|
||||
start(onExit: (code: number | null, signal: NodeJS.Signals | null) => void) {
|
||||
this.logger.info('Starting GOWS subprocess...');
|
||||
this.logger.debug(`GOWS path '${this.path}', socket: '${this.socket}'...`);
|
||||
|
||||
@@ -33,43 +34,51 @@ export class GowsSubprocess {
|
||||
detached: true,
|
||||
});
|
||||
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.child.on('close', (code, signal) => {
|
||||
const msg =
|
||||
code !== null
|
||||
? `GOWS subprocess closed with code ${code}`
|
||||
: `GOWS subprocess closed by signal ${signal}`;
|
||||
this.logger.debug(msg);
|
||||
onExit(code);
|
||||
onExit(code, signal);
|
||||
});
|
||||
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.stderr?.setEncoding('utf8');
|
||||
this.child.stderr?.on('data', (data) => {
|
||||
this.logger.error(data.toString().trim());
|
||||
});
|
||||
|
||||
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.child.stdout?.setEncoding('utf8');
|
||||
this.child.stdout?.on('data', (data) => {
|
||||
this.handleStdout(data.toString());
|
||||
});
|
||||
this.listenReady();
|
||||
}
|
||||
|
||||
listenReady() {
|
||||
this.child.stdout.on('data', async (data) => {
|
||||
if (this.ready) {
|
||||
private handleStdout(chunk: string) {
|
||||
this.stdoutBuffer += chunk;
|
||||
const parts = this.stdoutBuffer.split('\n');
|
||||
this.stdoutBuffer = parts.pop() ?? '';
|
||||
parts.forEach((line) => {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
if (!data.includes(this.readyText)) {
|
||||
return;
|
||||
}
|
||||
await sleep(this.readyDelayMs);
|
||||
this.ready = true;
|
||||
this.logger.info('GOWS is ready');
|
||||
this.log(trimmed);
|
||||
void this.checkReady(trimmed);
|
||||
});
|
||||
void this.checkReady(this.stdoutBuffer);
|
||||
}
|
||||
|
||||
private async checkReady(text: string) {
|
||||
if (this.ready || !text.includes(this.readyText)) {
|
||||
return;
|
||||
}
|
||||
await sleep(this.readyDelayMs);
|
||||
this.ready = true;
|
||||
this.logger.info('GOWS is ready');
|
||||
}
|
||||
|
||||
async waitWhenReady(timeout: number) {
|
||||
@@ -79,7 +88,7 @@ export class GowsSubprocess {
|
||||
timeout,
|
||||
);
|
||||
if (!started) {
|
||||
const msg = 'GOWS did not start after 10 seconds';
|
||||
const msg = `GOWS did not start after ${timeout} ms`;
|
||||
this.logger.error(msg);
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,22 @@
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { messages } from '@waha/core/engines/gows/grpc/gows';
|
||||
import { Pool, SizedPool } from '@waha/core/engines/gows/pools';
|
||||
|
||||
let CLIENTS: Pool<messages.MessageServiceClient> | null = null;
|
||||
let STREAM_CLIENTS: Pool<messages.EventStreamClient> | null = null;
|
||||
|
||||
export const GetMessageServiceClient = (
|
||||
session: string,
|
||||
export function BuildMessageServiceClient(
|
||||
address: string,
|
||||
credentials: grpc.ChannelCredentials,
|
||||
): messages.MessageServiceClient => {
|
||||
if (!CLIENTS) {
|
||||
const factory = () => {
|
||||
return new messages.MessageServiceClient(address, credentials, {
|
||||
'grpc.max_send_message_length': 128 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 128 * 1024 * 1024,
|
||||
});
|
||||
};
|
||||
CLIENTS = new SizedPool(16, factory);
|
||||
}
|
||||
return CLIENTS.get(session);
|
||||
};
|
||||
): messages.MessageServiceClient {
|
||||
return new messages.MessageServiceClient(address, credentials, {
|
||||
'grpc.max_send_message_length': 128 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 128 * 1024 * 1024,
|
||||
});
|
||||
}
|
||||
|
||||
export const GetEventStreamClient = (
|
||||
session: string,
|
||||
export function BuildEventStreamClient(
|
||||
address: string,
|
||||
credentials: grpc.ChannelCredentials,
|
||||
): messages.EventStreamClient => {
|
||||
if (!STREAM_CLIENTS) {
|
||||
const factory = () => {
|
||||
return new messages.EventStreamClient(address, credentials, {
|
||||
'grpc.max_send_message_length': 128 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 128 * 1024 * 1024,
|
||||
});
|
||||
};
|
||||
STREAM_CLIENTS = new SizedPool(16, factory);
|
||||
}
|
||||
return STREAM_CLIENTS.get(session);
|
||||
};
|
||||
): messages.EventStreamClient {
|
||||
return new messages.EventStreamClient(address, credentials, {
|
||||
'grpc.max_send_message_length': 128 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 128 * 1024 * 1024,
|
||||
});
|
||||
}
|
||||
@@ -23,7 +23,8 @@ export namespace messages {
|
||||
IMAGE = 0,
|
||||
AUDIO = 1,
|
||||
VIDEO = 2,
|
||||
DOCUMENT = 3
|
||||
DOCUMENT = 3,
|
||||
PTV = 4
|
||||
}
|
||||
export enum Presence {
|
||||
AVAILABLE = 0,
|
||||
@@ -878,6 +879,99 @@ export namespace messages {
|
||||
return EventJson.deserialize(bytes);
|
||||
}
|
||||
}
|
||||
export class StreamEventsRequest extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [];
|
||||
constructor(data?: any[] | {
|
||||
session?: Session;
|
||||
exclude?: string[];
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [2], this.#one_of_decls);
|
||||
if (!Array.isArray(data) && typeof data == "object") {
|
||||
if ("session" in data && data.session != undefined) {
|
||||
this.session = data.session;
|
||||
}
|
||||
if ("exclude" in data && data.exclude != undefined) {
|
||||
this.exclude = data.exclude;
|
||||
}
|
||||
}
|
||||
}
|
||||
get session() {
|
||||
return pb_1.Message.getWrapperField(this, Session, 1) as Session;
|
||||
}
|
||||
set session(value: Session) {
|
||||
pb_1.Message.setWrapperField(this, 1, value);
|
||||
}
|
||||
get has_session() {
|
||||
return pb_1.Message.getField(this, 1) != null;
|
||||
}
|
||||
get exclude() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 2, []) as string[];
|
||||
}
|
||||
set exclude(value: string[]) {
|
||||
pb_1.Message.setField(this, 2, value);
|
||||
}
|
||||
static fromObject(data: {
|
||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||
exclude?: string[];
|
||||
}): StreamEventsRequest {
|
||||
const message = new StreamEventsRequest({});
|
||||
if (data.session != null) {
|
||||
message.session = Session.fromObject(data.session);
|
||||
}
|
||||
if (data.exclude != null) {
|
||||
message.exclude = data.exclude;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
const data: {
|
||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||
exclude?: string[];
|
||||
} = {};
|
||||
if (this.session != null) {
|
||||
data.session = this.session.toObject();
|
||||
}
|
||||
if (this.exclude != null) {
|
||||
data.exclude = this.exclude;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
serialize(w: pb_1.BinaryWriter): void;
|
||||
serialize(w?: pb_1.BinaryWriter): Uint8Array | void {
|
||||
const writer = w || new pb_1.BinaryWriter();
|
||||
if (this.has_session)
|
||||
writer.writeMessage(1, this.session, () => this.session.serialize(writer));
|
||||
if (this.exclude.length)
|
||||
writer.writeRepeatedString(2, this.exclude);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
static deserialize(bytes: Uint8Array | pb_1.BinaryReader): StreamEventsRequest {
|
||||
const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new StreamEventsRequest();
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup())
|
||||
break;
|
||||
switch (reader.getFieldNumber()) {
|
||||
case 1:
|
||||
reader.readMessage(message.session, () => message.session = Session.deserialize(reader));
|
||||
break;
|
||||
case 2:
|
||||
pb_1.Message.addToRepeatedField(message, 2, reader.readString());
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
serializeBinary(): Uint8Array {
|
||||
return this.serialize();
|
||||
}
|
||||
static deserializeBinary(bytes: Uint8Array): StreamEventsRequest {
|
||||
return StreamEventsRequest.deserialize(bytes);
|
||||
}
|
||||
}
|
||||
export class PairCodeRequest extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [];
|
||||
constructor(data?: any[] | {
|
||||
@@ -1235,6 +1329,193 @@ export namespace messages {
|
||||
return SessionStoreConfig.deserialize(bytes);
|
||||
}
|
||||
}
|
||||
export class SessionStorageConfig extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [[1], [2], [3], [4]];
|
||||
constructor(data?: any[] | ({} & (({
|
||||
messages?: boolean;
|
||||
}) | ({
|
||||
groups?: boolean;
|
||||
}) | ({
|
||||
chats?: boolean;
|
||||
}) | ({
|
||||
labels?: boolean;
|
||||
})))) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
if (!Array.isArray(data) && typeof data == "object") {
|
||||
if ("messages" in data && data.messages != undefined) {
|
||||
this.messages = data.messages;
|
||||
}
|
||||
if ("groups" in data && data.groups != undefined) {
|
||||
this.groups = data.groups;
|
||||
}
|
||||
if ("chats" in data && data.chats != undefined) {
|
||||
this.chats = data.chats;
|
||||
}
|
||||
if ("labels" in data && data.labels != undefined) {
|
||||
this.labels = data.labels;
|
||||
}
|
||||
}
|
||||
}
|
||||
get messages() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 1, false) as boolean;
|
||||
}
|
||||
set messages(value: boolean) {
|
||||
pb_1.Message.setOneofField(this, 1, this.#one_of_decls[0], value);
|
||||
}
|
||||
get has_messages() {
|
||||
return pb_1.Message.getField(this, 1) != null;
|
||||
}
|
||||
get groups() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 2, false) as boolean;
|
||||
}
|
||||
set groups(value: boolean) {
|
||||
pb_1.Message.setOneofField(this, 2, this.#one_of_decls[1], value);
|
||||
}
|
||||
get has_groups() {
|
||||
return pb_1.Message.getField(this, 2) != null;
|
||||
}
|
||||
get chats() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 3, false) as boolean;
|
||||
}
|
||||
set chats(value: boolean) {
|
||||
pb_1.Message.setOneofField(this, 3, this.#one_of_decls[2], value);
|
||||
}
|
||||
get has_chats() {
|
||||
return pb_1.Message.getField(this, 3) != null;
|
||||
}
|
||||
get labels() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 4, false) as boolean;
|
||||
}
|
||||
set labels(value: boolean) {
|
||||
pb_1.Message.setOneofField(this, 4, this.#one_of_decls[3], value);
|
||||
}
|
||||
get has_labels() {
|
||||
return pb_1.Message.getField(this, 4) != null;
|
||||
}
|
||||
get _messages() {
|
||||
const cases: {
|
||||
[index: number]: "none" | "messages";
|
||||
} = {
|
||||
0: "none",
|
||||
1: "messages"
|
||||
};
|
||||
return cases[pb_1.Message.computeOneofCase(this, [1])];
|
||||
}
|
||||
get _groups() {
|
||||
const cases: {
|
||||
[index: number]: "none" | "groups";
|
||||
} = {
|
||||
0: "none",
|
||||
2: "groups"
|
||||
};
|
||||
return cases[pb_1.Message.computeOneofCase(this, [2])];
|
||||
}
|
||||
get _chats() {
|
||||
const cases: {
|
||||
[index: number]: "none" | "chats";
|
||||
} = {
|
||||
0: "none",
|
||||
3: "chats"
|
||||
};
|
||||
return cases[pb_1.Message.computeOneofCase(this, [3])];
|
||||
}
|
||||
get _labels() {
|
||||
const cases: {
|
||||
[index: number]: "none" | "labels";
|
||||
} = {
|
||||
0: "none",
|
||||
4: "labels"
|
||||
};
|
||||
return cases[pb_1.Message.computeOneofCase(this, [4])];
|
||||
}
|
||||
static fromObject(data: {
|
||||
messages?: boolean;
|
||||
groups?: boolean;
|
||||
chats?: boolean;
|
||||
labels?: boolean;
|
||||
}): SessionStorageConfig {
|
||||
const message = new SessionStorageConfig({});
|
||||
if (data.messages != null) {
|
||||
message.messages = data.messages;
|
||||
}
|
||||
if (data.groups != null) {
|
||||
message.groups = data.groups;
|
||||
}
|
||||
if (data.chats != null) {
|
||||
message.chats = data.chats;
|
||||
}
|
||||
if (data.labels != null) {
|
||||
message.labels = data.labels;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
const data: {
|
||||
messages?: boolean;
|
||||
groups?: boolean;
|
||||
chats?: boolean;
|
||||
labels?: boolean;
|
||||
} = {};
|
||||
if (this.messages != null) {
|
||||
data.messages = this.messages;
|
||||
}
|
||||
if (this.groups != null) {
|
||||
data.groups = this.groups;
|
||||
}
|
||||
if (this.chats != null) {
|
||||
data.chats = this.chats;
|
||||
}
|
||||
if (this.labels != null) {
|
||||
data.labels = this.labels;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
serialize(w: pb_1.BinaryWriter): void;
|
||||
serialize(w?: pb_1.BinaryWriter): Uint8Array | void {
|
||||
const writer = w || new pb_1.BinaryWriter();
|
||||
if (this.has_messages)
|
||||
writer.writeBool(1, this.messages);
|
||||
if (this.has_groups)
|
||||
writer.writeBool(2, this.groups);
|
||||
if (this.has_chats)
|
||||
writer.writeBool(3, this.chats);
|
||||
if (this.has_labels)
|
||||
writer.writeBool(4, this.labels);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
static deserialize(bytes: Uint8Array | pb_1.BinaryReader): SessionStorageConfig {
|
||||
const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new SessionStorageConfig();
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup())
|
||||
break;
|
||||
switch (reader.getFieldNumber()) {
|
||||
case 1:
|
||||
message.messages = reader.readBool();
|
||||
break;
|
||||
case 2:
|
||||
message.groups = reader.readBool();
|
||||
break;
|
||||
case 3:
|
||||
message.chats = reader.readBool();
|
||||
break;
|
||||
case 4:
|
||||
message.labels = reader.readBool();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
serializeBinary(): Uint8Array {
|
||||
return this.serialize();
|
||||
}
|
||||
static deserializeBinary(bytes: Uint8Array): SessionStorageConfig {
|
||||
return SessionStorageConfig.deserialize(bytes);
|
||||
}
|
||||
}
|
||||
export class SessionProxyConfig extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [];
|
||||
constructor(data?: any[] | {
|
||||
@@ -1439,13 +1720,15 @@ export namespace messages {
|
||||
}
|
||||
}
|
||||
export class SessionConfig extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [[4]];
|
||||
#one_of_decls: number[][] = [[4], [5]];
|
||||
constructor(data?: any[] | ({
|
||||
store?: SessionStoreConfig;
|
||||
log?: SessionLogConfig;
|
||||
proxy?: SessionProxyConfig;
|
||||
} & (({
|
||||
ignore?: SessionIgnoreJidsConfig;
|
||||
}) | ({
|
||||
storage?: SessionStorageConfig;
|
||||
})))) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
@@ -1462,6 +1745,9 @@ export namespace messages {
|
||||
if ("ignore" in data && data.ignore != undefined) {
|
||||
this.ignore = data.ignore;
|
||||
}
|
||||
if ("storage" in data && data.storage != undefined) {
|
||||
this.storage = data.storage;
|
||||
}
|
||||
}
|
||||
}
|
||||
get store() {
|
||||
@@ -1500,6 +1786,15 @@ export namespace messages {
|
||||
get has_ignore() {
|
||||
return pb_1.Message.getField(this, 4) != null;
|
||||
}
|
||||
get storage() {
|
||||
return pb_1.Message.getWrapperField(this, SessionStorageConfig, 5) as SessionStorageConfig;
|
||||
}
|
||||
set storage(value: SessionStorageConfig) {
|
||||
pb_1.Message.setOneofWrapperField(this, 5, this.#one_of_decls[1], value);
|
||||
}
|
||||
get has_storage() {
|
||||
return pb_1.Message.getField(this, 5) != null;
|
||||
}
|
||||
get _ignore() {
|
||||
const cases: {
|
||||
[index: number]: "none" | "ignore";
|
||||
@@ -1509,11 +1804,21 @@ export namespace messages {
|
||||
};
|
||||
return cases[pb_1.Message.computeOneofCase(this, [4])];
|
||||
}
|
||||
get _storage() {
|
||||
const cases: {
|
||||
[index: number]: "none" | "storage";
|
||||
} = {
|
||||
0: "none",
|
||||
5: "storage"
|
||||
};
|
||||
return cases[pb_1.Message.computeOneofCase(this, [5])];
|
||||
}
|
||||
static fromObject(data: {
|
||||
store?: ReturnType<typeof SessionStoreConfig.prototype.toObject>;
|
||||
log?: ReturnType<typeof SessionLogConfig.prototype.toObject>;
|
||||
proxy?: ReturnType<typeof SessionProxyConfig.prototype.toObject>;
|
||||
ignore?: ReturnType<typeof SessionIgnoreJidsConfig.prototype.toObject>;
|
||||
storage?: ReturnType<typeof SessionStorageConfig.prototype.toObject>;
|
||||
}): SessionConfig {
|
||||
const message = new SessionConfig({});
|
||||
if (data.store != null) {
|
||||
@@ -1528,6 +1833,9 @@ export namespace messages {
|
||||
if (data.ignore != null) {
|
||||
message.ignore = SessionIgnoreJidsConfig.fromObject(data.ignore);
|
||||
}
|
||||
if (data.storage != null) {
|
||||
message.storage = SessionStorageConfig.fromObject(data.storage);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
@@ -1536,6 +1844,7 @@ export namespace messages {
|
||||
log?: ReturnType<typeof SessionLogConfig.prototype.toObject>;
|
||||
proxy?: ReturnType<typeof SessionProxyConfig.prototype.toObject>;
|
||||
ignore?: ReturnType<typeof SessionIgnoreJidsConfig.prototype.toObject>;
|
||||
storage?: ReturnType<typeof SessionStorageConfig.prototype.toObject>;
|
||||
} = {};
|
||||
if (this.store != null) {
|
||||
data.store = this.store.toObject();
|
||||
@@ -1549,6 +1858,9 @@ export namespace messages {
|
||||
if (this.ignore != null) {
|
||||
data.ignore = this.ignore.toObject();
|
||||
}
|
||||
if (this.storage != null) {
|
||||
data.storage = this.storage.toObject();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
@@ -1563,6 +1875,8 @@ export namespace messages {
|
||||
writer.writeMessage(3, this.proxy, () => this.proxy.serialize(writer));
|
||||
if (this.has_ignore)
|
||||
writer.writeMessage(4, this.ignore, () => this.ignore.serialize(writer));
|
||||
if (this.has_storage)
|
||||
writer.writeMessage(5, this.storage, () => this.storage.serialize(writer));
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
@@ -1584,6 +1898,9 @@ export namespace messages {
|
||||
case 4:
|
||||
reader.readMessage(message.ignore, () => message.ignore = SessionIgnoreJidsConfig.deserialize(reader));
|
||||
break;
|
||||
case 5:
|
||||
reader.readMessage(message.storage, () => message.storage = SessionStorageConfig.deserialize(reader));
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -2679,6 +2996,73 @@ export namespace messages {
|
||||
return AudioInfo.deserialize(bytes);
|
||||
}
|
||||
}
|
||||
export class VideoInfo extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [];
|
||||
constructor(data?: any[] | {
|
||||
duration?: number;
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
if (!Array.isArray(data) && typeof data == "object") {
|
||||
if ("duration" in data && data.duration != undefined) {
|
||||
this.duration = data.duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
get duration() {
|
||||
return pb_1.Message.getFieldWithDefault(this, 1, 0) as number;
|
||||
}
|
||||
set duration(value: number) {
|
||||
pb_1.Message.setField(this, 1, value);
|
||||
}
|
||||
static fromObject(data: {
|
||||
duration?: number;
|
||||
}): VideoInfo {
|
||||
const message = new VideoInfo({});
|
||||
if (data.duration != null) {
|
||||
message.duration = data.duration;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
const data: {
|
||||
duration?: number;
|
||||
} = {};
|
||||
if (this.duration != null) {
|
||||
data.duration = this.duration;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
serialize(w: pb_1.BinaryWriter): void;
|
||||
serialize(w?: pb_1.BinaryWriter): Uint8Array | void {
|
||||
const writer = w || new pb_1.BinaryWriter();
|
||||
if (this.duration != 0)
|
||||
writer.writeFloat(1, this.duration);
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
static deserialize(bytes: Uint8Array | pb_1.BinaryReader): VideoInfo {
|
||||
const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new VideoInfo();
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup())
|
||||
break;
|
||||
switch (reader.getFieldNumber()) {
|
||||
case 1:
|
||||
message.duration = reader.readFloat();
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
serializeBinary(): Uint8Array {
|
||||
return this.serialize();
|
||||
}
|
||||
static deserializeBinary(bytes: Uint8Array): VideoInfo {
|
||||
return VideoInfo.deserialize(bytes);
|
||||
}
|
||||
}
|
||||
export class Media extends pb_1.Message {
|
||||
#one_of_decls: number[][] = [];
|
||||
constructor(data?: any[] | {
|
||||
@@ -2688,6 +3072,7 @@ export namespace messages {
|
||||
audio?: AudioInfo;
|
||||
filename?: string;
|
||||
contentPath?: string;
|
||||
video?: VideoInfo;
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
@@ -2710,6 +3095,9 @@ export namespace messages {
|
||||
if ("contentPath" in data && data.contentPath != undefined) {
|
||||
this.contentPath = data.contentPath;
|
||||
}
|
||||
if ("video" in data && data.video != undefined) {
|
||||
this.video = data.video;
|
||||
}
|
||||
}
|
||||
}
|
||||
get content() {
|
||||
@@ -2751,6 +3139,15 @@ export namespace messages {
|
||||
set contentPath(value: string) {
|
||||
pb_1.Message.setField(this, 6, value);
|
||||
}
|
||||
get video() {
|
||||
return pb_1.Message.getWrapperField(this, VideoInfo, 7) as VideoInfo;
|
||||
}
|
||||
set video(value: VideoInfo) {
|
||||
pb_1.Message.setWrapperField(this, 7, value);
|
||||
}
|
||||
get has_video() {
|
||||
return pb_1.Message.getField(this, 7) != null;
|
||||
}
|
||||
static fromObject(data: {
|
||||
content?: Uint8Array;
|
||||
type?: MediaType;
|
||||
@@ -2758,6 +3155,7 @@ export namespace messages {
|
||||
audio?: ReturnType<typeof AudioInfo.prototype.toObject>;
|
||||
filename?: string;
|
||||
contentPath?: string;
|
||||
video?: ReturnType<typeof VideoInfo.prototype.toObject>;
|
||||
}): Media {
|
||||
const message = new Media({});
|
||||
if (data.content != null) {
|
||||
@@ -2778,6 +3176,9 @@ export namespace messages {
|
||||
if (data.contentPath != null) {
|
||||
message.contentPath = data.contentPath;
|
||||
}
|
||||
if (data.video != null) {
|
||||
message.video = VideoInfo.fromObject(data.video);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
@@ -2788,6 +3189,7 @@ export namespace messages {
|
||||
audio?: ReturnType<typeof AudioInfo.prototype.toObject>;
|
||||
filename?: string;
|
||||
contentPath?: string;
|
||||
video?: ReturnType<typeof VideoInfo.prototype.toObject>;
|
||||
} = {};
|
||||
if (this.content != null) {
|
||||
data.content = this.content;
|
||||
@@ -2807,6 +3209,9 @@ export namespace messages {
|
||||
if (this.contentPath != null) {
|
||||
data.contentPath = this.contentPath;
|
||||
}
|
||||
if (this.video != null) {
|
||||
data.video = this.video.toObject();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
@@ -2825,6 +3230,8 @@ export namespace messages {
|
||||
writer.writeString(5, this.filename);
|
||||
if (this.contentPath.length)
|
||||
writer.writeString(6, this.contentPath);
|
||||
if (this.has_video)
|
||||
writer.writeMessage(7, this.video, () => this.video.serialize(writer));
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
@@ -2852,6 +3259,9 @@ export namespace messages {
|
||||
case 6:
|
||||
message.contentPath = reader.readString();
|
||||
break;
|
||||
case 7:
|
||||
reader.readMessage(message.video, () => message.video = VideoInfo.deserialize(reader));
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -10490,20 +10900,20 @@ export namespace messages {
|
||||
path: "/messages.EventStream/StreamEvents",
|
||||
requestStream: false,
|
||||
responseStream: true,
|
||||
requestSerialize: (message: Session) => Buffer.from(message.serialize()),
|
||||
requestDeserialize: (bytes: Buffer) => Session.deserialize(new Uint8Array(bytes)),
|
||||
requestSerialize: (message: StreamEventsRequest) => Buffer.from(message.serialize()),
|
||||
requestDeserialize: (bytes: Buffer) => StreamEventsRequest.deserialize(new Uint8Array(bytes)),
|
||||
responseSerialize: (message: EventJson) => Buffer.from(message.serialize()),
|
||||
responseDeserialize: (bytes: Buffer) => EventJson.deserialize(new Uint8Array(bytes))
|
||||
}
|
||||
};
|
||||
[method: string]: grpc_1.UntypedHandleCall;
|
||||
abstract StreamEvents(call: grpc_1.ServerWritableStream<Session, EventJson>): void;
|
||||
abstract StreamEvents(call: grpc_1.ServerWritableStream<StreamEventsRequest, EventJson>): void;
|
||||
}
|
||||
export class EventStreamClient extends grpc_1.makeGenericClientConstructor(UnimplementedEventStreamService.definition, "EventStream", {}) {
|
||||
constructor(address: string, credentials: grpc_1.ChannelCredentials, options?: Partial<grpc_1.ChannelOptions>) {
|
||||
super(address, credentials, options);
|
||||
}
|
||||
StreamEvents: GrpcStreamServiceInterface<Session, EventJson> = (message: Session, metadata?: grpc_1.Metadata | grpc_1.CallOptions, options?: grpc_1.CallOptions): grpc_1.ClientReadableStream<EventJson> => {
|
||||
StreamEvents: GrpcStreamServiceInterface<StreamEventsRequest, EventJson> = (message: StreamEventsRequest, metadata?: grpc_1.Metadata | grpc_1.CallOptions, options?: grpc_1.CallOptions): grpc_1.ClientReadableStream<EventJson> => {
|
||||
return super.StreamEvents(message, metadata, options);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -642,6 +642,17 @@ function deserialize_messages_StartSessionRequest(buffer_arg) {
|
||||
return gows_pb.StartSessionRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_StreamEventsRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.StreamEventsRequest)) {
|
||||
throw new Error('Expected argument of type messages.StreamEventsRequest');
|
||||
}
|
||||
return Buffer.from(arg.serializeBinary());
|
||||
}
|
||||
|
||||
function deserialize_messages_StreamEventsRequest(buffer_arg) {
|
||||
return gows_pb.StreamEventsRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_SubscribePresenceRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.SubscribePresenceRequest)) {
|
||||
throw new Error('Expected argument of type messages.SubscribePresenceRequest');
|
||||
@@ -695,10 +706,10 @@ var EventStreamService = exports.EventStreamService = {
|
||||
path: '/messages.EventStream/StreamEvents',
|
||||
requestStream: false,
|
||||
responseStream: true,
|
||||
requestType: gows_pb.Session,
|
||||
requestType: gows_pb.StreamEventsRequest,
|
||||
responseType: gows_pb.EventJson,
|
||||
requestSerialize: serialize_messages_Session,
|
||||
requestDeserialize: deserialize_messages_Session,
|
||||
requestSerialize: serialize_messages_StreamEventsRequest,
|
||||
requestDeserialize: deserialize_messages_StreamEventsRequest,
|
||||
responseSerialize: serialize_messages_EventJson,
|
||||
responseDeserialize: deserialize_messages_EventJson,
|
||||
},
|
||||
|
||||
@@ -108,16 +108,19 @@ goog.exportSymbol('proto.messages.SessionIgnoreJidsConfig', null, global);
|
||||
goog.exportSymbol('proto.messages.SessionLogConfig', null, global);
|
||||
goog.exportSymbol('proto.messages.SessionProxyConfig', null, global);
|
||||
goog.exportSymbol('proto.messages.SessionStateResponse', null, global);
|
||||
goog.exportSymbol('proto.messages.SessionStorageConfig', null, global);
|
||||
goog.exportSymbol('proto.messages.SessionStoreConfig', null, global);
|
||||
goog.exportSymbol('proto.messages.SetPictureRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.SetProfilePictureRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.SortBy', null, global);
|
||||
goog.exportSymbol('proto.messages.SortBy.Order', null, global);
|
||||
goog.exportSymbol('proto.messages.StartSessionRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.StreamEventsRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.SubscribePresenceRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.UpdateContactRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.UpdateParticipantsRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.UpsertLabelRequest', null, global);
|
||||
goog.exportSymbol('proto.messages.VideoInfo', null, global);
|
||||
goog.exportSymbol('proto.messages.vCardContact', null, global);
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
@@ -329,6 +332,27 @@ if (goog.DEBUG && !COMPILED) {
|
||||
*/
|
||||
proto.messages.EventJson.displayName = 'proto.messages.EventJson';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
* server response, or constructed directly in Javascript. The array is used
|
||||
* in place and becomes part of the constructed object. It is not cloned.
|
||||
* If no data is provided, the constructed object will be empty, but still
|
||||
* valid.
|
||||
* @extends {jspb.Message}
|
||||
* @constructor
|
||||
*/
|
||||
proto.messages.StreamEventsRequest = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, proto.messages.StreamEventsRequest.repeatedFields_, null);
|
||||
};
|
||||
goog.inherits(proto.messages.StreamEventsRequest, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
/**
|
||||
* @public
|
||||
* @override
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.displayName = 'proto.messages.StreamEventsRequest';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
@@ -434,6 +458,27 @@ if (goog.DEBUG && !COMPILED) {
|
||||
*/
|
||||
proto.messages.SessionStoreConfig.displayName = 'proto.messages.SessionStoreConfig';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
* server response, or constructed directly in Javascript. The array is used
|
||||
* in place and becomes part of the constructed object. It is not cloned.
|
||||
* If no data is provided, the constructed object will be empty, but still
|
||||
* valid.
|
||||
* @extends {jspb.Message}
|
||||
* @constructor
|
||||
*/
|
||||
proto.messages.SessionStorageConfig = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
|
||||
};
|
||||
goog.inherits(proto.messages.SessionStorageConfig, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
/**
|
||||
* @public
|
||||
* @override
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.displayName = 'proto.messages.SessionStorageConfig';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
@@ -728,6 +773,27 @@ if (goog.DEBUG && !COMPILED) {
|
||||
*/
|
||||
proto.messages.AudioInfo.displayName = 'proto.messages.AudioInfo';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
* server response, or constructed directly in Javascript. The array is used
|
||||
* in place and becomes part of the constructed object. It is not cloned.
|
||||
* If no data is provided, the constructed object will be empty, but still
|
||||
* valid.
|
||||
* @extends {jspb.Message}
|
||||
* @constructor
|
||||
*/
|
||||
proto.messages.VideoInfo = function(opt_data) {
|
||||
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
|
||||
};
|
||||
goog.inherits(proto.messages.VideoInfo, jspb.Message);
|
||||
if (goog.DEBUG && !COMPILED) {
|
||||
/**
|
||||
* @public
|
||||
* @override
|
||||
*/
|
||||
proto.messages.VideoInfo.displayName = 'proto.messages.VideoInfo';
|
||||
}
|
||||
/**
|
||||
* Generated by JsPbCodeGenerator.
|
||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||
@@ -3604,6 +3670,213 @@ proto.messages.EventJson.prototype.setData = function(value) {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* List of repeated fields within this message type.
|
||||
* @private {!Array<number>}
|
||||
* @const
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.repeatedFields_ = [2];
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
* Field names that are reserved in JavaScript and will be renamed to pb_name.
|
||||
* Optional fields that are not set will be set to undefined.
|
||||
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
|
||||
* For the list of reserved names please see:
|
||||
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
|
||||
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
|
||||
* JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @return {!Object}
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.messages.StreamEventsRequest.toObject(opt_includeInstance, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Static version of the {@see toObject} method.
|
||||
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
|
||||
* the JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @param {!proto.messages.StreamEventsRequest} msg The msg instance to transform.
|
||||
* @return {!Object}
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
|
||||
excludeList: (f = jspb.Message.getRepeatedField(msg, 2)) == null ? undefined : f
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
obj.$jspbMessageInstance = msg;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format).
|
||||
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||
* @return {!proto.messages.StreamEventsRequest}
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.deserializeBinary = function(bytes) {
|
||||
var reader = new jspb.BinaryReader(bytes);
|
||||
var msg = new proto.messages.StreamEventsRequest;
|
||||
return proto.messages.StreamEventsRequest.deserializeBinaryFromReader(msg, reader);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format) from the
|
||||
* given reader into the given message object.
|
||||
* @param {!proto.messages.StreamEventsRequest} msg The message object to deserialize into.
|
||||
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||
* @return {!proto.messages.StreamEventsRequest}
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.deserializeBinaryFromReader = function(msg, reader) {
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup()) {
|
||||
break;
|
||||
}
|
||||
var field = reader.getFieldNumber();
|
||||
switch (field) {
|
||||
case 1:
|
||||
var value = new proto.messages.Session;
|
||||
reader.readMessage(value,proto.messages.Session.deserializeBinaryFromReader);
|
||||
msg.setSession(value);
|
||||
break;
|
||||
case 2:
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.addExclude(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the message to binary data (in protobuf wire format).
|
||||
* @return {!Uint8Array}
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.prototype.serializeBinary = function() {
|
||||
var writer = new jspb.BinaryWriter();
|
||||
proto.messages.StreamEventsRequest.serializeBinaryToWriter(this, writer);
|
||||
return writer.getResultBuffer();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the given message to binary data (in protobuf wire
|
||||
* format), writing to the given BinaryWriter.
|
||||
* @param {!proto.messages.StreamEventsRequest} message
|
||||
* @param {!jspb.BinaryWriter} writer
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = message.getSession();
|
||||
if (f != null) {
|
||||
writer.writeMessage(
|
||||
1,
|
||||
f,
|
||||
proto.messages.Session.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
f = message.getExcludeList();
|
||||
if (f.length > 0) {
|
||||
writer.writeRepeatedString(
|
||||
2,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional Session session = 1;
|
||||
* @return {?proto.messages.Session}
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.prototype.getSession = function() {
|
||||
return /** @type{?proto.messages.Session} */ (
|
||||
jspb.Message.getWrapperField(this, proto.messages.Session, 1));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {?proto.messages.Session|undefined} value
|
||||
* @return {!proto.messages.StreamEventsRequest} returns this
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.prototype.setSession = function(value) {
|
||||
return jspb.Message.setWrapperField(this, 1, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the message field making it undefined.
|
||||
* @return {!proto.messages.StreamEventsRequest} returns this
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.prototype.clearSession = function() {
|
||||
return this.setSession(undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.prototype.hasSession = function() {
|
||||
return jspb.Message.getField(this, 1) != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* repeated string exclude = 2;
|
||||
* @return {!Array<string>}
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.prototype.getExcludeList = function() {
|
||||
return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 2));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {!Array<string>} value
|
||||
* @return {!proto.messages.StreamEventsRequest} returns this
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.prototype.setExcludeList = function(value) {
|
||||
return jspb.Message.setField(this, 2, value || []);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} value
|
||||
* @param {number=} opt_index
|
||||
* @return {!proto.messages.StreamEventsRequest} returns this
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.prototype.addExclude = function(value, opt_index) {
|
||||
return jspb.Message.addToRepeatedField(this, 2, value, opt_index);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the list making it empty but non-null.
|
||||
* @return {!proto.messages.StreamEventsRequest} returns this
|
||||
*/
|
||||
proto.messages.StreamEventsRequest.prototype.clearExcludeList = function() {
|
||||
return this.setExcludeList([]);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
@@ -4308,6 +4581,298 @@ proto.messages.SessionStoreConfig.prototype.setAddress = function(value) {
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
* Field names that are reserved in JavaScript and will be renamed to pb_name.
|
||||
* Optional fields that are not set will be set to undefined.
|
||||
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
|
||||
* For the list of reserved names please see:
|
||||
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
|
||||
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
|
||||
* JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @return {!Object}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.messages.SessionStorageConfig.toObject(opt_includeInstance, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Static version of the {@see toObject} method.
|
||||
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
|
||||
* the JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @param {!proto.messages.SessionStorageConfig} msg The msg instance to transform.
|
||||
* @return {!Object}
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
messages: jspb.Message.getBooleanFieldWithDefault(msg, 1, false),
|
||||
groups: jspb.Message.getBooleanFieldWithDefault(msg, 2, false),
|
||||
chats: jspb.Message.getBooleanFieldWithDefault(msg, 3, false),
|
||||
labels: jspb.Message.getBooleanFieldWithDefault(msg, 4, false)
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
obj.$jspbMessageInstance = msg;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format).
|
||||
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||
* @return {!proto.messages.SessionStorageConfig}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.deserializeBinary = function(bytes) {
|
||||
var reader = new jspb.BinaryReader(bytes);
|
||||
var msg = new proto.messages.SessionStorageConfig;
|
||||
return proto.messages.SessionStorageConfig.deserializeBinaryFromReader(msg, reader);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format) from the
|
||||
* given reader into the given message object.
|
||||
* @param {!proto.messages.SessionStorageConfig} msg The message object to deserialize into.
|
||||
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||
* @return {!proto.messages.SessionStorageConfig}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.deserializeBinaryFromReader = function(msg, reader) {
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup()) {
|
||||
break;
|
||||
}
|
||||
var field = reader.getFieldNumber();
|
||||
switch (field) {
|
||||
case 1:
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setMessages(value);
|
||||
break;
|
||||
case 2:
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setGroups(value);
|
||||
break;
|
||||
case 3:
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setChats(value);
|
||||
break;
|
||||
case 4:
|
||||
var value = /** @type {boolean} */ (reader.readBool());
|
||||
msg.setLabels(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the message to binary data (in protobuf wire format).
|
||||
* @return {!Uint8Array}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.serializeBinary = function() {
|
||||
var writer = new jspb.BinaryWriter();
|
||||
proto.messages.SessionStorageConfig.serializeBinaryToWriter(this, writer);
|
||||
return writer.getResultBuffer();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the given message to binary data (in protobuf wire
|
||||
* format), writing to the given BinaryWriter.
|
||||
* @param {!proto.messages.SessionStorageConfig} message
|
||||
* @param {!jspb.BinaryWriter} writer
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = /** @type {boolean} */ (jspb.Message.getField(message, 1));
|
||||
if (f != null) {
|
||||
writer.writeBool(
|
||||
1,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = /** @type {boolean} */ (jspb.Message.getField(message, 2));
|
||||
if (f != null) {
|
||||
writer.writeBool(
|
||||
2,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = /** @type {boolean} */ (jspb.Message.getField(message, 3));
|
||||
if (f != null) {
|
||||
writer.writeBool(
|
||||
3,
|
||||
f
|
||||
);
|
||||
}
|
||||
f = /** @type {boolean} */ (jspb.Message.getField(message, 4));
|
||||
if (f != null) {
|
||||
writer.writeBool(
|
||||
4,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional bool messages = 1;
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.getMessages = function() {
|
||||
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 1, false));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.setMessages = function(value) {
|
||||
return jspb.Message.setField(this, 1, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the field making it undefined.
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.clearMessages = function() {
|
||||
return jspb.Message.setField(this, 1, undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.hasMessages = function() {
|
||||
return jspb.Message.getField(this, 1) != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional bool groups = 2;
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.getGroups = function() {
|
||||
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 2, false));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.setGroups = function(value) {
|
||||
return jspb.Message.setField(this, 2, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the field making it undefined.
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.clearGroups = function() {
|
||||
return jspb.Message.setField(this, 2, undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.hasGroups = function() {
|
||||
return jspb.Message.getField(this, 2) != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional bool chats = 3;
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.getChats = function() {
|
||||
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 3, false));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.setChats = function(value) {
|
||||
return jspb.Message.setField(this, 3, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the field making it undefined.
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.clearChats = function() {
|
||||
return jspb.Message.setField(this, 3, undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.hasChats = function() {
|
||||
return jspb.Message.getField(this, 3) != null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional bool labels = 4;
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.getLabels = function() {
|
||||
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 4, false));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.setLabels = function(value) {
|
||||
return jspb.Message.setField(this, 4, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the field making it undefined.
|
||||
* @return {!proto.messages.SessionStorageConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.clearLabels = function() {
|
||||
return jspb.Message.setField(this, 4, undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionStorageConfig.prototype.hasLabels = function() {
|
||||
return jspb.Message.getField(this, 4) != null;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
@@ -4690,7 +5255,8 @@ proto.messages.SessionConfig.toObject = function(includeInstance, msg) {
|
||||
store: (f = msg.getStore()) && proto.messages.SessionStoreConfig.toObject(includeInstance, f),
|
||||
log: (f = msg.getLog()) && proto.messages.SessionLogConfig.toObject(includeInstance, f),
|
||||
proxy: (f = msg.getProxy()) && proto.messages.SessionProxyConfig.toObject(includeInstance, f),
|
||||
ignore: (f = msg.getIgnore()) && proto.messages.SessionIgnoreJidsConfig.toObject(includeInstance, f)
|
||||
ignore: (f = msg.getIgnore()) && proto.messages.SessionIgnoreJidsConfig.toObject(includeInstance, f),
|
||||
storage: (f = msg.getStorage()) && proto.messages.SessionStorageConfig.toObject(includeInstance, f)
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -4747,6 +5313,11 @@ proto.messages.SessionConfig.deserializeBinaryFromReader = function(msg, reader)
|
||||
reader.readMessage(value,proto.messages.SessionIgnoreJidsConfig.deserializeBinaryFromReader);
|
||||
msg.setIgnore(value);
|
||||
break;
|
||||
case 5:
|
||||
var value = new proto.messages.SessionStorageConfig;
|
||||
reader.readMessage(value,proto.messages.SessionStorageConfig.deserializeBinaryFromReader);
|
||||
msg.setStorage(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
@@ -4808,6 +5379,14 @@ proto.messages.SessionConfig.serializeBinaryToWriter = function(message, writer)
|
||||
proto.messages.SessionIgnoreJidsConfig.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
f = message.getStorage();
|
||||
if (f != null) {
|
||||
writer.writeMessage(
|
||||
5,
|
||||
f,
|
||||
proto.messages.SessionStorageConfig.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4959,6 +5538,43 @@ proto.messages.SessionConfig.prototype.hasIgnore = function() {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional SessionStorageConfig storage = 5;
|
||||
* @return {?proto.messages.SessionStorageConfig}
|
||||
*/
|
||||
proto.messages.SessionConfig.prototype.getStorage = function() {
|
||||
return /** @type{?proto.messages.SessionStorageConfig} */ (
|
||||
jspb.Message.getWrapperField(this, proto.messages.SessionStorageConfig, 5));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {?proto.messages.SessionStorageConfig|undefined} value
|
||||
* @return {!proto.messages.SessionConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionConfig.prototype.setStorage = function(value) {
|
||||
return jspb.Message.setWrapperField(this, 5, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the message field making it undefined.
|
||||
* @return {!proto.messages.SessionConfig} returns this
|
||||
*/
|
||||
proto.messages.SessionConfig.prototype.clearStorage = function() {
|
||||
return this.setStorage(undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.SessionConfig.prototype.hasStorage = function() {
|
||||
return jspb.Message.getField(this, 5) != null;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7104,6 +7720,136 @@ proto.messages.AudioInfo.prototype.setWaveform = function(value) {
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
* Field names that are reserved in JavaScript and will be renamed to pb_name.
|
||||
* Optional fields that are not set will be set to undefined.
|
||||
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
|
||||
* For the list of reserved names please see:
|
||||
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
|
||||
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
|
||||
* JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @return {!Object}
|
||||
*/
|
||||
proto.messages.VideoInfo.prototype.toObject = function(opt_includeInstance) {
|
||||
return proto.messages.VideoInfo.toObject(opt_includeInstance, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Static version of the {@see toObject} method.
|
||||
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
|
||||
* the JSPB instance for transitional soy proto support:
|
||||
* http://goto/soy-param-migration
|
||||
* @param {!proto.messages.VideoInfo} msg The msg instance to transform.
|
||||
* @return {!Object}
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.messages.VideoInfo.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
duration: jspb.Message.getFloatingPointFieldWithDefault(msg, 1, 0.0)
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
obj.$jspbMessageInstance = msg;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format).
|
||||
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||
* @return {!proto.messages.VideoInfo}
|
||||
*/
|
||||
proto.messages.VideoInfo.deserializeBinary = function(bytes) {
|
||||
var reader = new jspb.BinaryReader(bytes);
|
||||
var msg = new proto.messages.VideoInfo;
|
||||
return proto.messages.VideoInfo.deserializeBinaryFromReader(msg, reader);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Deserializes binary data (in protobuf wire format) from the
|
||||
* given reader into the given message object.
|
||||
* @param {!proto.messages.VideoInfo} msg The message object to deserialize into.
|
||||
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||
* @return {!proto.messages.VideoInfo}
|
||||
*/
|
||||
proto.messages.VideoInfo.deserializeBinaryFromReader = function(msg, reader) {
|
||||
while (reader.nextField()) {
|
||||
if (reader.isEndGroup()) {
|
||||
break;
|
||||
}
|
||||
var field = reader.getFieldNumber();
|
||||
switch (field) {
|
||||
case 1:
|
||||
var value = /** @type {number} */ (reader.readFloat());
|
||||
msg.setDuration(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the message to binary data (in protobuf wire format).
|
||||
* @return {!Uint8Array}
|
||||
*/
|
||||
proto.messages.VideoInfo.prototype.serializeBinary = function() {
|
||||
var writer = new jspb.BinaryWriter();
|
||||
proto.messages.VideoInfo.serializeBinaryToWriter(this, writer);
|
||||
return writer.getResultBuffer();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Serializes the given message to binary data (in protobuf wire
|
||||
* format), writing to the given BinaryWriter.
|
||||
* @param {!proto.messages.VideoInfo} message
|
||||
* @param {!jspb.BinaryWriter} writer
|
||||
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||
*/
|
||||
proto.messages.VideoInfo.serializeBinaryToWriter = function(message, writer) {
|
||||
var f = undefined;
|
||||
f = message.getDuration();
|
||||
if (f !== 0.0) {
|
||||
writer.writeFloat(
|
||||
1,
|
||||
f
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional float duration = 1;
|
||||
* @return {number}
|
||||
*/
|
||||
proto.messages.VideoInfo.prototype.getDuration = function() {
|
||||
return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 1, 0.0));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @return {!proto.messages.VideoInfo} returns this
|
||||
*/
|
||||
proto.messages.VideoInfo.prototype.setDuration = function(value) {
|
||||
return jspb.Message.setProto3FloatField(this, 1, value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||
/**
|
||||
* Creates an object representation of this proto.
|
||||
@@ -7138,7 +7884,8 @@ proto.messages.Media.toObject = function(includeInstance, msg) {
|
||||
mimetype: jspb.Message.getFieldWithDefault(msg, 3, ""),
|
||||
audio: (f = msg.getAudio()) && proto.messages.AudioInfo.toObject(includeInstance, f),
|
||||
filename: jspb.Message.getFieldWithDefault(msg, 5, ""),
|
||||
contentpath: jspb.Message.getFieldWithDefault(msg, 6, "")
|
||||
contentpath: jspb.Message.getFieldWithDefault(msg, 6, ""),
|
||||
video: (f = msg.getVideo()) && proto.messages.VideoInfo.toObject(includeInstance, f)
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -7200,6 +7947,11 @@ proto.messages.Media.deserializeBinaryFromReader = function(msg, reader) {
|
||||
var value = /** @type {string} */ (reader.readString());
|
||||
msg.setContentpath(value);
|
||||
break;
|
||||
case 7:
|
||||
var value = new proto.messages.VideoInfo;
|
||||
reader.readMessage(value,proto.messages.VideoInfo.deserializeBinaryFromReader);
|
||||
msg.setVideo(value);
|
||||
break;
|
||||
default:
|
||||
reader.skipField();
|
||||
break;
|
||||
@@ -7272,6 +8024,14 @@ proto.messages.Media.serializeBinaryToWriter = function(message, writer) {
|
||||
f
|
||||
);
|
||||
}
|
||||
f = message.getVideo();
|
||||
if (f != null) {
|
||||
writer.writeMessage(
|
||||
7,
|
||||
f,
|
||||
proto.messages.VideoInfo.serializeBinaryToWriter
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7426,6 +8186,43 @@ proto.messages.Media.prototype.setContentpath = function(value) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* optional VideoInfo video = 7;
|
||||
* @return {?proto.messages.VideoInfo}
|
||||
*/
|
||||
proto.messages.Media.prototype.getVideo = function() {
|
||||
return /** @type{?proto.messages.VideoInfo} */ (
|
||||
jspb.Message.getWrapperField(this, proto.messages.VideoInfo, 7));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {?proto.messages.VideoInfo|undefined} value
|
||||
* @return {!proto.messages.Media} returns this
|
||||
*/
|
||||
proto.messages.Media.prototype.setVideo = function(value) {
|
||||
return jspb.Message.setWrapperField(this, 7, value);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears the message field making it undefined.
|
||||
* @return {!proto.messages.Media} returns this
|
||||
*/
|
||||
proto.messages.Media.prototype.clearVideo = function() {
|
||||
return this.setVideo(undefined);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether this field is set.
|
||||
* @return {boolean}
|
||||
*/
|
||||
proto.messages.Media.prototype.hasVideo = function() {
|
||||
return jspb.Message.getField(this, 7) != null;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -21564,7 +22361,8 @@ proto.messages.MediaType = {
|
||||
IMAGE: 0,
|
||||
AUDIO: 1,
|
||||
VIDEO: 2,
|
||||
DOCUMENT: 3
|
||||
DOCUMENT: 3,
|
||||
PTV: 4
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -124,7 +124,11 @@ import {
|
||||
WAMessageReaction,
|
||||
} from '@waha/structures/responses.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import { MeInfo, ProxyConfig } from '@waha/structures/sessions.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
ProxyConfig,
|
||||
SessionConfig,
|
||||
} from '@waha/structures/sessions.dto';
|
||||
import {
|
||||
BROADCAST_ID,
|
||||
DeleteStatusRequest,
|
||||
@@ -174,8 +178,8 @@ import {
|
||||
isLabelUpsertEvent,
|
||||
} from './labels.gows';
|
||||
import {
|
||||
GetEventStreamClient,
|
||||
GetMessageServiceClient,
|
||||
BuildEventStreamClient,
|
||||
BuildMessageServiceClient,
|
||||
} from '@waha/core/engines/gows/clients';
|
||||
import esm from '@waha/vendor/esm';
|
||||
import { IsEditedMessage } from '@waha/core/utils/pwa';
|
||||
@@ -188,6 +192,19 @@ import * as path from 'path';
|
||||
import MessageServiceClient = messages.MessageServiceClient;
|
||||
import * as fsp from 'fs/promises';
|
||||
|
||||
function getGowsStorageConfig(
|
||||
sessionConfig?: SessionConfig,
|
||||
): messages.SessionStorageConfig {
|
||||
const storeConfig = sessionConfig?.gows?.storage;
|
||||
return new messages.SessionStorageConfig({
|
||||
// Only explicit false disables; undefined/null defaults to enabled.
|
||||
messages: storeConfig?.messages !== false,
|
||||
groups: storeConfig?.groups !== false,
|
||||
chats: storeConfig?.chats !== false,
|
||||
labels: storeConfig?.labels !== false,
|
||||
});
|
||||
}
|
||||
|
||||
enum WhatsMeowEvent {
|
||||
CONNECTED = 'gows.ConnectedEventData',
|
||||
DISCONNECTED = 'events.Disconnected',
|
||||
@@ -216,6 +233,12 @@ enum WhatsMeowEvent {
|
||||
CALL_REJECT = 'events.CallReject',
|
||||
CALL_TERMINATE = 'events.CallTerminate',
|
||||
CALL_OFFER_NOTICE = 'events.CallOfferNotice',
|
||||
// Other
|
||||
AppState = 'events.AppState',
|
||||
AppStateSyncComplete = 'AppStateSyncComplete',
|
||||
AppStateSyncError = 'AppStateSyncError',
|
||||
HistorySync = 'events.HistorySync',
|
||||
Contact = 'events.Contact',
|
||||
}
|
||||
|
||||
export interface GowsConfig {
|
||||
@@ -267,6 +290,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
address: auth.address(),
|
||||
dialect: auth.dialect(),
|
||||
}),
|
||||
storage: getGowsStorageConfig(this.sessionConfig),
|
||||
log: new messages.SessionLogConfig({
|
||||
level: level ?? messages.LogLevel.INFO,
|
||||
}),
|
||||
@@ -282,20 +306,17 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
}),
|
||||
});
|
||||
|
||||
this.client = GetMessageServiceClient(
|
||||
this.name,
|
||||
this.client = BuildMessageServiceClient(
|
||||
this.engineConfig.connection,
|
||||
grpc.credentials.createInsecure(),
|
||||
);
|
||||
|
||||
try {
|
||||
await promisify(this.client.StartSession)(request);
|
||||
} catch (err) {
|
||||
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;
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected getProxyUrl(config: ProxyConfig): string {
|
||||
@@ -312,12 +333,25 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
this.stream$ = new GowsEventStreamObservable(
|
||||
this.loggerBuilder.child({ grpc: 'stream' }),
|
||||
() => {
|
||||
const client = GetEventStreamClient(
|
||||
this.name,
|
||||
const client = BuildEventStreamClient(
|
||||
this.engineConfig.connection,
|
||||
grpc.credentials.createInsecure(),
|
||||
);
|
||||
const stream = client.StreamEvents(this.session);
|
||||
// Avoid having a lot of events after pairing the device
|
||||
// https://github.com/devlikeapro/waha/issues/1826
|
||||
// TODO: we need to make it more dynamic
|
||||
const exclude = [
|
||||
WhatsMeowEvent.AppState,
|
||||
WhatsMeowEvent.AppStateSyncComplete,
|
||||
WhatsMeowEvent.AppStateSyncError,
|
||||
WhatsMeowEvent.HistorySync,
|
||||
WhatsMeowEvent.Contact,
|
||||
];
|
||||
const request = new messages.StreamEventsRequest({
|
||||
session: this.session,
|
||||
exclude: exclude,
|
||||
});
|
||||
const stream = client.StreamEvents(request);
|
||||
return { client, stream };
|
||||
},
|
||||
);
|
||||
@@ -766,14 +800,14 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.cleanupPresenceTimeout();
|
||||
if (this.client) {
|
||||
const response = await promisify(this.client.StopSession)(this.session);
|
||||
response.toObject();
|
||||
}
|
||||
this.status = WAHASessionStatus.STOPPED;
|
||||
this.events?.stop();
|
||||
this.stopEvents();
|
||||
this.mediaManager.close();
|
||||
if (this.client) {
|
||||
await promisify(this.client.StopSession)(this.session);
|
||||
this.client?.close();
|
||||
}
|
||||
}
|
||||
|
||||
public async requestCode(phoneNumber: string, method: string, params?: any) {
|
||||
|
||||
@@ -64,10 +64,19 @@ export function ToGroupParticipant(
|
||||
interface GroupParticipantUpdate {
|
||||
id: string;
|
||||
author: string;
|
||||
participants: string[];
|
||||
participants: Array<string | NOWEBGroupParticipant>;
|
||||
action: ParticipantAction;
|
||||
}
|
||||
|
||||
function getParticipantId(
|
||||
participant: string | NOWEBGroupParticipant,
|
||||
): string | undefined {
|
||||
if (typeof participant === 'string') {
|
||||
return participant;
|
||||
}
|
||||
return participant?.id;
|
||||
}
|
||||
|
||||
export function ToGroupV2Participants(
|
||||
update: GroupParticipantUpdate,
|
||||
): GroupV2ParticipantsEvent {
|
||||
@@ -92,7 +101,8 @@ export function ToGroupV2Participants(
|
||||
break;
|
||||
}
|
||||
|
||||
const participants: GroupParticipant[] = update.participants.map((id) => {
|
||||
const participants: GroupParticipant[] = update.participants.map((item) => {
|
||||
const id = getParticipantId(item);
|
||||
return {
|
||||
id: toCusFormat(id),
|
||||
role: role,
|
||||
@@ -131,7 +141,11 @@ export function ToGroupV2LeaveEvent(
|
||||
return null;
|
||||
}
|
||||
const meId = esm.b.jidNormalizedUser(me.id);
|
||||
if (!update.participants.includes(meId)) {
|
||||
const includesMe = update.participants.some((participant) => {
|
||||
const id = getParticipantId(participant);
|
||||
return id === meId;
|
||||
});
|
||||
if (!includesMe) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WABrowserDescription } from '@adiwajshing/baileys';
|
||||
import { Browsers, WABrowserDescription } from '@adiwajshing/baileys';
|
||||
import makeWASocket, {
|
||||
Chat,
|
||||
Contact,
|
||||
@@ -205,6 +205,10 @@ import {
|
||||
import { extractWALocation } from '@waha/core/engines/waproto/locaiton';
|
||||
import { extractVCards } from '@waha/core/engines/waproto/vcards';
|
||||
import { Activity } from '@waha/core/abc/activity';
|
||||
import {
|
||||
WAHA_CLIENT_BROWSER_NAME,
|
||||
WAHA_CLIENT_DEVICE_NAME,
|
||||
} from '@waha/core/env';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const promiseRetry = require('promise-retry');
|
||||
|
||||
@@ -314,18 +318,45 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
getSocketConfig(agents: Agents | undefined, state): Partial<SocketConfig> {
|
||||
// Detect browser
|
||||
let browser = ['Ubuntu', 'Chrome', '22.04.4'] as WABrowserDescription;
|
||||
let deviceName =
|
||||
this.sessionConfig?.client?.deviceName ?? WAHA_CLIENT_DEVICE_NAME;
|
||||
let browserName =
|
||||
this.sessionConfig?.client?.browserName ?? WAHA_CLIENT_BROWSER_NAME;
|
||||
if (browserName && !deviceName) {
|
||||
browser = Browsers.appropriate(browserName);
|
||||
} else if (!browserName && deviceName) {
|
||||
browser = [deviceName, 'Chrome', '22.04.4'];
|
||||
} else if (browserName && deviceName) {
|
||||
switch (deviceName) {
|
||||
case 'Mac OS':
|
||||
case 'MacOS':
|
||||
case 'macos':
|
||||
browser = Browsers.macOS(browserName);
|
||||
break;
|
||||
case 'ubuntu':
|
||||
case 'Ubuntu':
|
||||
browser = Browsers.ubuntu(browserName);
|
||||
break;
|
||||
case 'windows':
|
||||
case 'Windows':
|
||||
browser = Browsers.windows(browserName);
|
||||
break;
|
||||
default:
|
||||
browser = [deviceName, browserName, '22.04.4'];
|
||||
}
|
||||
}
|
||||
|
||||
const fullSyncEnabled = this.sessionConfig?.noweb?.store?.fullSync || false;
|
||||
const browser = ['Ubuntu', 'Chrome', '20.0.04'] as WABrowserDescription;
|
||||
let markOnlineOnConnect = this.sessionConfig?.noweb?.markOnline;
|
||||
if (markOnlineOnConnect == undefined) {
|
||||
markOnlineOnConnect = true;
|
||||
}
|
||||
return {
|
||||
agent: agents?.socket,
|
||||
// Baileys types still expect a Node https.Agent here,
|
||||
// but 'undici' fetch requires a Dispatcher.
|
||||
// Cast keeps the compiler satisfied while we pass the ProxyAgent at runtime.
|
||||
fetchAgent: agents?.fetch as unknown as Agent,
|
||||
// Baileys media upload uses Node https.request in Node runtime.
|
||||
fetchAgent: agents?.fetch as Agent,
|
||||
auth: state,
|
||||
printQRInTerminal: false,
|
||||
browser: browser,
|
||||
@@ -923,24 +954,63 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
@Activity()
|
||||
public editMessage(
|
||||
public async editMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
request: EditMessageRequest,
|
||||
) {
|
||||
const jid = toJID(this.ensureSuffix(chatId));
|
||||
const key = parseMessageIdSerialized(messageId);
|
||||
const message = {
|
||||
const stored = await this.store
|
||||
?.loadMessage(key.remoteJid, key.id)
|
||||
.catch(() => null);
|
||||
const content = extractMessageContent(stored?.message);
|
||||
let editedMessage = undefined;
|
||||
if (content?.imageMessage) {
|
||||
editedMessage = {
|
||||
imageMessage: {
|
||||
caption: request.text,
|
||||
},
|
||||
};
|
||||
} else if (content?.videoMessage) {
|
||||
editedMessage = {
|
||||
videoMessage: {
|
||||
caption: request.text,
|
||||
},
|
||||
};
|
||||
} else if (content?.documentMessage) {
|
||||
editedMessage = {
|
||||
documentMessage: {
|
||||
caption: request.text,
|
||||
},
|
||||
};
|
||||
} else if (content?.documentWithCaptionMessage?.message?.documentMessage) {
|
||||
editedMessage = {
|
||||
documentWithCaptionMessage: {
|
||||
message: {
|
||||
documentMessage: {
|
||||
caption: request.text,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
let message: any = {
|
||||
text: request.text,
|
||||
mentions: request.mentions?.map(toJID),
|
||||
edit: key,
|
||||
editedMessage: editedMessage,
|
||||
linkPreview: this.getLinkPreview(request),
|
||||
linkPreviewHighQuality: request.linkPreviewHighQuality,
|
||||
};
|
||||
const options = {
|
||||
messageId: this.generateMessageID(),
|
||||
};
|
||||
return this.sock.sendMessage(jid, message, options);
|
||||
if (isJidNewsletter(jid)) {
|
||||
// Newsletter edits reuse the original message ID
|
||||
options.messageId = key.id;
|
||||
}
|
||||
return await this.sock.sendMessage(jid, message, options);
|
||||
}
|
||||
|
||||
@Activity()
|
||||
@@ -2877,6 +2947,12 @@ export function extractBody(message): string | null {
|
||||
// @ts-ignore - AudioMessage doesn't have caption field
|
||||
body = mediaContent?.caption;
|
||||
}
|
||||
if (!body && content.protocolMessage?.editedMessage) {
|
||||
body = extractBody(content.protocolMessage.editedMessage);
|
||||
}
|
||||
if (!body && content.associatedChildMessage?.message) {
|
||||
body = extractBody(content.associatedChildMessage.message);
|
||||
}
|
||||
// Response for buttons
|
||||
if (!body) {
|
||||
body = content.templateButtonReplyMessage?.selectedDisplayText;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Agent } from 'https';
|
||||
import type { Dispatcher } from 'undici';
|
||||
|
||||
export interface Agents {
|
||||
socket: Agent;
|
||||
fetch: Dispatcher;
|
||||
fetch: Agent;
|
||||
}
|
||||
@@ -10,8 +10,20 @@ export function extractMediaContent(
|
||||
content?.imageMessage ||
|
||||
content?.videoMessage ||
|
||||
content?.audioMessage ||
|
||||
content?.stickerMessage;
|
||||
return mediaContent;
|
||||
content?.ptvMessage ||
|
||||
content?.stickerMessage ||
|
||||
content?.templateMessage?.hydratedTemplate?.imageMessage ||
|
||||
content?.templateMessage?.hydratedTemplate?.videoMessage ||
|
||||
content?.templateMessage?.interactiveMessageTemplate?.header
|
||||
?.imageMessage ||
|
||||
content?.templateMessage?.interactiveMessageTemplate?.header?.videoMessage;
|
||||
if (mediaContent) {
|
||||
return mediaContent;
|
||||
}
|
||||
if (content?.associatedChildMessage?.message) {
|
||||
return extractMediaContent(content.associatedChildMessage.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface Long {
|
||||
|
||||
@@ -99,6 +99,19 @@ export class WPage extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// removeExposedFunction
|
||||
async removeExposedFunction(name: string) {
|
||||
try {
|
||||
return await this.page.removeExposedFunction(name);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'removeExposedFunction',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async reload(options?: any): Promise<any> {
|
||||
try {
|
||||
return await this.page.reload(options);
|
||||
|
||||
@@ -36,6 +36,10 @@ export class WebjsClientCore extends Client {
|
||||
) {
|
||||
super(options);
|
||||
// Wait until it's READY and inject more utils
|
||||
this.on(Events.AUTHENTICATED, async () => {
|
||||
await this.attachCustomEventListeners();
|
||||
await this.injectWaha();
|
||||
});
|
||||
this.on(Events.READY, async () => {
|
||||
await this.attachCustomEventListeners();
|
||||
await this.injectWaha();
|
||||
@@ -124,6 +128,7 @@ export class WebjsClientCore extends Client {
|
||||
}
|
||||
|
||||
async setPushName(name: string) {
|
||||
await this.ensureWahaInjected();
|
||||
await this.pupPage.evaluate(async (pushName) => {
|
||||
return await window['WAHA'].WAWebSetPushnameConnAction.setPushname(
|
||||
pushName,
|
||||
@@ -151,6 +156,7 @@ export class WebjsClientCore extends Client {
|
||||
}
|
||||
|
||||
async createLabel(name: string, color: number): Promise<number> {
|
||||
await this.ensureWahaInjected();
|
||||
const labelId: number = (await this.pupPage.evaluate(
|
||||
async (name, color) => {
|
||||
// @ts-ignore
|
||||
@@ -166,6 +172,7 @@ export class WebjsClientCore extends Client {
|
||||
}
|
||||
|
||||
async deleteLabel(label: Label) {
|
||||
await this.ensureWahaInjected();
|
||||
return await this.pupPage.evaluate(async (label) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelDeleteAction(
|
||||
@@ -177,6 +184,7 @@ export class WebjsClientCore extends Client {
|
||||
}
|
||||
|
||||
async updateLabel(label: Label) {
|
||||
await this.ensureWahaInjected();
|
||||
return await this.pupPage.evaluate(async (label) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelEditAction(
|
||||
@@ -193,6 +201,8 @@ export class WebjsClientCore extends Client {
|
||||
return await super.getChats();
|
||||
}
|
||||
|
||||
await this.ensureWahaInjected();
|
||||
|
||||
// Get paginated chats
|
||||
pagination.limit ||= Infinity;
|
||||
pagination.offset ||= 0;
|
||||
@@ -209,6 +219,16 @@ export class WebjsClientCore extends Client {
|
||||
return chats.map((chat) => ChatFactory.create(this, chat));
|
||||
}
|
||||
|
||||
protected async ensureWahaInjected() {
|
||||
const hasWaha = await this.pupPage.evaluate(() => {
|
||||
// @ts-ignore
|
||||
return Boolean(window.WAHA && window.WAHA.getChats);
|
||||
});
|
||||
if (!hasWaha) {
|
||||
await this.injectWaha();
|
||||
}
|
||||
}
|
||||
|
||||
async sendTextStatus(status: TextStatus) {
|
||||
// Convert from hex to number
|
||||
const waColor = 'FF' + status.backgroundColor.replace('#', '');
|
||||
|
||||
@@ -121,11 +121,7 @@ import {
|
||||
} from '@waha/structures/responses.dto';
|
||||
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import {
|
||||
DeleteStatusRequest,
|
||||
StatusRequest,
|
||||
TextStatus,
|
||||
} from '@waha/structures/status.dto';
|
||||
import { DeleteStatusRequest, TextStatus } from '@waha/structures/status.dto';
|
||||
import {
|
||||
EnginePayload,
|
||||
PollVote as WAHAPollVote,
|
||||
@@ -185,6 +181,10 @@ import {
|
||||
import { Activity } from '@waha/core/abc/activity';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import { Jid } from '@waha/core/engines/const';
|
||||
import {
|
||||
WAHA_CLIENT_BROWSER_NAME,
|
||||
WAHA_CLIENT_DEVICE_NAME,
|
||||
} from '@waha/core/env';
|
||||
|
||||
export interface WebJSConfig {
|
||||
webVersion?: string;
|
||||
@@ -246,6 +246,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
// add at the start
|
||||
args.unshift(`--a-waha-timestamp=${new Date()}`);
|
||||
args.unshift(`--a-waha-session=${this.name}`);
|
||||
const deviceName =
|
||||
this.sessionConfig?.client?.deviceName ?? WAHA_CLIENT_DEVICE_NAME;
|
||||
const browserName =
|
||||
this.sessionConfig?.client?.browserName ?? WAHA_CLIENT_BROWSER_NAME;
|
||||
return {
|
||||
puppeteer: {
|
||||
protocolTimeout: 300_000,
|
||||
@@ -255,7 +259,9 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
dumpio: this.isDebugEnabled(),
|
||||
},
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
|
||||
deviceName: deviceName,
|
||||
browserName: browserName,
|
||||
webVersion: webVersion,
|
||||
webVersionCache: {
|
||||
type: cacheType,
|
||||
@@ -521,10 +527,29 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.AUTHENTICATED, (args) => {
|
||||
this.whatsapp.on(Events.AUTHENTICATED, async (args) => {
|
||||
this.status = WAHASessionStatus.WORKING;
|
||||
this.qr.save('');
|
||||
this.logger.info({ args: args }, `Session has been authenticated!`);
|
||||
|
||||
// Try to get client info from puppeter if nothing set
|
||||
// Fix https://github.com/devlikeapro/waha/issues/1735
|
||||
await sleep(3_000);
|
||||
if (!this.whatsapp.info) {
|
||||
// try to load client info few times with a delay
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
await this.loadClientInfo().catch((error) =>
|
||||
this.logger.error(
|
||||
error,
|
||||
`Failed to load client info, attempt ${attempt + 1}`,
|
||||
),
|
||||
);
|
||||
if (this.whatsapp.info) {
|
||||
break;
|
||||
}
|
||||
await sleep(3_000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.AUTHENTICATION_FAILURE, (args) => {
|
||||
@@ -585,6 +610,21 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
});
|
||||
}
|
||||
|
||||
private async loadClientInfo() {
|
||||
const data = await this.whatsapp.pupPage.evaluate(() => {
|
||||
return {
|
||||
// @ts-ignore
|
||||
...window.Store.Conn.serialize(),
|
||||
wid:
|
||||
// @ts-ignore
|
||||
window.Store.User.getMaybeMePnUser() ||
|
||||
// @ts-ignore
|
||||
window.Store.User.getMaybeMeLidUser(),
|
||||
};
|
||||
});
|
||||
this.whatsapp.info = data as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* START - Methods for API
|
||||
*/
|
||||
|
||||
@@ -22,3 +22,11 @@ if (KNEX_SQLITE_CLIENT != 'sqlite3' && KNEX_SQLITE_CLIENT != 'better-sqlite3') {
|
||||
KNEX_SQLITE_CLIENT = 'sqlite3';
|
||||
}
|
||||
export { KNEX_SQLITE_CLIENT };
|
||||
|
||||
//
|
||||
// Client config
|
||||
//
|
||||
export const WAHA_CLIENT_DEVICE_NAME =
|
||||
process.env.WAHA_CLIENT_DEVICE_NAME || null;
|
||||
export const WAHA_CLIENT_BROWSER_NAME =
|
||||
process.env.WAHA_CLIENT_BROWSER_NAME || null;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import { URL } from 'url';
|
||||
import { ProxyAgent } from 'undici';
|
||||
|
||||
import { WhatsappConfigService } from '../config.service';
|
||||
import { ProxyConfig } from '../structures/sessions.dto';
|
||||
@@ -72,7 +71,9 @@ export function createAgentProxy(proxyConfig: ProxyConfig): Agents | undefined {
|
||||
);
|
||||
|
||||
const socketAgent = new HttpsProxyAgent(url);
|
||||
const fetchAgent = new ProxyAgent({ uri: url });
|
||||
// Baileys media upload uses Node.js https.request in Node runtime,
|
||||
// so fetchAgent must be a Node https.Agent (ProxyAgent from undici won't work).
|
||||
const fetchAgent = new HttpsProxyAgent(url);
|
||||
|
||||
return { socket: socketAgent, fetch: fetchAgent };
|
||||
}
|
||||
@@ -44,8 +44,10 @@ export class WebhookConductor {
|
||||
for (const event of events) {
|
||||
const obs$ = session.getEventObservable(event);
|
||||
obs$.subscribe((payload) => {
|
||||
const data = populateSessionInfo(event, session)(payload);
|
||||
sender.send(data);
|
||||
setImmediate(() => {
|
||||
const data = populateSessionInfo(event, session)(payload);
|
||||
sender.send(data);
|
||||
});
|
||||
});
|
||||
this.logger.debug(`Event '${event}' is enabled for url: ${url}`);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import { getProxyConfig } from './helpers.proxy';
|
||||
import { MediaManager } from './media/MediaManager';
|
||||
import { LocalSessionAuthRepository } from './storage/LocalSessionAuthRepository';
|
||||
import { LocalStoreCore } from './storage/LocalStoreCore';
|
||||
import { CoreApiKeyRepository } from './storage/CoreApiKeyRepository';
|
||||
|
||||
export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
|
||||
constructor(name: string) {
|
||||
@@ -137,6 +138,7 @@ export class SessionManagerCore extends SessionManager implements OnModuleInit {
|
||||
}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
this.apiKeyRepository = new CoreApiKeyRepository();
|
||||
await this.engineBootstrap.bootstrap();
|
||||
this.startPredefinedSessions();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import * as path from 'path';
|
||||
|
||||
@Injectable()
|
||||
export class MediaLocalStorageConfig {
|
||||
@@ -12,7 +13,14 @@ export class MediaLocalStorageConfig {
|
||||
}
|
||||
|
||||
get filesFolder(): string {
|
||||
return this.config.get('WHATSAPP_FILES_FOLDER', '/tmp/whatsapp-files');
|
||||
const configured = this.config.get('WHATSAPP_FILES_FOLDER', '');
|
||||
if (!configured) {
|
||||
return '/tmp/whatsapp-files';
|
||||
}
|
||||
if (path.isAbsolute(configured)) {
|
||||
return configured;
|
||||
}
|
||||
return path.resolve(process.cwd(), configured);
|
||||
}
|
||||
|
||||
get filesLifetime(): number {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import { DOCS_URL } from '@waha/core/exceptions';
|
||||
import {
|
||||
ApiKey,
|
||||
IApiKeyRepository,
|
||||
} from '@waha/core/storage/IApiKeyRepository';
|
||||
|
||||
export class CoreApiKeyRepository implements IApiKeyRepository {
|
||||
async init() {
|
||||
return;
|
||||
}
|
||||
|
||||
list(): Promise<ApiKey[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
getActiveByKey(key: string): Promise<ApiKey | null> {
|
||||
void key;
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
getById(id: string): Promise<ApiKey | null> {
|
||||
void id;
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
getByKey(key: string): Promise<ApiKey | null> {
|
||||
void key;
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
async upsert(key: ApiKey): Promise<ApiKey> {
|
||||
void key;
|
||||
throw new UnprocessableEntityException(
|
||||
`API key management is not available in this edition. See ${DOCS_URL}`,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteById(id: string): Promise<void> {
|
||||
void id;
|
||||
throw new UnprocessableEntityException(
|
||||
`API key management is not available in this edition. See ${DOCS_URL}`,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteBySession(session: string): Promise<void> {
|
||||
void session;
|
||||
throw new UnprocessableEntityException(
|
||||
`API key management is not available in this edition. See ${DOCS_URL}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { RawRuleOf } from '@casl/ability';
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import { AppAbility } from '../auth/casl.types';
|
||||
|
||||
export interface ApiKey {
|
||||
id: string;
|
||||
key: string;
|
||||
isActive: boolean;
|
||||
isAdmin: boolean;
|
||||
session: string | null;
|
||||
rules: RawRuleOf<AppAbility>[] | null;
|
||||
}
|
||||
|
||||
export interface IApiKeyRepository {
|
||||
init(): Promise<void>;
|
||||
|
||||
list(): Promise<ApiKey[]>;
|
||||
|
||||
upsert(key: ApiKey): Promise<ApiKey>;
|
||||
|
||||
getActiveByKey(key: string): Promise<ApiKey | null>;
|
||||
|
||||
getById(id: string): Promise<ApiKey | null>;
|
||||
|
||||
getByKey(key: string): Promise<ApiKey | null>;
|
||||
|
||||
deleteById(id: string): Promise<void>;
|
||||
|
||||
deleteBySession(session: string): Promise<void>;
|
||||
}
|
||||
|
||||
export function CheckInvariant(
|
||||
apiKey: Pick<ApiKey, 'isAdmin' | 'session'>,
|
||||
): void {
|
||||
if (apiKey.isAdmin && apiKey.session) {
|
||||
throw new UnprocessableEntityException(
|
||||
'Session is not allowed for admin keys',
|
||||
);
|
||||
}
|
||||
if (!apiKey.isAdmin && !apiKey.session) {
|
||||
throw new UnprocessableEntityException(
|
||||
'Either isAdmin must be true or session must be provided',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ export abstract class ISessionConfigRepository {
|
||||
|
||||
abstract getConfig(sessionName: string): Promise<SessionConfig | null>;
|
||||
|
||||
abstract getConfigBySessions(
|
||||
sessionNames: string[],
|
||||
): Promise<Map<string, SessionConfig | null>>;
|
||||
|
||||
abstract exists(sessionName: string): Promise<boolean>;
|
||||
|
||||
abstract deleteConfig(sessionName: string): Promise<void>;
|
||||
|
||||
@@ -5,6 +5,10 @@ export abstract class ISessionMeRepository {
|
||||
|
||||
abstract getMe(sessionName: string): Promise<MeInfo | null>;
|
||||
|
||||
abstract getMeBySessions(
|
||||
sessionNames: string[],
|
||||
): Promise<Map<string, MeInfo | null>>;
|
||||
|
||||
abstract removeMe(sessionName: string): Promise<void>;
|
||||
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
@@ -48,6 +48,26 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
async getConfigBySessions(
|
||||
sessionNames: string[],
|
||||
): Promise<Map<string, SessionConfig | null>> {
|
||||
const result = new Map<string, SessionConfig | null>();
|
||||
const uniqueNames = Array.from(new Set(sessionNames));
|
||||
if (uniqueNames.length === 0) {
|
||||
return result;
|
||||
}
|
||||
const items = await Promise.all(
|
||||
uniqueNames.map(async (sessionName) => ({
|
||||
sessionName,
|
||||
config: await this.getConfig(sessionName),
|
||||
})),
|
||||
);
|
||||
for (const item of items) {
|
||||
result.set(item.sessionName, item.config ?? null);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async saveConfig(sessionName: string, config: SessionConfig) {
|
||||
// Create a folder if not exist
|
||||
const folder = this.store.getSessionDirectory(sessionName);
|
||||
|
||||
@@ -52,3 +52,29 @@ export const SQLSessionWorkerMigrations: Migration[] = [
|
||||
// Worker can have multiple records
|
||||
'CREATE INDEX IF NOT EXISTS session_worker_worker_idx ON session_worker (worker)',
|
||||
];
|
||||
|
||||
/**
|
||||
* Api Keys
|
||||
*/
|
||||
export const SQLApiKeySchema = new Schema(
|
||||
'api_key',
|
||||
[
|
||||
new Field('id', 'TEXT'),
|
||||
new Field('key', 'TEXT'),
|
||||
new Field('isActive', 'INTEGER'),
|
||||
new Field('session', 'TEXT'),
|
||||
new Field('data', 'TEXT'),
|
||||
],
|
||||
[
|
||||
new Index('api_key_id_index', ['id']),
|
||||
new Index('api_key_key_idx', ['key']),
|
||||
new Index('api_key_session_idx', ['session']),
|
||||
],
|
||||
);
|
||||
|
||||
export const SQLApiKeyMigrations: Migration[] = [
|
||||
'CREATE TABLE IF NOT EXISTS api_key (id TEXT PRIMARY KEY, "key" TEXT, "isActive" INTEGER, session TEXT, data TEXT)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS api_key_id_index ON api_key (id)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS api_key_key_idx ON api_key ("key")',
|
||||
'CREATE INDEX IF NOT EXISTS api_key_session_idx ON api_key (session)',
|
||||
];
|
||||
@@ -36,6 +36,22 @@ export class Sqlite3SessionMeRepository
|
||||
return data?.me;
|
||||
}
|
||||
|
||||
async getMeBySessions(
|
||||
sessionNames: string[],
|
||||
): Promise<Map<string, MeInfo | null>> {
|
||||
const result = new Map<string, MeInfo | null>();
|
||||
const uniqueNames = Array.from(new Set(sessionNames));
|
||||
if (uniqueNames.length === 0) {
|
||||
return result;
|
||||
}
|
||||
const entities = await this.getEntitiesByIds(uniqueNames);
|
||||
for (const sessionName of uniqueNames) {
|
||||
const entity = entities.get(sessionName);
|
||||
result.set(sessionName, entity?.me ?? null);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
removeMe(sessionName: string): Promise<void> {
|
||||
return this.deleteById(sessionName);
|
||||
}
|
||||
|
||||
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user