Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4f88771a8 | ||
|
|
e885ea8e95 | ||
|
|
f8a9ed7d06 | ||
|
|
d89e9105d0 | ||
|
|
3ba92dec04 | ||
|
|
441ebbd525 | ||
|
|
690c43c245 | ||
|
|
26d8591a6c | ||
|
|
ac63c097ec | ||
|
|
d29e4f8351 | ||
|
|
615ccda4c3 | ||
|
|
d52eb71c76 | ||
|
|
8edd81a765 | ||
|
|
5830cee384 | ||
|
|
20f15b4370 | ||
|
|
8432cd4f8d | ||
|
|
55870b88bf | ||
|
|
6acf4373f4 | ||
|
|
2381f0843f | ||
|
|
b4e6f5e722 | ||
|
|
415bee3e8e | ||
|
|
aad924be79 | ||
|
|
81083e7f82 | ||
|
|
f8e52959a4 | ||
|
|
8eb542f49e | ||
|
|
45ae263ab9 | ||
|
|
f066f982bf | ||
|
|
0888640741 | ||
|
|
bde3722f5c | ||
|
|
0cd3df117a | ||
|
|
4c9525cc9f | ||
|
|
c14fe1e67d | ||
|
|
d1e3b209e7 | ||
|
|
f37bf62a30 | ||
|
|
538bfe0e24 | ||
|
|
93cd5307d9 | ||
|
|
b697f37b1e | ||
|
|
96a9cd8cbb | ||
|
|
a21b9a11a9 | ||
|
|
213b51e598 | ||
|
|
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 |
No files matched your search
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(yarn build:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(npm show:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(yarn test:unit:*)",
|
||||
"Bash(npx tsc:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ WHATSAPP_SWAGGER_ENABLED=True
|
||||
# ==================
|
||||
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
|
||||
WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
# So you don't have to create sessions/apps when switching between engines
|
||||
WAHA_NAMESPACE=all
|
||||
|
||||
# "Firefox (YourApp)" in Linked Devices
|
||||
# WAHA_CLIENT_DEVICE_NAME=YourApp
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,8 +75,14 @@ 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.
|
||||
- Do not write verbose ternaries like
|
||||
`condition !== undefined ? condition : default`; use idiomatic helpers such as
|
||||
`??` (nullish coalescing) or existing boolean parsers so flags stay readable.
|
||||
- Do not place `await` or other async calls inside ternary expressions (`?:`);
|
||||
use explicit `if/else` blocks instead.
|
||||
- For configs, prefer runtime configurability over constants. Environment keys
|
||||
follow `WAHA_*` for global values and `WAHA_SESSION_CONFIG_*` /
|
||||
`session.config.*` for per-session overrides. If both env and config are
|
||||
@@ -100,10 +111,10 @@ This guide summarizes how to explore, modify, and validate the WhatsApp HTTP API
|
||||
2. Lean on existing services/managers; extend the appropriate session manager
|
||||
rather than branching logic inline.
|
||||
3. After edits run:
|
||||
- `pre-commit run --all-files`
|
||||
- Do **not** run `pre-commit run --all-files` unless the user explicitly
|
||||
asks.
|
||||
- `yarn build`
|
||||
- `yarn test` Use Node 22. Address lint or formatting issues before
|
||||
proceeding.
|
||||
- `yarn test --watchman=false`
|
||||
4. Do **not** start the application yourself; ask the user to run it if runtime
|
||||
validation is required.
|
||||
5. Capture any assumptions or open questions for the user, especially when
|
||||
@@ -120,14 +131,80 @@ This guide summarizes how to explore, modify, and validate the WhatsApp HTTP API
|
||||
- Keep docs and code ASCII unless a file already uses other characters. When
|
||||
updating documentation, mirror the concise, actionable tone used here.
|
||||
|
||||
## Yes No
|
||||
|
||||
Always define key for objects (and in return too)
|
||||
|
||||
```js
|
||||
// NO
|
||||
const variable = 123;
|
||||
const b = { variable };
|
||||
|
||||
// YES
|
||||
const variable = 123;
|
||||
const b = { variable: variable };
|
||||
```
|
||||
|
||||
Use the three-line comment style for section headers inside files:
|
||||
|
||||
```ts
|
||||
// NO
|
||||
// ─── Section name ─────────────────────────────────────────────────────────────
|
||||
|
||||
// YES
|
||||
//
|
||||
// Section name
|
||||
//
|
||||
```
|
||||
|
||||
## Related Sources Code
|
||||
|
||||
You can find related source code in the following paths:
|
||||
You can find and read 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`
|
||||
- WPP: `../wa-js`, `../wppconnect`, `../wppconnect-server`
|
||||
- ChatWoot: `../chatwoot`
|
||||
|
||||
Following this playbook keeps contributions aligned with WAHA’s structure,
|
||||
automation hooks, and release process.
|
||||
|
||||
## How to run API
|
||||
|
||||
You can run the project outside of sandbox using the below command, then run
|
||||
queries against `default` session (if not asked to do something different) using
|
||||
`curl` and `X-Api-Key: 666` header.
|
||||
|
||||
```bash
|
||||
export DEBUG=1
|
||||
export WAHA_API_KEY=666
|
||||
export WAHA_DASHBOARD_PASSWORD=666
|
||||
export WAHA_DASHBOARD_USERNAME=admin
|
||||
export WWHATSAPP_SWAGGER_USERNAME=admin
|
||||
export WHATSAPP_SWAGGER_PASSWORD=666
|
||||
export WHATSAPP_DEFAULT_ENGINE={WEBJS|WPP|NOWEB|GOWS}
|
||||
export WAHA_DEBUG_MODE=True
|
||||
export WAHA_HTTP_STRICT_MODE=1
|
||||
export WAHA_MEDIA_STORAGE=LOCAL
|
||||
export WHATSAPP_FILES_FOLDER=./.media
|
||||
|
||||
npm run start
|
||||
```
|
||||
|
||||
- Ask user before running the server.
|
||||
- Before executing some queries make sure the session is in `WORKING` status.
|
||||
- If it's `FAILED` or `SCAN_QR_CODE` ask user to scan QR code or fix failed
|
||||
session.
|
||||
|
||||
## Code
|
||||
|
||||
### @Activity Decorator and Presence Tracking
|
||||
|
||||
Add `@Activity()` (from `src/core/abc/activity.ts`) to every engine method that
|
||||
makes a network call to WhatsApp servers. It triggers `maintainPresenceOnline()`
|
||||
before the method runs, keeping the session ONLINE during API activity and
|
||||
scheduling an OFFLINE transition after an idle period. Skip it on methods that
|
||||
only throw `NotImplementedByEngineError` / `AvailableInPlusVersion`.
|
||||
+3
-1
@@ -8,7 +8,9 @@ FROM node:${NODE_IMAGE_TAG} AS build
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
|
||||
# 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/*
|
||||
|
||||
# npm packages
|
||||
WORKDIR /git
|
||||
|
||||
@@ -32,16 +32,22 @@ 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-18
|
||||
|
||||
up-wpp:
|
||||
yarn up @wppconnect-team/wppconnect
|
||||
|
||||
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 +63,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
|
||||
|
||||
@@ -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
|
||||
|
||||
+57
-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",
|
||||
@@ -53,6 +57,7 @@
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/sqlite3": "^5.1.0",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@wppconnect-team/wppconnect": "^1.40.1",
|
||||
"adm-zip": "0.5.10",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"async-lock": "^1.4.1",
|
||||
@@ -107,7 +112,8 @@
|
||||
"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",
|
||||
"user-agents": "^1.1.669",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2026-02-18",
|
||||
"write-file-atomic": "^6.0.0",
|
||||
"yaml": "^2.7.1"
|
||||
},
|
||||
@@ -120,7 +126,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": "npm:@devlikeapro/whatsapp-rust-bridge@0.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grpc/grpc-js": "^1.14.1",
|
||||
@@ -134,6 +141,7 @@
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@types/user-agents": "^1",
|
||||
"dotenv": "^17.2.3",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"jest": "^29.7.0",
|
||||
@@ -148,21 +156,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,
|
||||
@@ -123,14 +131,15 @@ export class ChannelsController {
|
||||
})
|
||||
async previewChannelMessages(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') code: string,
|
||||
@Param('id') codeOrId: string,
|
||||
@Query() query: PreviewChannelMessages,
|
||||
): Promise<ChannelMessage[]> {
|
||||
if (isJidNewsletter(code)) {
|
||||
const channel = await session.channelsGetChannel(code);
|
||||
let code = null;
|
||||
if (isJidNewsletter(codeOrId)) {
|
||||
const channel = await session.channelsGetChannel(codeOrId);
|
||||
code = parseChannelInviteLink(channel.invite);
|
||||
}
|
||||
const inviteCode = parseChannelInviteLink(code);
|
||||
const inviteCode = parseChannelInviteLink(code || codeOrId);
|
||||
return session.previewChannelMessages(inviteCode, query);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
@@ -24,15 +25,15 @@ import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import {
|
||||
ChatPictureQuery,
|
||||
ChatPictureResponse,
|
||||
ChatsPaginationParams,
|
||||
ChatSummary,
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
GetChatsOverviewParams,
|
||||
GetChatsParams,
|
||||
MessageSortField,
|
||||
OverviewBodyRequest,
|
||||
OverviewFilter,
|
||||
OverviewPaginationParams,
|
||||
PinMessageRequest,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
@@ -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) {}
|
||||
|
||||
@@ -53,7 +61,7 @@ class ChatsController {
|
||||
@ApiOperation({ summary: 'Get chats' })
|
||||
getChats(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() pagination: ChatsPaginationParams,
|
||||
@Query() pagination: GetChatsParams,
|
||||
) {
|
||||
return session.getChats(pagination);
|
||||
}
|
||||
@@ -67,7 +75,7 @@ class ChatsController {
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
getChatsOverview(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() pagination: OverviewPaginationParams,
|
||||
@Query() pagination: GetChatsOverviewParams,
|
||||
@Query() filter: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
return session.getChatsOverview(pagination, filter);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Put,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiParam, 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 {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} 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 { 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) {}
|
||||
|
||||
@Get('/:id')
|
||||
@SessionApiParam
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Contact ID',
|
||||
example: '123456789@c.us',
|
||||
})
|
||||
@ApiOperation({
|
||||
summary: 'Get contact basic info',
|
||||
description:
|
||||
'The method always return result, even if the phone number is not registered in WhatsApp. For that - use /contacts/check-exists endpoint below.',
|
||||
})
|
||||
async get(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.getContact({ session: session.name, contactId: id });
|
||||
}
|
||||
|
||||
@Put('/:chatId')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Create or update contact',
|
||||
description:
|
||||
'Create or update contact on the phone address book. May not work if you have installed many WhatsApp apps on the same phone',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async put(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Body() body: ContactUpdateBody,
|
||||
): Promise<Result> {
|
||||
await session.upsertContact(chatId, body);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@@ -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/WPP** 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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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,15 @@ 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 {
|
||||
CanServer,
|
||||
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 +37,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 +47,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 +57,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);
|
||||
@@ -55,15 +70,39 @@ export class AppsController {
|
||||
|
||||
@Get('/:id')
|
||||
@ApiOperation({ summary: 'Get app by ID' })
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
@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' })
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
@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) {
|
||||
@@ -82,8 +121,16 @@ export class AppsController {
|
||||
|
||||
@Delete('/:id')
|
||||
@ApiOperation({ summary: 'Delete an app' })
|
||||
@CheckPolicies(CanServer(Action.Read))
|
||||
@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,
|
||||
|
||||
+33
-5
@@ -1,4 +1,10 @@
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import { APPS } from '@waha/apps/app_sdk/apps/definition';
|
||||
|
||||
// Apps that don't require queue (Redis) - derived from AppDefinition
|
||||
const IN_MEMORY_APPS = Object.values(APPS)
|
||||
.filter((app) => !app.queue)
|
||||
.map((app) => app.name);
|
||||
|
||||
function parseCommaSeparatedList(value: string | undefined): string[] {
|
||||
if (!value) {
|
||||
@@ -7,8 +13,30 @@ function parseCommaSeparatedList(value: string | undefined): string[] {
|
||||
return value.split(',').map((item) => item.trim());
|
||||
}
|
||||
|
||||
export const AppEnv = {
|
||||
enabled: parseBool(process.env.WAHA_APPS_ENABLED),
|
||||
on: parseCommaSeparatedList(process.env.WAHA_APPS_ON),
|
||||
off: parseCommaSeparatedList(process.env.WAHA_APPS_OFF),
|
||||
};
|
||||
function isMongoDB(): boolean {
|
||||
return !!process.env.WHATSAPP_SESSIONS_MONGO_URL;
|
||||
}
|
||||
|
||||
function buildAppEnv() {
|
||||
const enabled = process.env.WAHA_APPS_ENABLED;
|
||||
const on = process.env.WAHA_APPS_ON;
|
||||
const off = process.env.WAHA_APPS_OFF;
|
||||
|
||||
// Default if not mongodb
|
||||
const isUserConfigured = enabled !== undefined || on !== undefined;
|
||||
if (!isUserConfigured && !isMongoDB()) {
|
||||
return {
|
||||
enabled: true,
|
||||
on: IN_MEMORY_APPS,
|
||||
off: parseCommaSeparatedList(off),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: parseBool(enabled),
|
||||
on: parseCommaSeparatedList(on),
|
||||
off: parseCommaSeparatedList(off),
|
||||
};
|
||||
}
|
||||
|
||||
export const AppEnv = buildAppEnv();
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||
* Parse human string into milliseconds
|
||||
*/
|
||||
export function ParseMS(value: string) {
|
||||
const duration = ms(value);
|
||||
const duration = ms(value as ms.StringValue);
|
||||
if (duration == null) throw new Error(`Invalid duration: "${value}"`);
|
||||
if (duration < 0) throw new Error(`Duration cannot be negative: "${value}"`);
|
||||
return duration;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -347,7 +347,7 @@ export abstract class MessageBaseHandler<
|
||||
chat_id: toCusFormat(key.remoteJid),
|
||||
message_id: key.id,
|
||||
from_me: key.fromMe,
|
||||
participant: null,
|
||||
participant: isJidGroup(key.remoteJid) ? key.participant : null,
|
||||
};
|
||||
await this.mappingService.map(chatwoot, whatsapp, 1);
|
||||
return;
|
||||
|
||||
@@ -32,7 +32,10 @@ export class WAHAMessageReactionConsumer extends ChatWootWAHABaseConsumer {
|
||||
}
|
||||
|
||||
GetChatId(event: WAHAWebhookMessageReaction): string {
|
||||
if (event.environment?.engine == WAHAEngine.WEBJS) {
|
||||
if (
|
||||
event.environment?.engine == WAHAEngine.WEBJS ||
|
||||
event.environment?.engine == WAHAEngine.WPP
|
||||
) {
|
||||
// chat in "to" field for WEBJS for message.reaction
|
||||
// probably we need to set it to "from"
|
||||
// but for backward compatability we do this
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@ import { WAHAEngine } from '@waha/structures/enums.dto';
|
||||
import { getEngineName } from '@waha/version';
|
||||
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||
import { isLidUser, isPnUser, toCusFormat } from '@waha/core/utils/jids';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
|
||||
@@ -52,7 +53,7 @@ class NOWEBHelper implements IEngineHelper {
|
||||
}
|
||||
|
||||
FilterChatIdsForMessages(chats: string[]): string[] {
|
||||
return chats;
|
||||
return preferPnChats(chats);
|
||||
}
|
||||
|
||||
ContactIsMy(contact) {
|
||||
@@ -96,7 +97,7 @@ class GOWSHelper implements IEngineHelper {
|
||||
}
|
||||
|
||||
FilterChatIdsForMessages(chats: string[]): string[] {
|
||||
return chats;
|
||||
return preferPnChats(chats);
|
||||
}
|
||||
|
||||
SupportsAllChatForMessage(): boolean {
|
||||
@@ -150,17 +151,63 @@ class WEBJSHelper implements IEngineHelper {
|
||||
}
|
||||
|
||||
FilterChatIdsForMessages(chats: string[]): string[] {
|
||||
if (chats.length == 2) {
|
||||
const lidChat = chats.find(isLidUser);
|
||||
const cusChat = chats.find(isPnUser);
|
||||
if (lidChat && cusChat) {
|
||||
return [lidChat];
|
||||
}
|
||||
// WEBJS engine merges messages for @lid and @c.us
|
||||
// into single chat, so it's fine to pull only from one
|
||||
return preferPnChats(chats);
|
||||
}
|
||||
|
||||
SupportsAllChatForMessage(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
ContactIsMy(contact) {
|
||||
return contact.isMyContact;
|
||||
}
|
||||
}
|
||||
|
||||
class WPPHelper implements IEngineHelper {
|
||||
ChatID(message: WAMessage): string {
|
||||
return toCusFormat(parseMessageIdSerialized(message.id as any).remoteJid);
|
||||
}
|
||||
|
||||
CallChatID(call: CallData): string {
|
||||
return call.from;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse API response and get the data for WPP engine.
|
||||
* WPP returns message.id as a composite string "fromMe_chatId_msgId".
|
||||
*/
|
||||
WhatsAppMessageKeys(message: any): WhatsAppMessage {
|
||||
const parsed = parseMessageIdSerialized(message.id);
|
||||
return {
|
||||
timestamp: new Date(message.timestamp * 1000),
|
||||
from_me: parsed.fromMe,
|
||||
chat_id: toCusFormat(parsed.remoteJid),
|
||||
message_id: parsed.id,
|
||||
participant: message.author || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* WPP API lacks server-side sorting hooks, so we buffer and sort by the unix timestamp in memory.
|
||||
*/
|
||||
async *IterateMessages<T extends { timestamp: number }>(
|
||||
messages: AsyncGenerator<T>,
|
||||
): AsyncGenerator<T> {
|
||||
const buffer: T[] = [];
|
||||
|
||||
for await (const message of messages) {
|
||||
buffer.push(message);
|
||||
}
|
||||
// Otherwise - return the original
|
||||
return chats;
|
||||
|
||||
const sorted = lodash.sortBy(buffer, (item) => item.timestamp);
|
||||
|
||||
for (const message of sorted) {
|
||||
yield message;
|
||||
}
|
||||
}
|
||||
|
||||
FilterChatIdsForMessages(chats: string[]): string[] {
|
||||
return preferPnChats(chats);
|
||||
}
|
||||
|
||||
SupportsAllChatForMessage(): boolean {
|
||||
@@ -185,8 +232,22 @@ switch (getEngineName()) {
|
||||
case WAHAEngine.WEBJS:
|
||||
engineHelper = new WEBJSHelper();
|
||||
break;
|
||||
case WAHAEngine.WPP:
|
||||
engineHelper = new WPPHelper();
|
||||
break;
|
||||
default:
|
||||
engineHelper = new WEBJSHelper(); // Default to WEBJS as it's the default engine
|
||||
}
|
||||
|
||||
export const EngineHelper = engineHelper;
|
||||
|
||||
function preferPnChats(chats: string[]): string[] {
|
||||
const unique = lodash.uniq(chats ?? []);
|
||||
const hasPn = unique.some(isPnUser);
|
||||
const hasLid = unique.some(isLidUser);
|
||||
if (hasPn && hasLid) {
|
||||
// Prefer @c.us / phone chats when both formats exist to avoid duplicate fetches.
|
||||
return unique.filter(isPnUser);
|
||||
}
|
||||
return unique;
|
||||
}
|
||||
@@ -5,3 +5,15 @@ export function getEngineName(): string {
|
||||
// If not set - use WEBJS
|
||||
return process.env.WHATSAPP_DEFAULT_ENGINE || WAHAEngine.WEBJS;
|
||||
}
|
||||
|
||||
export function getNamespace(): string {
|
||||
// Controls the prefix used for the main database and folder (non-session data)
|
||||
// If not set - falls back to engine name for backward compatibility
|
||||
return (process.env.WAHA_NAMESPACE || getEngineName()).toLowerCase();
|
||||
}
|
||||
|
||||
export function getSessionNamespace(): string {
|
||||
// Controls the prefix used for session databases and folders
|
||||
// If not set - falls back to engine name for backward compatibility
|
||||
return (process.env.WAHA_SESSION_NAMESPACE || getEngineName()).toLowerCase();
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Body, Controller, Delete, Param, Put, UsePipes } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} 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';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/contacts')
|
||||
@ApiTags('👤 Contacts')
|
||||
export class ContactsSessionController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Put('/:chatId')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Create or update contact',
|
||||
description:
|
||||
'Create or update contact on the phone address book. May not work if you have installed many WhatsApp apps on the same phone',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async put(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Body() body: ContactUpdateBody,
|
||||
): Promise<Result> {
|
||||
await session.upsertContact(chatId, body);
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -9,18 +9,20 @@ 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';
|
||||
import { WPPEngineConfigService } from '@waha/core/config/WPPEngineConfigService';
|
||||
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
||||
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
@@ -64,6 +66,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 +145,7 @@ const IMPORTS = [...IMPORTS_CORE, ...IMPORTS_MEDIA];
|
||||
|
||||
export const CONTROLLERS = [
|
||||
AuthController,
|
||||
ApiKeysController,
|
||||
SessionsController,
|
||||
ProfileController,
|
||||
ChattingController,
|
||||
@@ -171,6 +177,7 @@ export const PROVIDERS_BASE: Provider[] = [
|
||||
DashboardConfigServiceCore,
|
||||
SwaggerConfigServiceCore,
|
||||
WebJSEngineConfigService,
|
||||
WPPEngineConfigService,
|
||||
GowsEngineConfigService,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
@@ -178,6 +185,9 @@ export const PROVIDERS_BASE: Provider[] = [
|
||||
MediaLocalStorageConfig,
|
||||
WebSocketAuth,
|
||||
ApiKeyStrategy,
|
||||
ApiKeyService,
|
||||
CaslAbilityFactory,
|
||||
PoliciesGuard,
|
||||
{
|
||||
provide: IApiKeyAuth,
|
||||
useFactory: ApiKeyAuthFactory,
|
||||
@@ -234,11 +244,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,37 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { IApiKeyAuth, NoAuth } from '@waha/core/auth/auth';
|
||||
import * as passport from 'passport';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyAuthMiddleware implements NestMiddleware {
|
||||
constructor(private auth: IApiKeyAuth) {}
|
||||
|
||||
use(req: any, res: any, next: () => void) {
|
||||
if (this.auth instanceof NoAuth) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { WPPConfig } from '@waha/core/engines/wpp/WppConfig';
|
||||
|
||||
@Injectable()
|
||||
export class WPPEngineConfigService {
|
||||
constructor(protected configService: ConfigService) {}
|
||||
|
||||
getConfig(): WPPConfig {
|
||||
let webVersion = this.configService.get<string>(
|
||||
'WAHA_WPP_WEB_VERSION',
|
||||
undefined,
|
||||
);
|
||||
if (!webVersion) {
|
||||
webVersion = this.configService.get<string>(
|
||||
'WAHA_WEBJS_WEB_VERSION',
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
return {
|
||||
webVersion: webVersion,
|
||||
puppeteerArgs: this.getPuppeterArgs(),
|
||||
};
|
||||
}
|
||||
|
||||
getPuppeterArgs(): string[] {
|
||||
let args = this.configService.get<string>('WAHA_WPP_PUPPETER_ARGS', '');
|
||||
if (!args) {
|
||||
args = this.configService.get<string>('WAHA_WEBJS_PUPPETER_ARGS', '');
|
||||
}
|
||||
return args
|
||||
.split(' ')
|
||||
.map((arg) => arg.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
}
|
||||
@@ -52,9 +52,11 @@ export class GowsEventStreamObservable extends Observable<EnginePayload> {
|
||||
};
|
||||
|
||||
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,24 +1,22 @@
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
import { messages } from '@waha/core/engines/gows/grpc/gows';
|
||||
|
||||
export const GetMessageServiceClient = (
|
||||
session: string,
|
||||
export function BuildMessageServiceClient(
|
||||
address: string,
|
||||
credentials: grpc.ChannelCredentials,
|
||||
): messages.MessageServiceClient => {
|
||||
): 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 => {
|
||||
): messages.EventStreamClient {
|
||||
return new messages.EventStreamClient(address, credentials, {
|
||||
'grpc.max_send_message_length': 128 * 1024 * 1024,
|
||||
'grpc.max_receive_message_length': 128 * 1024 * 1024,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -879,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[] | {
|
||||
@@ -1236,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[] | {
|
||||
@@ -1440,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);
|
||||
@@ -1463,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() {
|
||||
@@ -1501,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";
|
||||
@@ -1510,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) {
|
||||
@@ -1529,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() {
|
||||
@@ -1537,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();
|
||||
@@ -1550,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;
|
||||
@@ -1564,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();
|
||||
}
|
||||
@@ -1585,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();
|
||||
}
|
||||
}
|
||||
@@ -2680,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[] | {
|
||||
@@ -2689,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);
|
||||
@@ -2711,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() {
|
||||
@@ -2752,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;
|
||||
@@ -2759,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) {
|
||||
@@ -2779,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() {
|
||||
@@ -2789,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;
|
||||
@@ -2808,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;
|
||||
@@ -2826,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();
|
||||
}
|
||||
@@ -2853,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();
|
||||
}
|
||||
}
|
||||
@@ -8812,6 +9221,7 @@ export namespace messages {
|
||||
filters?: MessageFilters;
|
||||
pagination?: Pagination;
|
||||
sortBy?: SortBy;
|
||||
merge?: OptionalBool;
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
@@ -8828,6 +9238,9 @@ export namespace messages {
|
||||
if ("sortBy" in data && data.sortBy != undefined) {
|
||||
this.sortBy = data.sortBy;
|
||||
}
|
||||
if ("merge" in data && data.merge != undefined) {
|
||||
this.merge = data.merge;
|
||||
}
|
||||
}
|
||||
}
|
||||
get session() {
|
||||
@@ -8866,11 +9279,21 @@ export namespace messages {
|
||||
get has_sortBy() {
|
||||
return pb_1.Message.getField(this, 4) != null;
|
||||
}
|
||||
get merge() {
|
||||
return pb_1.Message.getWrapperField(this, OptionalBool, 5) as OptionalBool;
|
||||
}
|
||||
set merge(value: OptionalBool) {
|
||||
pb_1.Message.setWrapperField(this, 5, value);
|
||||
}
|
||||
get has_merge() {
|
||||
return pb_1.Message.getField(this, 5) != null;
|
||||
}
|
||||
static fromObject(data: {
|
||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||
filters?: ReturnType<typeof MessageFilters.prototype.toObject>;
|
||||
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
||||
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
||||
merge?: ReturnType<typeof OptionalBool.prototype.toObject>;
|
||||
}): GetMessagesRequest {
|
||||
const message = new GetMessagesRequest({});
|
||||
if (data.session != null) {
|
||||
@@ -8885,6 +9308,9 @@ export namespace messages {
|
||||
if (data.sortBy != null) {
|
||||
message.sortBy = SortBy.fromObject(data.sortBy);
|
||||
}
|
||||
if (data.merge != null) {
|
||||
message.merge = OptionalBool.fromObject(data.merge);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
@@ -8893,6 +9319,7 @@ export namespace messages {
|
||||
filters?: ReturnType<typeof MessageFilters.prototype.toObject>;
|
||||
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
||||
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
||||
merge?: ReturnType<typeof OptionalBool.prototype.toObject>;
|
||||
} = {};
|
||||
if (this.session != null) {
|
||||
data.session = this.session.toObject();
|
||||
@@ -8906,6 +9333,9 @@ export namespace messages {
|
||||
if (this.sortBy != null) {
|
||||
data.sortBy = this.sortBy.toObject();
|
||||
}
|
||||
if (this.merge != null) {
|
||||
data.merge = this.merge.toObject();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
@@ -8920,6 +9350,8 @@ export namespace messages {
|
||||
writer.writeMessage(3, this.pagination, () => this.pagination.serialize(writer));
|
||||
if (this.has_sortBy)
|
||||
writer.writeMessage(4, this.sortBy, () => this.sortBy.serialize(writer));
|
||||
if (this.has_merge)
|
||||
writer.writeMessage(5, this.merge, () => this.merge.serialize(writer));
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
@@ -8941,6 +9373,9 @@ export namespace messages {
|
||||
case 4:
|
||||
reader.readMessage(message.sortBy, () => message.sortBy = SortBy.deserialize(reader));
|
||||
break;
|
||||
case 5:
|
||||
reader.readMessage(message.merge, () => message.merge = OptionalBool.deserialize(reader));
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -9288,6 +9723,7 @@ export namespace messages {
|
||||
sortBy?: SortBy;
|
||||
pagination?: Pagination;
|
||||
filter?: ChatFilter;
|
||||
merge?: OptionalBool;
|
||||
}) {
|
||||
super();
|
||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||
@@ -9304,6 +9740,9 @@ export namespace messages {
|
||||
if ("filter" in data && data.filter != undefined) {
|
||||
this.filter = data.filter;
|
||||
}
|
||||
if ("merge" in data && data.merge != undefined) {
|
||||
this.merge = data.merge;
|
||||
}
|
||||
}
|
||||
}
|
||||
get session() {
|
||||
@@ -9342,11 +9781,21 @@ export namespace messages {
|
||||
get has_filter() {
|
||||
return pb_1.Message.getField(this, 4) != null;
|
||||
}
|
||||
get merge() {
|
||||
return pb_1.Message.getWrapperField(this, OptionalBool, 5) as OptionalBool;
|
||||
}
|
||||
set merge(value: OptionalBool) {
|
||||
pb_1.Message.setWrapperField(this, 5, value);
|
||||
}
|
||||
get has_merge() {
|
||||
return pb_1.Message.getField(this, 5) != null;
|
||||
}
|
||||
static fromObject(data: {
|
||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
||||
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
||||
filter?: ReturnType<typeof ChatFilter.prototype.toObject>;
|
||||
merge?: ReturnType<typeof OptionalBool.prototype.toObject>;
|
||||
}): GetChatsRequest {
|
||||
const message = new GetChatsRequest({});
|
||||
if (data.session != null) {
|
||||
@@ -9361,6 +9810,9 @@ export namespace messages {
|
||||
if (data.filter != null) {
|
||||
message.filter = ChatFilter.fromObject(data.filter);
|
||||
}
|
||||
if (data.merge != null) {
|
||||
message.merge = OptionalBool.fromObject(data.merge);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
toObject() {
|
||||
@@ -9369,6 +9821,7 @@ export namespace messages {
|
||||
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
||||
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
||||
filter?: ReturnType<typeof ChatFilter.prototype.toObject>;
|
||||
merge?: ReturnType<typeof OptionalBool.prototype.toObject>;
|
||||
} = {};
|
||||
if (this.session != null) {
|
||||
data.session = this.session.toObject();
|
||||
@@ -9382,6 +9835,9 @@ export namespace messages {
|
||||
if (this.filter != null) {
|
||||
data.filter = this.filter.toObject();
|
||||
}
|
||||
if (this.merge != null) {
|
||||
data.merge = this.merge.toObject();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
serialize(): Uint8Array;
|
||||
@@ -9396,6 +9852,8 @@ export namespace messages {
|
||||
writer.writeMessage(3, this.pagination, () => this.pagination.serialize(writer));
|
||||
if (this.has_filter)
|
||||
writer.writeMessage(4, this.filter, () => this.filter.serialize(writer));
|
||||
if (this.has_merge)
|
||||
writer.writeMessage(5, this.merge, () => this.merge.serialize(writer));
|
||||
if (!w)
|
||||
return writer.getResultBuffer();
|
||||
}
|
||||
@@ -9417,6 +9875,9 @@ export namespace messages {
|
||||
case 4:
|
||||
reader.readMessage(message.filter, () => message.filter = ChatFilter.deserialize(reader));
|
||||
break;
|
||||
case 5:
|
||||
reader.readMessage(message.merge, () => message.merge = OptionalBool.deserialize(reader));
|
||||
break;
|
||||
default: reader.skipField();
|
||||
}
|
||||
}
|
||||
@@ -10491,20 +10952,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,
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -65,6 +65,8 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
GetChatsOverviewParams,
|
||||
GetChatsParams,
|
||||
MessageSortField,
|
||||
OverviewFilter,
|
||||
ReadChatMessagesQuery,
|
||||
@@ -124,7 +126,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 +180,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 +194,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 +235,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 +292,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,8 +308,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
}),
|
||||
});
|
||||
|
||||
this.client = GetMessageServiceClient(
|
||||
this.name,
|
||||
this.client = BuildMessageServiceClient(
|
||||
this.engineConfig.connection,
|
||||
grpc.credentials.createInsecure(),
|
||||
);
|
||||
@@ -310,12 +335,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 };
|
||||
},
|
||||
);
|
||||
@@ -878,6 +916,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return data.id;
|
||||
}
|
||||
|
||||
@Activity()
|
||||
async rejectCall(from: string, id: string): Promise<void> {
|
||||
const request = new messages.RejectCallRequest({
|
||||
session: this.session,
|
||||
@@ -1041,6 +1080,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
};
|
||||
}
|
||||
|
||||
@Activity()
|
||||
async checkNumberStatus(
|
||||
request: CheckNumberStatusQuery,
|
||||
): Promise<WANumberExistResult> {
|
||||
@@ -1819,24 +1859,27 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
pagination.sortOrder = SortOrder.DESC;
|
||||
}
|
||||
const chats = await this.getChats(pagination, filter);
|
||||
const merge = (pagination as GetChatsOverviewParams).merge ?? true;
|
||||
|
||||
const promises = [];
|
||||
for (const chat of chats) {
|
||||
promises.push(this.fetchChatSummary(chat));
|
||||
promises.push(this.fetchChatSummary(chat, merge));
|
||||
}
|
||||
const result = await Promise.all(promises);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async fetchChatSummary(chat): Promise<ChatSummary> {
|
||||
protected async fetchChatSummary(chat, merge: boolean): Promise<ChatSummary> {
|
||||
const id = toCusFormat(chat.id);
|
||||
const name = chat.name;
|
||||
const picture = await this.getContactProfilePicture(chat.id, false);
|
||||
const messages = await this.getChatMessages(
|
||||
chat.id,
|
||||
{ limit: 1, offset: 0, downloadMedia: false },
|
||||
{},
|
||||
);
|
||||
const lastMessageQuery: GetChatMessagesQuery = {
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
downloadMedia: false,
|
||||
merge,
|
||||
};
|
||||
const messages = await this.getChatMessages(chat.id, lastMessageQuery, {});
|
||||
const message = messages.length > 0 ? messages[0] : null;
|
||||
return {
|
||||
id: id,
|
||||
@@ -1861,6 +1904,8 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
pagination: PaginationParams,
|
||||
filter: OverviewFilter | null = null,
|
||||
) {
|
||||
const merge =
|
||||
(pagination as GetChatsParams | GetChatsOverviewParams).merge ?? true;
|
||||
if (pagination.sortBy === ChatSortField.CONVERSATION_TIMESTAMP) {
|
||||
pagination.sortBy = 'timestamp';
|
||||
}
|
||||
@@ -1884,6 +1929,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
filter: new messages.ChatFilter({
|
||||
jids: jids,
|
||||
}),
|
||||
merge: optional(merge, messages.OptionalBool),
|
||||
});
|
||||
const response = await promisify(this.client.GetChats)(request);
|
||||
const data = parseJsonList(response);
|
||||
@@ -1896,6 +1942,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
filter: GetChatMessagesFilter,
|
||||
) {
|
||||
const downloadMedia = query.downloadMedia;
|
||||
const merge = query.merge ?? true;
|
||||
let jid: messages.OptionalString;
|
||||
if (chatId === 'all') {
|
||||
jid = null;
|
||||
@@ -1933,6 +1980,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
limit: query.limit,
|
||||
offset: query.offset,
|
||||
}),
|
||||
merge: optional(merge, messages.OptionalBool),
|
||||
});
|
||||
const response = await promisify(this.client.GetMessages)(request);
|
||||
const msgs = parseJsonList(response);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
GetChatsOverviewParams,
|
||||
GetChatsParams,
|
||||
OverviewFilter,
|
||||
PinDuration,
|
||||
ReadChatMessagesQuery,
|
||||
@@ -355,10 +357,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
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,
|
||||
@@ -908,6 +908,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
/**
|
||||
* Other methods
|
||||
*/
|
||||
@Activity()
|
||||
async checkNumberStatus(
|
||||
request: CheckNumberStatusQuery,
|
||||
): Promise<WANumberExistResult> {
|
||||
@@ -927,6 +928,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return this.generateMessageID();
|
||||
}
|
||||
|
||||
@Activity()
|
||||
async rejectCall(from: string, id: string): Promise<void> {
|
||||
const jid = toJID(this.ensureSuffix(from));
|
||||
await this.sock.rejectCall(id, jid);
|
||||
@@ -956,24 +958,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()
|
||||
@@ -1138,10 +1179,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
) {
|
||||
const downloadMedia = query.downloadMedia;
|
||||
const pagination = query as PaginationParams;
|
||||
const merge = query.merge ?? true;
|
||||
const messages = await this.store.getMessagesByJid(
|
||||
toJID(chatId),
|
||||
filter,
|
||||
pagination,
|
||||
merge,
|
||||
);
|
||||
|
||||
const promises = [];
|
||||
@@ -1167,7 +1210,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
query: GetChatMessageQuery,
|
||||
): Promise<null | WAMessage> {
|
||||
const key = parseMessageIdSerialized(messageId, true);
|
||||
const message = await this.store.getMessageById(toJID(chatId), key.id);
|
||||
const merge = query.merge ?? true;
|
||||
const message = await this.store.getMessageById(
|
||||
toJID(chatId),
|
||||
key.id,
|
||||
merge,
|
||||
);
|
||||
if (!message) return null;
|
||||
return await this.processIncomingMessage(message, query.downloadMedia);
|
||||
}
|
||||
@@ -1254,7 +1302,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
*/
|
||||
|
||||
async getChats(pagination: PaginationParams) {
|
||||
const chats = await this.store.getChats(pagination, true);
|
||||
const merge = (pagination as GetChatsParams).merge ?? true;
|
||||
const chats = await this.store.getChats(pagination, true, undefined, merge);
|
||||
// Remove unreadCount, it's not ready yet
|
||||
chats.forEach((chat) => delete chat.unreadCount);
|
||||
return chats;
|
||||
@@ -1264,6 +1313,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
pagination: PaginationParams,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
const merge = (pagination as GetChatsOverviewParams).merge ?? true;
|
||||
// Convert customer format IDs to JID format if filter is provided
|
||||
let jidFilter;
|
||||
if (filter?.ids && filter.ids.length > 0) {
|
||||
@@ -1272,19 +1322,27 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
};
|
||||
}
|
||||
|
||||
const chats = await this.store.getChats(pagination, false, jidFilter);
|
||||
const chats = await this.store.getChats(
|
||||
pagination,
|
||||
false,
|
||||
jidFilter,
|
||||
merge,
|
||||
);
|
||||
// Remove unreadCount, it's not ready yet
|
||||
chats.forEach((chat) => delete chat.unreadCount);
|
||||
|
||||
const promises = [];
|
||||
for (const chat of chats) {
|
||||
promises.push(this.fetchChatSummary(chat));
|
||||
promises.push(this.fetchChatSummary(chat, merge));
|
||||
}
|
||||
const result = await Promise.all(promises);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async fetchChatSummary(chat: Chat): Promise<ChatSummary> {
|
||||
protected async fetchChatSummary(
|
||||
chat: Chat,
|
||||
merge: boolean,
|
||||
): Promise<ChatSummary> {
|
||||
const id = toCusFormat(chat.id);
|
||||
let name = chat.name;
|
||||
if (!name) {
|
||||
@@ -1294,11 +1352,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
name = contact?.name || contact?.notify;
|
||||
}
|
||||
const picture = await this.getContactProfilePicture(chat.id, false);
|
||||
const messages = await this.getChatMessages(
|
||||
chat.id,
|
||||
{ limit: 1, offset: 0, downloadMedia: false },
|
||||
{},
|
||||
);
|
||||
const lastMessageQuery: GetChatMessagesQuery = {
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
downloadMedia: false,
|
||||
merge: merge,
|
||||
};
|
||||
const messages = await this.getChatMessages(chat.id, lastMessageQuery, {});
|
||||
const message = messages.length > 0 ? messages[0] : null;
|
||||
return {
|
||||
id: id,
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface IChatRepository {
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
merge?: boolean,
|
||||
): Promise<Chat[]>;
|
||||
|
||||
getById(id: string): Promise<Chat | null>;
|
||||
|
||||
@@ -14,9 +14,10 @@ export interface IMessagesRepository {
|
||||
jid: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
merge?: boolean,
|
||||
): Promise<any[]>;
|
||||
|
||||
getByJidById(jid: string, id: string): Promise<any | null>;
|
||||
getByJidById(jid: string, id: string, merge?: boolean): Promise<any | null>;
|
||||
|
||||
updateByJidAndId(jid: string, id: string, update: any): Promise<boolean>;
|
||||
|
||||
|
||||
@@ -31,14 +31,20 @@ export interface INowebStore {
|
||||
chatId: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
merge?: boolean,
|
||||
): Promise<any>;
|
||||
|
||||
getMessageById(chatId: string, messageId: string): Promise<any>;
|
||||
getMessageById(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
merge?: boolean,
|
||||
): Promise<any>;
|
||||
|
||||
getChats(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
merge?: boolean,
|
||||
): Promise<Chat[]>;
|
||||
|
||||
getChat(jid: string): Promise<Chat | null>;
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { Chat, Contact, GroupMetadata, proto } from '@adiwajshing/baileys';
|
||||
import type makeWASocket from '@adiwajshing/baileys';
|
||||
import type { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import {
|
||||
GetChatMessagesFilter,
|
||||
OverviewFilter,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import {
|
||||
LimitOffsetParams,
|
||||
@@ -57,15 +60,25 @@ export class NowebInMemoryStore implements INowebStore {
|
||||
chatId: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
merge?: boolean,
|
||||
): Promise<any> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getMessageById(chatId: string, messageId: string): Promise<any> {
|
||||
getMessageById(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
merge?: boolean,
|
||||
): Promise<any> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
|
||||
getChats(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
merge?: boolean,
|
||||
): Promise<Chat[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import { INowebStorage } from './INowebStorage';
|
||||
import { INowebStore } from './INowebStore';
|
||||
import { LabelAssociationType } from '../labels/LabelAssociationType';
|
||||
import esm from '@waha/vendor/esm';
|
||||
import { StatusStringToStatus } from '@waha/core/utils/acks';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
@@ -130,7 +131,7 @@ export class NowebPersistentStore implements INowebStore {
|
||||
return {
|
||||
id: message.key.remoteJid,
|
||||
lid: lid,
|
||||
jid: pn,
|
||||
phoneNumber: pn,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
@@ -249,6 +250,9 @@ export class NowebPersistentStore implements INowebStore {
|
||||
private async syncMessagesHistory(messages) {
|
||||
const realMessages = messages.filter(esm.b.isRealMessage);
|
||||
messages = messages.filter((msg) => this.jids.include(msg.key.remoteJid));
|
||||
for (const message of messages) {
|
||||
message.status = StatusStringToStatus(message.status);
|
||||
}
|
||||
await this.messagesRepo.upsert(realMessages);
|
||||
this.logger.info(
|
||||
`history sync - '${messages.length}' got messages, '${realMessages.length}' real messages`,
|
||||
@@ -288,7 +292,11 @@ export class NowebPersistentStore implements INowebStore {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const message = await this.messagesRepo.getByJidById(jid, update.key.id);
|
||||
let message = await this.messagesRepo.getByJidById(jid, update.key.id);
|
||||
if (!message) {
|
||||
// Fallback to just id
|
||||
message = await this.messagesRepo.getById(update.key.id);
|
||||
}
|
||||
if (!message) {
|
||||
this.logger.warn(
|
||||
`got update for non-existent message. update: '${JSON.stringify(
|
||||
@@ -599,24 +607,35 @@ export class NowebPersistentStore implements INowebStore {
|
||||
chatId: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
merge: boolean = true,
|
||||
): Promise<any> {
|
||||
pagination.sortBy = 'messageTimestamp';
|
||||
pagination.sortOrder = pagination.sortOrder || SortOrder.DESC;
|
||||
return this.messagesRepo.getAllByJid(chatId, filter, pagination);
|
||||
return this.messagesRepo.getAllByJid(chatId, filter, pagination, merge);
|
||||
}
|
||||
|
||||
getMessageById(chatId: string, messageId: string): Promise<any> {
|
||||
return this.messagesRepo.getByJidById(chatId, messageId);
|
||||
getMessageById(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
merge: boolean = true,
|
||||
): Promise<any> {
|
||||
return this.messagesRepo.getByJidById(chatId, messageId, merge);
|
||||
}
|
||||
|
||||
getChats(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
merge: boolean = true,
|
||||
): Promise<Chat[]> {
|
||||
pagination.sortBy ||= 'conversationTimestamp';
|
||||
pagination.sortOrder ||= SortOrder.DESC;
|
||||
return this.chatRepo.getAllWithMessages(pagination, broadcast, filter);
|
||||
return this.chatRepo.getAllWithMessages(
|
||||
pagination,
|
||||
broadcast,
|
||||
filter,
|
||||
merge,
|
||||
);
|
||||
}
|
||||
|
||||
async getChat(jid: string): Promise<Chat | null> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Chat } from '@adiwajshing/baileys';
|
||||
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
|
||||
import { OverviewFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export class SqlChatMethods {
|
||||
constructor(private repository: SqlKVRepository<any>) {}
|
||||
@@ -10,23 +11,112 @@ export class SqlChatMethods {
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
merge: boolean = true,
|
||||
): Promise<Chat[]> {
|
||||
// Get chats with conversationTimestamp is not Null
|
||||
let query = this.repository.select().whereNotNull('conversationTimestamp');
|
||||
const knex = this.repository.getKnex();
|
||||
const tableName = this.repository.table;
|
||||
let baseQuery = this.repository
|
||||
.select()
|
||||
.whereNotNull(`${tableName}.conversationTimestamp`);
|
||||
|
||||
if (!broadcast) {
|
||||
// filter out chat by id if it ends at @newsletter or @broadcast
|
||||
query = query
|
||||
.andWhereNot('id', 'like', '%@broadcast')
|
||||
.andWhereNot('id', 'like', '%@newsletter');
|
||||
if (!merge) {
|
||||
baseQuery = this.applyNonMergedFilters(baseQuery, tableName, {
|
||||
broadcast,
|
||||
filter,
|
||||
});
|
||||
const pagedQuery = this.repository.pagination(baseQuery, pagination);
|
||||
const rows = await pagedQuery;
|
||||
return rows.map((row) => this.repository.parse(row));
|
||||
}
|
||||
|
||||
// Filter by IDs if provided
|
||||
if (filter?.ids && filter.ids.length > 0) {
|
||||
query = query.whereIn('id', filter.ids);
|
||||
const annotatedQuery = this.annotateWithPnJid(knex, baseQuery, {
|
||||
tableName,
|
||||
broadcast,
|
||||
filter,
|
||||
}).as('annotated_chats');
|
||||
|
||||
const dedupedQuery = knex
|
||||
.select('*')
|
||||
.from(
|
||||
knex
|
||||
.select(
|
||||
'annotated_chats.*',
|
||||
knex.raw(
|
||||
'ROW_NUMBER() OVER (PARTITION BY annotated_chats.primary_jid ORDER BY annotated_chats."conversationTimestamp" DESC, annotated_chats.primary_priority ASC) as __rownum',
|
||||
),
|
||||
)
|
||||
.from(annotatedQuery)
|
||||
.as('ranked_chats'),
|
||||
)
|
||||
.where('__rownum', 1);
|
||||
|
||||
const pagedQuery = this.repository.pagination(dedupedQuery, pagination);
|
||||
const rows = await pagedQuery;
|
||||
|
||||
return rows.map((row) => {
|
||||
const chat = this.repository.parse(row);
|
||||
if (merge && row.primary_jid) {
|
||||
chat.id = row.primary_jid;
|
||||
}
|
||||
return chat;
|
||||
});
|
||||
}
|
||||
|
||||
private annotateWithPnJid(
|
||||
knex: Knex,
|
||||
query: Knex.QueryBuilder,
|
||||
opts: {
|
||||
tableName: string;
|
||||
broadcast: boolean;
|
||||
filter?: OverviewFilter;
|
||||
},
|
||||
) {
|
||||
const pnExpr = this.buildPnJidExpr(opts.tableName);
|
||||
let annotated = query
|
||||
.leftJoin('lid_map', 'lid_map.id', `${opts.tableName}.id`)
|
||||
.select(
|
||||
knex.raw(`${pnExpr} as primary_jid`),
|
||||
knex.raw(
|
||||
`CASE WHEN ${opts.tableName}.id LIKE '%@lid' THEN 1 ELSE 0 END as primary_priority`,
|
||||
),
|
||||
);
|
||||
|
||||
if (!opts.broadcast) {
|
||||
annotated = annotated
|
||||
.andWhereNot(`${opts.tableName}.id`, 'like', '%@broadcast')
|
||||
.andWhereNot(`${opts.tableName}.id`, 'like', '%@newsletter');
|
||||
}
|
||||
|
||||
query = this.repository.pagination(query, pagination);
|
||||
return await this.repository.all(query);
|
||||
if (opts.filter?.ids && opts.filter.ids.length > 0) {
|
||||
annotated = annotated.andWhere((builder) => {
|
||||
builder
|
||||
.whereIn(`${opts.tableName}.id`, opts.filter.ids)
|
||||
.orWhereIn('lid_map.pn', opts.filter.ids);
|
||||
});
|
||||
}
|
||||
|
||||
return annotated;
|
||||
}
|
||||
|
||||
private buildPnJidExpr(tableName: string) {
|
||||
const column = `"${tableName}"."id"`;
|
||||
return `CASE WHEN ${column} LIKE '%@lid' THEN COALESCE(lid_map.pn, ${column}) ELSE ${column} END`;
|
||||
}
|
||||
|
||||
private applyNonMergedFilters(
|
||||
query: Knex.QueryBuilder,
|
||||
tableName: string,
|
||||
opts: { broadcast: boolean; filter?: OverviewFilter },
|
||||
) {
|
||||
let filtered = query;
|
||||
if (!opts.broadcast) {
|
||||
filtered = filtered
|
||||
.andWhereNot(`${tableName}.id`, 'like', '%@broadcast')
|
||||
.andWhereNot(`${tableName}.id`, 'like', '%@newsletter');
|
||||
}
|
||||
if (opts.filter?.ids && opts.filter.ids.length > 0) {
|
||||
filtered = filtered.whereIn(`${tableName}.id`, opts.filter.ids);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
|
||||
import { AckToStatus } from '@waha/core/utils/acks';
|
||||
import { isLidUser } from '@waha/core/utils/jids';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { Knex } from 'knex';
|
||||
import { INowebLidPNRepository } from '../INowebLidPNRepository';
|
||||
|
||||
export class SqlMessagesMethods {
|
||||
constructor(private repository: SqlKVRepository<any>) {}
|
||||
constructor(
|
||||
private repository: SqlKVRepository<any>,
|
||||
private lidRepository?: INowebLidPNRepository,
|
||||
) {}
|
||||
|
||||
upsert(messages: any[]): Promise<void> {
|
||||
return this.repository.upsertMany(messages);
|
||||
@@ -15,10 +21,17 @@ export class SqlMessagesMethods {
|
||||
jid: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
merge: boolean = true,
|
||||
): Promise<any[]> {
|
||||
let query = this.repository.select();
|
||||
const tableName = this.repository.table;
|
||||
if (jid !== ALL_JID) {
|
||||
query = this.repository.select().where({ jid: jid });
|
||||
if (merge) {
|
||||
const pnJid = await this.resolvePnJid(jid);
|
||||
query = this.applyPnJidFilter(query, pnJid);
|
||||
} else {
|
||||
query = query.where(`${tableName}.jid`, jid);
|
||||
}
|
||||
}
|
||||
if (filter['filter.timestamp.lte'] != null) {
|
||||
query = query.where(
|
||||
@@ -34,10 +47,11 @@ export class SqlMessagesMethods {
|
||||
filter['filter.timestamp.gte'],
|
||||
);
|
||||
}
|
||||
const dataColumn = `${this.repository.table}.data`;
|
||||
if (filter['filter.fromMe'] != null) {
|
||||
// filter by data json inside
|
||||
const [sql, value] = this.repository.filterJson(
|
||||
'data',
|
||||
dataColumn,
|
||||
'key.fromMe',
|
||||
filter['filter.fromMe'],
|
||||
);
|
||||
@@ -45,18 +59,39 @@ export class SqlMessagesMethods {
|
||||
}
|
||||
if (filter['filter.ack'] != null) {
|
||||
const status = AckToStatus(filter['filter.ack']);
|
||||
const [sql, value] = this.repository.filterJson('data', 'status', status);
|
||||
const [sql, value] = this.repository.filterJson(
|
||||
dataColumn,
|
||||
'status',
|
||||
status,
|
||||
);
|
||||
query = query.whereRaw(sql, [value]);
|
||||
}
|
||||
query = this.repository.pagination(query, pagination);
|
||||
return this.repository.all(query);
|
||||
}
|
||||
|
||||
async getByJidById(jid: string, id: string): Promise<any> {
|
||||
async getByJidById(
|
||||
jid: string,
|
||||
id: string,
|
||||
merge: boolean = true,
|
||||
): Promise<any> {
|
||||
if (jid === ALL_JID) {
|
||||
return this.repository.getBy({ id: id });
|
||||
}
|
||||
return this.repository.getBy({ jid: jid, id: id });
|
||||
const tableName = this.repository.table;
|
||||
const baseQuery = this.repository.select().where(`${tableName}.id`, id);
|
||||
let query;
|
||||
if (merge) {
|
||||
const pnJid = await this.resolvePnJid(jid);
|
||||
query = this.applyPnJidFilter(baseQuery, pnJid);
|
||||
} else {
|
||||
query = baseQuery.andWhere(`${tableName}.jid`, jid);
|
||||
}
|
||||
const rows = await query.limit(1);
|
||||
if (!rows.length) {
|
||||
return null;
|
||||
}
|
||||
return this.repository.parse(rows[0]);
|
||||
}
|
||||
|
||||
async updateByJidAndId(
|
||||
@@ -83,4 +118,39 @@ export class SqlMessagesMethods {
|
||||
deleteAllByJid(jid: string): Promise<void> {
|
||||
return this.repository.deleteBy({ jid: jid });
|
||||
}
|
||||
|
||||
private applyPnJidFilter(
|
||||
query: Knex.QueryBuilder,
|
||||
pnJid: string,
|
||||
): Knex.QueryBuilder {
|
||||
const tableName = this.repository.table;
|
||||
const pnExpr = this.buildPnJidExpr(tableName, 'jid');
|
||||
return query
|
||||
.leftJoin('lid_map', 'lid_map.id', `${tableName}.jid`)
|
||||
.whereRaw(`${pnExpr} = ?`, [pnJid]);
|
||||
}
|
||||
|
||||
private async resolvePnJid(jid: string): Promise<string> {
|
||||
if (!isLidUser(jid)) {
|
||||
return jid;
|
||||
}
|
||||
if (this.lidRepository) {
|
||||
const mapped = await this.lidRepository.findPNByLid(jid);
|
||||
if (mapped) {
|
||||
return mapped;
|
||||
}
|
||||
}
|
||||
const row = await this.repository
|
||||
.getKnex()
|
||||
.select('pn')
|
||||
.from('lid_map')
|
||||
.where('id', jid)
|
||||
.first();
|
||||
return row?.pn || jid;
|
||||
}
|
||||
|
||||
private buildPnJidExpr(tableName: string, column: string) {
|
||||
const colRef = `"${tableName}"."${column}"`;
|
||||
return `CASE WHEN ${colRef} LIKE '%@lid' THEN COALESCE(lid_map.pn, ${colRef}) ELSE ${colRef} END`;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export class NOWEBSqlite3KVRepository<
|
||||
return JSON.stringify(data, esm.b.BufferJSON.replacer);
|
||||
}
|
||||
|
||||
protected parse(row: any): any {
|
||||
public parse(row: any): any {
|
||||
return JSON.parse(row.data, esm.b.BufferJSON.reviver);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,13 @@ export class Sqlite3ChatRepository
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
merge?: boolean,
|
||||
): Promise<Chat[]> {
|
||||
return this.methods.getAllWithMessages(pagination, broadcast, filter);
|
||||
return this.methods.getAllWithMessages(
|
||||
pagination,
|
||||
broadcast,
|
||||
filter,
|
||||
merge,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,26 @@ import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
import { IMessagesRepository } from '../IMessagesRepository';
|
||||
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
|
||||
import { INowebLidPNRepository } from '../INowebLidPNRepository';
|
||||
import Knex from 'knex';
|
||||
|
||||
export class Sqlite3MessagesRepository
|
||||
extends NOWEBSqlite3KVRepository<any>
|
||||
implements IMessagesRepository
|
||||
{
|
||||
constructor(
|
||||
knex: Knex.Knex,
|
||||
private readonly lidRepository: INowebLidPNRepository,
|
||||
) {
|
||||
super(knex);
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return NowebMessagesSchema;
|
||||
}
|
||||
|
||||
get methods() {
|
||||
return new SqlMessagesMethods(this);
|
||||
return new SqlMessagesMethods(this, this.lidRepository);
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
@@ -31,12 +40,13 @@ export class Sqlite3MessagesRepository
|
||||
jid: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
merge?: boolean,
|
||||
): Promise<any[]> {
|
||||
return this.methods.getAllByJid(jid, filter, pagination);
|
||||
return this.methods.getAllByJid(jid, filter, pagination, merge);
|
||||
}
|
||||
|
||||
async getByJidById(jid: string, id: string): Promise<any> {
|
||||
return this.methods.getByJidById(jid, id);
|
||||
async getByJidById(jid: string, id: string, merge?: boolean): Promise<any> {
|
||||
return this.methods.getByJidById(jid, id, merge);
|
||||
}
|
||||
|
||||
async updateByJidAndId(
|
||||
|
||||
@@ -19,6 +19,7 @@ import { KNEX_SQLITE_CLIENT } from '@waha/core/env';
|
||||
export class Sqlite3Storage extends INowebStorage {
|
||||
private readonly tables: Schema[];
|
||||
private readonly knex: Knex.Knex;
|
||||
private lidRepository: INowebLidPNRepository | null = null;
|
||||
|
||||
constructor(filePath: string) {
|
||||
super();
|
||||
@@ -84,10 +85,17 @@ export class Sqlite3Storage extends INowebStorage {
|
||||
}
|
||||
|
||||
getMessagesRepository() {
|
||||
return new Sqlite3MessagesRepository(this.knex);
|
||||
return new Sqlite3MessagesRepository(this.knex, this.getLidRepository());
|
||||
}
|
||||
|
||||
getLidPNRepository(): INowebLidPNRepository {
|
||||
return new Sqlite3LidPNRepository(this.knex);
|
||||
return this.getLidRepository();
|
||||
}
|
||||
|
||||
private getLidRepository(): INowebLidPNRepository {
|
||||
if (!this.lidRepository) {
|
||||
this.lidRepository = new Sqlite3LidPNRepository(this.knex);
|
||||
}
|
||||
return this.lidRepository;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Agent } from 'https';
|
||||
import type { Dispatcher } from 'undici';
|
||||
|
||||
export interface Agents {
|
||||
socket: Agent;
|
||||
fetch: Dispatcher;
|
||||
fetch: Agent;
|
||||
}
|
||||
@@ -11,7 +11,12 @@ export function extractMediaContent(
|
||||
content?.videoMessage ||
|
||||
content?.audioMessage ||
|
||||
content?.ptvMessage ||
|
||||
content?.stickerMessage;
|
||||
content?.stickerMessage ||
|
||||
content?.templateMessage?.hydratedTemplate?.imageMessage ||
|
||||
content?.templateMessage?.hydratedTemplate?.videoMessage ||
|
||||
content?.templateMessage?.interactiveMessageTemplate?.header
|
||||
?.imageMessage ||
|
||||
content?.templateMessage?.interactiveMessageTemplate?.header?.videoMessage;
|
||||
if (mediaContent) {
|
||||
return mediaContent;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -40,6 +40,10 @@ export class WebjsClientCore extends Client {
|
||||
await this.attachCustomEventListeners();
|
||||
await this.injectWaha();
|
||||
});
|
||||
this.on(Events.READY, async () => {
|
||||
await this.attachCustomEventListeners();
|
||||
await this.injectWaha();
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
@@ -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('#', '');
|
||||
|
||||
@@ -185,6 +185,9 @@ import {
|
||||
WAHA_CLIENT_BROWSER_NAME,
|
||||
WAHA_CLIENT_DEVICE_NAME,
|
||||
} from '@waha/core/env';
|
||||
import { removeSingletonFiles } from '@waha/core/utils/chrome';
|
||||
import { killProcessesByPatterns } from '@waha/core/utils/processes';
|
||||
import { IsChrome } from '@waha/version';
|
||||
|
||||
export interface WebJSConfig {
|
||||
webVersion?: string;
|
||||
@@ -271,6 +274,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
};
|
||||
}
|
||||
|
||||
protected getUserDataDir(): string {
|
||||
const base = process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
|
||||
return `${base}/webjs/default/session-${this.name}`;
|
||||
}
|
||||
|
||||
protected async buildClient() {
|
||||
const clientOptions = this.getClientOptions();
|
||||
const base = process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
|
||||
@@ -358,6 +366,16 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
PAGE_CALL_ERROR_EVENT,
|
||||
(event: CallErrorEvent) => {
|
||||
if (event.error instanceof ProtocolError) {
|
||||
if (this.shouldIgnoreProtocolError(event.error)) {
|
||||
this.logger.warn(
|
||||
`ProtocolError when calling page method: ${String(
|
||||
event.method,
|
||||
)}, ignoring...`,
|
||||
);
|
||||
this.logger.warn(event.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.error(
|
||||
`ProtocolError when calling page method: ${String(
|
||||
event.method,
|
||||
@@ -393,6 +411,13 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async start() {
|
||||
await killProcessesByPatterns(
|
||||
[IsChrome ? 'chrome' : 'chromium', `--a-waha-session=${this.name}`],
|
||||
'SIGKILL',
|
||||
this.logger,
|
||||
);
|
||||
await removeSingletonFiles(this.getUserDataDir());
|
||||
|
||||
this.status = WAHASessionStatus.STARTING;
|
||||
await this.init().catch((err) => {
|
||||
this.logger.error('Failed to start the client');
|
||||
@@ -418,6 +443,16 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
this.restartClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Certain Puppeteer ProtocolErrors (e.g. Network.getResponseBody) are harmless.
|
||||
* Ignore them so we do not thrash the session state machine.
|
||||
* https://github.com/devlikeapro/waha/issues/1918
|
||||
*/
|
||||
private shouldIgnoreProtocolError(error: ProtocolError): boolean {
|
||||
const message = error?.message ?? String(error ?? '');
|
||||
return message.includes('Network.getResponseBody');
|
||||
}
|
||||
|
||||
async unpair() {
|
||||
this.unpairing = true;
|
||||
this.shouldRestart = false;
|
||||
@@ -674,6 +709,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return screenshot as Buffer;
|
||||
}
|
||||
|
||||
@Activity()
|
||||
async checkNumberStatus(
|
||||
request: CheckNumberStatusQuery,
|
||||
): Promise<WANumberExistResult> {
|
||||
@@ -717,6 +753,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
/**
|
||||
* Other methods
|
||||
*/
|
||||
@Activity()
|
||||
async rejectCall(from: string, id: string): Promise<void> {
|
||||
const peerJid = normalizeJid(this.ensureSuffix(from));
|
||||
const call = new CallInstance(this.whatsapp, null);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export interface IWPPAuthManager {
|
||||
/**
|
||||
* Called BEFORE WPP browser starts.
|
||||
* Downloads and extracts any existing remote session to userDataDir.
|
||||
*/
|
||||
beforeStart(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Called AFTER the session reaches WORKING status.
|
||||
* Waits for stabilization, saves first snapshot, then starts periodic backup.
|
||||
*/
|
||||
afterConnected(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Stops the periodic backup runner.
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface WPPConfig {
|
||||
webVersion?: string;
|
||||
puppeteerArgs: string[];
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Whatsapp as WPPWhatsapp } from '@wppconnect-team/wppconnect';
|
||||
import type {
|
||||
PoolMessageOptions,
|
||||
TextMessageOptions,
|
||||
} from '@wppconnect/wa-js/dist/chat';
|
||||
import type { EditMessageOptions } from '@wppconnect/wa-js/dist/chat/functions/editMessage';
|
||||
import type { TextStatusOptions } from '@wppconnect/wa-js/dist/status/functions/sendTextStatus';
|
||||
|
||||
//
|
||||
// Event data types extracted directly from wppconnect on* method signatures.
|
||||
// Using Parameters<> keeps these automatically in sync with wppconnect:
|
||||
// if an inline type changes upstream, TypeScript will catch mismatches here.
|
||||
//
|
||||
|
||||
// Extracts the last parameter of a function (handles overloaded on* methods
|
||||
// that accept an optional id filter as the first arg before the callback).
|
||||
type LastParam<F extends (...args: any[]) => any> = Parameters<F> extends [
|
||||
...any[],
|
||||
infer Last,
|
||||
]
|
||||
? Last
|
||||
: never;
|
||||
|
||||
// Extracts the event data type: first arg of the (last) callback parameter.
|
||||
type WppEventData<Method extends (...args: any[]) => any> =
|
||||
LastParam<Method> extends (...args: any[]) => any
|
||||
? Parameters<LastParam<Method>>[0]
|
||||
: never;
|
||||
|
||||
type WppTextMessageOptions = TextMessageOptions;
|
||||
type WppMentionedList = WppTextMessageOptions extends {
|
||||
mentionedList?: infer Value;
|
||||
}
|
||||
? Value
|
||||
: string[];
|
||||
|
||||
export type WppPresenceEvent = WppEventData<WPPWhatsapp['onPresenceChanged']>;
|
||||
export type WppParticipantEvent = WppEventData<
|
||||
WPPWhatsapp['onParticipantsChanged']
|
||||
>;
|
||||
|
||||
export enum WppParticipantAction {
|
||||
ADD = 'add',
|
||||
JOIN = 'join',
|
||||
REMOVE = 'remove',
|
||||
LEAVE = 'leave',
|
||||
LEAVER = 'leaver',
|
||||
PROMOTE = 'promote',
|
||||
DEMOTE = 'demote',
|
||||
}
|
||||
|
||||
export enum WppParticipantOperation {
|
||||
ADD = 'add',
|
||||
REMOVE = 'remove',
|
||||
PROMOTE = 'promote',
|
||||
DEMOTE = 'demote',
|
||||
}
|
||||
|
||||
export type WppReactionEvent = WppEventData<WPPWhatsapp['onReactionMessage']>;
|
||||
export type WppRevokedMessageEvent = WppEventData<
|
||||
WPPWhatsapp['onRevokedMessage']
|
||||
>;
|
||||
export type WppPollResponseEvent = WppEventData<WPPWhatsapp['onPollResponse']>;
|
||||
export type WppUpdateLabelEvent = WppEventData<WPPWhatsapp['onUpdateLabel']>;
|
||||
export type WppIncomingCallEvent = WppEventData<WPPWhatsapp['onIncomingCall']>;
|
||||
export type WppSendPollOptions = PoolMessageOptions;
|
||||
export type WppSendTextOptions = WppTextMessageOptions;
|
||||
export type WppSendTextStatusOptions = Omit<
|
||||
TextStatusOptions,
|
||||
'backgroundColor'
|
||||
> & {
|
||||
backgroundColor?: string;
|
||||
};
|
||||
export type WppEditMessageOptions = EditMessageOptions & {
|
||||
mentions?: WppMentionedList;
|
||||
};
|
||||
|
||||
// onMessageEdit callback has three separate args: (chat: Wid, id: string, msg: Message).
|
||||
// fromWppCallback packs them into a tuple since args.length > 1.
|
||||
export type WppMessageEditArgs = Parameters<
|
||||
LastParam<WPPWhatsapp['onMessageEdit']>
|
||||
>;
|
||||
@@ -0,0 +1,216 @@
|
||||
import { toCusFormat } from '@waha/core/utils/jids';
|
||||
import { SerializeMsgKey } from '@waha/core/utils/ids';
|
||||
import { WAMessageReaction } from '@waha/structures/responses.dto';
|
||||
import { WAHAPresenceStatus } from '@waha/structures/enums.dto';
|
||||
import {
|
||||
GroupId,
|
||||
GroupParticipant,
|
||||
GroupParticipantRole,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import {
|
||||
GroupParticipantType,
|
||||
GroupV2LeaveEvent,
|
||||
GroupV2ParticipantsEvent,
|
||||
GroupV2UpdateEvent,
|
||||
} from '@waha/structures/groups.events.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import { LabelChatAssociation } from '@waha/structures/labels.dto';
|
||||
import { WAHAChatPresences } from '@waha/structures/presence.dto';
|
||||
import {
|
||||
WppParticipantAction,
|
||||
WppParticipantEvent,
|
||||
WppPresenceEvent,
|
||||
WppReactionEvent,
|
||||
WppUpdateLabelEvent,
|
||||
} from '@waha/core/engines/wpp/WppTypes';
|
||||
|
||||
//
|
||||
// Converters
|
||||
//
|
||||
|
||||
function presenceStateToWAHA(
|
||||
state: string,
|
||||
isOnline?: boolean,
|
||||
): WAHAPresenceStatus {
|
||||
switch (state) {
|
||||
case 'available':
|
||||
return WAHAPresenceStatus.ONLINE;
|
||||
case 'composing':
|
||||
case 'typing':
|
||||
return WAHAPresenceStatus.TYPING;
|
||||
case 'recording':
|
||||
return WAHAPresenceStatus.RECORDING;
|
||||
case 'unavailable':
|
||||
return WAHAPresenceStatus.OFFLINE;
|
||||
default:
|
||||
return isOnline ? WAHAPresenceStatus.ONLINE : WAHAPresenceStatus.OFFLINE;
|
||||
}
|
||||
}
|
||||
|
||||
export function WppPresenceToPresence(
|
||||
data: WppPresenceEvent,
|
||||
): WAHAChatPresences {
|
||||
const chatId = toCusFormat(data.id);
|
||||
|
||||
// Group: iterate over per-participant states when available
|
||||
if (data.isGroup && data.participants?.length) {
|
||||
return {
|
||||
id: chatId,
|
||||
presences: data.participants.map((p) => ({
|
||||
participant: toCusFormat(p.id),
|
||||
lastKnownPresence: presenceStateToWAHA(p.state),
|
||||
lastSeen: null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: chatId,
|
||||
presences: [
|
||||
{
|
||||
participant: chatId,
|
||||
lastKnownPresence: presenceStateToWAHA(data.state, data.isOnline),
|
||||
lastSeen: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function WppParticipantsToGroupV2Participants(
|
||||
data: WppParticipantEvent,
|
||||
): GroupV2ParticipantsEvent | null {
|
||||
let type: GroupParticipantType;
|
||||
let role: GroupParticipantRole;
|
||||
|
||||
switch (data.action) {
|
||||
case WppParticipantAction.ADD:
|
||||
case WppParticipantAction.JOIN:
|
||||
type = GroupParticipantType.JOIN;
|
||||
role = GroupParticipantRole.PARTICIPANT;
|
||||
break;
|
||||
case WppParticipantAction.REMOVE:
|
||||
case WppParticipantAction.LEAVE as any:
|
||||
case WppParticipantAction.LEAVER:
|
||||
type = GroupParticipantType.LEAVE;
|
||||
role = GroupParticipantRole.LEFT;
|
||||
break;
|
||||
case WppParticipantAction.PROMOTE:
|
||||
type = GroupParticipantType.PROMOTE;
|
||||
role = GroupParticipantRole.ADMIN;
|
||||
break;
|
||||
case WppParticipantAction.DEMOTE:
|
||||
type = GroupParticipantType.DEMOTE;
|
||||
role = GroupParticipantRole.PARTICIPANT;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
const group: GroupId = { id: toCusFormat(data.groupId) };
|
||||
const participants: GroupParticipant[] = data.who.map((id) => ({
|
||||
id: id,
|
||||
pn: null,
|
||||
role: role,
|
||||
}));
|
||||
|
||||
return {
|
||||
group: group,
|
||||
type: type,
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
participants: participants,
|
||||
_data: data,
|
||||
};
|
||||
}
|
||||
|
||||
export function WppParticipantsIsMyJoin(
|
||||
data: WppParticipantEvent,
|
||||
me: MeInfo | null | undefined,
|
||||
): boolean {
|
||||
const joinActions = [WppParticipantAction.ADD, WppParticipantAction.JOIN];
|
||||
if (!joinActions.includes(data.action as WppParticipantAction)) return false;
|
||||
return data.who.some((id) => id === me?.id || id === me?.lid);
|
||||
}
|
||||
|
||||
export function WppParticipantsIsMyLeave(
|
||||
data: WppParticipantEvent,
|
||||
me: MeInfo | null | undefined,
|
||||
): boolean {
|
||||
const leaveActions = [
|
||||
WppParticipantAction.REMOVE,
|
||||
WppParticipantAction.LEAVE,
|
||||
WppParticipantAction.LEAVER,
|
||||
];
|
||||
if (!leaveActions.includes(data.action as WppParticipantAction)) return false;
|
||||
return data.who.some((id) => id === me?.id || id === me?.lid);
|
||||
}
|
||||
|
||||
export function WppParticipantsToGroupV2Leave(
|
||||
data: WppParticipantEvent,
|
||||
): GroupV2LeaveEvent {
|
||||
return {
|
||||
timestamp: Math.floor(Date.now() / 1000),
|
||||
group: { id: toCusFormat(data.groupId) },
|
||||
_data: data,
|
||||
};
|
||||
}
|
||||
|
||||
export function WppReactionToMessageReaction(
|
||||
data: WppReactionEvent,
|
||||
): WAMessageReaction {
|
||||
// sender is present at runtime but absent from the installed package's type
|
||||
const raw = data as any;
|
||||
const sender: string =
|
||||
raw.sender?._serialized ?? raw.sender?.toString?.() ?? raw.sender ?? '';
|
||||
// remote is the chat ID (peer in DM, group JID in group chats)
|
||||
const remote = toCusFormat(raw.id?.remote ?? '');
|
||||
const isGroup = remote.includes('@g.us');
|
||||
// In DMs, the remote party IS the sender when sender field is missing
|
||||
const from = sender ? toCusFormat(sender) : isGroup ? '' : remote;
|
||||
const participant = sender ? toCusFormat(sender) : isGroup ? '' : remote;
|
||||
const id = SerializeMsgKey(data.id);
|
||||
const messageId = SerializeMsgKey(data.msgId);
|
||||
return {
|
||||
id: id,
|
||||
from: from,
|
||||
fromMe: raw.id?.fromMe ?? false,
|
||||
source: null,
|
||||
participant: participant,
|
||||
to: remote,
|
||||
timestamp: data.timestamp,
|
||||
reaction: {
|
||||
text: data.reactionText ?? '',
|
||||
messageId: messageId,
|
||||
},
|
||||
// @ts-ignore
|
||||
_data: data,
|
||||
};
|
||||
}
|
||||
|
||||
export function WppGp2ToGroupV2Update(msg: any): GroupV2UpdateEvent {
|
||||
const id = toCusFormat(msg.chatId);
|
||||
const group: Partial<Record<string, any>> = { id: id };
|
||||
if (msg.subtype === 'subject') {
|
||||
group.subject = msg.body;
|
||||
} else if (msg.subtype === 'description') {
|
||||
group.description = msg.body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
timestamp: msg.timestamp,
|
||||
group: group as any,
|
||||
_data: msg,
|
||||
};
|
||||
}
|
||||
|
||||
export function WppUpdateLabelToAssociations(
|
||||
data: WppUpdateLabelEvent,
|
||||
): LabelChatAssociation[] {
|
||||
const chat = data.chat as any;
|
||||
const chatId: string = chat?.id?._serialized ?? chat?.id ?? chat ?? '';
|
||||
return data.ids.map((labelId, i) => ({
|
||||
labelId: labelId,
|
||||
label: (data.labels?.[i] ?? null) as any,
|
||||
chatId: toCusFormat(chatId),
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Observable } from 'rxjs';
|
||||
import { EnginePayload } from '@waha/structures/webhooks.dto';
|
||||
|
||||
/**
|
||||
* Wraps a WPPConnect `on*` listener method into a lazy RxJS Observable.
|
||||
* The listener is registered only when subscribed and disposed on teardown.
|
||||
*
|
||||
* @param event Event name label stored in EnginePayload, e.g. `'onMessage'`
|
||||
* @param fn Bound wppconnect listener invocation, e.g. `(cb) => wpp.onMessage(cb)`
|
||||
*/
|
||||
export function fromWppCallback(
|
||||
event: string,
|
||||
fn: (callback: (...args: any[]) => void) => { dispose: () => void },
|
||||
): Observable<EnginePayload> {
|
||||
return new Observable<EnginePayload>((subscriber) => {
|
||||
const { dispose } = fn((...args: any[]) => {
|
||||
// Multi-arg callbacks (e.g. onMessageEdit receives chat, id, msg) are
|
||||
// packed into an array so EnginePayload.data is always a single value.
|
||||
const data = args.length === 1 ? args[0] : args;
|
||||
subscriber.next({ event: event, data: data });
|
||||
});
|
||||
return () => dispose();
|
||||
});
|
||||
}
|
||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user