Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
495857ee41 | ||
|
|
e04e55c4c5 | ||
|
|
743e740860 | ||
|
|
65623ec1ea | ||
|
|
72a301c579 | ||
|
|
f35fd8d7c6 | ||
|
|
0594ca0709 | ||
|
|
4e5ebab97e | ||
|
|
ba976138b7 | ||
|
|
bb3328f5a3 | ||
|
|
6db12d7913 | ||
|
|
d968fa90f1 | ||
|
|
06c874fea5 | ||
|
|
d2b7d94607 | ||
|
|
c8f067229b | ||
|
|
798d782869 | ||
|
|
f70bcd0b53 | ||
|
|
17a7764b97 | ||
|
|
58affc9fe2 | ||
|
|
3678cb4392 | ||
|
|
6eda96908f | ||
|
|
ddf161bb2b | ||
|
|
5659dc7296 | ||
|
|
bca90199f1 | ||
|
|
bd263b071c | ||
|
|
e54603085c | ||
|
|
7f7857141e | ||
|
|
3b19dc3a61 | ||
|
|
06e5951c68 | ||
|
|
bcf4ed07d8 | ||
|
|
f711fcca98 | ||
|
|
32e57cab98 | ||
|
|
b092d2bfe1 | ||
|
|
f655f715d0 | ||
|
|
e5c77c60f2 | ||
|
|
a38d4145c2 | ||
|
|
2c47b784b3 | ||
|
|
724dc61717 | ||
|
|
9c44256209 | ||
|
|
1fb82c7678 | ||
|
|
55f7d4dac7 | ||
|
|
10e8869903 | ||
|
|
554d2bc6bc | ||
|
|
99aabb0957 | ||
|
|
055b014c41 | ||
|
|
172676ae6f | ||
|
|
0056951485 | ||
|
|
8c91792473 | ||
|
|
a26e89a2b7 | ||
|
|
7674c4ca67 | ||
|
|
cc7c11e2fd | ||
|
|
9f3c28b6ac | ||
|
|
3ca9545a58 | ||
|
|
4d08cffeab | ||
|
|
241340e0a0 | ||
|
|
556f0fa775 | ||
|
|
975cf2f580 | ||
|
|
90795a7eaf | ||
|
|
e64b60ef3a | ||
|
|
03e9588581 | ||
|
|
393f5ef7da | ||
|
|
f778678514 | ||
|
|
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 |
No files matched your search
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(yarn build:*)",
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(node:*)",
|
||||||
|
"Bash(npm show:*)",
|
||||||
|
"Bash(python3:*)",
|
||||||
|
"Bash(yarn test:unit:*)",
|
||||||
|
"Bash(npx tsc:*)",
|
||||||
|
"Bash(pre-commit run:*)",
|
||||||
|
"Bash(yarn test:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ WHATSAPP_SWAGGER_ENABLED=True
|
|||||||
# ==================
|
# ==================
|
||||||
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
|
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
|
||||||
WHATSAPP_DEFAULT_ENGINE=WEBJS
|
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
|
# "Firefox (YourApp)" in Linked Devices
|
||||||
# WAHA_CLIENT_DEVICE_NAME=YourApp
|
# WAHA_CLIENT_DEVICE_NAME=YourApp
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ jobs:
|
|||||||
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
|
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
|
||||||
${{ matrix.tag }}
|
${{ matrix.tag }}
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Chromium - x86
|
# Chromium - x86
|
||||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
- runner: 'ubuntu-latest'
|
||||||
tag: 'latest'
|
tag: 'latest'
|
||||||
platform: 'amd64'
|
platform: 'amd64'
|
||||||
browser: 'chromium'
|
browser: 'chromium'
|
||||||
@@ -23,7 +24,7 @@ jobs:
|
|||||||
goss: 'goss-linux-amd64'
|
goss: 'goss-linux-amd64'
|
||||||
|
|
||||||
# Chromium - ARM
|
# Chromium - ARM
|
||||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
- runner: 'ubuntu-24.04-arm'
|
||||||
tag: 'arm'
|
tag: 'arm'
|
||||||
platform: 'linux/arm64'
|
platform: 'linux/arm64'
|
||||||
browser: 'chromium'
|
browser: 'chromium'
|
||||||
@@ -31,7 +32,7 @@ jobs:
|
|||||||
goss: 'goss-linux-arm'
|
goss: 'goss-linux-arm'
|
||||||
|
|
||||||
# Chrome - x86
|
# Chrome - x86
|
||||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
- runner: 'ubuntu-latest'
|
||||||
tag: 'chrome'
|
tag: 'chrome'
|
||||||
platform: 'amd64'
|
platform: 'amd64'
|
||||||
browser: 'chrome'
|
browser: 'chrome'
|
||||||
@@ -39,14 +40,14 @@ jobs:
|
|||||||
goss: 'goss-linux-amd64'
|
goss: 'goss-linux-amd64'
|
||||||
|
|
||||||
# Chrome - ARM (Chrome is not available for ARM)
|
# Chrome - ARM (Chrome is not available for ARM)
|
||||||
#- runner: "buildjet-4vcpu-ubuntu-2204-arm"
|
#- runner: "ubuntu-24.04-arm"
|
||||||
# tag: "chrome-arm"
|
# tag: "chrome-arm"
|
||||||
# platform: "linux/arm64"
|
# platform: "linux/arm64"
|
||||||
# browser: "chrome"
|
# browser: "chrome"
|
||||||
# goss: 'goss-linux-arm'
|
# goss: 'goss-linux-arm'
|
||||||
|
|
||||||
# NOWEB - x86
|
# NOWEB - x86
|
||||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
- runner: 'ubuntu-latest'
|
||||||
tag: 'noweb'
|
tag: 'noweb'
|
||||||
platform: 'amd64'
|
platform: 'amd64'
|
||||||
browser: 'none'
|
browser: 'none'
|
||||||
@@ -54,7 +55,7 @@ jobs:
|
|||||||
goss: 'goss-linux-amd64'
|
goss: 'goss-linux-amd64'
|
||||||
|
|
||||||
# NOWEB - ARM
|
# NOWEB - ARM
|
||||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
- runner: 'ubuntu-24.04-arm'
|
||||||
tag: 'noweb-arm'
|
tag: 'noweb-arm'
|
||||||
platform: 'linux/arm64'
|
platform: 'linux/arm64'
|
||||||
browser: 'none'
|
browser: 'none'
|
||||||
@@ -62,7 +63,7 @@ jobs:
|
|||||||
goss: 'goss-linux-arm'
|
goss: 'goss-linux-arm'
|
||||||
|
|
||||||
# GOWS - x86
|
# GOWS - x86
|
||||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
- runner: 'ubuntu-latest'
|
||||||
tag: 'gows'
|
tag: 'gows'
|
||||||
platform: 'amd64'
|
platform: 'amd64'
|
||||||
browser: 'none'
|
browser: 'none'
|
||||||
@@ -70,7 +71,7 @@ jobs:
|
|||||||
goss: 'goss-linux-amd64'
|
goss: 'goss-linux-amd64'
|
||||||
|
|
||||||
# No browser - ARM
|
# No browser - ARM
|
||||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
- runner: 'ubuntu-24.04-arm'
|
||||||
tag: 'gows-arm'
|
tag: 'gows-arm'
|
||||||
platform: 'linux/arm64'
|
platform: 'linux/arm64'
|
||||||
browser: 'none'
|
browser: 'none'
|
||||||
|
|||||||
+31
-31
@@ -6,37 +6,37 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
unit-tests:
|
# unit-tests:
|
||||||
if: github.repository == 'devlikeapro/waha-plus'
|
# if: github.repository == 'devlikeapro/waha-plus'
|
||||||
runs-on: ubuntu-22.04
|
# runs-on: ubuntu-22.04
|
||||||
name: Unit tests
|
# name: Unit tests
|
||||||
steps:
|
# steps:
|
||||||
- name: Checkout
|
# - name: Checkout
|
||||||
uses: actions/checkout@v4
|
# uses: actions/checkout@v4
|
||||||
with:
|
# with:
|
||||||
fetch-depth: 0
|
# fetch-depth: 0
|
||||||
|
#
|
||||||
- name: Set up Node
|
# - name: Set up Node
|
||||||
uses: actions/setup-node@v4
|
# uses: actions/setup-node@v4
|
||||||
with:
|
# with:
|
||||||
node-version: 22
|
# node-version: 22
|
||||||
|
#
|
||||||
- name: Enable Corepack
|
# - name: Enable Corepack
|
||||||
run: |
|
# run: |
|
||||||
corepack enable
|
# corepack enable
|
||||||
corepack prepare yarn@4.9.2 --activate
|
# corepack prepare yarn@4.9.2 --activate
|
||||||
|
#
|
||||||
- name: Install dependencies
|
# - name: Install dependencies
|
||||||
run: |
|
# run: |
|
||||||
unset GIT_CONFIG_PARAMETERS
|
# unset GIT_CONFIG_PARAMETERS
|
||||||
yarn install --immutable
|
# yarn install --immutable
|
||||||
|
#
|
||||||
- name: Run unit tests
|
# - name: Run unit tests
|
||||||
run: yarn test:unit
|
# run: yarn test:unit
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
if: github.repository == 'devlikeapro/waha-plus'
|
if: github.repository == 'devlikeapro/waha-plus'
|
||||||
needs: unit-tests
|
# needs: unit-tests
|
||||||
runs-on: ${{ matrix.runner }}
|
runs-on: ${{ matrix.runner }}
|
||||||
name:
|
name:
|
||||||
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
|
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Chromium - x86
|
# Chromium - x86
|
||||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
- runner: 'ubuntu-latest'
|
||||||
tag: 'dev'
|
tag: 'dev'
|
||||||
platform: 'amd64'
|
platform: 'amd64'
|
||||||
browser: 'chromium'
|
browser: 'chromium'
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
goss: 'goss-linux-amd64'
|
goss: 'goss-linux-amd64'
|
||||||
|
|
||||||
# Chrome - x86
|
# Chrome - x86
|
||||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
- runner: 'ubuntu-latest'
|
||||||
tag: 'dev-chrome'
|
tag: 'dev-chrome'
|
||||||
platform: 'amd64'
|
platform: 'amd64'
|
||||||
browser: 'chrome'
|
browser: 'chrome'
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
goss: 'goss-linux-amd64'
|
goss: 'goss-linux-amd64'
|
||||||
|
|
||||||
# Chromium - ARM
|
# Chromium - ARM
|
||||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
- runner: 'ubuntu-24.04-arm'
|
||||||
tag: 'dev-arm'
|
tag: 'dev-arm'
|
||||||
platform: 'linux/arm64'
|
platform: 'linux/arm64'
|
||||||
browser: 'chromium'
|
browser: 'chromium'
|
||||||
|
|||||||
+20
@@ -3,3 +3,23 @@ compressionLevel: mixed
|
|||||||
enableGlobalCache: false
|
enableGlobalCache: false
|
||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
|
supportedArchitectures:
|
||||||
|
cpu:
|
||||||
|
- arm
|
||||||
|
- arm64
|
||||||
|
- ia32
|
||||||
|
- loong64
|
||||||
|
- mips
|
||||||
|
- mipsel
|
||||||
|
- ppc64
|
||||||
|
- riscv64
|
||||||
|
- s390
|
||||||
|
- s390x
|
||||||
|
- x64
|
||||||
|
- wasm32
|
||||||
|
os:
|
||||||
|
- current
|
||||||
|
- darwin
|
||||||
|
- linux
|
||||||
|
- win32
|
||||||
@@ -37,6 +37,10 @@ This guide summarizes how to explore, modify, and validate the WhatsApp HTTP API
|
|||||||
- **Utilities**: RxJS streams (`SwitchObservable`, `DefaultMap`) drive webhook
|
- **Utilities**: RxJS streams (`SwitchObservable`, `DefaultMap`) drive webhook
|
||||||
event fan-out. Prefer existing helpers in `src/utils` and `src/core/utils`
|
event fan-out. Prefer existing helpers in `src/utils` and `src/core/utils`
|
||||||
before adding bespoke logic.
|
before adding bespoke logic.
|
||||||
|
- **OS** - the project works on any OS inside the Docker container; the base
|
||||||
|
image is defined in `Dockerfile`.
|
||||||
|
- **CPU** - the image must run on both `x86_64` (including pre-v2 CPUs without
|
||||||
|
SSE4.2) and `aarch64`.
|
||||||
|
|
||||||
## Repository Landmarks
|
## Repository Landmarks
|
||||||
|
|
||||||
@@ -78,6 +82,11 @@ This guide summarizes how to explore, modify, and validate the WhatsApp HTTP API
|
|||||||
- Prefer named function declarations over `const` arrow functions when possible.
|
- Prefer named function declarations over `const` arrow functions when possible.
|
||||||
- Avoid naming unused variables with a leading underscore; if a parameter is
|
- Avoid naming unused variables with a leading underscore; if a parameter is
|
||||||
required by a signature, explicitly `void` it instead.
|
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
|
- For configs, prefer runtime configurability over constants. Environment keys
|
||||||
follow `WAHA_*` for global values and `WAHA_SESSION_CONFIG_*` /
|
follow `WAHA_*` for global values and `WAHA_SESSION_CONFIG_*` /
|
||||||
`session.config.*` for per-session overrides. If both env and config are
|
`session.config.*` for per-session overrides. If both env and config are
|
||||||
@@ -108,8 +117,7 @@ This guide summarizes how to explore, modify, and validate the WhatsApp HTTP API
|
|||||||
3. After edits run:
|
3. After edits run:
|
||||||
- `pre-commit run --all-files`
|
- `pre-commit run --all-files`
|
||||||
- `yarn build`
|
- `yarn build`
|
||||||
- `yarn test` Use Node 22. Address lint or formatting issues before
|
- `yarn test --watchman=false`
|
||||||
proceeding.
|
|
||||||
4. Do **not** start the application yourself; ask the user to run it if runtime
|
4. Do **not** start the application yourself; ask the user to run it if runtime
|
||||||
validation is required.
|
validation is required.
|
||||||
5. Capture any assumptions or open questions for the user, especially when
|
5. Capture any assumptions or open questions for the user, especially when
|
||||||
@@ -126,14 +134,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
|
- Keep docs and code ASCII unless a file already uses other characters. When
|
||||||
updating documentation, mirror the concise, actionable tone used here.
|
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
|
## 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`
|
- WEBJS: `../whatsapp-web.js`
|
||||||
- NOWEB: `../WhiskeySockets-Baileys`
|
- NOWEB: `../WhiskeySockets-Baileys`
|
||||||
- GOWS: `../gows` + `../whatsmeow`
|
- whatsapp-rust-bridge - `../whatsapp-rust-bridge`
|
||||||
|
- GOWS: `../gows`
|
||||||
|
- whatsmeow - `../whatsmeow`
|
||||||
|
- WPP: `../wa-js`, `../wppconnect`, `../wppconnect-server`
|
||||||
- ChatWoot: `../chatwoot`
|
- ChatWoot: `../chatwoot`
|
||||||
|
|
||||||
Following this playbook keeps contributions aligned with WAHA’s structure,
|
Following this playbook keeps contributions aligned with WAHA’s structure,
|
||||||
automation hooks, and release process.
|
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`.
|
||||||
+18
-1
@@ -8,7 +8,9 @@ FROM node:${NODE_IMAGE_TAG} AS build
|
|||||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||||
|
|
||||||
# git + build toolchain for git deps
|
# 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
|
# npm packages
|
||||||
WORKDIR /git
|
WORKDIR /git
|
||||||
@@ -26,6 +28,21 @@ ADD . /git
|
|||||||
RUN yarn install
|
RUN yarn install
|
||||||
RUN yarn build && find ./dist -name "*.d.ts" -delete
|
RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||||
|
|
||||||
|
# Rebuild sharp from source on x86-64 so it runs on pre-v2 CPUs (no SSE4.2 requirement).
|
||||||
|
# The prebuilt @img/sharp-linux-x64 binary requires x86-64-v2; -march=x86-64 limits
|
||||||
|
# code generation to baseline x86-64 (SSE2 only). ARM64 prebuilts have no such
|
||||||
|
# constraint, so no rebuild is needed there.
|
||||||
|
# We call sharp's own install/build.js (not npm rebuild) so it first generates config.gypi
|
||||||
|
# with include/lib paths pointing to its bundled @img/sharp-libvips-linux-x64 package,
|
||||||
|
# then invokes node-gyp with those paths. Without this step node-gyp cannot find vips headers.
|
||||||
|
RUN if [ "$(uname -m)" = "x86_64" ]; then \
|
||||||
|
LIBVIPS_DEV_VER=$(node -e "require('/git/node_modules/sharp/package.json').devDependencies['@img/sharp-libvips-dev']" | tr -d '^') && \
|
||||||
|
npm install --prefix /tmp/sharp-dev --no-package-lock "@img/sharp-libvips-dev@${LIBVIPS_DEV_VER}" && \
|
||||||
|
cp -r /tmp/sharp-dev/node_modules/@img/sharp-libvips-dev /git/node_modules/@img/ && \
|
||||||
|
cd /git/node_modules/sharp && \
|
||||||
|
PATH="/git/node_modules/.bin:$PATH" CFLAGS="-march=x86-64" CXXFLAGS="-march=x86-64" node install/build.js; \
|
||||||
|
fi
|
||||||
|
|
||||||
#
|
#
|
||||||
# Dashboard
|
# Dashboard
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -35,13 +35,20 @@ for-swagger:
|
|||||||
export WHATSAPP_SWAGGER_CONFIG_ADVANCED=true && export WHATSAPP_SWAGGER_PASSWORD=666 && yarn start
|
export WHATSAPP_SWAGGER_CONFIG_ADVANCED=true && export WHATSAPP_SWAGGER_PASSWORD=666 && yarn start
|
||||||
|
|
||||||
up-noweb:
|
up-noweb:
|
||||||
yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2026-01-25
|
yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2026-02-11
|
||||||
|
|
||||||
up-noweb-libsignal:
|
up-noweb-libsignal:
|
||||||
yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
|
yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
|
||||||
|
|
||||||
up-webjs:
|
up-webjs:
|
||||||
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-02-01
|
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2026-02-18
|
||||||
|
|
||||||
|
up-wpp:
|
||||||
|
yarn up @wppconnect-team/wppconnect
|
||||||
|
yarn up @wppconnect/wa-js
|
||||||
|
|
||||||
|
up-rust-bridge:
|
||||||
|
yarn up -R whatsapp-rust-bridge
|
||||||
|
|
||||||
start-proxy:
|
start-proxy:
|
||||||
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
|
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
|
||||||
@@ -51,10 +58,12 @@ proto-gows:
|
|||||||
|
|
||||||
gows:
|
gows:
|
||||||
cd ../gows && \
|
cd ../gows && \
|
||||||
export PATH=${HOME}/go/bin:${PATH} && \
|
(export PATH=${HOME}/go/bin:${PATH} || echo failed) && \
|
||||||
make all
|
make all
|
||||||
|
|
||||||
|
up-dashboard:
|
||||||
|
node scripts/up-dashboard.js
|
||||||
|
|
||||||
copy-dashboard:
|
copy-dashboard:
|
||||||
cd ../waha-hub/ui && \
|
cd ../waha-hub/ui && \
|
||||||
make copy-waha
|
make copy-waha
|
||||||
|
|
||||||
@@ -147,7 +147,18 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
|
|||||||
## Start the project
|
## Start the project
|
||||||
1. Clone the repository
|
1. Clone the repository
|
||||||
2. Make sure you're using node>=22 (check [.nvmrc](/.nvmrc) to get the version)
|
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
|
```bash
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
yarn install
|
yarn install
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
- 8.8.8.8
|
- 8.8.8.8
|
||||||
|
|
||||||
base: &base
|
base: &base
|
||||||
image: chatwoot/chatwoot:v4.7.0
|
image: chatwoot/chatwoot:v4.12.1
|
||||||
env_file: .chatwoot.env ## Change this file for customized env variables
|
env_file: .chatwoot.env ## Change this file for customized env variables
|
||||||
volumes:
|
volumes:
|
||||||
- chatwoot_storage:/app/storage
|
- chatwoot_storage:/app/storage
|
||||||
|
|||||||
+9
-4
@@ -29,7 +29,7 @@
|
|||||||
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
|
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2026-01-25",
|
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2026-02-11",
|
||||||
"@adiwajshing/keyed-db": "^0.2.4",
|
"@adiwajshing/keyed-db": "^0.2.4",
|
||||||
"@aws-sdk/client-s3": "^3.633.0",
|
"@aws-sdk/client-s3": "^3.633.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.633.0",
|
"@aws-sdk/s3-request-presigner": "^3.633.0",
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
"@types/passport": "^1.0.17",
|
"@types/passport": "^1.0.17",
|
||||||
"@types/sqlite3": "^5.1.0",
|
"@types/sqlite3": "^5.1.0",
|
||||||
"@types/ws": "^8.5.4",
|
"@types/ws": "^8.5.4",
|
||||||
|
"@wppconnect-team/wppconnect": "^1.41.1",
|
||||||
|
"@wppconnect/wa-js": "^3.23.3",
|
||||||
"adm-zip": "0.5.10",
|
"adm-zip": "0.5.10",
|
||||||
"agentkeepalive": "^4.5.0",
|
"agentkeepalive": "^4.5.0",
|
||||||
"async-lock": "^1.4.1",
|
"async-lock": "^1.4.1",
|
||||||
@@ -104,14 +106,15 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^4.3.0",
|
"rimraf": "^4.3.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sharp": "^0.33.4",
|
"sharp": "^0.34.5",
|
||||||
"shell-quote": "^1.8.3",
|
"shell-quote": "^1.8.3",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"swagger-ui-express": "^4.1.4",
|
"swagger-ui-express": "^4.1.4",
|
||||||
"ulid": "^2.3.0",
|
"ulid": "^2.3.0",
|
||||||
"undici": "^7.16.0",
|
"undici": "^7.16.0",
|
||||||
"uniqid": "^5.4.0",
|
"uniqid": "^5.4.0",
|
||||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2026-02-01",
|
"user-agents": "^1.1.669",
|
||||||
|
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2026-02-18",
|
||||||
"write-file-atomic": "^6.0.0",
|
"write-file-atomic": "^6.0.0",
|
||||||
"yaml": "^2.7.1"
|
"yaml": "^2.7.1"
|
||||||
},
|
},
|
||||||
@@ -124,7 +127,8 @@
|
|||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"puppeteer": "^24.31.0",
|
"puppeteer": "^24.31.0",
|
||||||
"whatwg-url": "13.0.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": {
|
"devDependencies": {
|
||||||
"@grpc/grpc-js": "^1.14.1",
|
"@grpc/grpc-js": "^1.14.1",
|
||||||
@@ -138,6 +142,7 @@
|
|||||||
"@types/node-fetch": "^2.6.13",
|
"@types/node-fetch": "^2.6.13",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.0",
|
||||||
"@types/supertest": "^2.0.8",
|
"@types/supertest": "^2.0.8",
|
||||||
|
"@types/user-agents": "^1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"grpc-tools": "^1.13.0",
|
"grpc-tools": "^1.13.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
|
|||||||
+17
-3
@@ -76,17 +76,31 @@ const updatedContent = exampleContent
|
|||||||
|
|
||||||
fs.writeFileSync(targetPath, updatedContent, { encoding: 'utf8' });
|
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 = [
|
const lines = [
|
||||||
'Credentials generated.',
|
'Credentials generated.',
|
||||||
'',
|
'',
|
||||||
'Dashboard and Swagger:',
|
'Generated env values:',
|
||||||
|
...generatedEnvValues.map((line) => ` - ${line}`),
|
||||||
|
'',
|
||||||
|
'Use these credentials to login in Dashboard or Swagger:',
|
||||||
' - Username: admin',
|
' - Username: admin',
|
||||||
` - Password: ${adminPassword}`,
|
` - Password: ${adminPassword}`,
|
||||||
'',
|
'',
|
||||||
'API key: ',
|
'Use this API key in the x-api-key header:',
|
||||||
` - ${apiKey}`,
|
` - ${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'));
|
console.log(lines.join('\n'));
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const fs = require('fs');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
const CONFIG_FILE = 'waha.config.json';
|
||||||
|
|
||||||
|
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
||||||
|
const { repo } = config.waha.dashboard;
|
||||||
|
if (!repo) {
|
||||||
|
console.error(`Missing dashboard.repo in ${CONFIG_FILE}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const branch = 'gh-pages';
|
||||||
|
console.log(`Fetching latest commit for ${repo}@${branch}...`);
|
||||||
|
|
||||||
|
const output = execSync(
|
||||||
|
`git ls-remote https://github.com/${repo} refs/heads/${branch}`,
|
||||||
|
{ encoding: 'utf8' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const sha = output.split('\t')[0].trim();
|
||||||
|
if (!sha) {
|
||||||
|
console.error(`Could not resolve ref ${branch} for ${repo}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.waha.dashboard.ref = sha;
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
||||||
|
console.log(`Updated ${CONFIG_FILE}: dashboard.ref = ${sha}`);
|
||||||
@@ -131,14 +131,15 @@ export class ChannelsController {
|
|||||||
})
|
})
|
||||||
async previewChannelMessages(
|
async previewChannelMessages(
|
||||||
@WorkingSessionParam session: WhatsappSession,
|
@WorkingSessionParam session: WhatsappSession,
|
||||||
@Param('id') code: string,
|
@Param('id') codeOrId: string,
|
||||||
@Query() query: PreviewChannelMessages,
|
@Query() query: PreviewChannelMessages,
|
||||||
): Promise<ChannelMessage[]> {
|
): Promise<ChannelMessage[]> {
|
||||||
if (isJidNewsletter(code)) {
|
let code = null;
|
||||||
const channel = await session.channelsGetChannel(code);
|
if (isJidNewsletter(codeOrId)) {
|
||||||
|
const channel = await session.channelsGetChannel(codeOrId);
|
||||||
code = parseChannelInviteLink(channel.invite);
|
code = parseChannelInviteLink(channel.invite);
|
||||||
}
|
}
|
||||||
const inviteCode = parseChannelInviteLink(code);
|
const inviteCode = parseChannelInviteLink(code || codeOrId);
|
||||||
return session.previewChannelMessages(inviteCode, query);
|
return session.previewChannelMessages(inviteCode, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ import { WhatsappSession } from '../core/abc/session.abc';
|
|||||||
import {
|
import {
|
||||||
ChatPictureQuery,
|
ChatPictureQuery,
|
||||||
ChatPictureResponse,
|
ChatPictureResponse,
|
||||||
ChatsPaginationParams,
|
|
||||||
ChatSummary,
|
ChatSummary,
|
||||||
GetChatMessageQuery,
|
GetChatMessageQuery,
|
||||||
GetChatMessagesFilter,
|
GetChatMessagesFilter,
|
||||||
GetChatMessagesQuery,
|
GetChatMessagesQuery,
|
||||||
|
GetChatsOverviewParams,
|
||||||
|
GetChatsParams,
|
||||||
MessageSortField,
|
MessageSortField,
|
||||||
OverviewBodyRequest,
|
OverviewBodyRequest,
|
||||||
OverviewFilter,
|
OverviewFilter,
|
||||||
OverviewPaginationParams,
|
|
||||||
PinMessageRequest,
|
PinMessageRequest,
|
||||||
ReadChatMessagesQuery,
|
ReadChatMessagesQuery,
|
||||||
ReadChatMessagesResponse,
|
ReadChatMessagesResponse,
|
||||||
@@ -61,7 +61,7 @@ class ChatsController {
|
|||||||
@ApiOperation({ summary: 'Get chats' })
|
@ApiOperation({ summary: 'Get chats' })
|
||||||
getChats(
|
getChats(
|
||||||
@WorkingSessionParam session: WhatsappSession,
|
@WorkingSessionParam session: WhatsappSession,
|
||||||
@Query() pagination: ChatsPaginationParams,
|
@Query() pagination: GetChatsParams,
|
||||||
) {
|
) {
|
||||||
return session.getChats(pagination);
|
return session.getChats(pagination);
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ class ChatsController {
|
|||||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||||
getChatsOverview(
|
getChatsOverview(
|
||||||
@WorkingSessionParam session: WhatsappSession,
|
@WorkingSessionParam session: WhatsappSession,
|
||||||
@Query() pagination: OverviewPaginationParams,
|
@Query() pagination: GetChatsOverviewParams,
|
||||||
@Query() filter: OverviewFilter,
|
@Query() filter: OverviewFilter,
|
||||||
): Promise<ChatSummary[]> {
|
): Promise<ChatSummary[]> {
|
||||||
return session.getChatsOverview(pagination, filter);
|
return session.getChatsOverview(pagination, filter);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
Get,
|
||||||
Param,
|
Param,
|
||||||
Put,
|
Put,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UsePipes,
|
UsePipes,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiParam, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||||
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
import { CanSession, FromParam } from '@waha/core/auth/policies';
|
||||||
@@ -31,6 +32,27 @@ import { Action } from '@waha/core/auth/casl.types';
|
|||||||
export class ContactsSessionController {
|
export class ContactsSessionController {
|
||||||
constructor(private manager: SessionManager) {}
|
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')
|
@Put('/:chatId')
|
||||||
@SessionApiParam
|
@SessionApiParam
|
||||||
@ChatIdApiParam
|
@ChatIdApiParam
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export class ScreenshotController {
|
|||||||
@Get('/screenshot')
|
@Get('/screenshot')
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary:
|
summary:
|
||||||
'Get a screenshot of the current WhatsApp session (**WEBJS** only)',
|
'Get a screenshot of the current WhatsApp session (**WEBJS/WPP** only)',
|
||||||
})
|
})
|
||||||
@UseInterceptors(new BufferResponseInterceptor('image/jpeg'))
|
@UseInterceptors(new BufferResponseInterceptor('image/jpeg'))
|
||||||
@ApiFileAcceptHeader('image/jpeg')
|
@ApiFileAcceptHeader('image/jpeg')
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class SessionsController {
|
|||||||
@Req() req,
|
@Req() req,
|
||||||
): Promise<SessionInfo[]> {
|
): Promise<SessionInfo[]> {
|
||||||
let sessions = await this.manager.getSessions(query.all);
|
let sessions = await this.manager.getSessions(query.all);
|
||||||
if (!req.user.isAdmin) {
|
if (!req.user?.isAdmin) {
|
||||||
sessions = FilterSessions(req.ability, Action.Read, sessions);
|
sessions = FilterSessions(req.ability, Action.Read, sessions);
|
||||||
}
|
}
|
||||||
if (query.expand?.includes(SessionExpand.apps)) {
|
if (query.expand?.includes(SessionExpand.apps)) {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export class WebsocketGatewayCore
|
|||||||
|
|
||||||
if (!ability.can(Action.Use, new SessionName(session))) {
|
if (!ability.can(Action.Use, new SessionName(session))) {
|
||||||
socket.close(WebSocketCloseCode.POLICY_VIOLATION, 'Forbidden');
|
socket.close(WebSocketCloseCode.POLICY_VIOLATION, 'Forbidden');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug(`New client connected: ${request.url} - ${socket.id}`);
|
this.logger.debug(`New client connected: ${request.url} - ${socket.id}`);
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ import {
|
|||||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||||
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
import { CheckPolicies } from '@waha/core/auth/policies.decorator';
|
||||||
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
import { PoliciesGuard } from '@waha/core/auth/policies.guard';
|
||||||
import { CanSession, FromBody, FromQuery } from '@waha/core/auth/policies';
|
import {
|
||||||
|
CanServer,
|
||||||
|
CanSession,
|
||||||
|
FromBody,
|
||||||
|
FromQuery,
|
||||||
|
} from '@waha/core/auth/policies';
|
||||||
import { Action, session as SessionName } from '@waha/core/auth/casl.types';
|
import { Action, session as SessionName } from '@waha/core/auth/casl.types';
|
||||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||||
|
|
||||||
@@ -65,6 +70,7 @@ export class AppsController {
|
|||||||
|
|
||||||
@Get('/:id')
|
@Get('/:id')
|
||||||
@ApiOperation({ summary: 'Get app by ID' })
|
@ApiOperation({ summary: 'Get app by ID' })
|
||||||
|
@CheckPolicies(CanServer(Action.Read))
|
||||||
@UsePipes(new WAHAValidationPipe())
|
@UsePipes(new WAHAValidationPipe())
|
||||||
async get(@Param('id') id: string, @Req() req: any): Promise<App> {
|
async get(@Param('id') id: string, @Req() req: any): Promise<App> {
|
||||||
const app = await this.appsService.get(this.manager, id);
|
const app = await this.appsService.get(this.manager, id);
|
||||||
@@ -79,6 +85,7 @@ export class AppsController {
|
|||||||
|
|
||||||
@Put('/:id')
|
@Put('/:id')
|
||||||
@ApiOperation({ summary: 'Update an existing app' })
|
@ApiOperation({ summary: 'Update an existing app' })
|
||||||
|
@CheckPolicies(CanServer(Action.Read))
|
||||||
@UsePipes(new WAHAValidationPipe())
|
@UsePipes(new WAHAValidationPipe())
|
||||||
async update(
|
async update(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@@ -114,6 +121,7 @@ export class AppsController {
|
|||||||
|
|
||||||
@Delete('/:id')
|
@Delete('/:id')
|
||||||
@ApiOperation({ summary: 'Delete an app' })
|
@ApiOperation({ summary: 'Delete an app' })
|
||||||
|
@CheckPolicies(CanServer(Action.Read))
|
||||||
@UsePipes(new WAHAValidationPipe())
|
@UsePipes(new WAHAValidationPipe())
|
||||||
async delete(@Param('id') id: string, @Req() req: any): Promise<void> {
|
async delete(@Param('id') id: string, @Req() req: any): Promise<void> {
|
||||||
const existing = await this.appsService.get(this.manager, id);
|
const existing = await this.appsService.get(this.manager, id);
|
||||||
|
|||||||
+33
-5
@@ -1,4 +1,10 @@
|
|||||||
import { parseBool } from '@waha/helpers';
|
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[] {
|
function parseCommaSeparatedList(value: string | undefined): string[] {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -7,8 +13,30 @@ function parseCommaSeparatedList(value: string | undefined): string[] {
|
|||||||
return value.split(',').map((item) => item.trim());
|
return value.split(',').map((item) => item.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppEnv = {
|
function isMongoDB(): boolean {
|
||||||
enabled: parseBool(process.env.WAHA_APPS_ENABLED),
|
return !!process.env.WHATSAPP_SESSIONS_MONGO_URL;
|
||||||
on: parseCommaSeparatedList(process.env.WAHA_APPS_ON),
|
}
|
||||||
off: parseCommaSeparatedList(process.env.WAHA_APPS_OFF),
|
|
||||||
};
|
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();
|
||||||
@@ -90,10 +90,10 @@ export class AppsEnabledService implements IAppsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const service = this.getAppService(app);
|
const service = this.getAppService(app);
|
||||||
if (!service && !AppRuntimeConfig.HasApp(app.app)) {
|
if (app.enabled && !service && !AppRuntimeConfig.HasApp(app.app)) {
|
||||||
throw new AppDisableError(app.app);
|
throw new AppDisableError(app.app);
|
||||||
}
|
}
|
||||||
service.validate(app);
|
service?.validate(app);
|
||||||
// Only run beforeCreated when app is enabled (default true if omitted)
|
// Only run beforeCreated when app is enabled (default true if omitted)
|
||||||
if (app.enabled !== false) {
|
if (app.enabled !== false) {
|
||||||
await service.beforeCreated(app);
|
await service.beforeCreated(app);
|
||||||
@@ -147,21 +147,21 @@ export class AppsEnabledService implements IAppsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const service = this.getAppService(app);
|
const service = this.getAppService(app);
|
||||||
if (!service && !AppRuntimeConfig.HasApp(app.app)) {
|
if (app.enabled && !service && !AppRuntimeConfig.HasApp(app.app)) {
|
||||||
throw new AppDisableError(app.app);
|
throw new AppDisableError(app.app);
|
||||||
}
|
}
|
||||||
service.validate(app);
|
service?.validate(app);
|
||||||
|
|
||||||
const hasEnabledChange = savedApp.enabled !== app.enabled;
|
const hasEnabledChange = savedApp.enabled !== app.enabled;
|
||||||
|
|
||||||
if (hasEnabledChange) {
|
if (hasEnabledChange) {
|
||||||
if (app.enabled) {
|
if (app.enabled) {
|
||||||
await service.beforeEnabled(savedApp, app);
|
await service?.beforeEnabled(savedApp, app);
|
||||||
} else {
|
} else {
|
||||||
await service.beforeDisabled(savedApp, app);
|
await service?.beforeDisabled(savedApp, app);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await service.beforeUpdated(savedApp, app);
|
await service?.beforeUpdated(savedApp, app);
|
||||||
}
|
}
|
||||||
await repo.update(app.id, app);
|
await repo.update(app.id, app);
|
||||||
const updated = await repo.getById(app.id);
|
const updated = await repo.getById(app.id);
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { ApiProperty } from '@nestjs/swagger';
|
|||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
|
Min,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
@@ -23,6 +25,26 @@ export class CallsAppChannelConfig {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description:
|
||||||
|
'Seconds to wait before declining the call. If not set or undefined, the call is declined immediately (0 seconds).',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
waitBeforeDecline?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description:
|
||||||
|
'Seconds to wait before sending the auto-reply message. If not set or undefined, the message is sent immediately (0 seconds).',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
waitBeforeResponse?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CallsAppConfig {
|
export class CallsAppConfig {
|
||||||
|
|||||||
@@ -92,10 +92,14 @@ export class CallsListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldReject) {
|
if (shouldReject) {
|
||||||
|
const waitBeforeDeclineMs = (config.waitBeforeDecline ?? 0) * 1000;
|
||||||
|
await sleep(waitBeforeDeclineMs);
|
||||||
await this.rejectCall(call);
|
await this.rejectCall(call);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldMessage) {
|
if (shouldMessage) {
|
||||||
|
const waitBeforeResponseMs = (config.waitBeforeResponse ?? 0) * 1000;
|
||||||
|
await sleep(waitBeforeResponseMs);
|
||||||
await this.replyWithTyping(call.from, message);
|
await this.replyWithTyping(call.from, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
NoRetriesJobOptions,
|
NoRetriesJobOptions,
|
||||||
} from '@waha/apps/app_sdk/constants';
|
} from '@waha/apps/app_sdk/constants';
|
||||||
import { MessageCleanupConsumer } from '@waha/apps/chatwoot/consumers/scheduled/message.cleanup';
|
import { MessageCleanupConsumer } from '@waha/apps/chatwoot/consumers/scheduled/message.cleanup';
|
||||||
|
import { CheckTierConsumer } from '@waha/apps/chatwoot/consumers/scheduled/check.tier';
|
||||||
import { ChatWootAppService } from '@waha/apps/chatwoot/services/ChatWootAppService';
|
import { ChatWootAppService } from '@waha/apps/chatwoot/services/ChatWootAppService';
|
||||||
import * as lodash from 'lodash';
|
import * as lodash from 'lodash';
|
||||||
|
|
||||||
@@ -51,6 +52,10 @@ const IMPORTS = lodash.flatten([
|
|||||||
name: QueueName.SCHEDULED_CHECK_VERSION,
|
name: QueueName.SCHEDULED_CHECK_VERSION,
|
||||||
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
|
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
|
||||||
}),
|
}),
|
||||||
|
RegisterAppQueue({
|
||||||
|
name: QueueName.SCHEDULED_CHECK_TIER,
|
||||||
|
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
|
||||||
|
}),
|
||||||
RegisterAppQueue({
|
RegisterAppQueue({
|
||||||
name: QueueName.TASK_CONTACTS_PULL,
|
name: QueueName.TASK_CONTACTS_PULL,
|
||||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||||
@@ -145,6 +150,7 @@ const PROVIDERS = [
|
|||||||
// Scheduled
|
// Scheduled
|
||||||
MessageCleanupConsumer,
|
MessageCleanupConsumer,
|
||||||
CheckVersionConsumer,
|
CheckVersionConsumer,
|
||||||
|
CheckTierConsumer,
|
||||||
// Services
|
// Services
|
||||||
ChatWootWAHAQueueService,
|
ChatWootWAHAQueueService,
|
||||||
ChatWootQueueService,
|
ChatWootQueueService,
|
||||||
|
|||||||
@@ -20,8 +20,18 @@ export async function QueueStatus(ctx: QueueCommandContext, name: string) {
|
|||||||
for (const status of result) {
|
for (const status of result) {
|
||||||
status.name = QueueNameRepr(status.name);
|
status.name = QueueNameRepr(status.name);
|
||||||
}
|
}
|
||||||
// locked: true - last
|
const categoryOrder = ['inbox', 'whatsapp', 'task', 'scheduled'];
|
||||||
result = lodash.sortBy(result, [(x) => !!x.locked, 'name']);
|
const categoryIndex = (name: string) => {
|
||||||
|
const category = name.split(' | ')[0];
|
||||||
|
const index = categoryOrder.indexOf(category);
|
||||||
|
return index === -1 ? categoryOrder.length : index;
|
||||||
|
};
|
||||||
|
// locked: true - last, then by category (inbox/whatsapp/task/scheduled), then by name
|
||||||
|
result = lodash.sortBy(result, [
|
||||||
|
(x) => !!x.locked,
|
||||||
|
(x) => categoryIndex(x.name),
|
||||||
|
'name',
|
||||||
|
]);
|
||||||
const msg = ctx.l.r('cli.cmd.queue.status.result', {
|
const msg = ctx.l.r('cli.cmd.queue.status.result', {
|
||||||
queues: result,
|
queues: result,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export async function runText(
|
|||||||
.map((line) => line.trimStart()) // remove indentation
|
.map((line) => line.trimStart()) // remove indentation
|
||||||
.join(' ') // join with space
|
.join(' ') // join with space
|
||||||
.trim(); // final cleanup
|
.trim(); // final cleanup
|
||||||
|
// Strip WhatsApp/markdown formatting characters so shell-quote doesn't
|
||||||
|
// treat them as glob operators and return non-string tokens
|
||||||
|
text = text.replace(/[*_~`]/g, '');
|
||||||
const output = new BufferedOutput();
|
const output = new BufferedOutput();
|
||||||
const program = BuildProgram(commands, ctx, output);
|
const program = BuildProgram(commands, ctx, output);
|
||||||
const argv = parse(text);
|
const argv = parse(text);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
|||||||
* Parse human string into milliseconds
|
* Parse human string into milliseconds
|
||||||
*/
|
*/
|
||||||
export function ParseMS(value: string) {
|
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 == null) throw new Error(`Invalid duration: "${value}"`);
|
||||||
if (duration < 0) throw new Error(`Duration cannot be negative: "${value}"`);
|
if (duration < 0) throw new Error(`Duration cannot be negative: "${value}"`);
|
||||||
return duration;
|
return duration;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { AttributeKey, INBOX_CONTACT_CHAT_ID } from '@waha/apps/chatwoot/const';
|
|||||||
import * as lodash from 'lodash';
|
import * as lodash from 'lodash';
|
||||||
import { WhatsAppMessage } from '@waha/apps/chatwoot/storage';
|
import { WhatsAppMessage } from '@waha/apps/chatwoot/storage';
|
||||||
import { buildMessageId } from '@waha/core/engines/noweb/session.noweb.core';
|
import { buildMessageId } from '@waha/core/engines/noweb/session.noweb.core';
|
||||||
import { isLidUser } from '@waha/core/utils/jids';
|
import { isLidUser, normalizeJid } from '@waha/core/utils/jids';
|
||||||
|
|
||||||
export function GetJID(contact: any): string | null {
|
export function GetJID(contact: any): string | null {
|
||||||
return contact?.custom_attributes?.[AttributeKey.WA_JID];
|
return contact?.custom_attributes?.[AttributeKey.WA_JID];
|
||||||
@@ -23,20 +23,13 @@ export function GetAllChatIDs(contact: any): Array<string> {
|
|||||||
attrs[AttributeKey.WA_JID],
|
attrs[AttributeKey.WA_JID],
|
||||||
attrs[AttributeKey.WA_LID],
|
attrs[AttributeKey.WA_LID],
|
||||||
];
|
];
|
||||||
return lodash.uniq(ids.filter(Boolean));
|
return lodash.uniq(ids.filter(Boolean).map(normalizeJid));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FindChatID(contact: any): string | null {
|
export function FindChatID(contact: any): string | null {
|
||||||
if (GetChatID(contact)) {
|
const chatId =
|
||||||
return GetChatID(contact);
|
GetChatID(contact) ?? GetJID(contact) ?? GetLID(contact) ?? null;
|
||||||
}
|
return chatId ? normalizeJid(chatId) : null;
|
||||||
if (GetJID(contact)) {
|
|
||||||
return GetJID(contact);
|
|
||||||
}
|
|
||||||
if (GetLID(contact)) {
|
|
||||||
return GetLID(contact);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SerializeWhatsAppKey(message: WhatsAppMessage): string {
|
export function SerializeWhatsAppKey(message: WhatsAppMessage): string {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export enum QueueName {
|
|||||||
//
|
//
|
||||||
SCHEDULED_MESSAGE_CLEANUP = 'chatwoot.scheduled | message.cleanup',
|
SCHEDULED_MESSAGE_CLEANUP = 'chatwoot.scheduled | message.cleanup',
|
||||||
SCHEDULED_CHECK_VERSION = 'chatwoot.scheduled | check.version',
|
SCHEDULED_CHECK_VERSION = 'chatwoot.scheduled | check.version',
|
||||||
|
SCHEDULED_CHECK_TIER = 'chatwoot.scheduled | check.tier',
|
||||||
|
|
||||||
//
|
//
|
||||||
// Task
|
// Task
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Processor } from '@nestjs/bullmq';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||||
|
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||||
|
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||||
|
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||||
|
import { SUPPORT_US_URL } from '@waha/core/constants';
|
||||||
|
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||||
|
import { VERSION, WAHAVersion } from '@waha/version';
|
||||||
|
import { Job } from 'bullmq';
|
||||||
|
import { PinoLogger } from 'nestjs-pino';
|
||||||
|
|
||||||
|
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||||
|
import { ChatWootScheduledConsumer } from './base';
|
||||||
|
|
||||||
|
@Processor(QueueName.SCHEDULED_CHECK_TIER, { concurrency: JOB_CONCURRENCY })
|
||||||
|
export class CheckTierConsumer extends ChatWootScheduledConsumer {
|
||||||
|
constructor(manager: SessionManager, log: PinoLogger, rmutex: RMutexService) {
|
||||||
|
super(manager, log, rmutex, CheckTierConsumer.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ErrorHeaderKey(): TKey {
|
||||||
|
return TKey.JOB_SCHEDULED_ERROR_HEADER;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Process(container: DIContainer, job: Job): Promise<any> {
|
||||||
|
const logger = container.Logger();
|
||||||
|
const locale = container.Locale();
|
||||||
|
const conversation = await container
|
||||||
|
.ContactConversationService()
|
||||||
|
.InboxNotifications();
|
||||||
|
if (VERSION.tier !== WAHAVersion.CORE) {
|
||||||
|
logger.info('WAHA is not using the CORE version');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info('WAHA is using the CORE version');
|
||||||
|
const supportMessage = locale.key(TKey.WAHA_CORE_VERSION_USED).render({
|
||||||
|
supportUrl: SUPPORT_US_URL,
|
||||||
|
});
|
||||||
|
await conversation.incoming(supportMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,19 @@
|
|||||||
import { Processor } from '@nestjs/bullmq';
|
import { Processor } from '@nestjs/bullmq';
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||||
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||||
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||||
import { CHANGELOG_URL, SUPPORT_US_URL } from '@waha/core/constants';
|
import { CHANGELOG_URL } from '@waha/core/constants';
|
||||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||||
import { VERSION, WAHAVersion } from '@waha/version';
|
import { VERSION } from '@waha/version';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Job } from 'bullmq';
|
import { Job } from 'bullmq';
|
||||||
import { PinoLogger } from 'nestjs-pino';
|
import { PinoLogger } from 'nestjs-pino';
|
||||||
|
|
||||||
import { ChatWootScheduledConsumer } from './base';
|
|
||||||
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||||
|
import { ChatWootScheduledConsumer } from './base';
|
||||||
|
|
||||||
/**
|
|
||||||
* Scheduled consumer for version checking
|
|
||||||
* Periodically checks for new versions and notifies if updates are available
|
|
||||||
*/
|
|
||||||
@Processor(QueueName.SCHEDULED_CHECK_VERSION, { concurrency: JOB_CONCURRENCY })
|
@Processor(QueueName.SCHEDULED_CHECK_VERSION, { concurrency: JOB_CONCURRENCY })
|
||||||
export class CheckVersionConsumer extends ChatWootScheduledConsumer {
|
export class CheckVersionConsumer extends ChatWootScheduledConsumer {
|
||||||
constructor(manager: SessionManager, log: PinoLogger, rmutex: RMutexService) {
|
constructor(manager: SessionManager, log: PinoLogger, rmutex: RMutexService) {
|
||||||
@@ -29,34 +24,7 @@ export class CheckVersionConsumer extends ChatWootScheduledConsumer {
|
|||||||
return TKey.JOB_SCHEDULED_ERROR_HEADER;
|
return TKey.JOB_SCHEDULED_ERROR_HEADER;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Process the version check job
|
|
||||||
* Checks for new versions and notifies if updates are available
|
|
||||||
*/
|
|
||||||
protected async Process(container: DIContainer, job: Job): Promise<any> {
|
protected async Process(container: DIContainer, job: Job): Promise<any> {
|
||||||
await this.CheckWAHACoreVersion(container);
|
|
||||||
await this.CheckNewVersionAvailable(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async CheckWAHACoreVersion(container: DIContainer) {
|
|
||||||
const logger = container.Logger();
|
|
||||||
const locale = container.Locale();
|
|
||||||
const conversation = await container
|
|
||||||
.ContactConversationService()
|
|
||||||
.InboxNotifications();
|
|
||||||
// If using the CORE version, send a message encouraging support
|
|
||||||
if (VERSION.tier !== WAHAVersion.CORE) {
|
|
||||||
logger.info('WAHA is not using the CORE version');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.info('WAHA is using the CORE version');
|
|
||||||
const supportMessage = locale.key(TKey.WAHA_CORE_VERSION_USED).render({
|
|
||||||
supportUrl: SUPPORT_US_URL,
|
|
||||||
});
|
|
||||||
await conversation.incoming(supportMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async CheckNewVersionAvailable(container: DIContainer) {
|
|
||||||
const logger = container.Logger();
|
const logger = container.Logger();
|
||||||
logger.info('Processing version check job');
|
logger.info('Processing version check job');
|
||||||
const currentVersion = VERSION.version;
|
const currentVersion = VERSION.version;
|
||||||
@@ -72,7 +40,6 @@ export class CheckVersionConsumer extends ChatWootScheduledConsumer {
|
|||||||
`New version available: ${latestVersion} (current: ${currentVersion})`,
|
`New version available: ${latestVersion} (current: ${currentVersion})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send a message to Chatwoot conversation about a new version
|
|
||||||
const locale = container.Locale();
|
const locale = container.Locale();
|
||||||
const message = locale.key(TKey.WAHA_NEW_VERSION_AVAILABLE).render({
|
const message = locale.key(TKey.WAHA_NEW_VERSION_AVAILABLE).render({
|
||||||
currentVersion: currentVersion,
|
currentVersion: currentVersion,
|
||||||
|
|||||||
@@ -212,7 +212,11 @@ class MessagesPullHandler {
|
|||||||
|
|
||||||
// Perform some actions before any job started
|
// Perform some actions before any job started
|
||||||
if (options.pause) {
|
if (options.pause) {
|
||||||
await this.queueManager.pause();
|
const queues = [
|
||||||
|
...this.queueManager.resolve('inbox'),
|
||||||
|
...this.queueManager.resolve('waha'),
|
||||||
|
];
|
||||||
|
await this.queueManager.pause(queues);
|
||||||
await this.activity.queue(true);
|
await this.activity.queue(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,7 +232,11 @@ class MessagesPullHandler {
|
|||||||
|
|
||||||
// Resume queues if paused
|
// Resume queues if paused
|
||||||
if (options.pause) {
|
if (options.pause) {
|
||||||
await this.queueManager.resume();
|
const queues = [
|
||||||
|
...this.queueManager.resolve('inbox'),
|
||||||
|
...this.queueManager.resolve('waha'),
|
||||||
|
];
|
||||||
|
await this.queueManager.resume(queues);
|
||||||
await this.activity.queue(false);
|
await this.activity.queue(false);
|
||||||
}
|
}
|
||||||
// Progress summary
|
// Progress summary
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ export abstract class MessageBaseHandler<
|
|||||||
chat_id: toCusFormat(key.remoteJid),
|
chat_id: toCusFormat(key.remoteJid),
|
||||||
message_id: key.id,
|
message_id: key.id,
|
||||||
from_me: key.fromMe,
|
from_me: key.fromMe,
|
||||||
participant: null,
|
participant: isJidGroup(key.remoteJid) ? key.participant : null,
|
||||||
};
|
};
|
||||||
await this.mappingService.map(chatwoot, whatsapp, 1);
|
await this.mappingService.map(chatwoot, whatsapp, 1);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
PollMessage,
|
PollMessage,
|
||||||
ListMessage,
|
ListMessage,
|
||||||
ShareContactMessage,
|
ShareContactMessage,
|
||||||
|
StatusReplyMessage,
|
||||||
TextMessage,
|
TextMessage,
|
||||||
UnsupportedMessage,
|
UnsupportedMessage,
|
||||||
AlbumMessage,
|
AlbumMessage,
|
||||||
@@ -86,6 +87,18 @@ export class MessageAnyHandler extends MessageBaseHandler<WAMessage> {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
converter = new StatusReplyMessage(
|
||||||
|
this.l,
|
||||||
|
this.logger,
|
||||||
|
this.waha,
|
||||||
|
this.job,
|
||||||
|
this.session,
|
||||||
|
);
|
||||||
|
msg = await converter.convert(payload, protoMessage);
|
||||||
|
if (msg) {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
converter = new TextMessage(this.l, this.logger, this.waha, this.job);
|
converter = new TextMessage(this.l, this.logger, this.waha, this.job);
|
||||||
msg = await converter.convert(payload, null);
|
msg = await converter.convert(payload, null);
|
||||||
if (msg) {
|
if (msg) {
|
||||||
|
|||||||
@@ -32,7 +32,10 @@ export class WAHAMessageReactionConsumer extends ChatWootWAHABaseConsumer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GetChatId(event: WAHAWebhookMessageReaction): string {
|
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
|
// chat in "to" field for WEBJS for message.reaction
|
||||||
// probably we need to set it to "from"
|
// probably we need to set it to "from"
|
||||||
// but for backward compatability we do this
|
// but for backward compatability we do this
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
isJidStatusBroadcast,
|
isJidStatusBroadcast,
|
||||||
isLidUser,
|
isLidUser,
|
||||||
isPnUser,
|
isPnUser,
|
||||||
|
normalizeJid,
|
||||||
} from '@waha/core/utils/jids';
|
} from '@waha/core/utils/jids';
|
||||||
import { UnknownJIDFormat } from '@waha/apps/chatwoot/errors';
|
import { UnknownJIDFormat } from '@waha/apps/chatwoot/errors';
|
||||||
import { E164Parser } from '@waha/core/utils/PhoneJidNormalizer';
|
import { E164Parser } from '@waha/core/utils/PhoneJidNormalizer';
|
||||||
@@ -270,7 +271,7 @@ export function WhatsAppContactInfo(
|
|||||||
} else if (isJidBroadcast(chatId)) {
|
} else if (isJidBroadcast(chatId)) {
|
||||||
return new BroadcastContactInfo(session, chatId, locale);
|
return new BroadcastContactInfo(session, chatId, locale);
|
||||||
} else if (isLidUser(chatId)) {
|
} else if (isLidUser(chatId)) {
|
||||||
return new LidContactInfo(session, chatId, locale);
|
return new LidContactInfo(session, normalizeJid(chatId), locale);
|
||||||
} else if (isPnUser(chatId)) {
|
} else if (isPnUser(chatId)) {
|
||||||
return new JidContactInfo(session, chatId, locale);
|
return new JidContactInfo(session, chatId, locale);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 تحقق مما هو جديد في [**🆕 سجل التغييرات**]({{{changelogUrl}}})
|
👉 تحقق مما هو جديد في [**🆕 سجل التغييرات**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_لإيقاف هذه الإشعارات، أرسل_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **ادعم WAHA!**
|
❤️ **ادعم WAHA!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **تم إرسال PIX نسخ ولصق**
|
💳 **تم إرسال PIX نسخ ولصق**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **رد على حالة واتساب**
|
||||||
|
🏷️ **النوع:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **نص الحالة:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **هذا النوع من الرسائل غير مدعوم في هذا الصندوق الوارد.**
|
⚠️ **هذا النوع من الرسائل غير مدعوم في هذا الصندوق الوارد.**
|
||||||
📱 يرجى فتح **WhatsApp** لعرضه.
|
📱 يرجى فتح **WhatsApp** لعرضه.
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 [**🆕 পরিবর্তন লগ**]({{{changelogUrl}}}) এ নতুন কি আছে তা দেখুন
|
👉 [**🆕 পরিবর্তন লগ**]({{{changelogUrl}}}) এ নতুন কি আছে তা দেখুন
|
||||||
|
|
||||||
|
_এই বিজ্ঞপ্তিগুলি অক্ষম করতে, পাঠান_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **WAHA সমর্থন করুন!**
|
❤️ **WAHA সমর্থন করুন!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX কপি এবং পেস্ট পাঠানো হয়েছে**
|
💳 **PIX কপি এবং পেস্ট পাঠানো হয়েছে**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **WhatsApp স্ট্যাটাসের জবাব**
|
||||||
|
🏷️ **ধরন:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **স্ট্যাটাসের টেক্সট:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **এই ধরনের বার্তা এই ইনবক্সে সমর্থিত নয়।**
|
⚠️ **এই ধরনের বার্তা এই ইনবক্সে সমর্থিত নয়।**
|
||||||
📱 এটি দেখতে অনুগ্রহ করে **WhatsApp** খুলুন।
|
📱 এটি দেখতে অনুগ্রহ করে **WhatsApp** খুলুন।
|
||||||
|
|||||||
@@ -328,6 +328,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 Sehen Sie, was neu ist im [**🆕 Änderungsprotokoll**]({{{changelogUrl}}})
|
👉 Sehen Sie, was neu ist im [**🆕 Änderungsprotokoll**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_Um diese Benachrichtigungen zu deaktivieren, senden Sie_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **Unterstützen Sie WAHA!**
|
❤️ **Unterstützen Sie WAHA!**
|
||||||
|
|
||||||
@@ -490,6 +492,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX Kopieren und Einfügen gesendet**
|
💳 **PIX Kopieren und Einfügen gesendet**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **Antwort auf WhatsApp-Status**
|
||||||
|
🏷️ **Typ:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **Status-Text:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **Dieser Nachrichtentyp wird in dieser Inbox nicht unterstützt.**
|
⚠️ **Dieser Nachrichtentyp wird in dieser Inbox nicht unterstützt.**
|
||||||
📱 Bitte öffne **WhatsApp**, um ihn anzusehen.
|
📱 Bitte öffne **WhatsApp**, um ihn anzusehen.
|
||||||
|
|||||||
@@ -268,6 +268,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX Copy and Paste sent**
|
💳 **PIX Copy and Paste sent**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **Reply to WhatsApp Status**
|
||||||
|
🏷️ **Type:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **Status Text:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.has.media.no.media: |-
|
whatsapp.to.chatwoot.message.has.media.no.media: |-
|
||||||
{{#content}}
|
{{#content}}
|
||||||
{{{content}}}
|
{{{content}}}
|
||||||
@@ -705,6 +714,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 Check out what's new in the [**🆕 Changelog**]({{{changelogUrl}}})
|
👉 Check out what's new in the [**🆕 Changelog**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_To disable these notifications, send_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **Support WAHA!**
|
❤️ **Support WAHA!**
|
||||||
|
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 Mira qué hay de nuevo en el [**🆕 Registro de Cambios**]({{{changelogUrl}}})
|
👉 Mira qué hay de nuevo en el [**🆕 Registro de Cambios**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_Para desactivar estas notificaciones, envía_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **¡Apoya a WAHA!**
|
❤️ **¡Apoya a WAHA!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX Copiar y Pegar enviado**
|
💳 **PIX Copiar y Pegar enviado**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **Respuesta al estado de WhatsApp**
|
||||||
|
🏷️ **Tipo:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **Texto del Estado:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **Este tipo de mensaje no es compatible con esta bandeja de entrada.**
|
⚠️ **Este tipo de mensaje no es compatible con esta bandeja de entrada.**
|
||||||
📱 Por favor, abre **WhatsApp** para verlo.
|
📱 Por favor, abre **WhatsApp** para verlo.
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 موارد جدید را در [**🆕 تغییرات**]({{{changelogUrl}}}) مشاهده کنید
|
👉 موارد جدید را در [**🆕 تغییرات**]({{{changelogUrl}}}) مشاهده کنید
|
||||||
|
|
||||||
|
_برای غیرفعال کردن این اعلانها، ارسال کنید_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **از WAHA پشتیبانی کنید!**
|
❤️ **از WAHA پشتیبانی کنید!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX کپی و چسباندن ارسال شد**
|
💳 **PIX کپی و چسباندن ارسال شد**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **پاسخ به وضعیت واتساپ**
|
||||||
|
🏷️ **نوع:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **متن وضعیت:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **این نوع پیام در این صندوق ورودی پشتیبانی نمیشود.**
|
⚠️ **این نوع پیام در این صندوق ورودی پشتیبانی نمیشود.**
|
||||||
📱 لطفاً برای مشاهده آن **WhatsApp** را باز کنید.
|
📱 لطفاً برای مشاهده آن **WhatsApp** را باز کنید.
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 Découvrez les nouveautés dans le [**🆕 Journal des modifications**]({{{changelogUrl}}})
|
👉 Découvrez les nouveautés dans le [**🆕 Journal des modifications**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_Pour désactiver ces notifications, envoyez_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **Soutenez WAHA !**
|
❤️ **Soutenez WAHA !**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX Copier et Coller envoyé**
|
💳 **PIX Copier et Coller envoyé**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **Réponse au statut WhatsApp**
|
||||||
|
🏷️ **Type :** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **Texte du Statut :** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **Ce type de message n’est pas pris en charge dans cette boîte de réception.**
|
⚠️ **Ce type de message n’est pas pris en charge dans cette boîte de réception.**
|
||||||
📱 Veuillez ouvrir **WhatsApp** pour l’afficher.
|
📱 Veuillez ouvrir **WhatsApp** pour l’afficher.
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 בדוק מה חדש ב[**🆕 יומן שינויים**]({{{changelogUrl}}})
|
👉 בדוק מה חדש ב[**🆕 יומן שינויים**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_כדי לבטל התראות אלו, שלח_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **תמוך ב-WAHA!**
|
❤️ **תמוך ב-WAHA!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX העתק והדבק נשלח**
|
💳 **PIX העתק והדבק נשלח**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **תגובה לסטטוס ב-WhatsApp**
|
||||||
|
🏷️ **סוג:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **טקסט הסטטוס:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **סוג הודעה זה אינו נתמך בתיבה זו.**
|
⚠️ **סוג הודעה זה אינו נתמך בתיבה זו.**
|
||||||
📱 אנא פתח/י את **WhatsApp** כדי לצפות בזה.
|
📱 אנא פתח/י את **WhatsApp** כדי לצפות בזה.
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 [**🆕 चेंजलॉग**]({{{changelogUrl}}}) में क्या नया है देखें
|
👉 [**🆕 चेंजलॉग**]({{{changelogUrl}}}) में क्या नया है देखें
|
||||||
|
|
||||||
|
_इन सूचनाओं को अक्षम करने के लिए, भेजें_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **WAHA का समर्थन करें!**
|
❤️ **WAHA का समर्थन करें!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX कॉपी और पेस्ट भेजा गया**
|
💳 **PIX कॉपी और पेस्ट भेजा गया**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **WhatsApp स्टेटस का जवाब**
|
||||||
|
🏷️ **प्रकार:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **स्टेटस टेक्स्ट:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **इस प्रकार का संदेश इस इनबॉक्स में समर्थित नहीं है।**
|
⚠️ **इस प्रकार का संदेश इस इनबॉक्स में समर्थित नहीं है।**
|
||||||
📱 इसे देखने के लिए कृपया **WhatsApp** खोलें।
|
📱 इसे देखने के लिए कृपया **WhatsApp** खोलें।
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 Lihat apa yang baru di [**🆕 Changelog**]({{{changelogUrl}}})
|
👉 Lihat apa yang baru di [**🆕 Changelog**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_Untuk menonaktifkan notifikasi ini, kirim_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **Dukung WAHA!**
|
❤️ **Dukung WAHA!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX Salin dan Tempel terkirim**
|
💳 **PIX Salin dan Tempel terkirim**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **Balasan ke Status WhatsApp**
|
||||||
|
🏷️ **Tipe:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **Teks Status:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **Jenis pesan ini tidak didukung di kotak masuk ini.**
|
⚠️ **Jenis pesan ini tidak didukung di kotak masuk ini.**
|
||||||
📱 Silakan buka **WhatsApp** untuk melihatnya.
|
📱 Silakan buka **WhatsApp** untuk melihatnya.
|
||||||
|
|||||||
@@ -268,6 +268,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX Copia e Incolla inviato**
|
💳 **PIX Copia e Incolla inviato**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **Risposta allo stato di WhatsApp**
|
||||||
|
🏷️ **Tipo:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **Testo dello Stato:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.has.media.no.media: |-
|
whatsapp.to.chatwoot.message.has.media.no.media: |-
|
||||||
{{#content}}
|
{{#content}}
|
||||||
{{{content}}}
|
{{{content}}}
|
||||||
@@ -705,6 +714,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 Scopri le novità nel [**🆕 Changelog**]({{{changelogUrl}}})
|
👉 Scopri le novità nel [**🆕 Changelog**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_Per disattivare queste notifiche, invia_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **Supporta WAHA!**
|
❤️ **Supporta WAHA!**
|
||||||
|
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 [**🆕 ਬਦਲਾਅ ਲੌਗ**]({{{changelogUrl}}}) ਵਿੱਚ ਨਵਾਂ ਕੀ ਹੈ ਦੇਖੋ
|
👉 [**🆕 ਬਦਲਾਅ ਲੌਗ**]({{{changelogUrl}}}) ਵਿੱਚ ਨਵਾਂ ਕੀ ਹੈ ਦੇਖੋ
|
||||||
|
|
||||||
|
_ਇਹ ਸੂਚਨਾਵਾਂ ਬੰਦ ਕਰਨ ਲਈ, ਭੇਜੋ_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **WAHA ਦਾ ਸਮਰਥਨ ਕਰੋ!**
|
❤️ **WAHA ਦਾ ਸਮਰਥਨ ਕਰੋ!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX ਕਾਪੀ ਅਤੇ ਪੇਸਟ ਭੇਜਿਆ ਗਿਆ**
|
💳 **PIX ਕਾਪੀ ਅਤੇ ਪੇਸਟ ਭੇਜਿਆ ਗਿਆ**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **WhatsApp ਸਟੇਟਸ ਦਾ ਜਵਾਬ**
|
||||||
|
🏷️ **ਕਿਸਮ:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **ਸਟੇਟਸ ਲਿਖਤ:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **ਇਹ ਸੁਨੇਹੇ ਦੀ ਕਿਸਮ ਇਸ ਇਨਬਾਕਸ ਵਿੱਚ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ।**
|
⚠️ **ਇਹ ਸੁਨੇਹੇ ਦੀ ਕਿਸਮ ਇਸ ਇਨਬਾਕਸ ਵਿੱਚ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ।**
|
||||||
📱 ਇਸਨੂੰ ਦੇਖਣ ਲਈ ਕਿਰਪਾ ਕਰਕੇ **WhatsApp** ਖੋਲ੍ਹੋ।
|
📱 ਇਸਨੂੰ ਦੇਖਣ ਲਈ ਕਿਰਪਾ ਕਰਕੇ **WhatsApp** ਖੋਲ੍ਹੋ।
|
||||||
|
|||||||
@@ -326,6 +326,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 Confira o que há de novo no [**🆕 Registro de Alterações**]({{{changelogUrl}}})
|
👉 Confira o que há de novo no [**🆕 Registro de Alterações**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_Para desativar essas notificações, envie_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **Apoie o WAHA!**
|
❤️ **Apoie o WAHA!**
|
||||||
|
|
||||||
@@ -492,6 +494,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX Copia e Cola enviado**
|
💳 **PIX Copia e Cola enviado**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **Resposta a Status do WhatsApp**
|
||||||
|
🏷️ **Tipo:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **Texto do Status:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **Esse tipo de mensagem não é suportado nesta caixa de entrada.**
|
⚠️ **Esse tipo de mensagem não é suportado nesta caixa de entrada.**
|
||||||
📱 Por favor, abra o **WhatsApp** para visualizá-la.
|
📱 Por favor, abra o **WhatsApp** para visualizá-la.
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 Узнайте, что нового в [**🆕 Списке изменений**]({{{changelogUrl}}})
|
👉 Узнайте, что нового в [**🆕 Списке изменений**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_Чтобы отключить эти уведомления, отправьте_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **Поддержите WAHA!**
|
❤️ **Поддержите WAHA!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX Копировать и вставить отправлен**
|
💳 **PIX Копировать и вставить отправлен**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **Ответ на статус WhatsApp**
|
||||||
|
🏷️ **Тип:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **Текст Статуса:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **Этот тип сообщения не поддерживается в этом Inbox.**
|
⚠️ **Этот тип сообщения не поддерживается в этом Inbox.**
|
||||||
📱 Пожалуйста, откройте **WhatsApp**, чтобы просмотреть.
|
📱 Пожалуйста, откройте **WhatsApp**, чтобы просмотреть.
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 [**🆕 Değişiklik Günlüğü**]({{{changelogUrl}}}) içinde yenilikleri kontrol edin
|
👉 [**🆕 Değişiklik Günlüğü**]({{{changelogUrl}}}) içinde yenilikleri kontrol edin
|
||||||
|
|
||||||
|
_Bu bildirimleri devre dışı bırakmak için gönderin_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **WAHA'yı Destekleyin!**
|
❤️ **WAHA'yı Destekleyin!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX Kopyala ve Yapıştır gönderildi**
|
💳 **PIX Kopyala ve Yapıştır gönderildi**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **WhatsApp Durumuna Yanıt**
|
||||||
|
🏷️ **Tür:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **Durum Metni:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **Bu mesaj türü bu gelen kutusunda desteklenmiyor.**
|
⚠️ **Bu mesaj türü bu gelen kutusunda desteklenmiyor.**
|
||||||
📱 Görüntülemek için lütfen **WhatsApp’ı** açın.
|
📱 Görüntülemek için lütfen **WhatsApp’ı** açın.
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 Перевірте, що нового у [**🆕 Журналі змін**]({{{changelogUrl}}})
|
👉 Перевірте, що нового у [**🆕 Журналі змін**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_Щоб вимкнути ці сповіщення, надішліть_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **Підтримайте WAHA!**
|
❤️ **Підтримайте WAHA!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX Копіювати та Вставити надіслано**
|
💳 **PIX Копіювати та Вставити надіслано**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **Відповідь на статус WhatsApp**
|
||||||
|
🏷️ **Тип:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **Текст Статусу:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **Цей тип повідомлення не підтримується в цих вхідних.**
|
⚠️ **Цей тип повідомлення не підтримується в цих вхідних.**
|
||||||
📱 Будь ласка, відкрийте **WhatsApp**, щоб переглянути.
|
📱 Будь ласка, відкрийте **WhatsApp**, щоб переглянути.
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 [**🆕 تبدیلیوں کی فہرست**]({{{changelogUrl}}}) میں نیا کیا ہے دیکھیں
|
👉 [**🆕 تبدیلیوں کی فہرست**]({{{changelogUrl}}}) میں نیا کیا ہے دیکھیں
|
||||||
|
|
||||||
|
_ان اطلاعات کو بند کرنے کے لیے بھیجیں_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **WAHA کی حمایت کریں!**
|
❤️ **WAHA کی حمایت کریں!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX کاپی اور پیسٹ بھیج دیا گیا**
|
💳 **PIX کاپی اور پیسٹ بھیج دیا گیا**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **WhatsApp اسٹیٹس کا جواب**
|
||||||
|
🏷️ **قسم:** {{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **اسٹیٹس کا متن:** {{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **اس قسم کا پیغام اس انبکس میں معاونت یافتہ نہیں ہے۔**
|
⚠️ **اس قسم کا پیغام اس انبکس میں معاونت یافتہ نہیں ہے۔**
|
||||||
📱 اسے دیکھنے کے لیے براہ کرم **WhatsApp** کھولیں۔
|
📱 اسے دیکھنے کے لیے براہ کرم **WhatsApp** کھولیں۔
|
||||||
|
|||||||
@@ -325,6 +325,8 @@ waha.new.version.available: |-
|
|||||||
|
|
||||||
👉 查看[**🆕 更新日志**]({{{changelogUrl}}})
|
👉 查看[**🆕 更新日志**]({{{changelogUrl}}})
|
||||||
|
|
||||||
|
_要禁用这些通知,请发送_ `queue stop check.version`
|
||||||
|
|
||||||
waha.core.version.used: |-
|
waha.core.version.used: |-
|
||||||
❤️ **支持 WAHA!**
|
❤️ **支持 WAHA!**
|
||||||
|
|
||||||
@@ -487,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |-
|
|||||||
💳 **PIX 复制粘贴已发送**
|
💳 **PIX 复制粘贴已发送**
|
||||||
{{/pixData}}
|
{{/pixData}}
|
||||||
|
|
||||||
|
whatsapp.to.chatwoot.message.status.reply: |-
|
||||||
|
📣 **回复 WhatsApp 状态**
|
||||||
|
🏷️ **类型:**{{type}}
|
||||||
|
{{#quotedText}}
|
||||||
|
📝 **状态文本:**{{quotedText}}
|
||||||
|
{{/quotedText}}
|
||||||
|
|
||||||
|
{{content}}
|
||||||
|
|
||||||
whatsapp.to.chatwoot.message.unsupported: |-
|
whatsapp.to.chatwoot.message.unsupported: |-
|
||||||
⚠️ **此收件箱不支持此消息类型。**
|
⚠️ **此收件箱不支持此消息类型。**
|
||||||
📱 请打开 **WhatsApp** 查看。
|
📱 请打开 **WhatsApp** 查看。
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export enum TKey {
|
|||||||
WA_TO_CW_MESSAGE_POLL = 'whatsapp.to.chatwoot.message.poll',
|
WA_TO_CW_MESSAGE_POLL = 'whatsapp.to.chatwoot.message.poll',
|
||||||
WA_TO_CW_MESSAGE_EVENT = 'whatsapp.to.chatwoot.message.event',
|
WA_TO_CW_MESSAGE_EVENT = 'whatsapp.to.chatwoot.message.event',
|
||||||
WA_TO_CW_MESSAGE_PIX = 'whatsapp.to.chatwoot.message.pix',
|
WA_TO_CW_MESSAGE_PIX = 'whatsapp.to.chatwoot.message.pix',
|
||||||
|
WA_TO_CW_MESSAGE_STATUS_REPLY = 'whatsapp.to.chatwoot.message.status.reply',
|
||||||
WA_TO_CW_MESSAGE_UNSUPPORTED = 'whatsapp.to.chatwoot.message.unsupported',
|
WA_TO_CW_MESSAGE_UNSUPPORTED = 'whatsapp.to.chatwoot.message.unsupported',
|
||||||
WA_TO_CW_MESSAGE_FACEBOOK_AD = 'whatsapp.to.chatwoot.message.facebook.ad',
|
WA_TO_CW_MESSAGE_FACEBOOK_AD = 'whatsapp.to.chatwoot.message.facebook.ad',
|
||||||
WA_TO_CW_MESSAGE_LIST = 'whatsapp.to.chatwoot.message.list',
|
WA_TO_CW_MESSAGE_LIST = 'whatsapp.to.chatwoot.message.list',
|
||||||
@@ -189,6 +190,11 @@ export type TemplatePayloads = {
|
|||||||
message: proto.Message | null;
|
message: proto.Message | null;
|
||||||
pixData: PixTemplatePayload;
|
pixData: PixTemplatePayload;
|
||||||
};
|
};
|
||||||
|
[TKey.WA_TO_CW_MESSAGE_STATUS_REPLY]: {
|
||||||
|
type: string;
|
||||||
|
quotedText?: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
[TKey.WA_TO_CW_MESSAGE_UNSUPPORTED]: { details: Link };
|
[TKey.WA_TO_CW_MESSAGE_UNSUPPORTED]: { details: Link };
|
||||||
[TKey.WA_TO_CW_MESSAGE_FACEBOOK_AD]: {
|
[TKey.WA_TO_CW_MESSAGE_FACEBOOK_AD]: {
|
||||||
payload: WAMessage;
|
payload: WAMessage;
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import type { proto } from '@adiwajshing/baileys';
|
||||||
|
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||||
|
import { WAHASelf, WAHASessionAPI } from '@waha/apps/app_sdk/waha/WAHASelf';
|
||||||
|
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
|
||||||
|
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
|
||||||
|
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
|
||||||
|
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
|
||||||
|
import { EngineHelper, QuotedMedia } from '@waha/apps/chatwoot/waha';
|
||||||
|
import { Jid } from '@waha/core/engines/const';
|
||||||
|
import { WAMessage } from '@waha/structures/responses.dto';
|
||||||
|
import { Job } from 'bullmq';
|
||||||
|
import { TextMessage } from './TextMessage';
|
||||||
|
import { isEmptyString } from './utils/text';
|
||||||
|
import { SendAttachment } from '@waha/apps/chatwoot/client/types';
|
||||||
|
import esm from '@waha/vendor/esm';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const mime = require('mime-types');
|
||||||
|
|
||||||
|
type StatusReplyType = 'text' | 'image' | 'audio' | 'video' | 'unknown';
|
||||||
|
|
||||||
|
interface StatusQuotedMessageInfo {
|
||||||
|
data?: any;
|
||||||
|
id: string;
|
||||||
|
type: StatusReplyType;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StatusReplyMessage extends TextMessage {
|
||||||
|
constructor(
|
||||||
|
locale: Locale,
|
||||||
|
logger: ILogger,
|
||||||
|
waha: WAHASelf,
|
||||||
|
job: Job,
|
||||||
|
private readonly session: WAHASessionAPI,
|
||||||
|
) {
|
||||||
|
super(locale, logger, waha, job);
|
||||||
|
}
|
||||||
|
|
||||||
|
async convert(
|
||||||
|
payload: WAMessage,
|
||||||
|
protoMessage: proto.Message | null,
|
||||||
|
): Promise<ChatWootMessagePartial | null> {
|
||||||
|
if (!payload.replyTo?.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!EngineHelper.IsReplyToStatus(payload, protoMessage)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachments: SendAttachment[] = [];
|
||||||
|
const quoted = this.getStatusQuotedMessageInfo(payload);
|
||||||
|
let wamessage = null; // aka "quoted from store"
|
||||||
|
|
||||||
|
// Media - Use provided replyTo.media if present
|
||||||
|
if (attachments.length === 0 && payload.replyTo.media) {
|
||||||
|
const extension = mime.extension(payload.replyTo.media.mimetype);
|
||||||
|
payload.replyTo.media.filename = `status-message.${extension}`;
|
||||||
|
const quotedAttachments = await super.getAttachments(payload.replyTo);
|
||||||
|
attachments = [...attachments, ...quotedAttachments];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media - fetch message by id from store and use it's "media"
|
||||||
|
if (attachments.length == 0) {
|
||||||
|
wamessage = await this.fetchStatusMessage(quoted.id);
|
||||||
|
if (wamessage?.media?.url) {
|
||||||
|
const extension = mime.extension(wamessage.media.mimetype);
|
||||||
|
wamessage.media.filename = `status-message.${extension}`;
|
||||||
|
const quotedAttachments = await super.getAttachments(wamessage);
|
||||||
|
attachments = [...attachments, ...quotedAttachments];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media - get media url, key and try to decrypt it
|
||||||
|
if (attachments.length == 0) {
|
||||||
|
const replyData = payload.replyTo._data;
|
||||||
|
const quotedMedia = EngineHelper.ExtractQuotedMedia(replyData);
|
||||||
|
if (quotedMedia) {
|
||||||
|
const attachment =
|
||||||
|
await this.downloadQuotedMediaAttachment(quotedMedia);
|
||||||
|
if (attachment) {
|
||||||
|
attachments.push(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act as regular text message, but we'll adjust some properties
|
||||||
|
const msg = await super.convert(payload, protoMessage);
|
||||||
|
// First goes Quoted attachments, then what sender sent
|
||||||
|
msg.attachments = [...attachments, ...msg.attachments];
|
||||||
|
|
||||||
|
// Wrap content with additional info
|
||||||
|
const quotedText = quoted.text ?? wamessage?.body ?? payload.replyTo?.body;
|
||||||
|
if (
|
||||||
|
isEmptyString(quotedText) &&
|
||||||
|
isEmptyString(msg.content) &&
|
||||||
|
attachments.length === 0
|
||||||
|
) {
|
||||||
|
// Nothing to send.
|
||||||
|
// Must check msg.content before wrapping it in status reply
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.content = this.locale.r(TKey.WA_TO_CW_MESSAGE_STATUS_REPLY, {
|
||||||
|
type: quoted.type,
|
||||||
|
quotedText: WhatsappToMarkdown(quotedText),
|
||||||
|
content: msg.content,
|
||||||
|
});
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStatusQuotedMessageInfo(
|
||||||
|
payload: WAMessage,
|
||||||
|
): StatusQuotedMessageInfo {
|
||||||
|
const replyData = payload.replyTo._data;
|
||||||
|
const imageMessage =
|
||||||
|
replyData?.imageMessage || this.getWebjsMediaData(replyData, 'image');
|
||||||
|
if (imageMessage) {
|
||||||
|
return {
|
||||||
|
data: replyData,
|
||||||
|
id: payload.replyTo.id,
|
||||||
|
type: 'image',
|
||||||
|
text: imageMessage.caption ?? payload.replyTo.body ?? replyData?.body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoMessage =
|
||||||
|
replyData?.videoMessage || this.getWebjsMediaData(replyData, 'video');
|
||||||
|
if (videoMessage) {
|
||||||
|
return {
|
||||||
|
data: replyData,
|
||||||
|
id: payload.replyTo.id,
|
||||||
|
type: 'video',
|
||||||
|
text: videoMessage.caption ?? payload.replyTo.body ?? replyData?.body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioMessage =
|
||||||
|
replyData?.audioMessage || this.getWebjsMediaData(replyData, 'audio');
|
||||||
|
if (audioMessage) {
|
||||||
|
return {
|
||||||
|
data: replyData,
|
||||||
|
id: payload.replyTo.id,
|
||||||
|
type: 'audio',
|
||||||
|
text: audioMessage.caption ?? payload.replyTo.body ?? replyData?.body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotedText =
|
||||||
|
replyData?.extendedTextMessage?.text ??
|
||||||
|
replyData?.conversation ??
|
||||||
|
payload.replyTo.body ??
|
||||||
|
replyData?.body;
|
||||||
|
return {
|
||||||
|
data: replyData,
|
||||||
|
id: payload.replyTo.id,
|
||||||
|
type: quotedText ? 'text' : 'unknown',
|
||||||
|
text: quotedText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWebjsMediaData(
|
||||||
|
replyData: any,
|
||||||
|
expectedKind: 'image' | 'video' | 'audio',
|
||||||
|
): any | null {
|
||||||
|
if (!replyData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const kind = replyData.kind ?? replyData.type;
|
||||||
|
if (kind !== expectedKind) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return replyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async downloadQuotedMediaAttachment(
|
||||||
|
quotedMedia: QuotedMedia,
|
||||||
|
): Promise<SendAttachment | null> {
|
||||||
|
try {
|
||||||
|
const decrypt = !!quotedMedia.mediaKey;
|
||||||
|
const mediaKeyBuffer = decrypt
|
||||||
|
? Buffer.from(quotedMedia.mediaKey, 'base64')
|
||||||
|
: undefined;
|
||||||
|
const stream = await esm.b.downloadContentFromMessage(
|
||||||
|
{
|
||||||
|
mediaKey: mediaKeyBuffer,
|
||||||
|
directPath: quotedMedia.directPath,
|
||||||
|
url: quotedMedia.url,
|
||||||
|
},
|
||||||
|
quotedMedia.mediaType as any,
|
||||||
|
{},
|
||||||
|
decrypt,
|
||||||
|
);
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
const extension = mime.extension(quotedMedia.mimetype);
|
||||||
|
const filename = `status-message.${extension}`;
|
||||||
|
return {
|
||||||
|
content: buffer.toString('base64'),
|
||||||
|
filename: filename,
|
||||||
|
encoding: 'base64',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn('Failed to download quoted status media directly');
|
||||||
|
this.logger.debug(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchStatusMessage(
|
||||||
|
replyMessageId: string,
|
||||||
|
): Promise<WAMessage | null> {
|
||||||
|
try {
|
||||||
|
const quotedMessage = await this.session.getMessageById(
|
||||||
|
Jid.BROADCAST,
|
||||||
|
replyMessageId,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return quotedMessage || null;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to fetch original status message '${replyMessageId}'`,
|
||||||
|
);
|
||||||
|
this.logger.debug(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,16 +11,17 @@ import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chat
|
|||||||
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
|
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
|
||||||
import { JobLink } from '@waha/apps/app_sdk/JobUtils';
|
import { JobLink } from '@waha/apps/app_sdk/JobUtils';
|
||||||
import { Job } from 'bullmq';
|
import { Job } from 'bullmq';
|
||||||
|
import { WAMedia } from '@waha/structures/media.dto';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const mime = require('mime-types');
|
const mime = require('mime-types');
|
||||||
|
|
||||||
export class TextMessage implements MessageToChatWootConverter {
|
export class TextMessage implements MessageToChatWootConverter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly locale: Locale,
|
protected readonly locale: Locale,
|
||||||
private readonly logger: ILogger,
|
protected readonly logger: ILogger,
|
||||||
private readonly waha: WAHASelf,
|
protected readonly waha: WAHASelf,
|
||||||
private readonly job: Job,
|
protected readonly job: Job,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async convert(
|
async convert(
|
||||||
@@ -52,12 +53,14 @@ export class TextMessage implements MessageToChatWootConverter {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
content: WhatsappToMarkdown(content),
|
content: WhatsappToMarkdown(content),
|
||||||
attachments,
|
attachments: attachments,
|
||||||
private: undefined,
|
private: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAttachments(payload: WAMessage): Promise<SendAttachment[]> {
|
protected async getAttachments(payload: {
|
||||||
|
media?: WAMedia;
|
||||||
|
}): Promise<SendAttachment[]> {
|
||||||
const hasMedia = payload.media?.url;
|
const hasMedia = payload.media?.url;
|
||||||
if (!hasMedia) {
|
if (!hasMedia) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export { EventMessage } from './EventMessage';
|
|||||||
export { PixMessage } from './PixMessage';
|
export { PixMessage } from './PixMessage';
|
||||||
export { ListMessage } from './ListMessage';
|
export { ListMessage } from './ListMessage';
|
||||||
export { AlbumMessage } from './AlbumMessage';
|
export { AlbumMessage } from './AlbumMessage';
|
||||||
|
export { StatusReplyMessage } from './StatusReplyMessage';
|
||||||
|
|
||||||
export type Awaitable<T> = T | Promise<T>;
|
export type Awaitable<T> = T | Promise<T>;
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ describe('MarkdownToWhatsApp', () => {
|
|||||||
expect(MarkdownToWhatsApp(input)).toBe(expected);
|
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
|
// Tests for URL protection
|
||||||
it('preserves URLs with multiple underscores', () => {
|
it('preserves URLs with multiple underscores', () => {
|
||||||
const input = 'https://example.com/page__name__test';
|
const input = 'https://example.com/page__name__test';
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export function MarkdownToWhatsApp(text: string): string {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
text
|
text
|
||||||
|
// Remove trailing backslash before newlines (ChatWoot v4.10.1+ escaping)
|
||||||
|
.replace(/\\\n/g, '\n')
|
||||||
// Triple-backtick blocks → WhatsApp monospace blocks (same syntax)
|
// Triple-backtick blocks → WhatsApp monospace blocks (same syntax)
|
||||||
.replace(/```([\s\S]*?)```/g, '```$1```')
|
.replace(/```([\s\S]*?)```/g, '```$1```')
|
||||||
// Italic **first**, but only single‐star or single‐underscore
|
// Italic **first**, but only single‐star or single‐underscore
|
||||||
|
|||||||
@@ -54,6 +54,21 @@ export class ChatWootScheduleService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
// Check the tier
|
||||||
|
const checkTierQueue = this.queueRegistry.queue(
|
||||||
|
QueueName.SCHEDULED_CHECK_TIER,
|
||||||
|
);
|
||||||
|
await checkTierQueue.upsertJobScheduler(
|
||||||
|
this.JobId(QueueName.SCHEDULED_CHECK_TIER, appId),
|
||||||
|
// Every Monday (1) at 14:00
|
||||||
|
{ pattern: '0 0 14 * * 1' },
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
app: appId,
|
||||||
|
session: sessionName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async unschedule(appId: string, sessionName: string): Promise<void> {
|
async unschedule(appId: string, sessionName: string): Promise<void> {
|
||||||
@@ -71,6 +86,13 @@ export class ChatWootScheduleService {
|
|||||||
await checkVersionQueue.removeJobScheduler(
|
await checkVersionQueue.removeJobScheduler(
|
||||||
this.JobId(QueueName.SCHEDULED_CHECK_VERSION, appId),
|
this.JobId(QueueName.SCHEDULED_CHECK_VERSION, appId),
|
||||||
);
|
);
|
||||||
|
// Check the tier
|
||||||
|
const checkTierQueue = this.queueRegistry.queue(
|
||||||
|
QueueName.SCHEDULED_CHECK_TIER,
|
||||||
|
);
|
||||||
|
await checkTierQueue.removeJobScheduler(
|
||||||
|
this.JobId(QueueName.SCHEDULED_CHECK_TIER, appId),
|
||||||
|
);
|
||||||
|
|
||||||
// contacts
|
// contacts
|
||||||
const contactsPullQueue = this.queueRegistry.queue(
|
const contactsPullQueue = this.queueRegistry.queue(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { QueueName } from '../consumers/QueueName';
|
import { QueueName } from '../consumers/QueueName';
|
||||||
import { QueueRegistry } from '@waha/apps/chatwoot/services/QueueRegistry';
|
import { QueueRegistry } from '@waha/apps/chatwoot/services/QueueRegistry';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { QueueNameRepr } from '@waha/apps/app_sdk/JobUtils';
|
||||||
|
|
||||||
const Managable = true;
|
const Managable = true;
|
||||||
const Locked = false;
|
const Locked = false;
|
||||||
@@ -18,7 +19,8 @@ export class QueueManager {
|
|||||||
constructor(private readonly registry: QueueRegistry) {
|
constructor(private readonly registry: QueueRegistry) {
|
||||||
this.queues = {
|
this.queues = {
|
||||||
[QueueName.SCHEDULED_MESSAGE_CLEANUP]: Locked,
|
[QueueName.SCHEDULED_MESSAGE_CLEANUP]: Locked,
|
||||||
[QueueName.SCHEDULED_CHECK_VERSION]: Locked,
|
[QueueName.SCHEDULED_CHECK_VERSION]: Managable,
|
||||||
|
[QueueName.SCHEDULED_CHECK_TIER]: Locked,
|
||||||
[QueueName.TASK_CONTACTS_PULL]: Locked,
|
[QueueName.TASK_CONTACTS_PULL]: Locked,
|
||||||
[QueueName.TASK_MESSAGES_PULL]: Locked,
|
[QueueName.TASK_MESSAGES_PULL]: Locked,
|
||||||
[QueueName.WAHA_SESSION_STATUS]: Locked,
|
[QueueName.WAHA_SESSION_STATUS]: Locked,
|
||||||
@@ -75,8 +77,19 @@ export class QueueManager {
|
|||||||
case undefined:
|
case undefined:
|
||||||
return queues;
|
return queues;
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
|
const matches = queues.filter((q) => q.split(' | ')[1] === shortcut);
|
||||||
|
if (matches.length === 1) {
|
||||||
|
return [matches[0]];
|
||||||
|
}
|
||||||
|
if (matches.length > 1) {
|
||||||
|
const names = matches.map(QueueNameRepr).join(', ');
|
||||||
|
throw new Error(
|
||||||
|
`Ambiguous queue name "${shortcut}", matches: ${names}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
return [shortcut as QueueName];
|
return [shortcut as QueueName];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export class QueueRegistry {
|
|||||||
private readonly scheduledMessageCleanupQueue: Queue,
|
private readonly scheduledMessageCleanupQueue: Queue,
|
||||||
@InjectQueue(QueueName.SCHEDULED_CHECK_VERSION)
|
@InjectQueue(QueueName.SCHEDULED_CHECK_VERSION)
|
||||||
private readonly scheduledCheckVersionQueue: Queue,
|
private readonly scheduledCheckVersionQueue: Queue,
|
||||||
|
@InjectQueue(QueueName.SCHEDULED_CHECK_TIER)
|
||||||
|
private readonly scheduledCheckTierQueue: Queue,
|
||||||
@InjectQueue(QueueName.TASK_CONTACTS_PULL)
|
@InjectQueue(QueueName.TASK_CONTACTS_PULL)
|
||||||
private readonly taskContactsPullQueue: Queue,
|
private readonly taskContactsPullQueue: Queue,
|
||||||
@InjectQueue(QueueName.TASK_MESSAGES_PULL)
|
@InjectQueue(QueueName.TASK_MESSAGES_PULL)
|
||||||
@@ -55,6 +57,7 @@ export class QueueRegistry {
|
|||||||
this.queues = {
|
this.queues = {
|
||||||
[QueueName.SCHEDULED_MESSAGE_CLEANUP]: this.scheduledMessageCleanupQueue,
|
[QueueName.SCHEDULED_MESSAGE_CLEANUP]: this.scheduledMessageCleanupQueue,
|
||||||
[QueueName.SCHEDULED_CHECK_VERSION]: this.scheduledCheckVersionQueue,
|
[QueueName.SCHEDULED_CHECK_VERSION]: this.scheduledCheckVersionQueue,
|
||||||
|
[QueueName.SCHEDULED_CHECK_TIER]: this.scheduledCheckTierQueue,
|
||||||
[QueueName.TASK_CONTACTS_PULL]: this.taskContactsPullQueue,
|
[QueueName.TASK_CONTACTS_PULL]: this.taskContactsPullQueue,
|
||||||
[QueueName.TASK_MESSAGES_PULL]: this.taskMessagesPullQueue,
|
[QueueName.TASK_MESSAGES_PULL]: this.taskMessagesPullQueue,
|
||||||
[QueueName.WAHA_SESSION_STATUS]: this.wahaSessionStatusQueue,
|
[QueueName.WAHA_SESSION_STATUS]: this.wahaSessionStatusQueue,
|
||||||
|
|||||||
@@ -1,17 +1,34 @@
|
|||||||
import * as lodash from 'lodash';
|
import * as lodash from 'lodash';
|
||||||
|
import type { proto } from '@adiwajshing/baileys';
|
||||||
import { WhatsAppMessage } from '@waha/apps/chatwoot/storage';
|
import { WhatsAppMessage } from '@waha/apps/chatwoot/storage';
|
||||||
import { WAHAEngine } from '@waha/structures/enums.dto';
|
import { WAHAEngine } from '@waha/structures/enums.dto';
|
||||||
import { getEngineName } from '@waha/version';
|
import { getEngineName } from '@waha/version';
|
||||||
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||||
|
import { Jid } from '@waha/core/engines/const';
|
||||||
import { isLidUser, isPnUser, toCusFormat } from '@waha/core/utils/jids';
|
import { isLidUser, isPnUser, toCusFormat } from '@waha/core/utils/jids';
|
||||||
|
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||||
import { WAMessage } from '@waha/structures/responses.dto';
|
import { WAMessage } from '@waha/structures/responses.dto';
|
||||||
import { CallData } from '@waha/structures/calls.dto';
|
import { CallData } from '@waha/structures/calls.dto';
|
||||||
|
import { getContextInfo } from '@waha/core/utils/pwa';
|
||||||
|
|
||||||
|
export interface QuotedMedia {
|
||||||
|
url?: string;
|
||||||
|
directPath: string;
|
||||||
|
mediaKey?: string;
|
||||||
|
mimetype: string;
|
||||||
|
mediaType: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IEngineHelper {
|
interface IEngineHelper {
|
||||||
ChatID(message: WAMessage | any): string;
|
ChatID(message: WAMessage | any): string;
|
||||||
|
|
||||||
CallChatID(call: CallData | any): string;
|
CallChatID(call: CallData | any): string;
|
||||||
|
|
||||||
|
IsReplyToStatus(
|
||||||
|
message: WAMessage,
|
||||||
|
protoMessage: proto.Message | null,
|
||||||
|
): boolean;
|
||||||
|
|
||||||
WhatsAppMessageKeys(message: any): WhatsAppMessage;
|
WhatsAppMessageKeys(message: any): WhatsAppMessage;
|
||||||
|
|
||||||
IterateMessages<T extends { timestamp: number }>(
|
IterateMessages<T extends { timestamp: number }>(
|
||||||
@@ -23,6 +40,8 @@ interface IEngineHelper {
|
|||||||
FilterChatIdsForMessages(chats: string[]): string[];
|
FilterChatIdsForMessages(chats: string[]): string[];
|
||||||
|
|
||||||
SupportsAllChatForMessage(): boolean;
|
SupportsAllChatForMessage(): boolean;
|
||||||
|
|
||||||
|
ExtractQuotedMedia(replyData: any): QuotedMedia | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NOWEBHelper implements IEngineHelper {
|
class NOWEBHelper implements IEngineHelper {
|
||||||
@@ -34,6 +53,14 @@ class NOWEBHelper implements IEngineHelper {
|
|||||||
return call.from;
|
return call.from;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IsReplyToStatus(
|
||||||
|
message: WAMessage,
|
||||||
|
protoMessage: proto.Message | null,
|
||||||
|
): boolean {
|
||||||
|
void message;
|
||||||
|
return getContextInfo(protoMessage)?.remoteJid === Jid.BROADCAST;
|
||||||
|
}
|
||||||
|
|
||||||
WhatsAppMessageKeys(message: any): WhatsAppMessage {
|
WhatsAppMessageKeys(message: any): WhatsAppMessage {
|
||||||
const timestamp = parseInt(message.messageTimestamp) * 1000;
|
const timestamp = parseInt(message.messageTimestamp) * 1000;
|
||||||
return {
|
return {
|
||||||
@@ -52,7 +79,7 @@ class NOWEBHelper implements IEngineHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FilterChatIdsForMessages(chats: string[]): string[] {
|
FilterChatIdsForMessages(chats: string[]): string[] {
|
||||||
return chats;
|
return preferPnChats(chats);
|
||||||
}
|
}
|
||||||
|
|
||||||
ContactIsMy(contact) {
|
ContactIsMy(contact) {
|
||||||
@@ -62,6 +89,10 @@ class NOWEBHelper implements IEngineHelper {
|
|||||||
SupportsAllChatForMessage(): boolean {
|
SupportsAllChatForMessage(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExtractQuotedMedia(replyData: any): QuotedMedia | null {
|
||||||
|
return extractProtoQuotedMedia(replyData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GOWSHelper implements IEngineHelper {
|
class GOWSHelper implements IEngineHelper {
|
||||||
@@ -73,6 +104,14 @@ class GOWSHelper implements IEngineHelper {
|
|||||||
return call._data?.CallCreator || call.from;
|
return call._data?.CallCreator || call.from;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IsReplyToStatus(
|
||||||
|
message: WAMessage,
|
||||||
|
protoMessage: proto.Message | null,
|
||||||
|
): boolean {
|
||||||
|
void message;
|
||||||
|
return getContextInfo(protoMessage)?.remoteJid === Jid.BROADCAST;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse API response and get the data
|
* Parse API response and get the data
|
||||||
* API Response depends on engine right now
|
* API Response depends on engine right now
|
||||||
@@ -96,7 +135,7 @@ class GOWSHelper implements IEngineHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FilterChatIdsForMessages(chats: string[]): string[] {
|
FilterChatIdsForMessages(chats: string[]): string[] {
|
||||||
return chats;
|
return preferPnChats(chats);
|
||||||
}
|
}
|
||||||
|
|
||||||
SupportsAllChatForMessage(): boolean {
|
SupportsAllChatForMessage(): boolean {
|
||||||
@@ -106,6 +145,10 @@ class GOWSHelper implements IEngineHelper {
|
|||||||
ContactIsMy(contact) {
|
ContactIsMy(contact) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExtractQuotedMedia(replyData: any): QuotedMedia | null {
|
||||||
|
return extractProtoQuotedMedia(replyData, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WEBJSHelper implements IEngineHelper {
|
class WEBJSHelper implements IEngineHelper {
|
||||||
@@ -117,6 +160,14 @@ class WEBJSHelper implements IEngineHelper {
|
|||||||
return call.from;
|
return call.from;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IsReplyToStatus(
|
||||||
|
message: WAMessage,
|
||||||
|
protoMessage: proto.Message | null,
|
||||||
|
): boolean {
|
||||||
|
void protoMessage;
|
||||||
|
return message._data?.quotedRemoteJid === Jid.BROADCAST;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse API response and get the data for WEBJS engine
|
* Parse API response and get the data for WEBJS engine
|
||||||
*/
|
*/
|
||||||
@@ -150,17 +201,7 @@ class WEBJSHelper implements IEngineHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FilterChatIdsForMessages(chats: string[]): string[] {
|
FilterChatIdsForMessages(chats: string[]): string[] {
|
||||||
if (chats.length == 2) {
|
return preferPnChats(chats);
|
||||||
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
|
|
||||||
}
|
|
||||||
// Otherwise - return the original
|
|
||||||
return chats;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SupportsAllChatForMessage(): boolean {
|
SupportsAllChatForMessage(): boolean {
|
||||||
@@ -170,6 +211,78 @@ class WEBJSHelper implements IEngineHelper {
|
|||||||
ContactIsMy(contact) {
|
ContactIsMy(contact) {
|
||||||
return contact.isMyContact;
|
return contact.isMyContact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExtractQuotedMedia(replyData: any): QuotedMedia | null {
|
||||||
|
return extractBrowserQuotedMedia(replyData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WPPHelper implements IEngineHelper {
|
||||||
|
ChatID(message: WAMessage): string {
|
||||||
|
return toCusFormat(parseMessageIdSerialized(message.id as any).remoteJid);
|
||||||
|
}
|
||||||
|
|
||||||
|
CallChatID(call: CallData): string {
|
||||||
|
return call.from;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsReplyToStatus(
|
||||||
|
message: WAMessage,
|
||||||
|
protoMessage: proto.Message | null,
|
||||||
|
): boolean {
|
||||||
|
void protoMessage;
|
||||||
|
return message._data?.quotedRemoteJid === Jid.BROADCAST;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = lodash.sortBy(buffer, (item) => item.timestamp);
|
||||||
|
|
||||||
|
for (const message of sorted) {
|
||||||
|
yield message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterChatIdsForMessages(chats: string[]): string[] {
|
||||||
|
return preferPnChats(chats);
|
||||||
|
}
|
||||||
|
|
||||||
|
SupportsAllChatForMessage(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContactIsMy(contact) {
|
||||||
|
return contact.isMyContact;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtractQuotedMedia(replyData: any): QuotedMedia | null {
|
||||||
|
return extractBrowserQuotedMedia(replyData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choose the right EngineHelper based on getEngineName() function
|
// Choose the right EngineHelper based on getEngineName() function
|
||||||
@@ -185,8 +298,82 @@ switch (getEngineName()) {
|
|||||||
case WAHAEngine.WEBJS:
|
case WAHAEngine.WEBJS:
|
||||||
engineHelper = new WEBJSHelper();
|
engineHelper = new WEBJSHelper();
|
||||||
break;
|
break;
|
||||||
|
case WAHAEngine.WPP:
|
||||||
|
engineHelper = new WPPHelper();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
engineHelper = new WEBJSHelper(); // Default to WEBJS as it's the default engine
|
engineHelper = new WEBJSHelper(); // Default to WEBJS as it's the default engine
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EngineHelper = engineHelper;
|
export const EngineHelper = engineHelper;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Quoted media extraction helpers
|
||||||
|
//
|
||||||
|
|
||||||
|
function extractProtoQuotedMedia(
|
||||||
|
replyData: any,
|
||||||
|
capitalUrl = false,
|
||||||
|
): QuotedMedia | null {
|
||||||
|
if (!replyData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const mediaTypes: Array<[string, string]> = [
|
||||||
|
['imageMessage', 'image'],
|
||||||
|
['videoMessage', 'video'],
|
||||||
|
['audioMessage', 'audio'],
|
||||||
|
];
|
||||||
|
for (const [key, mediaType] of mediaTypes) {
|
||||||
|
const msg = replyData[key];
|
||||||
|
if (!msg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const url: string | undefined = capitalUrl ? msg.URL ?? msg.url : msg.url;
|
||||||
|
const directPath: string = msg.directPath;
|
||||||
|
const mimetype: string = msg.mimetype;
|
||||||
|
if (!directPath || !mimetype) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
url: url,
|
||||||
|
directPath: directPath,
|
||||||
|
mediaKey: msg.mediaKey,
|
||||||
|
mimetype: mimetype,
|
||||||
|
mediaType: mediaType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractBrowserQuotedMedia(replyData: any): QuotedMedia | null {
|
||||||
|
if (!replyData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const kind: string = replyData.kind ?? replyData.type;
|
||||||
|
if (kind !== 'image' && kind !== 'video' && kind !== 'audio') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const directPath: string = replyData.directPath;
|
||||||
|
const mimetype: string = replyData.mimetype;
|
||||||
|
if (!directPath || !mimetype) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
url: replyData.deprecatedMms3Url,
|
||||||
|
directPath: directPath,
|
||||||
|
mediaKey: replyData.mediaKey,
|
||||||
|
mimetype: mimetype,
|
||||||
|
mediaType: kind,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
import { EngineHelper } from './engines';
|
import { EngineHelper } from './engines';
|
||||||
|
import type { QuotedMedia } from './engines';
|
||||||
|
|
||||||
export { EngineHelper };
|
export { EngineHelper };
|
||||||
|
export type { QuotedMedia };
|
||||||
@@ -5,3 +5,15 @@ export function getEngineName(): string {
|
|||||||
// If not set - use WEBJS
|
// If not set - use WEBJS
|
||||||
return process.env.WHATSAPP_DEFAULT_ENGINE || WAHAEngine.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();
|
||||||
|
}
|
||||||
@@ -195,6 +195,8 @@ export class SwaggerConfiguratorCore {
|
|||||||
'/ping',
|
'/ping',
|
||||||
'/ws',
|
'/ws',
|
||||||
'/webhooks/',
|
'/webhooks/',
|
||||||
|
'/jobs',
|
||||||
|
'/jobs/',
|
||||||
...config.getExcludedFullPaths(),
|
...config.getExcludedFullPaths(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import {
|
||||||
|
getBrowserExecutablePath as getBrowserExecutablePathAutodetect,
|
||||||
|
} from '@waha/core/abc/session.browser';
|
||||||
import {
|
import {
|
||||||
CoreMediaConverter,
|
CoreMediaConverter,
|
||||||
IMediaConverter,
|
IMediaConverter,
|
||||||
@@ -142,15 +145,6 @@ const qrcode = require('qrcode-terminal');
|
|||||||
|
|
||||||
axiosRetry(axios, { retries: 3 });
|
axiosRetry(axios, { retries: 3 });
|
||||||
|
|
||||||
const CHROME_PATH = '/usr/bin/google-chrome-stable';
|
|
||||||
const CHROMIUM_PATH = '/usr/bin/chromium';
|
|
||||||
|
|
||||||
export function getBrowserExecutablePath() {
|
|
||||||
if (fs.existsSync(CHROME_PATH)) {
|
|
||||||
return CHROME_PATH;
|
|
||||||
}
|
|
||||||
return CHROMIUM_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ensureSuffix(phone) {
|
export function ensureSuffix(phone) {
|
||||||
const suffix = '@c.us';
|
const suffix = '@c.us';
|
||||||
@@ -360,7 +354,7 @@ export abstract class WhatsappSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getBrowserExecutablePath() {
|
getBrowserExecutablePath() {
|
||||||
return getBrowserExecutablePath();
|
return getBrowserExecutablePathAutodetect();
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowserArgsForPuppeteer() {
|
getBrowserArgsForPuppeteer() {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Browser executable path detection
|
||||||
|
//
|
||||||
|
|
||||||
|
// Ordered: Chrome paths first, then Chromium.
|
||||||
|
// Windows: only global Program Files paths (no per-user %LOCALAPPDATA%).
|
||||||
|
const BROWSER_PATHS: Record<string, string[]> = {
|
||||||
|
darwin: [
|
||||||
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
||||||
|
],
|
||||||
|
win32: [
|
||||||
|
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
||||||
|
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
||||||
|
'C:\\Program Files\\Chromium\\Application\\chrome.exe',
|
||||||
|
],
|
||||||
|
linux: [
|
||||||
|
'/usr/bin/google-chrome',
|
||||||
|
'/usr/bin/google-chrome-stable',
|
||||||
|
'/usr/bin/chromium',
|
||||||
|
'/usr/bin/chromium-browser',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getBrowserExecutablePath(): string {
|
||||||
|
const paths = BROWSER_PATHS[process.platform] ?? BROWSER_PATHS['linux'];
|
||||||
|
for (const p of paths) {
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// A path belongs to Google Chrome when it contains "google" in any casing.
|
||||||
|
// All Chrome paths include "Google" (macOS/Windows) or "google-chrome" (Linux),
|
||||||
|
// while Chromium paths never do.
|
||||||
|
export function isChromeExecutablePath(executablePath: string): boolean {
|
||||||
|
return executablePath.toLowerCase().includes('google');
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import { ApiKeyAuthMiddleware } from '@waha/core/auth/api-key-auth.middleware';
|
|||||||
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
||||||
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
|
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
|
||||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||||
|
import { WPPEngineConfigService } from '@waha/core/config/WPPEngineConfigService';
|
||||||
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
||||||
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
|
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
|
||||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||||
@@ -176,6 +177,7 @@ export const PROVIDERS_BASE: Provider[] = [
|
|||||||
DashboardConfigServiceCore,
|
DashboardConfigServiceCore,
|
||||||
SwaggerConfigServiceCore,
|
SwaggerConfigServiceCore,
|
||||||
WebJSEngineConfigService,
|
WebJSEngineConfigService,
|
||||||
|
WPPEngineConfigService,
|
||||||
GowsEngineConfigService,
|
GowsEngineConfigService,
|
||||||
WhatsappConfigService,
|
WhatsappConfigService,
|
||||||
EngineConfigService,
|
EngineConfigService,
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
InternalServerErrorException,
|
|
||||||
NestMiddleware,
|
NestMiddleware,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { IApiKeyAuth, NoAuth } from '@waha/core/auth/auth';
|
||||||
import * as passport from 'passport';
|
import * as passport from 'passport';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApiKeyAuthMiddleware implements NestMiddleware {
|
export class ApiKeyAuthMiddleware implements NestMiddleware {
|
||||||
constructor() {}
|
constructor(private auth: IApiKeyAuth) {}
|
||||||
|
|
||||||
use(req: any, res: any, next: () => void) {
|
use(req: any, res: any, next: () => void) {
|
||||||
|
if (this.auth instanceof NoAuth) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
passport.authenticate('headerapikey', { session: false }, (err, user?) => {
|
passport.authenticate('headerapikey', { session: false }, (err, user?) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
const exception =
|
const exception =
|
||||||
err instanceof UnauthorizedException
|
err instanceof UnauthorizedException
|
||||||
? err
|
? err
|
||||||
: new InternalServerErrorException();
|
: new UnauthorizedException();
|
||||||
res.status(exception.getStatus()).json(exception.getResponse());
|
res.status(exception.getStatus()).json(exception.getResponse());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export class PoliciesGuard implements CanActivate {
|
|||||||
context.getClass(),
|
context.getClass(),
|
||||||
]) || [];
|
]) || [];
|
||||||
|
|
||||||
|
if (handlers.length === 0) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
const user = req.user;
|
const user = req.user;
|
||||||
const ability = this.caslAbilityFactory.createForUser(user);
|
const ability = this.caslAbilityFactory.createForUser(user);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2996,6 +2996,73 @@ export namespace messages {
|
|||||||
return AudioInfo.deserialize(bytes);
|
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 {
|
export class Media extends pb_1.Message {
|
||||||
#one_of_decls: number[][] = [];
|
#one_of_decls: number[][] = [];
|
||||||
constructor(data?: any[] | {
|
constructor(data?: any[] | {
|
||||||
@@ -3005,6 +3072,7 @@ export namespace messages {
|
|||||||
audio?: AudioInfo;
|
audio?: AudioInfo;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
contentPath?: string;
|
contentPath?: string;
|
||||||
|
video?: VideoInfo;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||||
@@ -3027,6 +3095,9 @@ export namespace messages {
|
|||||||
if ("contentPath" in data && data.contentPath != undefined) {
|
if ("contentPath" in data && data.contentPath != undefined) {
|
||||||
this.contentPath = data.contentPath;
|
this.contentPath = data.contentPath;
|
||||||
}
|
}
|
||||||
|
if ("video" in data && data.video != undefined) {
|
||||||
|
this.video = data.video;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
get content() {
|
get content() {
|
||||||
@@ -3068,6 +3139,15 @@ export namespace messages {
|
|||||||
set contentPath(value: string) {
|
set contentPath(value: string) {
|
||||||
pb_1.Message.setField(this, 6, value);
|
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: {
|
static fromObject(data: {
|
||||||
content?: Uint8Array;
|
content?: Uint8Array;
|
||||||
type?: MediaType;
|
type?: MediaType;
|
||||||
@@ -3075,6 +3155,7 @@ export namespace messages {
|
|||||||
audio?: ReturnType<typeof AudioInfo.prototype.toObject>;
|
audio?: ReturnType<typeof AudioInfo.prototype.toObject>;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
contentPath?: string;
|
contentPath?: string;
|
||||||
|
video?: ReturnType<typeof VideoInfo.prototype.toObject>;
|
||||||
}): Media {
|
}): Media {
|
||||||
const message = new Media({});
|
const message = new Media({});
|
||||||
if (data.content != null) {
|
if (data.content != null) {
|
||||||
@@ -3095,6 +3176,9 @@ export namespace messages {
|
|||||||
if (data.contentPath != null) {
|
if (data.contentPath != null) {
|
||||||
message.contentPath = data.contentPath;
|
message.contentPath = data.contentPath;
|
||||||
}
|
}
|
||||||
|
if (data.video != null) {
|
||||||
|
message.video = VideoInfo.fromObject(data.video);
|
||||||
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
toObject() {
|
toObject() {
|
||||||
@@ -3105,6 +3189,7 @@ export namespace messages {
|
|||||||
audio?: ReturnType<typeof AudioInfo.prototype.toObject>;
|
audio?: ReturnType<typeof AudioInfo.prototype.toObject>;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
contentPath?: string;
|
contentPath?: string;
|
||||||
|
video?: ReturnType<typeof VideoInfo.prototype.toObject>;
|
||||||
} = {};
|
} = {};
|
||||||
if (this.content != null) {
|
if (this.content != null) {
|
||||||
data.content = this.content;
|
data.content = this.content;
|
||||||
@@ -3124,6 +3209,9 @@ export namespace messages {
|
|||||||
if (this.contentPath != null) {
|
if (this.contentPath != null) {
|
||||||
data.contentPath = this.contentPath;
|
data.contentPath = this.contentPath;
|
||||||
}
|
}
|
||||||
|
if (this.video != null) {
|
||||||
|
data.video = this.video.toObject();
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
serialize(): Uint8Array;
|
serialize(): Uint8Array;
|
||||||
@@ -3142,6 +3230,8 @@ export namespace messages {
|
|||||||
writer.writeString(5, this.filename);
|
writer.writeString(5, this.filename);
|
||||||
if (this.contentPath.length)
|
if (this.contentPath.length)
|
||||||
writer.writeString(6, this.contentPath);
|
writer.writeString(6, this.contentPath);
|
||||||
|
if (this.has_video)
|
||||||
|
writer.writeMessage(7, this.video, () => this.video.serialize(writer));
|
||||||
if (!w)
|
if (!w)
|
||||||
return writer.getResultBuffer();
|
return writer.getResultBuffer();
|
||||||
}
|
}
|
||||||
@@ -3169,6 +3259,9 @@ export namespace messages {
|
|||||||
case 6:
|
case 6:
|
||||||
message.contentPath = reader.readString();
|
message.contentPath = reader.readString();
|
||||||
break;
|
break;
|
||||||
|
case 7:
|
||||||
|
reader.readMessage(message.video, () => message.video = VideoInfo.deserialize(reader));
|
||||||
|
break;
|
||||||
default: reader.skipField();
|
default: reader.skipField();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9128,6 +9221,7 @@ export namespace messages {
|
|||||||
filters?: MessageFilters;
|
filters?: MessageFilters;
|
||||||
pagination?: Pagination;
|
pagination?: Pagination;
|
||||||
sortBy?: SortBy;
|
sortBy?: SortBy;
|
||||||
|
merge?: OptionalBool;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||||
@@ -9144,6 +9238,9 @@ export namespace messages {
|
|||||||
if ("sortBy" in data && data.sortBy != undefined) {
|
if ("sortBy" in data && data.sortBy != undefined) {
|
||||||
this.sortBy = data.sortBy;
|
this.sortBy = data.sortBy;
|
||||||
}
|
}
|
||||||
|
if ("merge" in data && data.merge != undefined) {
|
||||||
|
this.merge = data.merge;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
get session() {
|
get session() {
|
||||||
@@ -9182,11 +9279,21 @@ export namespace messages {
|
|||||||
get has_sortBy() {
|
get has_sortBy() {
|
||||||
return pb_1.Message.getField(this, 4) != null;
|
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: {
|
static fromObject(data: {
|
||||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||||
filters?: ReturnType<typeof MessageFilters.prototype.toObject>;
|
filters?: ReturnType<typeof MessageFilters.prototype.toObject>;
|
||||||
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
||||||
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
||||||
|
merge?: ReturnType<typeof OptionalBool.prototype.toObject>;
|
||||||
}): GetMessagesRequest {
|
}): GetMessagesRequest {
|
||||||
const message = new GetMessagesRequest({});
|
const message = new GetMessagesRequest({});
|
||||||
if (data.session != null) {
|
if (data.session != null) {
|
||||||
@@ -9201,6 +9308,9 @@ export namespace messages {
|
|||||||
if (data.sortBy != null) {
|
if (data.sortBy != null) {
|
||||||
message.sortBy = SortBy.fromObject(data.sortBy);
|
message.sortBy = SortBy.fromObject(data.sortBy);
|
||||||
}
|
}
|
||||||
|
if (data.merge != null) {
|
||||||
|
message.merge = OptionalBool.fromObject(data.merge);
|
||||||
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
toObject() {
|
toObject() {
|
||||||
@@ -9209,6 +9319,7 @@ export namespace messages {
|
|||||||
filters?: ReturnType<typeof MessageFilters.prototype.toObject>;
|
filters?: ReturnType<typeof MessageFilters.prototype.toObject>;
|
||||||
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
||||||
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
||||||
|
merge?: ReturnType<typeof OptionalBool.prototype.toObject>;
|
||||||
} = {};
|
} = {};
|
||||||
if (this.session != null) {
|
if (this.session != null) {
|
||||||
data.session = this.session.toObject();
|
data.session = this.session.toObject();
|
||||||
@@ -9222,6 +9333,9 @@ export namespace messages {
|
|||||||
if (this.sortBy != null) {
|
if (this.sortBy != null) {
|
||||||
data.sortBy = this.sortBy.toObject();
|
data.sortBy = this.sortBy.toObject();
|
||||||
}
|
}
|
||||||
|
if (this.merge != null) {
|
||||||
|
data.merge = this.merge.toObject();
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
serialize(): Uint8Array;
|
serialize(): Uint8Array;
|
||||||
@@ -9236,6 +9350,8 @@ export namespace messages {
|
|||||||
writer.writeMessage(3, this.pagination, () => this.pagination.serialize(writer));
|
writer.writeMessage(3, this.pagination, () => this.pagination.serialize(writer));
|
||||||
if (this.has_sortBy)
|
if (this.has_sortBy)
|
||||||
writer.writeMessage(4, this.sortBy, () => this.sortBy.serialize(writer));
|
writer.writeMessage(4, this.sortBy, () => this.sortBy.serialize(writer));
|
||||||
|
if (this.has_merge)
|
||||||
|
writer.writeMessage(5, this.merge, () => this.merge.serialize(writer));
|
||||||
if (!w)
|
if (!w)
|
||||||
return writer.getResultBuffer();
|
return writer.getResultBuffer();
|
||||||
}
|
}
|
||||||
@@ -9257,6 +9373,9 @@ export namespace messages {
|
|||||||
case 4:
|
case 4:
|
||||||
reader.readMessage(message.sortBy, () => message.sortBy = SortBy.deserialize(reader));
|
reader.readMessage(message.sortBy, () => message.sortBy = SortBy.deserialize(reader));
|
||||||
break;
|
break;
|
||||||
|
case 5:
|
||||||
|
reader.readMessage(message.merge, () => message.merge = OptionalBool.deserialize(reader));
|
||||||
|
break;
|
||||||
default: reader.skipField();
|
default: reader.skipField();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9604,6 +9723,7 @@ export namespace messages {
|
|||||||
sortBy?: SortBy;
|
sortBy?: SortBy;
|
||||||
pagination?: Pagination;
|
pagination?: Pagination;
|
||||||
filter?: ChatFilter;
|
filter?: ChatFilter;
|
||||||
|
merge?: OptionalBool;
|
||||||
}) {
|
}) {
|
||||||
super();
|
super();
|
||||||
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
|
||||||
@@ -9620,6 +9740,9 @@ export namespace messages {
|
|||||||
if ("filter" in data && data.filter != undefined) {
|
if ("filter" in data && data.filter != undefined) {
|
||||||
this.filter = data.filter;
|
this.filter = data.filter;
|
||||||
}
|
}
|
||||||
|
if ("merge" in data && data.merge != undefined) {
|
||||||
|
this.merge = data.merge;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
get session() {
|
get session() {
|
||||||
@@ -9658,11 +9781,21 @@ export namespace messages {
|
|||||||
get has_filter() {
|
get has_filter() {
|
||||||
return pb_1.Message.getField(this, 4) != null;
|
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: {
|
static fromObject(data: {
|
||||||
session?: ReturnType<typeof Session.prototype.toObject>;
|
session?: ReturnType<typeof Session.prototype.toObject>;
|
||||||
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
||||||
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
||||||
filter?: ReturnType<typeof ChatFilter.prototype.toObject>;
|
filter?: ReturnType<typeof ChatFilter.prototype.toObject>;
|
||||||
|
merge?: ReturnType<typeof OptionalBool.prototype.toObject>;
|
||||||
}): GetChatsRequest {
|
}): GetChatsRequest {
|
||||||
const message = new GetChatsRequest({});
|
const message = new GetChatsRequest({});
|
||||||
if (data.session != null) {
|
if (data.session != null) {
|
||||||
@@ -9677,6 +9810,9 @@ export namespace messages {
|
|||||||
if (data.filter != null) {
|
if (data.filter != null) {
|
||||||
message.filter = ChatFilter.fromObject(data.filter);
|
message.filter = ChatFilter.fromObject(data.filter);
|
||||||
}
|
}
|
||||||
|
if (data.merge != null) {
|
||||||
|
message.merge = OptionalBool.fromObject(data.merge);
|
||||||
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
toObject() {
|
toObject() {
|
||||||
@@ -9685,6 +9821,7 @@ export namespace messages {
|
|||||||
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
|
||||||
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
|
||||||
filter?: ReturnType<typeof ChatFilter.prototype.toObject>;
|
filter?: ReturnType<typeof ChatFilter.prototype.toObject>;
|
||||||
|
merge?: ReturnType<typeof OptionalBool.prototype.toObject>;
|
||||||
} = {};
|
} = {};
|
||||||
if (this.session != null) {
|
if (this.session != null) {
|
||||||
data.session = this.session.toObject();
|
data.session = this.session.toObject();
|
||||||
@@ -9698,6 +9835,9 @@ export namespace messages {
|
|||||||
if (this.filter != null) {
|
if (this.filter != null) {
|
||||||
data.filter = this.filter.toObject();
|
data.filter = this.filter.toObject();
|
||||||
}
|
}
|
||||||
|
if (this.merge != null) {
|
||||||
|
data.merge = this.merge.toObject();
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
serialize(): Uint8Array;
|
serialize(): Uint8Array;
|
||||||
@@ -9712,6 +9852,8 @@ export namespace messages {
|
|||||||
writer.writeMessage(3, this.pagination, () => this.pagination.serialize(writer));
|
writer.writeMessage(3, this.pagination, () => this.pagination.serialize(writer));
|
||||||
if (this.has_filter)
|
if (this.has_filter)
|
||||||
writer.writeMessage(4, this.filter, () => this.filter.serialize(writer));
|
writer.writeMessage(4, this.filter, () => this.filter.serialize(writer));
|
||||||
|
if (this.has_merge)
|
||||||
|
writer.writeMessage(5, this.merge, () => this.merge.serialize(writer));
|
||||||
if (!w)
|
if (!w)
|
||||||
return writer.getResultBuffer();
|
return writer.getResultBuffer();
|
||||||
}
|
}
|
||||||
@@ -9733,6 +9875,9 @@ export namespace messages {
|
|||||||
case 4:
|
case 4:
|
||||||
reader.readMessage(message.filter, () => message.filter = ChatFilter.deserialize(reader));
|
reader.readMessage(message.filter, () => message.filter = ChatFilter.deserialize(reader));
|
||||||
break;
|
break;
|
||||||
|
case 5:
|
||||||
|
reader.readMessage(message.merge, () => message.merge = OptionalBool.deserialize(reader));
|
||||||
|
break;
|
||||||
default: reader.skipField();
|
default: reader.skipField();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ goog.exportSymbol('proto.messages.SubscribePresenceRequest', null, global);
|
|||||||
goog.exportSymbol('proto.messages.UpdateContactRequest', null, global);
|
goog.exportSymbol('proto.messages.UpdateContactRequest', null, global);
|
||||||
goog.exportSymbol('proto.messages.UpdateParticipantsRequest', null, global);
|
goog.exportSymbol('proto.messages.UpdateParticipantsRequest', null, global);
|
||||||
goog.exportSymbol('proto.messages.UpsertLabelRequest', null, global);
|
goog.exportSymbol('proto.messages.UpsertLabelRequest', null, global);
|
||||||
|
goog.exportSymbol('proto.messages.VideoInfo', null, global);
|
||||||
goog.exportSymbol('proto.messages.vCardContact', null, global);
|
goog.exportSymbol('proto.messages.vCardContact', null, global);
|
||||||
/**
|
/**
|
||||||
* Generated by JsPbCodeGenerator.
|
* Generated by JsPbCodeGenerator.
|
||||||
@@ -772,6 +773,27 @@ if (goog.DEBUG && !COMPILED) {
|
|||||||
*/
|
*/
|
||||||
proto.messages.AudioInfo.displayName = 'proto.messages.AudioInfo';
|
proto.messages.AudioInfo.displayName = 'proto.messages.AudioInfo';
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Generated by JsPbCodeGenerator.
|
||||||
|
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||||
|
* server response, or constructed directly in Javascript. The array is used
|
||||||
|
* in place and becomes part of the constructed object. It is not cloned.
|
||||||
|
* If no data is provided, the constructed object will be empty, but still
|
||||||
|
* valid.
|
||||||
|
* @extends {jspb.Message}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
proto.messages.VideoInfo = function(opt_data) {
|
||||||
|
jspb.Message.initialize(this, opt_data, 0, -1, null, null);
|
||||||
|
};
|
||||||
|
goog.inherits(proto.messages.VideoInfo, jspb.Message);
|
||||||
|
if (goog.DEBUG && !COMPILED) {
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
proto.messages.VideoInfo.displayName = 'proto.messages.VideoInfo';
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Generated by JsPbCodeGenerator.
|
* Generated by JsPbCodeGenerator.
|
||||||
* @param {Array=} opt_data Optional initial data array, typically from a
|
* @param {Array=} opt_data Optional initial data array, typically from a
|
||||||
@@ -7698,6 +7720,136 @@ proto.messages.AudioInfo.prototype.setWaveform = function(value) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||||
|
/**
|
||||||
|
* Creates an object representation of this proto.
|
||||||
|
* Field names that are reserved in JavaScript and will be renamed to pb_name.
|
||||||
|
* Optional fields that are not set will be set to undefined.
|
||||||
|
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
|
||||||
|
* For the list of reserved names please see:
|
||||||
|
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
|
||||||
|
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
|
||||||
|
* JSPB instance for transitional soy proto support:
|
||||||
|
* http://goto/soy-param-migration
|
||||||
|
* @return {!Object}
|
||||||
|
*/
|
||||||
|
proto.messages.VideoInfo.prototype.toObject = function(opt_includeInstance) {
|
||||||
|
return proto.messages.VideoInfo.toObject(opt_includeInstance, this);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static version of the {@see toObject} method.
|
||||||
|
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
|
||||||
|
* the JSPB instance for transitional soy proto support:
|
||||||
|
* http://goto/soy-param-migration
|
||||||
|
* @param {!proto.messages.VideoInfo} msg The msg instance to transform.
|
||||||
|
* @return {!Object}
|
||||||
|
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||||
|
*/
|
||||||
|
proto.messages.VideoInfo.toObject = function(includeInstance, msg) {
|
||||||
|
var f, obj = {
|
||||||
|
duration: jspb.Message.getFloatingPointFieldWithDefault(msg, 1, 0.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (includeInstance) {
|
||||||
|
obj.$jspbMessageInstance = msg;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserializes binary data (in protobuf wire format).
|
||||||
|
* @param {jspb.ByteSource} bytes The bytes to deserialize.
|
||||||
|
* @return {!proto.messages.VideoInfo}
|
||||||
|
*/
|
||||||
|
proto.messages.VideoInfo.deserializeBinary = function(bytes) {
|
||||||
|
var reader = new jspb.BinaryReader(bytes);
|
||||||
|
var msg = new proto.messages.VideoInfo;
|
||||||
|
return proto.messages.VideoInfo.deserializeBinaryFromReader(msg, reader);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserializes binary data (in protobuf wire format) from the
|
||||||
|
* given reader into the given message object.
|
||||||
|
* @param {!proto.messages.VideoInfo} msg The message object to deserialize into.
|
||||||
|
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
|
||||||
|
* @return {!proto.messages.VideoInfo}
|
||||||
|
*/
|
||||||
|
proto.messages.VideoInfo.deserializeBinaryFromReader = function(msg, reader) {
|
||||||
|
while (reader.nextField()) {
|
||||||
|
if (reader.isEndGroup()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var field = reader.getFieldNumber();
|
||||||
|
switch (field) {
|
||||||
|
case 1:
|
||||||
|
var value = /** @type {number} */ (reader.readFloat());
|
||||||
|
msg.setDuration(value);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reader.skipField();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes the message to binary data (in protobuf wire format).
|
||||||
|
* @return {!Uint8Array}
|
||||||
|
*/
|
||||||
|
proto.messages.VideoInfo.prototype.serializeBinary = function() {
|
||||||
|
var writer = new jspb.BinaryWriter();
|
||||||
|
proto.messages.VideoInfo.serializeBinaryToWriter(this, writer);
|
||||||
|
return writer.getResultBuffer();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes the given message to binary data (in protobuf wire
|
||||||
|
* format), writing to the given BinaryWriter.
|
||||||
|
* @param {!proto.messages.VideoInfo} message
|
||||||
|
* @param {!jspb.BinaryWriter} writer
|
||||||
|
* @suppress {unusedLocalVariables} f is only used for nested messages
|
||||||
|
*/
|
||||||
|
proto.messages.VideoInfo.serializeBinaryToWriter = function(message, writer) {
|
||||||
|
var f = undefined;
|
||||||
|
f = message.getDuration();
|
||||||
|
if (f !== 0.0) {
|
||||||
|
writer.writeFloat(
|
||||||
|
1,
|
||||||
|
f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* optional float duration = 1;
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
proto.messages.VideoInfo.prototype.getDuration = function() {
|
||||||
|
return /** @type {number} */ (jspb.Message.getFloatingPointFieldWithDefault(this, 1, 0.0));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} value
|
||||||
|
* @return {!proto.messages.VideoInfo} returns this
|
||||||
|
*/
|
||||||
|
proto.messages.VideoInfo.prototype.setDuration = function(value) {
|
||||||
|
return jspb.Message.setProto3FloatField(this, 1, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (jspb.Message.GENERATE_TO_OBJECT) {
|
if (jspb.Message.GENERATE_TO_OBJECT) {
|
||||||
/**
|
/**
|
||||||
* Creates an object representation of this proto.
|
* Creates an object representation of this proto.
|
||||||
@@ -7732,7 +7884,8 @@ proto.messages.Media.toObject = function(includeInstance, msg) {
|
|||||||
mimetype: jspb.Message.getFieldWithDefault(msg, 3, ""),
|
mimetype: jspb.Message.getFieldWithDefault(msg, 3, ""),
|
||||||
audio: (f = msg.getAudio()) && proto.messages.AudioInfo.toObject(includeInstance, f),
|
audio: (f = msg.getAudio()) && proto.messages.AudioInfo.toObject(includeInstance, f),
|
||||||
filename: jspb.Message.getFieldWithDefault(msg, 5, ""),
|
filename: jspb.Message.getFieldWithDefault(msg, 5, ""),
|
||||||
contentpath: jspb.Message.getFieldWithDefault(msg, 6, "")
|
contentpath: jspb.Message.getFieldWithDefault(msg, 6, ""),
|
||||||
|
video: (f = msg.getVideo()) && proto.messages.VideoInfo.toObject(includeInstance, f)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeInstance) {
|
if (includeInstance) {
|
||||||
@@ -7794,6 +7947,11 @@ proto.messages.Media.deserializeBinaryFromReader = function(msg, reader) {
|
|||||||
var value = /** @type {string} */ (reader.readString());
|
var value = /** @type {string} */ (reader.readString());
|
||||||
msg.setContentpath(value);
|
msg.setContentpath(value);
|
||||||
break;
|
break;
|
||||||
|
case 7:
|
||||||
|
var value = new proto.messages.VideoInfo;
|
||||||
|
reader.readMessage(value,proto.messages.VideoInfo.deserializeBinaryFromReader);
|
||||||
|
msg.setVideo(value);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
reader.skipField();
|
reader.skipField();
|
||||||
break;
|
break;
|
||||||
@@ -7866,6 +8024,14 @@ proto.messages.Media.serializeBinaryToWriter = function(message, writer) {
|
|||||||
f
|
f
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
f = message.getVideo();
|
||||||
|
if (f != null) {
|
||||||
|
writer.writeMessage(
|
||||||
|
7,
|
||||||
|
f,
|
||||||
|
proto.messages.VideoInfo.serializeBinaryToWriter
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -8020,6 +8186,43 @@ proto.messages.Media.prototype.setContentpath = function(value) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* optional VideoInfo video = 7;
|
||||||
|
* @return {?proto.messages.VideoInfo}
|
||||||
|
*/
|
||||||
|
proto.messages.Media.prototype.getVideo = function() {
|
||||||
|
return /** @type{?proto.messages.VideoInfo} */ (
|
||||||
|
jspb.Message.getWrapperField(this, proto.messages.VideoInfo, 7));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?proto.messages.VideoInfo|undefined} value
|
||||||
|
* @return {!proto.messages.Media} returns this
|
||||||
|
*/
|
||||||
|
proto.messages.Media.prototype.setVideo = function(value) {
|
||||||
|
return jspb.Message.setWrapperField(this, 7, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the message field making it undefined.
|
||||||
|
* @return {!proto.messages.Media} returns this
|
||||||
|
*/
|
||||||
|
proto.messages.Media.prototype.clearVideo = function() {
|
||||||
|
return this.setVideo(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this field is set.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
proto.messages.Media.prototype.hasVideo = function() {
|
||||||
|
return jspb.Message.getField(this, 7) != null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -18928,7 +19131,8 @@ proto.messages.GetMessagesRequest.toObject = function(includeInstance, msg) {
|
|||||||
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
|
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
|
||||||
filters: (f = msg.getFilters()) && proto.messages.MessageFilters.toObject(includeInstance, f),
|
filters: (f = msg.getFilters()) && proto.messages.MessageFilters.toObject(includeInstance, f),
|
||||||
pagination: (f = msg.getPagination()) && proto.messages.Pagination.toObject(includeInstance, f),
|
pagination: (f = msg.getPagination()) && proto.messages.Pagination.toObject(includeInstance, f),
|
||||||
sortby: (f = msg.getSortby()) && proto.messages.SortBy.toObject(includeInstance, f)
|
sortby: (f = msg.getSortby()) && proto.messages.SortBy.toObject(includeInstance, f),
|
||||||
|
merge: (f = msg.getMerge()) && proto.messages.OptionalBool.toObject(includeInstance, f)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeInstance) {
|
if (includeInstance) {
|
||||||
@@ -18985,6 +19189,11 @@ proto.messages.GetMessagesRequest.deserializeBinaryFromReader = function(msg, re
|
|||||||
reader.readMessage(value,proto.messages.SortBy.deserializeBinaryFromReader);
|
reader.readMessage(value,proto.messages.SortBy.deserializeBinaryFromReader);
|
||||||
msg.setSortby(value);
|
msg.setSortby(value);
|
||||||
break;
|
break;
|
||||||
|
case 5:
|
||||||
|
var value = new proto.messages.OptionalBool;
|
||||||
|
reader.readMessage(value,proto.messages.OptionalBool.deserializeBinaryFromReader);
|
||||||
|
msg.setMerge(value);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
reader.skipField();
|
reader.skipField();
|
||||||
break;
|
break;
|
||||||
@@ -19046,6 +19255,14 @@ proto.messages.GetMessagesRequest.serializeBinaryToWriter = function(message, wr
|
|||||||
proto.messages.SortBy.serializeBinaryToWriter
|
proto.messages.SortBy.serializeBinaryToWriter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
f = message.getMerge();
|
||||||
|
if (f != null) {
|
||||||
|
writer.writeMessage(
|
||||||
|
5,
|
||||||
|
f,
|
||||||
|
proto.messages.OptionalBool.serializeBinaryToWriter
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -19197,6 +19414,43 @@ proto.messages.GetMessagesRequest.prototype.hasSortby = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* optional OptionalBool merge = 5;
|
||||||
|
* @return {?proto.messages.OptionalBool}
|
||||||
|
*/
|
||||||
|
proto.messages.GetMessagesRequest.prototype.getMerge = function() {
|
||||||
|
return /** @type{?proto.messages.OptionalBool} */ (
|
||||||
|
jspb.Message.getWrapperField(this, proto.messages.OptionalBool, 5));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?proto.messages.OptionalBool|undefined} value
|
||||||
|
* @return {!proto.messages.GetMessagesRequest} returns this
|
||||||
|
*/
|
||||||
|
proto.messages.GetMessagesRequest.prototype.setMerge = function(value) {
|
||||||
|
return jspb.Message.setWrapperField(this, 5, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the message field making it undefined.
|
||||||
|
* @return {!proto.messages.GetMessagesRequest} returns this
|
||||||
|
*/
|
||||||
|
proto.messages.GetMessagesRequest.prototype.clearMerge = function() {
|
||||||
|
return this.setMerge(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this field is set.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
proto.messages.GetMessagesRequest.prototype.hasMerge = function() {
|
||||||
|
return jspb.Message.getField(this, 5) != null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -19882,7 +20136,8 @@ proto.messages.GetChatsRequest.toObject = function(includeInstance, msg) {
|
|||||||
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
|
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
|
||||||
sortby: (f = msg.getSortby()) && proto.messages.SortBy.toObject(includeInstance, f),
|
sortby: (f = msg.getSortby()) && proto.messages.SortBy.toObject(includeInstance, f),
|
||||||
pagination: (f = msg.getPagination()) && proto.messages.Pagination.toObject(includeInstance, f),
|
pagination: (f = msg.getPagination()) && proto.messages.Pagination.toObject(includeInstance, f),
|
||||||
filter: (f = msg.getFilter()) && proto.messages.ChatFilter.toObject(includeInstance, f)
|
filter: (f = msg.getFilter()) && proto.messages.ChatFilter.toObject(includeInstance, f),
|
||||||
|
merge: (f = msg.getMerge()) && proto.messages.OptionalBool.toObject(includeInstance, f)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeInstance) {
|
if (includeInstance) {
|
||||||
@@ -19939,6 +20194,11 @@ proto.messages.GetChatsRequest.deserializeBinaryFromReader = function(msg, reade
|
|||||||
reader.readMessage(value,proto.messages.ChatFilter.deserializeBinaryFromReader);
|
reader.readMessage(value,proto.messages.ChatFilter.deserializeBinaryFromReader);
|
||||||
msg.setFilter(value);
|
msg.setFilter(value);
|
||||||
break;
|
break;
|
||||||
|
case 5:
|
||||||
|
var value = new proto.messages.OptionalBool;
|
||||||
|
reader.readMessage(value,proto.messages.OptionalBool.deserializeBinaryFromReader);
|
||||||
|
msg.setMerge(value);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
reader.skipField();
|
reader.skipField();
|
||||||
break;
|
break;
|
||||||
@@ -20000,6 +20260,14 @@ proto.messages.GetChatsRequest.serializeBinaryToWriter = function(message, write
|
|||||||
proto.messages.ChatFilter.serializeBinaryToWriter
|
proto.messages.ChatFilter.serializeBinaryToWriter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
f = message.getMerge();
|
||||||
|
if (f != null) {
|
||||||
|
writer.writeMessage(
|
||||||
|
5,
|
||||||
|
f,
|
||||||
|
proto.messages.OptionalBool.serializeBinaryToWriter
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -20151,6 +20419,43 @@ proto.messages.GetChatsRequest.prototype.hasFilter = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* optional OptionalBool merge = 5;
|
||||||
|
* @return {?proto.messages.OptionalBool}
|
||||||
|
*/
|
||||||
|
proto.messages.GetChatsRequest.prototype.getMerge = function() {
|
||||||
|
return /** @type{?proto.messages.OptionalBool} */ (
|
||||||
|
jspb.Message.getWrapperField(this, proto.messages.OptionalBool, 5));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {?proto.messages.OptionalBool|undefined} value
|
||||||
|
* @return {!proto.messages.GetChatsRequest} returns this
|
||||||
|
*/
|
||||||
|
proto.messages.GetChatsRequest.prototype.setMerge = function(value) {
|
||||||
|
return jspb.Message.setWrapperField(this, 5, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the message field making it undefined.
|
||||||
|
* @return {!proto.messages.GetChatsRequest} returns this
|
||||||
|
*/
|
||||||
|
proto.messages.GetChatsRequest.prototype.clearMerge = function() {
|
||||||
|
return this.setMerge(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether this field is set.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
proto.messages.GetChatsRequest.prototype.hasMerge = function() {
|
||||||
|
return jspb.Message.getField(this, 5) != null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
|||||||
import {
|
import {
|
||||||
isJidBroadcast,
|
isJidBroadcast,
|
||||||
isJidGroup,
|
isJidGroup,
|
||||||
|
normalizeJid,
|
||||||
toCusFormat,
|
toCusFormat,
|
||||||
toJID,
|
toJID,
|
||||||
} from '@waha/core/utils/jids';
|
} from '@waha/core/utils/jids';
|
||||||
@@ -65,6 +66,8 @@ import {
|
|||||||
GetChatMessageQuery,
|
GetChatMessageQuery,
|
||||||
GetChatMessagesFilter,
|
GetChatMessagesFilter,
|
||||||
GetChatMessagesQuery,
|
GetChatMessagesQuery,
|
||||||
|
GetChatsOverviewParams,
|
||||||
|
GetChatsParams,
|
||||||
MessageSortField,
|
MessageSortField,
|
||||||
OverviewFilter,
|
OverviewFilter,
|
||||||
ReadChatMessagesQuery,
|
ReadChatMessagesQuery,
|
||||||
@@ -162,7 +165,7 @@ import { promisify } from 'util';
|
|||||||
import * as gows from './types';
|
import * as gows from './types';
|
||||||
import { MessageStatus } from './types';
|
import { MessageStatus } from './types';
|
||||||
import { isFromFullSync } from '@waha/core/engines/gows/appstate';
|
import { isFromFullSync } from '@waha/core/engines/gows/appstate';
|
||||||
import { toVcardV3 } from '@waha/core/vcard';
|
import { parseVCardV3, toVcardV3 } from '@waha/core/vcard';
|
||||||
import { AckToStatus } from '@waha/core/utils/acks';
|
import { AckToStatus } from '@waha/core/utils/acks';
|
||||||
import { ParseEventResponseType } from '@waha/core/utils/events';
|
import { ParseEventResponseType } from '@waha/core/utils/events';
|
||||||
import { DistinctAck, DistinctMessages } from '@waha/core/utils/reactive';
|
import { DistinctAck, DistinctMessages } from '@waha/core/utils/reactive';
|
||||||
@@ -311,7 +314,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
grpc.credentials.createInsecure(),
|
grpc.credentials.createInsecure(),
|
||||||
);
|
);
|
||||||
|
|
||||||
promisify(this.client.StartSession)(request).catch((err) => {
|
await promisify(this.client.StartSession)(request).catch((err) => {
|
||||||
this.logger.error('Failed to start the client');
|
this.logger.error('Failed to start the client');
|
||||||
this.logger.error(err, err.stack);
|
this.logger.error(err, err.stack);
|
||||||
this.status = WAHASessionStatus.FAILED;
|
this.status = WAHASessionStatus.FAILED;
|
||||||
@@ -782,7 +785,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
async fetchContactProfilePicture(id: string): Promise<string> {
|
async fetchContactProfilePicture(id: string): Promise<string> {
|
||||||
const jid = toJID(this.ensureSuffix(id));
|
const jid = normalizeJid(toJID(this.ensureSuffix(id)));
|
||||||
const request = new messages.ProfilePictureRequest({
|
const request = new messages.ProfilePictureRequest({
|
||||||
jid: jid,
|
jid: jid,
|
||||||
session: this.session,
|
session: this.session,
|
||||||
@@ -914,10 +917,11 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
return data.id;
|
return data.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Activity()
|
||||||
async rejectCall(from: string, id: string): Promise<void> {
|
async rejectCall(from: string, id: string): Promise<void> {
|
||||||
const request = new messages.RejectCallRequest({
|
const request = new messages.RejectCallRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
from: toJID(this.ensureSuffix(from)),
|
from: normalizeJid(toJID(this.ensureSuffix(from))),
|
||||||
id: id,
|
id: id,
|
||||||
});
|
});
|
||||||
await promisify(this.client.RejectCall)(request);
|
await promisify(this.client.RejectCall)(request);
|
||||||
@@ -925,7 +929,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
async sendText(request: MessageTextRequest) {
|
async sendText(request: MessageTextRequest) {
|
||||||
const jid = toJID(this.ensureSuffix(request.chatId));
|
const jid = normalizeJid(toJID(this.ensureSuffix(request.chatId)));
|
||||||
const message = new messages.MessageRequest({
|
const message = new messages.MessageRequest({
|
||||||
jid: jid,
|
jid: jid,
|
||||||
text: request.text,
|
text: request.text,
|
||||||
@@ -933,7 +937,9 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
linkPreview: request.linkPreview ?? true,
|
linkPreview: request.linkPreview ?? true,
|
||||||
linkPreviewHighQuality: request.linkPreviewHighQuality,
|
linkPreviewHighQuality: request.linkPreviewHighQuality,
|
||||||
replyTo: getMessageIdFromSerialized(request.reply_to),
|
replyTo: getMessageIdFromSerialized(request.reply_to),
|
||||||
mentions: request.mentions?.map((mention) => toJID(mention)),
|
mentions: request.mentions?.map((mention) =>
|
||||||
|
normalizeJid(toJID(mention)),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
const response = await promisify(this.client.SendMessage)(message);
|
const response = await promisify(this.client.SendMessage)(message);
|
||||||
const data = response.toObject();
|
const data = response.toObject();
|
||||||
@@ -946,7 +952,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
messageId: string,
|
messageId: string,
|
||||||
request: EditMessageRequest,
|
request: EditMessageRequest,
|
||||||
) {
|
) {
|
||||||
const jid = toJID(this.ensureSuffix(chatId));
|
const jid = normalizeJid(toJID(this.ensureSuffix(chatId)));
|
||||||
const key = parseMessageIdSerialized(messageId, true);
|
const key = parseMessageIdSerialized(messageId, true);
|
||||||
const message = new messages.EditMessageRequest({
|
const message = new messages.EditMessageRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
@@ -963,8 +969,12 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
async sendContactVCard(request: MessageContactVcardRequest) {
|
async sendContactVCard(request: MessageContactVcardRequest) {
|
||||||
const jid = toJID(this.ensureSuffix(request.chatId));
|
const jid = normalizeJid(toJID(this.ensureSuffix(request.chatId)));
|
||||||
const contacts = request.contacts.map((el) => ({ vcard: toVcardV3(el) }));
|
const contacts = request.contacts.map((el) => ({
|
||||||
|
displayName:
|
||||||
|
(el as any).fullName || parseVCardV3(el.vcard || '').fullName,
|
||||||
|
vcard: toVcardV3(el),
|
||||||
|
}));
|
||||||
const message = new messages.MessageRequest({
|
const message = new messages.MessageRequest({
|
||||||
jid: jid,
|
jid: jid,
|
||||||
session: this.session,
|
session: this.session,
|
||||||
@@ -978,7 +988,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
async sendPoll(request: MessagePollRequest) {
|
async sendPoll(request: MessagePollRequest) {
|
||||||
const jid = toJID(request.chatId);
|
const jid = normalizeJid(toJID(request.chatId));
|
||||||
const message = new messages.MessageRequest({
|
const message = new messages.MessageRequest({
|
||||||
jid: jid,
|
jid: jid,
|
||||||
session: this.session,
|
session: this.session,
|
||||||
@@ -1004,7 +1014,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
public async deleteMessage(chatId: string, messageId: string) {
|
public async deleteMessage(chatId: string, messageId: string) {
|
||||||
const jid = toJID(this.ensureSuffix(chatId));
|
const jid = normalizeJid(toJID(this.ensureSuffix(chatId)));
|
||||||
const key = parseMessageIdSerialized(messageId);
|
const key = parseMessageIdSerialized(messageId);
|
||||||
const message = new messages.RevokeMessageRequest({
|
const message = new messages.RevokeMessageRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
@@ -1021,7 +1031,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
if (!contacts || contacts.length == 0) {
|
if (!contacts || contacts.length == 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return contacts.map(toJID);
|
return contacts.map((c) => normalizeJid(toJID(c)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
@@ -1077,6 +1087,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Activity()
|
||||||
async checkNumberStatus(
|
async checkNumberStatus(
|
||||||
request: CheckNumberStatusQuery,
|
request: CheckNumberStatusQuery,
|
||||||
): Promise<WANumberExistResult> {
|
): Promise<WANumberExistResult> {
|
||||||
@@ -1096,7 +1107,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
async sendLocation(request: MessageLocationRequest) {
|
async sendLocation(request: MessageLocationRequest) {
|
||||||
const jid = toJID(this.ensureSuffix(request.chatId));
|
const jid = normalizeJid(toJID(this.ensureSuffix(request.chatId)));
|
||||||
const message = new messages.MessageRequest({
|
const message = new messages.MessageRequest({
|
||||||
jid: jid,
|
jid: jid,
|
||||||
session: this.session,
|
session: this.session,
|
||||||
@@ -1180,7 +1191,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
const req = new messages.CreateGroupRequest({
|
const req = new messages.CreateGroupRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
name: request.name,
|
name: request.name,
|
||||||
participants: request.participants.map((p) => toJID(p.id)),
|
participants: request.participants.map((p) => normalizeJid(toJID(p.id))),
|
||||||
});
|
});
|
||||||
const response = await promisify(this.client.CreateGroup)(req);
|
const response = await promisify(this.client.CreateGroup)(req);
|
||||||
const data = parseJson(response);
|
const data = parseJson(response);
|
||||||
@@ -1355,7 +1366,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
participants: Array<Participant>,
|
participants: Array<Participant>,
|
||||||
action: messages.ParticipantAction,
|
action: messages.ParticipantAction,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const jids = participants.map((p) => toJID(p.id));
|
const jids = participants.map((p) => normalizeJid(toJID(p.id)));
|
||||||
const req = new messages.UpdateParticipantsRequest({
|
const req = new messages.UpdateParticipantsRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
jid: id,
|
jid: id,
|
||||||
@@ -1408,7 +1419,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
async sendEvent(request: EventMessageRequest): Promise<WAMessage> {
|
async sendEvent(request: EventMessageRequest): Promise<WAMessage> {
|
||||||
const jid = toJID(this.ensureSuffix(request.chatId));
|
const jid = normalizeJid(toJID(this.ensureSuffix(request.chatId)));
|
||||||
const event = request.event;
|
const event = request.event;
|
||||||
|
|
||||||
// Create EventLocation if provided
|
// Create EventLocation if provided
|
||||||
@@ -1464,7 +1475,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) {
|
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) {
|
||||||
let request: any;
|
let request: any;
|
||||||
let method: any;
|
let method: any;
|
||||||
const jid = chatId ? toJID(this.ensureSuffix(chatId)) : null;
|
const jid = chatId ? normalizeJid(toJID(this.ensureSuffix(chatId))) : null;
|
||||||
switch (presence) {
|
switch (presence) {
|
||||||
case WAHAPresenceStatus.ONLINE:
|
case WAHAPresenceStatus.ONLINE:
|
||||||
request = new messages.PresenceRequest({
|
request = new messages.PresenceRequest({
|
||||||
@@ -1525,7 +1536,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
|
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
|
||||||
const jid = toJID(chatId);
|
const jid = normalizeJid(toJID(chatId));
|
||||||
await this.subscribePresence(jid);
|
await this.subscribePresence(jid);
|
||||||
if (!(jid in this.presences.keys())) {
|
if (!(jid in this.presences.keys())) {
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
@@ -1536,7 +1547,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
async subscribePresence(chatId: string) {
|
async subscribePresence(chatId: string) {
|
||||||
const jid = toJID(chatId);
|
const jid = normalizeJid(toJID(chatId));
|
||||||
const req = new messages.SubscribePresenceRequest({
|
const req = new messages.SubscribePresenceRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
jid: jid,
|
jid: jid,
|
||||||
@@ -1735,7 +1746,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
*/
|
*/
|
||||||
@Activity()
|
@Activity()
|
||||||
public async upsertContact(chatId: string, body: ContactUpdateBody) {
|
public async upsertContact(chatId: string, body: ContactUpdateBody) {
|
||||||
const jid = toJID(chatId);
|
const jid = normalizeJid(toJID(chatId));
|
||||||
const request = new messages.UpdateContactRequest({
|
const request = new messages.UpdateContactRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
jid: jid,
|
jid: jid,
|
||||||
@@ -1755,7 +1766,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getContact(query: ContactQuery) {
|
public async getContact(query: ContactQuery) {
|
||||||
const jid = toJID(query.contactId);
|
const jid = normalizeJid(toJID(query.contactId));
|
||||||
const request = new messages.EntityByIdRequest({
|
const request = new messages.EntityByIdRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
id: jid,
|
id: jid,
|
||||||
@@ -1828,7 +1839,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
public async findLIDByPhoneNumber(
|
public async findLIDByPhoneNumber(
|
||||||
phoneNumber: string,
|
phoneNumber: string,
|
||||||
): Promise<LidToPhoneNumber> {
|
): Promise<LidToPhoneNumber> {
|
||||||
const pn = toJID(phoneNumber);
|
const pn = normalizeJid(toJID(phoneNumber));
|
||||||
const request = new messages.EntityByIdRequest({
|
const request = new messages.EntityByIdRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
id: pn,
|
id: pn,
|
||||||
@@ -1855,24 +1866,27 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
pagination.sortOrder = SortOrder.DESC;
|
pagination.sortOrder = SortOrder.DESC;
|
||||||
}
|
}
|
||||||
const chats = await this.getChats(pagination, filter);
|
const chats = await this.getChats(pagination, filter);
|
||||||
|
const merge = (pagination as GetChatsOverviewParams).merge ?? true;
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const chat of chats) {
|
for (const chat of chats) {
|
||||||
promises.push(this.fetchChatSummary(chat));
|
promises.push(this.fetchChatSummary(chat, merge));
|
||||||
}
|
}
|
||||||
const result = await Promise.all(promises);
|
const result = await Promise.all(promises);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async fetchChatSummary(chat): Promise<ChatSummary> {
|
protected async fetchChatSummary(chat, merge: boolean): Promise<ChatSummary> {
|
||||||
const id = toCusFormat(chat.id);
|
const id = toCusFormat(chat.id);
|
||||||
const name = chat.name;
|
const name = chat.name;
|
||||||
const picture = await this.getContactProfilePicture(chat.id, false);
|
const picture = await this.getContactProfilePicture(chat.id, false);
|
||||||
const messages = await this.getChatMessages(
|
const lastMessageQuery: GetChatMessagesQuery = {
|
||||||
chat.id,
|
limit: 1,
|
||||||
{ limit: 1, offset: 0, downloadMedia: false },
|
offset: 0,
|
||||||
{},
|
downloadMedia: false,
|
||||||
);
|
merge,
|
||||||
|
};
|
||||||
|
const messages = await this.getChatMessages(chat.id, lastMessageQuery, {});
|
||||||
const message = messages.length > 0 ? messages[0] : null;
|
const message = messages.length > 0 ? messages[0] : null;
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -1897,12 +1911,14 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
filter: OverviewFilter | null = null,
|
filter: OverviewFilter | null = null,
|
||||||
) {
|
) {
|
||||||
|
const merge =
|
||||||
|
(pagination as GetChatsParams | GetChatsOverviewParams).merge ?? true;
|
||||||
if (pagination.sortBy === ChatSortField.CONVERSATION_TIMESTAMP) {
|
if (pagination.sortBy === ChatSortField.CONVERSATION_TIMESTAMP) {
|
||||||
pagination.sortBy = 'timestamp';
|
pagination.sortBy = 'timestamp';
|
||||||
}
|
}
|
||||||
let jids = [];
|
let jids = [];
|
||||||
if (filter?.ids && filter.ids.length > 0) {
|
if (filter?.ids && filter.ids.length > 0) {
|
||||||
jids = filter.ids.map((id) => toJID(id));
|
jids = filter.ids.map((id) => normalizeJid(toJID(id)));
|
||||||
}
|
}
|
||||||
const request = new messages.GetChatsRequest({
|
const request = new messages.GetChatsRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
@@ -1920,6 +1936,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
filter: new messages.ChatFilter({
|
filter: new messages.ChatFilter({
|
||||||
jids: jids,
|
jids: jids,
|
||||||
}),
|
}),
|
||||||
|
merge: optional(merge, messages.OptionalBool),
|
||||||
});
|
});
|
||||||
const response = await promisify(this.client.GetChats)(request);
|
const response = await promisify(this.client.GetChats)(request);
|
||||||
const data = parseJsonList(response);
|
const data = parseJsonList(response);
|
||||||
@@ -1932,12 +1949,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
filter: GetChatMessagesFilter,
|
filter: GetChatMessagesFilter,
|
||||||
) {
|
) {
|
||||||
const downloadMedia = query.downloadMedia;
|
const downloadMedia = query.downloadMedia;
|
||||||
|
const merge = query.merge ?? true;
|
||||||
let jid: messages.OptionalString;
|
let jid: messages.OptionalString;
|
||||||
if (chatId === 'all') {
|
if (chatId === 'all') {
|
||||||
jid = null;
|
jid = null;
|
||||||
} else {
|
} else {
|
||||||
jid = new messages.OptionalString({
|
jid = new messages.OptionalString({
|
||||||
value: toJID(this.ensureSuffix(chatId)),
|
value: normalizeJid(toJID(this.ensureSuffix(chatId))),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1969,6 +1987,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
limit: query.limit,
|
limit: query.limit,
|
||||||
offset: query.offset,
|
offset: query.offset,
|
||||||
}),
|
}),
|
||||||
|
merge: optional(merge, messages.OptionalBool),
|
||||||
});
|
});
|
||||||
const response = await promisify(this.client.GetMessages)(request);
|
const response = await promisify(this.client.GetMessages)(request);
|
||||||
const msgs = parseJsonList(response);
|
const msgs = parseJsonList(response);
|
||||||
@@ -2085,7 +2104,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getChatLabels(chatId: string): Promise<Label[]> {
|
public async getChatLabels(chatId: string): Promise<Label[]> {
|
||||||
const jid = toJID(chatId);
|
const jid = normalizeJid(toJID(chatId));
|
||||||
const request = new messages.EntityByIdRequest({
|
const request = new messages.EntityByIdRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
id: jid,
|
id: jid,
|
||||||
@@ -2097,7 +2116,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
public async chatsUnreadChat(chatId: string): Promise<any> {
|
public async chatsUnreadChat(chatId: string): Promise<any> {
|
||||||
const jid = toJID(this.ensureSuffix(chatId));
|
const jid = normalizeJid(toJID(this.ensureSuffix(chatId)));
|
||||||
const request = new messages.ChatUnreadRequest({
|
const request = new messages.ChatUnreadRequest({
|
||||||
session: this.session,
|
session: this.session,
|
||||||
jid: jid,
|
jid: jid,
|
||||||
@@ -2109,7 +2128,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
public async putLabelsToChat(chatId: string, labels: LabelID[]) {
|
public async putLabelsToChat(chatId: string, labels: LabelID[]) {
|
||||||
const jid = toJID(chatId);
|
const jid = normalizeJid(toJID(chatId));
|
||||||
const labelsIds = labels.map((label) => label.id);
|
const labelsIds = labels.map((label) => label.id);
|
||||||
const currentLabels = await this.getChatLabels(jid);
|
const currentLabels = await this.getChatLabels(jid);
|
||||||
const currentLabelsIds = currentLabels.map((label) => label.id);
|
const currentLabelsIds = currentLabels.map((label) => label.id);
|
||||||
@@ -2177,6 +2196,16 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
const media = await this.downloadMediaSafe(message);
|
const media = await this.downloadMediaSafe(message);
|
||||||
wamessage.media = media;
|
wamessage.media = media;
|
||||||
}
|
}
|
||||||
|
if (downloadMedia && wamessage.replyTo?.hasMedia) {
|
||||||
|
const msg = {
|
||||||
|
Message: wamessage.replyTo._data,
|
||||||
|
Info: {
|
||||||
|
Chat: message.Info.Chat,
|
||||||
|
ID: wamessage.replyTo.id || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
wamessage.replyTo.media = await this.downloadMediaSafe(msg);
|
||||||
|
}
|
||||||
return wamessage;
|
return wamessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2386,10 +2415,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const body = extractBody(quotedMessage);
|
const body = extractBody(quotedMessage);
|
||||||
|
const mediaContent = extractMediaContent(quotedMessage);
|
||||||
return {
|
return {
|
||||||
id: contextInfo.stanzaID,
|
id: contextInfo.stanzaID,
|
||||||
participant: toCusFormat(contextInfo.participant),
|
participant: toCusFormat(contextInfo.participant),
|
||||||
body: body,
|
body: body,
|
||||||
|
hasMedia: Boolean(mediaContent),
|
||||||
|
media: null,
|
||||||
_data: quotedMessage,
|
_data: quotedMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,10 +64,19 @@ export function ToGroupParticipant(
|
|||||||
interface GroupParticipantUpdate {
|
interface GroupParticipantUpdate {
|
||||||
id: string;
|
id: string;
|
||||||
author: string;
|
author: string;
|
||||||
participants: string[];
|
participants: Array<string | NOWEBGroupParticipant>;
|
||||||
action: ParticipantAction;
|
action: ParticipantAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getParticipantId(
|
||||||
|
participant: string | NOWEBGroupParticipant,
|
||||||
|
): string | undefined {
|
||||||
|
if (typeof participant === 'string') {
|
||||||
|
return participant;
|
||||||
|
}
|
||||||
|
return participant?.id;
|
||||||
|
}
|
||||||
|
|
||||||
export function ToGroupV2Participants(
|
export function ToGroupV2Participants(
|
||||||
update: GroupParticipantUpdate,
|
update: GroupParticipantUpdate,
|
||||||
): GroupV2ParticipantsEvent {
|
): GroupV2ParticipantsEvent {
|
||||||
@@ -92,7 +101,8 @@ export function ToGroupV2Participants(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const participants: GroupParticipant[] = update.participants.map((id) => {
|
const participants: GroupParticipant[] = update.participants.map((item) => {
|
||||||
|
const id = getParticipantId(item);
|
||||||
return {
|
return {
|
||||||
id: toCusFormat(id),
|
id: toCusFormat(id),
|
||||||
role: role,
|
role: role,
|
||||||
@@ -131,7 +141,11 @@ export function ToGroupV2LeaveEvent(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const meId = esm.b.jidNormalizedUser(me.id);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Browsers, WABrowserDescription } from '@adiwajshing/baileys';
|
|
||||||
import makeWASocket, {
|
import makeWASocket, {
|
||||||
|
Browsers,
|
||||||
Chat,
|
Chat,
|
||||||
Contact,
|
Contact,
|
||||||
decryptPollVote,
|
decryptPollVote,
|
||||||
@@ -19,6 +19,7 @@ import makeWASocket, {
|
|||||||
PresenceData,
|
PresenceData,
|
||||||
proto,
|
proto,
|
||||||
SocketConfig,
|
SocketConfig,
|
||||||
|
WABrowserDescription,
|
||||||
WAMessageContent,
|
WAMessageContent,
|
||||||
WAMessageKey,
|
WAMessageKey,
|
||||||
WAMessageUpdate,
|
WAMessageUpdate,
|
||||||
@@ -91,6 +92,8 @@ import {
|
|||||||
GetChatMessageQuery,
|
GetChatMessageQuery,
|
||||||
GetChatMessagesFilter,
|
GetChatMessagesFilter,
|
||||||
GetChatMessagesQuery,
|
GetChatMessagesQuery,
|
||||||
|
GetChatsOverviewParams,
|
||||||
|
GetChatsParams,
|
||||||
OverviewFilter,
|
OverviewFilter,
|
||||||
PinDuration,
|
PinDuration,
|
||||||
ReadChatMessagesQuery,
|
ReadChatMessagesQuery,
|
||||||
@@ -181,13 +184,13 @@ import * as NodeCache from 'node-cache';
|
|||||||
import {
|
import {
|
||||||
filter,
|
filter,
|
||||||
fromEvent,
|
fromEvent,
|
||||||
|
groupBy,
|
||||||
identity,
|
identity,
|
||||||
merge,
|
merge,
|
||||||
mergeAll,
|
mergeAll,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
Observable,
|
Observable,
|
||||||
partition,
|
partition,
|
||||||
groupBy,
|
|
||||||
share,
|
share,
|
||||||
tap,
|
tap,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
@@ -906,6 +909,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
/**
|
/**
|
||||||
* Other methods
|
* Other methods
|
||||||
*/
|
*/
|
||||||
|
@Activity()
|
||||||
async checkNumberStatus(
|
async checkNumberStatus(
|
||||||
request: CheckNumberStatusQuery,
|
request: CheckNumberStatusQuery,
|
||||||
): Promise<WANumberExistResult> {
|
): Promise<WANumberExistResult> {
|
||||||
@@ -925,6 +929,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
return this.generateMessageID();
|
return this.generateMessageID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Activity()
|
||||||
async rejectCall(from: string, id: string): Promise<void> {
|
async rejectCall(from: string, id: string): Promise<void> {
|
||||||
const jid = toJID(this.ensureSuffix(from));
|
const jid = toJID(this.ensureSuffix(from));
|
||||||
await this.sock.rejectCall(id, jid);
|
await this.sock.rejectCall(id, jid);
|
||||||
@@ -954,24 +959,63 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Activity()
|
@Activity()
|
||||||
public editMessage(
|
public async editMessage(
|
||||||
chatId: string,
|
chatId: string,
|
||||||
messageId: string,
|
messageId: string,
|
||||||
request: EditMessageRequest,
|
request: EditMessageRequest,
|
||||||
) {
|
) {
|
||||||
const jid = toJID(this.ensureSuffix(chatId));
|
const jid = toJID(this.ensureSuffix(chatId));
|
||||||
const key = parseMessageIdSerialized(messageId);
|
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,
|
text: request.text,
|
||||||
mentions: request.mentions?.map(toJID),
|
mentions: request.mentions?.map(toJID),
|
||||||
edit: key,
|
edit: key,
|
||||||
|
editedMessage: editedMessage,
|
||||||
linkPreview: this.getLinkPreview(request),
|
linkPreview: this.getLinkPreview(request),
|
||||||
linkPreviewHighQuality: request.linkPreviewHighQuality,
|
linkPreviewHighQuality: request.linkPreviewHighQuality,
|
||||||
};
|
};
|
||||||
const options = {
|
const options = {
|
||||||
messageId: this.generateMessageID(),
|
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()
|
@Activity()
|
||||||
@@ -1136,10 +1180,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
) {
|
) {
|
||||||
const downloadMedia = query.downloadMedia;
|
const downloadMedia = query.downloadMedia;
|
||||||
const pagination = query as PaginationParams;
|
const pagination = query as PaginationParams;
|
||||||
|
const merge = query.merge ?? true;
|
||||||
const messages = await this.store.getMessagesByJid(
|
const messages = await this.store.getMessagesByJid(
|
||||||
toJID(chatId),
|
toJID(chatId),
|
||||||
filter,
|
filter,
|
||||||
pagination,
|
pagination,
|
||||||
|
merge,
|
||||||
);
|
);
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
@@ -1165,7 +1211,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
query: GetChatMessageQuery,
|
query: GetChatMessageQuery,
|
||||||
): Promise<null | WAMessage> {
|
): Promise<null | WAMessage> {
|
||||||
const key = parseMessageIdSerialized(messageId, true);
|
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;
|
if (!message) return null;
|
||||||
return await this.processIncomingMessage(message, query.downloadMedia);
|
return await this.processIncomingMessage(message, query.downloadMedia);
|
||||||
}
|
}
|
||||||
@@ -1252,7 +1303,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async getChats(pagination: PaginationParams) {
|
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
|
// Remove unreadCount, it's not ready yet
|
||||||
chats.forEach((chat) => delete chat.unreadCount);
|
chats.forEach((chat) => delete chat.unreadCount);
|
||||||
return chats;
|
return chats;
|
||||||
@@ -1262,6 +1314,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
filter?: OverviewFilter,
|
filter?: OverviewFilter,
|
||||||
): Promise<ChatSummary[]> {
|
): Promise<ChatSummary[]> {
|
||||||
|
const merge = (pagination as GetChatsOverviewParams).merge ?? true;
|
||||||
// Convert customer format IDs to JID format if filter is provided
|
// Convert customer format IDs to JID format if filter is provided
|
||||||
let jidFilter;
|
let jidFilter;
|
||||||
if (filter?.ids && filter.ids.length > 0) {
|
if (filter?.ids && filter.ids.length > 0) {
|
||||||
@@ -1270,19 +1323,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
|
// Remove unreadCount, it's not ready yet
|
||||||
chats.forEach((chat) => delete chat.unreadCount);
|
chats.forEach((chat) => delete chat.unreadCount);
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const chat of chats) {
|
for (const chat of chats) {
|
||||||
promises.push(this.fetchChatSummary(chat));
|
promises.push(this.fetchChatSummary(chat, merge));
|
||||||
}
|
}
|
||||||
const result = await Promise.all(promises);
|
const result = await Promise.all(promises);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async fetchChatSummary(chat: Chat): Promise<ChatSummary> {
|
protected async fetchChatSummary(
|
||||||
|
chat: Chat,
|
||||||
|
merge: boolean,
|
||||||
|
): Promise<ChatSummary> {
|
||||||
const id = toCusFormat(chat.id);
|
const id = toCusFormat(chat.id);
|
||||||
let name = chat.name;
|
let name = chat.name;
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -1292,11 +1353,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
name = contact?.name || contact?.notify;
|
name = contact?.name || contact?.notify;
|
||||||
}
|
}
|
||||||
const picture = await this.getContactProfilePicture(chat.id, false);
|
const picture = await this.getContactProfilePicture(chat.id, false);
|
||||||
const messages = await this.getChatMessages(
|
const lastMessageQuery: GetChatMessagesQuery = {
|
||||||
chat.id,
|
limit: 1,
|
||||||
{ limit: 1, offset: 0, downloadMedia: false },
|
offset: 0,
|
||||||
{},
|
downloadMedia: false,
|
||||||
);
|
merge: merge,
|
||||||
|
};
|
||||||
|
const messages = await this.getChatMessages(chat.id, lastMessageQuery, {});
|
||||||
const message = messages.length > 0 ? messages[0] : null;
|
const message = messages.length > 0 ? messages[0] : null;
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -2358,13 +2421,16 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
shouldProcessIncomingMessage(message): boolean {
|
shouldProcessIncomingMessage(message): boolean {
|
||||||
// if there is no text or media message
|
// if there is no text or media message
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
if (!message.message) return;
|
// View-once (self-destructing) messages arrive with key.isViewOnce=true but
|
||||||
|
// no message content (burned by sender). Allow them through so a webhook
|
||||||
|
// is still fired with key/timestamp metadata.
|
||||||
|
if (!message.message && !message.key?.isViewOnce) return;
|
||||||
// Ignore reactions, we have dedicated handler for that
|
// Ignore reactions, we have dedicated handler for that
|
||||||
if (message.message.reactionMessage) return;
|
if (message.message?.reactionMessage) return;
|
||||||
// Ignore poll votes, we have dedicated handler for that
|
// Ignore poll votes, we have dedicated handler for that
|
||||||
if (message.message.pollUpdateMessage) return;
|
if (message.message?.pollUpdateMessage) return;
|
||||||
// Ignore calls, we have dedicated handler for that
|
// Ignore calls, we have dedicated handler for that
|
||||||
if (message.message.call?.callKey) return;
|
if (message.message?.call?.callKey) return;
|
||||||
// Ignore revoke, we have a dedicated event for that
|
// Ignore revoke, we have a dedicated event for that
|
||||||
if (
|
if (
|
||||||
message.message?.protocolMessage?.type ===
|
message.message?.protocolMessage?.type ===
|
||||||
@@ -2417,9 +2483,24 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Media
|
// Media
|
||||||
if (downloadMedia) {
|
if (downloadMedia && wamessage.hasMedia) {
|
||||||
const media = await this.downloadMediaSafe(message);
|
wamessage.media = await this.downloadMediaSafe(message);
|
||||||
wamessage.media = media;
|
}
|
||||||
|
|
||||||
|
if (downloadMedia && wamessage.replyTo?.hasMedia) {
|
||||||
|
const mediaContent = extractMediaContent(wamessage.replyTo._data);
|
||||||
|
const m = {
|
||||||
|
message: wamessage.replyTo._data,
|
||||||
|
key: {
|
||||||
|
id:
|
||||||
|
wamessage.replyTo.id ||
|
||||||
|
mediaContent.fileSha256 ||
|
||||||
|
mediaContent.fileEncSha256 ||
|
||||||
|
mediaContent.mediaKeyTimestamp,
|
||||||
|
remoteJid: message.key.remoteJid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
wamessage.replyTo.media = await this.downloadMediaSafe(m);
|
||||||
}
|
}
|
||||||
return wamessage;
|
return wamessage;
|
||||||
}
|
}
|
||||||
@@ -2468,6 +2549,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected extractReplyTo(message): ReplyToMessage | null {
|
protected extractReplyTo(message): ReplyToMessage | null {
|
||||||
|
if (!message) return null;
|
||||||
const msgType = getContentType(message);
|
const msgType = getContentType(message);
|
||||||
const contextInfo = message[msgType]?.contextInfo;
|
const contextInfo = message[msgType]?.contextInfo;
|
||||||
if (!contextInfo) {
|
if (!contextInfo) {
|
||||||
@@ -2478,10 +2560,15 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const body = extractBody(quotedMessage);
|
const body = extractBody(quotedMessage);
|
||||||
|
const mediaContent = extractMediaContent(quotedMessage);
|
||||||
return {
|
return {
|
||||||
id: contextInfo.stanzaId,
|
id: contextInfo.stanzaId,
|
||||||
participant: toCusFormat(contextInfo.participant),
|
participant: toCusFormat(contextInfo.participant),
|
||||||
body: body,
|
body: body,
|
||||||
|
// Media
|
||||||
|
hasMedia: Boolean(mediaContent),
|
||||||
|
media: null,
|
||||||
|
// Data
|
||||||
_data: quotedMessage,
|
_data: quotedMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2782,9 +2869,12 @@ export class NOWEBEngineMediaProcessor implements IMediaEngineProcessor<any> {
|
|||||||
content.url = null;
|
content.url = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (await downloadMediaMessage(
|
// Use 'stream' mode instead of 'buffer' to fix 0-byte audio files
|
||||||
|
// 'buffer' mode silently returns empty buffer for audio/voice messages
|
||||||
|
// See: https://github.com/devlikeapro/waha/issues/1996
|
||||||
|
const stream = await downloadMediaMessage(
|
||||||
message,
|
message,
|
||||||
'buffer',
|
'stream',
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
@@ -2793,7 +2883,12 @@ export class NOWEBEngineMediaProcessor implements IMediaEngineProcessor<any> {
|
|||||||
).finally(() => {
|
).finally(() => {
|
||||||
// Set url back in case we removed it
|
// Set url back in case we removed it
|
||||||
content.url = url;
|
content.url = url;
|
||||||
})) as Buffer;
|
});
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
return Buffer.concat(chunks);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilename(message: any): string | null {
|
getFilename(message: any): string | null {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface IChatRepository {
|
|||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
broadcast: boolean,
|
broadcast: boolean,
|
||||||
filter?: OverviewFilter,
|
filter?: OverviewFilter,
|
||||||
|
merge?: boolean,
|
||||||
): Promise<Chat[]>;
|
): Promise<Chat[]>;
|
||||||
|
|
||||||
getById(id: string): Promise<Chat | null>;
|
getById(id: string): Promise<Chat | null>;
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ export interface IMessagesRepository {
|
|||||||
jid: string,
|
jid: string,
|
||||||
filter: GetChatMessagesFilter,
|
filter: GetChatMessagesFilter,
|
||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
|
merge?: boolean,
|
||||||
): Promise<any[]>;
|
): 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>;
|
updateByJidAndId(jid: string, id: string, update: any): Promise<boolean>;
|
||||||
|
|
||||||
|
|||||||
@@ -31,14 +31,20 @@ export interface INowebStore {
|
|||||||
chatId: string,
|
chatId: string,
|
||||||
filter: GetChatMessagesFilter,
|
filter: GetChatMessagesFilter,
|
||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
|
merge?: boolean,
|
||||||
): Promise<any>;
|
): Promise<any>;
|
||||||
|
|
||||||
getMessageById(chatId: string, messageId: string): Promise<any>;
|
getMessageById(
|
||||||
|
chatId: string,
|
||||||
|
messageId: string,
|
||||||
|
merge?: boolean,
|
||||||
|
): Promise<any>;
|
||||||
|
|
||||||
getChats(
|
getChats(
|
||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
broadcast: boolean,
|
broadcast: boolean,
|
||||||
filter?: OverviewFilter,
|
filter?: OverviewFilter,
|
||||||
|
merge?: boolean,
|
||||||
): Promise<Chat[]>;
|
): Promise<Chat[]>;
|
||||||
|
|
||||||
getChat(jid: string): Promise<Chat | null>;
|
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 makeWASocket from '@adiwajshing/baileys';
|
||||||
import type { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
import type { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||||
import { BadRequestException } from '@nestjs/common';
|
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 { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||||
import {
|
import {
|
||||||
LimitOffsetParams,
|
LimitOffsetParams,
|
||||||
@@ -57,15 +60,25 @@ export class NowebInMemoryStore implements INowebStore {
|
|||||||
chatId: string,
|
chatId: string,
|
||||||
filter: GetChatMessagesFilter,
|
filter: GetChatMessagesFilter,
|
||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
|
merge?: boolean,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
throw new BadRequestException(this.errorMessage);
|
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);
|
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);
|
throw new BadRequestException(this.errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { INowebStorage } from './INowebStorage';
|
|||||||
import { INowebStore } from './INowebStore';
|
import { INowebStore } from './INowebStore';
|
||||||
import { LabelAssociationType } from '../labels/LabelAssociationType';
|
import { LabelAssociationType } from '../labels/LabelAssociationType';
|
||||||
import esm from '@waha/vendor/esm';
|
import esm from '@waha/vendor/esm';
|
||||||
|
import { StatusStringToStatus } from '@waha/core/utils/acks';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const AsyncLock = require('async-lock');
|
const AsyncLock = require('async-lock');
|
||||||
@@ -130,7 +131,7 @@ export class NowebPersistentStore implements INowebStore {
|
|||||||
return {
|
return {
|
||||||
id: message.key.remoteJid,
|
id: message.key.remoteJid,
|
||||||
lid: lid,
|
lid: lid,
|
||||||
jid: pn,
|
phoneNumber: pn,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
@@ -249,6 +250,9 @@ export class NowebPersistentStore implements INowebStore {
|
|||||||
private async syncMessagesHistory(messages) {
|
private async syncMessagesHistory(messages) {
|
||||||
const realMessages = messages.filter(esm.b.isRealMessage);
|
const realMessages = messages.filter(esm.b.isRealMessage);
|
||||||
messages = messages.filter((msg) => this.jids.include(msg.key.remoteJid));
|
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);
|
await this.messagesRepo.upsert(realMessages);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`history sync - '${messages.length}' got messages, '${realMessages.length}' real messages`,
|
`history sync - '${messages.length}' got messages, '${realMessages.length}' real messages`,
|
||||||
@@ -288,7 +292,11 @@ export class NowebPersistentStore implements INowebStore {
|
|||||||
);
|
);
|
||||||
continue;
|
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) {
|
if (!message) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`got update for non-existent message. update: '${JSON.stringify(
|
`got update for non-existent message. update: '${JSON.stringify(
|
||||||
@@ -588,6 +596,9 @@ export class NowebPersistentStore implements INowebStore {
|
|||||||
data = await this.messagesRepo.getById(id);
|
data = await this.messagesRepo.getById(id);
|
||||||
} else {
|
} else {
|
||||||
data = await this.messagesRepo.getByJidById(jid, id);
|
data = await this.messagesRepo.getByJidById(jid, id);
|
||||||
|
if (!data) {
|
||||||
|
data = await this.messagesRepo.getById(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
@@ -599,24 +610,35 @@ export class NowebPersistentStore implements INowebStore {
|
|||||||
chatId: string,
|
chatId: string,
|
||||||
filter: GetChatMessagesFilter,
|
filter: GetChatMessagesFilter,
|
||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
|
merge: boolean = true,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
pagination.sortBy = 'messageTimestamp';
|
pagination.sortBy = 'messageTimestamp';
|
||||||
pagination.sortOrder = pagination.sortOrder || SortOrder.DESC;
|
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> {
|
getMessageById(
|
||||||
return this.messagesRepo.getByJidById(chatId, messageId);
|
chatId: string,
|
||||||
|
messageId: string,
|
||||||
|
merge: boolean = true,
|
||||||
|
): Promise<any> {
|
||||||
|
return this.messagesRepo.getByJidById(chatId, messageId, merge);
|
||||||
}
|
}
|
||||||
|
|
||||||
getChats(
|
getChats(
|
||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
broadcast: boolean,
|
broadcast: boolean,
|
||||||
filter?: OverviewFilter,
|
filter?: OverviewFilter,
|
||||||
|
merge: boolean = true,
|
||||||
): Promise<Chat[]> {
|
): Promise<Chat[]> {
|
||||||
pagination.sortBy ||= 'conversationTimestamp';
|
pagination.sortBy ||= 'conversationTimestamp';
|
||||||
pagination.sortOrder ||= SortOrder.DESC;
|
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> {
|
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 { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
|
||||||
import { OverviewFilter } from '@waha/structures/chats.dto';
|
import { OverviewFilter } from '@waha/structures/chats.dto';
|
||||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
export class SqlChatMethods {
|
export class SqlChatMethods {
|
||||||
constructor(private repository: SqlKVRepository<any>) {}
|
constructor(private repository: SqlKVRepository<any>) {}
|
||||||
@@ -10,23 +11,112 @@ export class SqlChatMethods {
|
|||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
broadcast: boolean,
|
broadcast: boolean,
|
||||||
filter?: OverviewFilter,
|
filter?: OverviewFilter,
|
||||||
|
merge: boolean = true,
|
||||||
): Promise<Chat[]> {
|
): Promise<Chat[]> {
|
||||||
// Get chats with conversationTimestamp is not Null
|
const knex = this.repository.getKnex();
|
||||||
let query = this.repository.select().whereNotNull('conversationTimestamp');
|
const tableName = this.repository.table;
|
||||||
|
let baseQuery = this.repository
|
||||||
|
.select()
|
||||||
|
.whereNotNull(`${tableName}.conversationTimestamp`);
|
||||||
|
|
||||||
if (!broadcast) {
|
if (!merge) {
|
||||||
// filter out chat by id if it ends at @newsletter or @broadcast
|
baseQuery = this.applyNonMergedFilters(baseQuery, tableName, {
|
||||||
query = query
|
broadcast,
|
||||||
.andWhereNot('id', 'like', '%@broadcast')
|
filter,
|
||||||
.andWhereNot('id', 'like', '%@newsletter');
|
});
|
||||||
|
const pagedQuery = this.repository.pagination(baseQuery, pagination);
|
||||||
|
const rows = await pagedQuery;
|
||||||
|
return rows.map((row) => this.repository.parse(row));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by IDs if provided
|
const annotatedQuery = this.annotateWithPnJid(knex, baseQuery, {
|
||||||
if (filter?.ids && filter.ids.length > 0) {
|
tableName,
|
||||||
query = query.whereIn('id', filter.ids);
|
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);
|
if (opts.filter?.ids && opts.filter.ids.length > 0) {
|
||||||
return await this.repository.all(query);
|
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 { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
|
||||||
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
|
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
|
||||||
import { AckToStatus } from '@waha/core/utils/acks';
|
import { AckToStatus } from '@waha/core/utils/acks';
|
||||||
|
import { isLidUser } from '@waha/core/utils/jids';
|
||||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { INowebLidPNRepository } from '../INowebLidPNRepository';
|
||||||
|
|
||||||
export class SqlMessagesMethods {
|
export class SqlMessagesMethods {
|
||||||
constructor(private repository: SqlKVRepository<any>) {}
|
constructor(
|
||||||
|
private repository: SqlKVRepository<any>,
|
||||||
|
private lidRepository?: INowebLidPNRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
upsert(messages: any[]): Promise<void> {
|
upsert(messages: any[]): Promise<void> {
|
||||||
return this.repository.upsertMany(messages);
|
return this.repository.upsertMany(messages);
|
||||||
@@ -15,10 +21,17 @@ export class SqlMessagesMethods {
|
|||||||
jid: string,
|
jid: string,
|
||||||
filter: GetChatMessagesFilter,
|
filter: GetChatMessagesFilter,
|
||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
|
merge: boolean = true,
|
||||||
): Promise<any[]> {
|
): Promise<any[]> {
|
||||||
let query = this.repository.select();
|
let query = this.repository.select();
|
||||||
|
const tableName = this.repository.table;
|
||||||
if (jid !== ALL_JID) {
|
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) {
|
if (filter['filter.timestamp.lte'] != null) {
|
||||||
query = query.where(
|
query = query.where(
|
||||||
@@ -34,10 +47,11 @@ export class SqlMessagesMethods {
|
|||||||
filter['filter.timestamp.gte'],
|
filter['filter.timestamp.gte'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const dataColumn = `${this.repository.table}.data`;
|
||||||
if (filter['filter.fromMe'] != null) {
|
if (filter['filter.fromMe'] != null) {
|
||||||
// filter by data json inside
|
// filter by data json inside
|
||||||
const [sql, value] = this.repository.filterJson(
|
const [sql, value] = this.repository.filterJson(
|
||||||
'data',
|
dataColumn,
|
||||||
'key.fromMe',
|
'key.fromMe',
|
||||||
filter['filter.fromMe'],
|
filter['filter.fromMe'],
|
||||||
);
|
);
|
||||||
@@ -45,18 +59,39 @@ export class SqlMessagesMethods {
|
|||||||
}
|
}
|
||||||
if (filter['filter.ack'] != null) {
|
if (filter['filter.ack'] != null) {
|
||||||
const status = AckToStatus(filter['filter.ack']);
|
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 = query.whereRaw(sql, [value]);
|
||||||
}
|
}
|
||||||
query = this.repository.pagination(query, pagination);
|
query = this.repository.pagination(query, pagination);
|
||||||
return this.repository.all(query);
|
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) {
|
if (jid === ALL_JID) {
|
||||||
return this.repository.getBy({ id: id });
|
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(
|
async updateByJidAndId(
|
||||||
@@ -83,4 +118,41 @@ export class SqlMessagesMethods {
|
|||||||
deleteAllByJid(jid: string): Promise<void> {
|
deleteAllByJid(jid: string): Promise<void> {
|
||||||
return this.repository.deleteBy({ jid: jid });
|
return this.repository.deleteBy({ jid: jid });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyPnJidFilter(
|
||||||
|
query: Knex.QueryBuilder,
|
||||||
|
pnJid: string,
|
||||||
|
): Knex.QueryBuilder {
|
||||||
|
const tableName = this.repository.table;
|
||||||
|
const knex = this.repository.getKnex();
|
||||||
|
// Use OR conditions on the raw jid column so the (jid, messageTimestamp) index
|
||||||
|
// can be used for both branches instead of a per-row CASE expression.
|
||||||
|
return query.where((builder) => {
|
||||||
|
builder
|
||||||
|
.where(`${tableName}.jid`, pnJid)
|
||||||
|
.orWhereIn(
|
||||||
|
`${tableName}.jid`,
|
||||||
|
knex.select('id').from('lid_map').where('pn', 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ export class NOWEBSqlite3KVRepository<
|
|||||||
return JSON.stringify(data, esm.b.BufferJSON.replacer);
|
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);
|
return JSON.parse(row.data, esm.b.BufferJSON.reviver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,13 @@ export class Sqlite3ChatRepository
|
|||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
broadcast: boolean,
|
broadcast: boolean,
|
||||||
filter?: OverviewFilter,
|
filter?: OverviewFilter,
|
||||||
|
merge?: boolean,
|
||||||
): Promise<Chat[]> {
|
): 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 { IMessagesRepository } from '../IMessagesRepository';
|
||||||
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
|
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
|
||||||
|
import { INowebLidPNRepository } from '../INowebLidPNRepository';
|
||||||
|
import Knex from 'knex';
|
||||||
|
|
||||||
export class Sqlite3MessagesRepository
|
export class Sqlite3MessagesRepository
|
||||||
extends NOWEBSqlite3KVRepository<any>
|
extends NOWEBSqlite3KVRepository<any>
|
||||||
implements IMessagesRepository
|
implements IMessagesRepository
|
||||||
{
|
{
|
||||||
|
constructor(
|
||||||
|
knex: Knex.Knex,
|
||||||
|
private readonly lidRepository: INowebLidPNRepository,
|
||||||
|
) {
|
||||||
|
super(knex);
|
||||||
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return NowebMessagesSchema;
|
return NowebMessagesSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
get methods() {
|
get methods() {
|
||||||
return new SqlMessagesMethods(this);
|
return new SqlMessagesMethods(this, this.lidRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
get metadata() {
|
get metadata() {
|
||||||
@@ -31,12 +40,13 @@ export class Sqlite3MessagesRepository
|
|||||||
jid: string,
|
jid: string,
|
||||||
filter: GetChatMessagesFilter,
|
filter: GetChatMessagesFilter,
|
||||||
pagination: PaginationParams,
|
pagination: PaginationParams,
|
||||||
|
merge?: boolean,
|
||||||
): Promise<any[]> {
|
): 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> {
|
async getByJidById(jid: string, id: string, merge?: boolean): Promise<any> {
|
||||||
return this.methods.getByJidById(jid, id);
|
return this.methods.getByJidById(jid, id, merge);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateByJidAndId(
|
async updateByJidAndId(
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { KNEX_SQLITE_CLIENT } from '@waha/core/env';
|
|||||||
export class Sqlite3Storage extends INowebStorage {
|
export class Sqlite3Storage extends INowebStorage {
|
||||||
private readonly tables: Schema[];
|
private readonly tables: Schema[];
|
||||||
private readonly knex: Knex.Knex;
|
private readonly knex: Knex.Knex;
|
||||||
|
private lidRepository: INowebLidPNRepository | null = null;
|
||||||
|
|
||||||
constructor(filePath: string) {
|
constructor(filePath: string) {
|
||||||
super();
|
super();
|
||||||
@@ -84,10 +85,17 @@ export class Sqlite3Storage extends INowebStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getMessagesRepository() {
|
getMessagesRepository() {
|
||||||
return new Sqlite3MessagesRepository(this.knex);
|
return new Sqlite3MessagesRepository(this.knex, this.getLidRepository());
|
||||||
}
|
}
|
||||||
|
|
||||||
getLidPNRepository(): INowebLidPNRepository {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,12 @@ export function extractMediaContent(
|
|||||||
content?.videoMessage ||
|
content?.videoMessage ||
|
||||||
content?.audioMessage ||
|
content?.audioMessage ||
|
||||||
content?.ptvMessage ||
|
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) {
|
if (mediaContent) {
|
||||||
return mediaContent;
|
return mediaContent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
/**
|
|
||||||
* Expose a function to the page if it does not exist
|
|
||||||
*
|
|
||||||
* NOTE:
|
|
||||||
* Rewrite it to 'upsertFunction' after updating Puppeteer to 20.6 or higher
|
|
||||||
* using page.removeExposedFunction
|
|
||||||
* https://pptr.dev/api/puppeteer.page.removeExposedFunction
|
|
||||||
*
|
|
||||||
* @param {import(puppeteer).Page} page
|
|
||||||
* @param {string} name
|
|
||||||
* @param {Function} fn
|
|
||||||
*/
|
|
||||||
export async function exposeFunctionIfAbsent(page, name, fn) {
|
|
||||||
const exist = await page.evaluate((name) => {
|
|
||||||
return !!window[name];
|
|
||||||
}, name);
|
|
||||||
if (exist) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await page.exposeFunction(name, fn);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { exposeFunctionIfAbsent } from '@waha/core/engines/webjs/Puppeteer';
|
|
||||||
import { WebJSPresence } from '@waha/core/engines/webjs/types';
|
import { WebJSPresence } from '@waha/core/engines/webjs/types';
|
||||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||||
import { Label } from '@waha/structures/labels.dto';
|
import { Label } from '@waha/structures/labels.dto';
|
||||||
@@ -26,6 +25,11 @@ const { LoadPaginator } = require('./_Paginator.js');
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
|
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const {
|
||||||
|
exposeFunctionIfAbsent,
|
||||||
|
} = require('whatsapp-web.js/src/util/Puppeteer');
|
||||||
|
|
||||||
export class WebjsClientCore extends Client {
|
export class WebjsClientCore extends Client {
|
||||||
public events = new EventEmitter();
|
public events = new EventEmitter();
|
||||||
private wpage: WPage = null;
|
private wpage: WPage = null;
|
||||||
@@ -300,25 +304,132 @@ export class WebjsClientCore extends Client {
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
|
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
|
||||||
let msgs = chat.msgs.getModelsArray().filter(msgFilter);
|
if (!chat) return [];
|
||||||
|
|
||||||
while (msgs.length < pagination.limit + pagination.offset) {
|
let msgs = [];
|
||||||
const loadedMessages =
|
|
||||||
|
// Try new WhatsApp API (>= 2.3000.1034162388) which replaced loadEarlierMsgs
|
||||||
|
// @ts-ignore
|
||||||
|
const WAWebDBMessageFindLocal = window.require(
|
||||||
|
'WAWebDBMessageFindLocal',
|
||||||
|
);
|
||||||
|
if (WAWebDBMessageFindLocal?.msgFindByDirection) {
|
||||||
|
const BATCH_SIZE = 20;
|
||||||
|
|
||||||
|
// Construct the initial anchor the same way wa-js does:
|
||||||
|
// serialize to string then reconstruct via MsgKey.fromString so the
|
||||||
|
// object has the exact shape msgFindByDirection expects.
|
||||||
|
const lastReceivedSerialized = chat.lastReceivedKey?._serialized;
|
||||||
|
if (!lastReceivedSerialized) return [];
|
||||||
|
// @ts-ignore
|
||||||
|
let currentAnchorKey = window.Store.MsgKey.fromString(
|
||||||
|
lastReceivedSerialized,
|
||||||
|
);
|
||||||
|
|
||||||
|
// msgFindByDirection is exclusive of the anchor; include the anchor
|
||||||
|
// message itself (the most recent message in the chat) upfront
|
||||||
|
// @ts-ignore
|
||||||
|
const anchorMsg = window.Store.Msg.get(lastReceivedSerialized);
|
||||||
|
if (anchorMsg) {
|
||||||
|
msgs.push(anchorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const neededFiltered = Number.isFinite(
|
||||||
|
pagination.limit + pagination.offset,
|
||||||
|
)
|
||||||
|
? pagination.limit + pagination.offset
|
||||||
|
: Infinity;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const toModel = (m) => {
|
||||||
|
if (m && typeof m.serialize === 'function') return m;
|
||||||
|
const serializedId = m?.id?._serialized;
|
||||||
|
if (serializedId) {
|
||||||
|
// @ts-ignore
|
||||||
|
const stored = window.Store.Msg.get(serializedId);
|
||||||
|
if (stored) return stored;
|
||||||
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await window.Store.ConversationMsgs.loadEarlierMsgs(chat);
|
return new window.Store.Msg.modelClass(m);
|
||||||
if (!loadedMessages || loadedMessages.length == 0) break;
|
};
|
||||||
|
|
||||||
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
|
while (true) {
|
||||||
msgs = msgs.sort((a, b) => b.t - a.t);
|
const result = await WAWebDBMessageFindLocal.msgFindByDirection({
|
||||||
|
anchor: currentAnchorKey,
|
||||||
|
count: BATCH_SIZE,
|
||||||
|
direction: 'before',
|
||||||
|
});
|
||||||
|
|
||||||
// Check if the earliest message is already outside the timerange filter
|
const batch = Array.isArray(result)
|
||||||
const earliest = msgs[msgs.length - 1];
|
? result
|
||||||
if (earliest.t < (filter['filter.timestamp.gte'] || Infinity)) {
|
: result?.messages || [];
|
||||||
// Add only messages that pass the filter and stop loading more
|
if (!batch || batch.length === 0) break;
|
||||||
break;
|
|
||||||
|
const batchModels = batch.map(toModel);
|
||||||
|
|
||||||
|
// msgFindByDirection returns newest-first (descending) so prepend
|
||||||
|
// to keep the accumulated list in approximate ascending order
|
||||||
|
msgs = [...batchModels, ...msgs];
|
||||||
|
|
||||||
|
// Deduplicate by serialized id — the same Backbone model object can
|
||||||
|
// appear in multiple batches when anchors overlap
|
||||||
|
const seenIds = new Set();
|
||||||
|
msgs = msgs.filter((m) => {
|
||||||
|
const sid = m?.id?._serialized;
|
||||||
|
if (!sid || seenIds.has(sid)) return false;
|
||||||
|
seenIds.add(sid);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stop once we have enough messages that pass the filter
|
||||||
|
if (msgs.filter(msgFilter).length >= neededFiltered) break;
|
||||||
|
|
||||||
|
// Stop if the oldest message in this batch is already before the
|
||||||
|
// lower timestamp bound - no earlier messages can be in range
|
||||||
|
if (filter['filter.timestamp.gte'] != null) {
|
||||||
|
const batchMinT = batchModels.reduce(
|
||||||
|
(min, m) => Math.min(min, m.t ?? Infinity),
|
||||||
|
Infinity,
|
||||||
|
);
|
||||||
|
if (batchMinT < filter['filter.timestamp.gte']) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No more messages available
|
||||||
|
if (batch.length < BATCH_SIZE) break;
|
||||||
|
|
||||||
|
// msgFindByDirection returns newest-first, so the LAST element is
|
||||||
|
// the oldest message in this batch — use it as the next anchor to
|
||||||
|
// walk further back in history without overlap
|
||||||
|
const oldestInBatch = batchModels[batchModels.length - 1];
|
||||||
|
const oldestSerialized = oldestInBatch?.id?._serialized;
|
||||||
|
if (!oldestSerialized) break;
|
||||||
|
// @ts-ignore
|
||||||
|
currentAnchorKey = window.Store.MsgKey.fromString(oldestSerialized);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Legacy fallback: loadEarlierMsgs loop
|
||||||
|
msgs = chat.msgs.getModelsArray();
|
||||||
|
while (msgs.length < pagination.limit + pagination.offset) {
|
||||||
|
const loadedMessages =
|
||||||
|
// @ts-ignore
|
||||||
|
await window.Store.ConversationMsgs.loadEarlierMsgs(
|
||||||
|
chat,
|
||||||
|
chat.msgs,
|
||||||
|
);
|
||||||
|
if (!loadedMessages || loadedMessages.length == 0) break;
|
||||||
|
|
||||||
|
msgs = [...loadedMessages, ...msgs];
|
||||||
|
msgs = msgs.sort((a, b) => b.t - a.t);
|
||||||
|
|
||||||
|
const earliest = msgs[msgs.length - 1];
|
||||||
|
if (earliest.t < (filter['filter.timestamp.gte'] || Infinity)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msgs = msgs.filter(msgFilter);
|
||||||
|
|
||||||
// Always sort newest first before applying the pagination window.
|
// Always sort newest first before applying the pagination window.
|
||||||
msgs = msgs.sort((a, b) => b.t - a.t);
|
msgs = msgs.sort((a, b) => b.t - a.t);
|
||||||
|
|
||||||
|
|||||||
@@ -173,8 +173,10 @@ import {
|
|||||||
|
|
||||||
import { WAJSPresenceChatStateType, WebJSPresence } from './types';
|
import { WAJSPresenceChatStateType, WebJSPresence } from './types';
|
||||||
import {
|
import {
|
||||||
|
isJidCus,
|
||||||
isJidGroup,
|
isJidGroup,
|
||||||
isJidStatusBroadcast,
|
isJidStatusBroadcast,
|
||||||
|
isLidUser,
|
||||||
normalizeJid,
|
normalizeJid,
|
||||||
toCusFormat,
|
toCusFormat,
|
||||||
} from '@waha/core/utils/jids';
|
} from '@waha/core/utils/jids';
|
||||||
@@ -185,6 +187,9 @@ import {
|
|||||||
WAHA_CLIENT_BROWSER_NAME,
|
WAHA_CLIENT_BROWSER_NAME,
|
||||||
WAHA_CLIENT_DEVICE_NAME,
|
WAHA_CLIENT_DEVICE_NAME,
|
||||||
} from '@waha/core/env';
|
} 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 {
|
export interface WebJSConfig {
|
||||||
webVersion?: string;
|
webVersion?: string;
|
||||||
@@ -202,6 +207,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
|||||||
private engineStateCheckDelayedJob: SingleDelayedJobRunner;
|
private engineStateCheckDelayedJob: SingleDelayedJobRunner;
|
||||||
private shouldRestart: boolean;
|
private shouldRestart: boolean;
|
||||||
private lastQRDate: Date = null;
|
private lastQRDate: Date = null;
|
||||||
|
private static readonly REACTION_MAX_AGE_MS = 2 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
whatsapp: WebjsClientCore;
|
whatsapp: WebjsClientCore;
|
||||||
protected qr: QR;
|
protected qr: QR;
|
||||||
@@ -271,6 +277,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() {
|
protected async buildClient() {
|
||||||
const clientOptions = this.getClientOptions();
|
const clientOptions = this.getClientOptions();
|
||||||
const base = process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
|
const base = process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
|
||||||
@@ -358,6 +369,16 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
|||||||
PAGE_CALL_ERROR_EVENT,
|
PAGE_CALL_ERROR_EVENT,
|
||||||
(event: CallErrorEvent) => {
|
(event: CallErrorEvent) => {
|
||||||
if (event.error instanceof ProtocolError) {
|
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(
|
this.logger.error(
|
||||||
`ProtocolError when calling page method: ${String(
|
`ProtocolError when calling page method: ${String(
|
||||||
event.method,
|
event.method,
|
||||||
@@ -393,6 +414,13 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
|
await killProcessesByPatterns(
|
||||||
|
[IsChrome ? 'chrome' : 'chromium', `--a-waha-session=${this.name}`],
|
||||||
|
'SIGKILL',
|
||||||
|
this.logger,
|
||||||
|
);
|
||||||
|
await removeSingletonFiles(this.getUserDataDir());
|
||||||
|
|
||||||
this.status = WAHASessionStatus.STARTING;
|
this.status = WAHASessionStatus.STARTING;
|
||||||
await this.init().catch((err) => {
|
await this.init().catch((err) => {
|
||||||
this.logger.error('Failed to start the client');
|
this.logger.error('Failed to start the client');
|
||||||
@@ -418,6 +446,16 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
|||||||
this.restartClient();
|
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() {
|
async unpair() {
|
||||||
this.unpairing = true;
|
this.unpairing = true;
|
||||||
this.shouldRestart = false;
|
this.shouldRestart = false;
|
||||||
@@ -674,6 +712,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
|||||||
return screenshot as Buffer;
|
return screenshot as Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Activity()
|
||||||
async checkNumberStatus(
|
async checkNumberStatus(
|
||||||
request: CheckNumberStatusQuery,
|
request: CheckNumberStatusQuery,
|
||||||
): Promise<WANumberExistResult> {
|
): Promise<WANumberExistResult> {
|
||||||
@@ -717,6 +756,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
|||||||
/**
|
/**
|
||||||
* Other methods
|
* Other methods
|
||||||
*/
|
*/
|
||||||
|
@Activity()
|
||||||
async rejectCall(from: string, id: string): Promise<void> {
|
async rejectCall(from: string, id: string): Promise<void> {
|
||||||
const peerJid = normalizeJid(this.ensureSuffix(from));
|
const peerJid = normalizeJid(this.ensureSuffix(from));
|
||||||
const call = new CallInstance(this.whatsapp, null);
|
const call = new CallInstance(this.whatsapp, null);
|
||||||
@@ -991,7 +1031,50 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
|||||||
messageId: string,
|
messageId: string,
|
||||||
query: GetChatMessageQuery,
|
query: GetChatMessageQuery,
|
||||||
): Promise<null | WAMessage> {
|
): Promise<null | WAMessage> {
|
||||||
const message = await this.whatsapp.getMessageById(messageId);
|
chatId = this.ensureSuffix(chatId);
|
||||||
|
|
||||||
|
// WEBJS waits the serializer messageId
|
||||||
|
// {fromMe}_{chatId}_{id}[_{participant}]
|
||||||
|
if (isJidStatusBroadcast(chatId) && !messageId.includes('_')) {
|
||||||
|
// For status - resolve it as "my" if no details provided
|
||||||
|
const me = this.getSessionMeInfo();
|
||||||
|
const lid = me.lid || (await this.whatsapp.findLIDByPhoneNumber(me.id));
|
||||||
|
messageId = SerializeMessageKey({
|
||||||
|
fromMe: true,
|
||||||
|
id: messageId,
|
||||||
|
remoteJid: Jid.BROADCAST,
|
||||||
|
participant: lid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let message: Message | null = null;
|
||||||
|
if (isLidUser(chatId) || isJidCus(chatId)) {
|
||||||
|
// If in DM chats and not fromMe specified - try both fromMe and not
|
||||||
|
if (!messageId.includes('_')) {
|
||||||
|
// FromMe - true
|
||||||
|
if (!message) {
|
||||||
|
const id = SerializeMessageKey({
|
||||||
|
fromMe: true,
|
||||||
|
id: messageId,
|
||||||
|
remoteJid: chatId,
|
||||||
|
});
|
||||||
|
message = await this.whatsapp.getMessageById(id);
|
||||||
|
}
|
||||||
|
// FromMe - false
|
||||||
|
if (!message) {
|
||||||
|
const id = SerializeMessageKey({
|
||||||
|
fromMe: false,
|
||||||
|
id: messageId,
|
||||||
|
remoteJid: chatId,
|
||||||
|
});
|
||||||
|
message = await this.whatsapp.getMessageById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
message = await this.whatsapp.getMessageById(messageId);
|
||||||
|
}
|
||||||
if (!message) return null;
|
if (!message) return null;
|
||||||
if (
|
if (
|
||||||
isJidGroup(message.id.remote) ||
|
isJidGroup(message.id.remote) ||
|
||||||
@@ -1909,6 +1992,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
|||||||
const media = await this.downloadMediaSafe(message);
|
const media = await this.downloadMediaSafe(message);
|
||||||
wamessage.media = media;
|
wamessage.media = media;
|
||||||
}
|
}
|
||||||
|
if (downloadMedia && wamessage.replyTo?.hasMedia) {
|
||||||
|
const quotedMessage = await message.getQuotedMessage().catch(() => null);
|
||||||
|
if (quotedMessage) {
|
||||||
|
wamessage.replyTo.media = await this.downloadMediaSafe(quotedMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
return wamessage;
|
return wamessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1938,6 +2027,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore reactions older than 2 days to prevent stale reactions
|
||||||
|
// when session reconnects and replays buffered events
|
||||||
|
const twoDaysAgoSec =
|
||||||
|
(Date.now() - WhatsappSessionWebJSCore.REACTION_MAX_AGE_MS) / 1000;
|
||||||
|
if (reaction.timestamp < twoDaysAgoSec) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const source = this.getMessageSource(reaction.id.id);
|
const source = this.getMessageSource(reaction.id.id);
|
||||||
return {
|
return {
|
||||||
id: reaction.id._serialized,
|
id: reaction.id._serialized,
|
||||||
@@ -2063,15 +2160,21 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected extractReplyTo(message: Message): ReplyToMessage | null {
|
protected extractReplyTo(message: Message): ReplyToMessage | null {
|
||||||
|
const rawData: any = message.rawData;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const quotedMsg = message.rawData?.quotedMsg;
|
const quotedMsg = rawData?.quotedMsg;
|
||||||
if (!quotedMsg) {
|
const quotedStanzaId = rawData?.quotedStanzaID || rawData?.quotedStanzaId;
|
||||||
|
if (!quotedMsg && !quotedStanzaId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const quotedParticipant =
|
||||||
|
rawData?.quotedParticipant || quotedMsg?.author || quotedMsg?.from;
|
||||||
return {
|
return {
|
||||||
id: quotedMsg.id?.id,
|
id: quotedStanzaId || quotedMsg?.id?.id,
|
||||||
participant: quotedMsg.author || quotedMsg.from,
|
participant: quotedParticipant,
|
||||||
body: quotedMsg.caption || quotedMsg.body,
|
body: quotedMsg?.caption || quotedMsg?.body,
|
||||||
|
hasMedia: Boolean(quotedMsg?.directPath),
|
||||||
|
media: null,
|
||||||
_data: quotedMsg,
|
_data: quotedMsg,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 129 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user