Compare commits

...
112 Commits
Author SHA1 Message Date
devlikepro 81854ad7ec [core] Fix smoke test
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-10-16 17:07:47 +07:00
devlikepro 5261d4f281 [core] 2025.10.3 2025-10-16 16:57:01 +07:00
devlikepro 72d9d94342 [core] NOWEB - use upstream libsignal 2025-10-16 16:57:01 +07:00
devlikepro b782ad27a2 [core] ChatWoot - mark read fix one more time 2025-10-16 16:56:36 +07:00
devlikepro d7dad5e6c2 [core] rm unused imports 2025-10-16 16:56:35 +07:00
devlikepro 8765bb4412 [core] init-waha 2025-10-16 16:56:35 +07:00
devlikepro 984be25d51 [core] Replace 111 as well 2025-10-16 16:56:30 +07:00
devlikepro 70620207dd [core] Adjust .env 2025-10-16 16:56:29 +07:00
devlikepro a96200f981 [core] Adjust message 2025-10-16 16:56:29 +07:00
devlikepro 4978abf3d8 [core] Up GOWS
fix #1503
fix #1511
2025-10-16 16:56:27 +07:00
devlikepro a67d84f21e [core] ChatWoot - List Message 2025-10-16 16:56:27 +07:00
devlikepro 91f59b38fd [core] Dashboard - Chats
fix #1454
2025-10-16 16:56:27 +07:00
devlikepro d0ec351910 [core] Fix /api/sendImage when using proxy
fix #1449
fix #1510
2025-10-16 16:56:26 +07:00
devlikepro a7b12874fb [core] Require auth by default 2025-10-16 16:56:24 +07:00
devlikepro cd2c7bdd86 [core] Up creds in .env 2025-10-16 16:56:24 +07:00
devlikepro 045077143f [core] no warning about plain key 2025-10-16 16:56:24 +07:00
devlikepro bcd2199687 [core] ChatWoot - remove (WAHA) from contact name 2025-10-16 16:56:20 +07:00
devlikepro af108eede4 [core] NOWEB - normalize @lid
fix #1493
2025-10-16 16:56:19 +07:00
devlikepro ee1cf838b8 [core] NOWEB - use IPv4 by default 2025-10-16 16:56:19 +07:00
devlikepro 0fc74895da [core] Up NOWEB - add more logs for media 2025-10-16 16:56:19 +07:00
devlikepro 394937c154 [core] 2025.10.2
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-10-14 15:21:40 +07:00
devlikepro 71f9adeff4 [core] ChatWoot - ignore fromMe in ack
mention #1492
2025-10-14 15:21:40 +07:00
devlikepro e13ccdab9c [core] ChatWoot - disable error/succeeded messages
mention #1492
mention #1395
2025-10-14 15:21:40 +07:00
devlikepro 2074748a7a [core] ChatWoot - no message.ack error
mention #1492
2025-10-14 15:21:39 +07:00
devlikepro 283d63394e [core] Up NOWEB
fix #1495
fix #1499
2025-10-14 15:21:39 +07:00
devlikepro 5cfb53c367 [core] 2025.10.1
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-10-12 18:45:57 +07:00
devlikepro 9466407b8a [core] ChatWoot - update last seen (on message.ack)
closes #1436
fixes #1319
2025-10-12 18:45:56 +07:00
devlikepro 4b65f0c462 [core] ChatWoot - up to v4.6.0 2025-10-12 18:45:56 +07:00
devlikepro 23504b35c8 [core] ChatWoot - remove /n at the end of any templated 2025-10-12 18:45:55 +07:00
devlikepro dd6ec4ca3a [core] ChatWoot - Pix fixes 2025-10-12 18:45:55 +07:00
devlikepro 8235e5a111 [core] ChatWoot - i18n - pt enable timeZone Sao Paulo 2025-10-12 18:45:55 +07:00
devlikepro 55499a6144 [core] ChatWoot - i18n - datetime 2025-10-12 18:45:54 +07:00
devlikepro 9dbc4a6085 [core] ChatWoot - i18n - poll, event, pix 2025-10-12 18:45:54 +07:00
devlikepro f7c6ce847c [core] ChatWoot - i18n - poll no empty line 2025-10-12 18:45:53 +07:00
devlikepro 84eed00415 [core] ChatWoot - i18n - location url (not URL) 2025-10-12 18:45:53 +07:00
devlikepro 51aa0e3154 [core] ChatWoot - i18n - datetime 2025-10-12 18:45:52 +07:00
devlikepro 77893a0eb0 [core] ChatWoot - i18n - locale base 2025-10-12 18:45:52 +07:00
devlikepro 4a866d8a08 [core] ChatWoot - PIX
(cherry picked from bergpinheiro/waha@696de94859)

Thanks @bergpinheiro
Co-authored-by: bergpinheiro <berg.pinheiro2009@gmail.com>
2025-10-12 18:45:52 +07:00
devlikepro f912353a5c [core] ChatWoot - Event Message
(cherry picked from bergpinheiro/waha@696de94859)

Thanks @bergpinheiro
Co-authored-by: bergpinheiro <berg.pinheiro2009@gmail.com>
2025-10-12 18:45:51 +07:00
devlikepro a6a1d8dc28 [core] ChatWoot - Poll message
(cherry picked from bergpinheiro/waha@696de94859)

Thanks @bergpinheiro
Co-authored-by: bergpinheiro <berg.pinheiro2009@gmail.com>
2025-10-12 18:45:50 +07:00
devlikepro 150e73aa74 [core] ChatWoot - adjust location message
(cherry picked from bergpinheiro/waha@696de94859)

Thanks @bergpinheiro
Co-authored-by: bergpinheiro <berg.pinheiro2009@gmail.com>
2025-10-12 18:45:49 +07:00
devlikepro a9300b3134 [core] ChatWoot add URL test 2025-10-12 18:45:48 +07:00
devlikepro bbe3859085 [core] ChatWoot - Facebook Ads Message Template
closes #1461

(cherry picked from bergpinheiro/waha@5863590208)

Thanks @bergpinheiro
Co-authored-by: bergpinheiro <berg.pinheiro2009@gmail.com>
2025-10-12 18:45:48 +07:00
devlikepro b420548dd9 [core] export fetchBuffer 2025-10-12 18:45:47 +07:00
devlikepro 741483eec7 [core] ChatWoot - remove unused getAttachments in message.reaction 2025-10-12 18:45:47 +07:00
devlikepro 2acf7e4e17 [core] ChatWoot - add name for location message 2025-10-12 18:45:46 +07:00
devlikepro 9bca6d904d [core] ChatWoot - Refactor message.any 2025-10-12 18:45:45 +07:00
devlikepro 857c586df6 [core] AGENTS.md 2025-10-12 18:45:45 +07:00
devlikepro df8cb7cecb [core] NOWEB - fix ssl issue
fix #1474
2025-10-12 18:45:45 +07:00
devlikepro 9d0050adb8 [core] NOWEB - filter PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE
fix #1459
2025-10-12 18:45:44 +07:00
devlikepro df927ab04a [core] Remove + at the start
fix #1475
2025-10-12 18:45:43 +07:00
devlikepro 02e5934e8f [core] Up NOWEB 2025-10-10 - fixes 2025-10-12 18:45:42 +07:00
devlikepro 825a4152db [core] Up NOWEB 2025-10-10 2025-10-12 18:45:41 +07:00
devlikepro 81b10099af [core] Up WEBJS
fix #1469
fix #1468
2025-10-12 18:45:41 +07:00
devlikepro 3be5f328b0 [core] Up GOWS 2025-10-12 18:45:41 +07:00
devlikepro 5caa3b8b81 [core] do not rename title 2025-10-10 10:07:02 +07:00
devlikepro 8231bf7d8e [core] GitHub - en/pt by default 2025-10-06 18:30:15 +07:00
devlikepro 22d94808fb [core] Fix lint
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-10-04 17:12:34 +07:00
devlikepro 57daefa680 [core] 2025.9.8 2025-10-04 17:00:06 +07:00
devlikepro 5cf8704973 [core] NOWEB - no await in start
fix #1397
2025-10-04 17:00:05 +07:00
devlikepro 28da9c34bb [core] Fix NOWEB Apps on start
fix #1397
2025-10-04 17:00:05 +07:00
devlikepro 51ba5e3266 [core] Up GOWS
fix #1293
fix #1412
fix #1406
2025-10-04 17:00:05 +07:00
devlikepro f696794ee1 [core] Up WEBJS
fix #1276
fix #1407
2025-10-04 17:00:04 +07:00
devlikepro c6eca9510b [core] NOWEB - 2025-10-03 2025-10-04 17:00:04 +07:00
devlikepro 9a2fd2fccd [core] make up* 2025-10-04 17:00:04 +07:00
devlikepro c0a2d02f0d [core] NOWEB - send poll to newsletters
fix #1270
fix #1415
2025-10-04 17:00:04 +07:00
devlikepro 6493d16fd2 [core] NOWEB - try to get message just by id for poll
fix #1396
2025-10-04 17:00:04 +07:00
devlikeapro 9f96c678dc [core] support CSAT messages in webhook processing
fix(chatwoot): support CSAT messages in webhook processing
2025-10-04 16:59:36 +07:00
Berg Pinheiro a81f2ab553 fix(chatwoot): support CSAT messages in webhook processing
- Add support for 'input_csat' content_type in MessageHandler
- Previously CSAT messages were being ignored due to strict content_type check
- CSAT messages now processed as text messages and sent to WhatsApp
- Fixes issue where Chatwoot CSAT surveys were not being delivered to users

Changes:
- Modified content_type validation in message_created.ts to accept both 'text' and 'input_csat'
- CSAT messages will now be sent to WhatsApp with their survey content and links
- Maintains existing behavior for all other message types
2025-10-01 22:23:36 -03:00
devlikepro 8fbf7fa0e5 [core] ChatWoot - brazil phone number
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
fix #1261
2025-09-29 12:10:09 +07:00
devlikepro af0bba313a [core] 2025.9.7 2025-09-29 12:10:09 +07:00
devlikepro 76307dec12 [core] 2025.9.6
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-09-29 09:09:47 +07:00
devlikepro 0b441054ae [core] Up dashboard 2025-09-29 09:09:46 +07:00
devlikepro 2e8b78aebc [core] ChatWoot fix duplicating messages for transcribe feature
fix #1371
2025-09-29 09:09:45 +07:00
devlikepro d01b221a24 [core] Add ChatWootAppConfig to openapi
fix #1416
2025-09-29 09:09:44 +07:00
devlikepro 61bb111768 [core] ChatWoot - conversation selector
fix #1216
fix #1357
fix #1237
close #1343
close #1213
2025-09-29 09:09:43 +07:00
devlikepro de47d6e445 [core] ChatWoot - list custom attributes once on upsert 2025-09-29 09:09:41 +07:00
devlikepro 59fe4b30f0 [core] ChatWoot - use Service suffix 2025-09-29 09:09:40 +07:00
devlikepro 166dae8e27 [core] ChatWoot - fix brazil numbers
fix #1261
2025-09-29 09:09:39 +07:00
devlikepro 5ce64fd30d [core] ChatWoot - check JID format 2025-09-29 09:09:37 +07:00
devlikepro 1a4513f437 [core] GOWS - do not fail in case of wrong numbers
fix #1173
2025-09-29 09:09:37 +07:00
devlikepro 59be4a780f [core] ChatWoot - check attributes before updating
fix #1173
2025-09-29 09:09:37 +07:00
devlikepro 74001c2b49 [core] ChatWoot - get group name
fix #1245
2025-09-29 09:09:37 +07:00
devlikepro a9ce0fb2d9 [core] ChatWoot - fix /jobs auth
fix #1398
2025-09-29 09:09:36 +07:00
devlikepro 4c3fd1c214 [core] ChatWoot - do not send STOPPED if server restarts 2025-09-29 09:09:34 +07:00
devlikepro a9459fe746 [core] ChatWoot - logging 2025-09-29 09:09:32 +07:00
devlikepro 5a3228f05b [core] ChatWoot - safe get avatar from WhatsApp 2025-09-29 09:09:31 +07:00
devlikepro befc238db9 [core] ChatWoot - fix 500 Error - use Account API to fetch contact conversations list
fix #1389
2025-09-29 09:09:31 +07:00
devlikepro ae75a1f8d4 [core] ChatWoot - use JobLink for success 2025-09-29 09:09:30 +07:00
devlikepro d1c50bfd73 [core] ChatWoot - add WAHA_PUBLIC_URL 2025-09-29 09:09:30 +07:00
devlikepro 6f540366f4 [core] ChatWoot - add RACK_TIMEOUT_SERVICE_TIMEOUT 60s
mention #1389
2025-09-29 09:09:29 +07:00
devlikepro 151fc760fe [core] ChatWoot log additional errors 2025-09-29 09:09:29 +07:00
devlikepro be4391a4b6 [core] ChatWoot fix reply if not found 2025-09-29 09:09:29 +07:00
devlikepro 76ae3e0f6b [core] 2025.9.5
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-09-23 15:51:01 +07:00
devlikepro f37b6edaa6 [core] Up Dashboard 2025-09-23 15:51:00 +07:00
devlikepro 7095f77f93 [core] GOWS - v1.0.15 2025-09-23 15:51:00 +07:00
devlikepro 5853b778f2 [core] GOWS - fix "typing" after message
fix #1364
2025-09-23 15:51:00 +07:00
devlikepro 6405725ce6 [core] WEBJS - send vcard
fix #1341
2025-09-23 15:50:59 +07:00
devlikepro 111d6141aa [core] WEBJS - fix login via code on new version 2025-09-23 15:50:58 +07:00
devlikepro 5560b3c8e6 [core] NOWEB - login via phone
fix #1385
2025-09-23 15:50:58 +07:00
devlikepro d528d541ea [core] NOWEB - fix fromMe in group messages
fix #1350
2025-09-23 15:50:57 +07:00
devlikepro ab6b51cc1f [core] NOWEB - normalized lid 2025-09-23 15:50:57 +07:00
devlikepro 35aa4f2658 [core] WEBJS - fix status messages 2025-09-23 15:50:56 +07:00
devlikepro 36db3c37e8 [core] WEBJS - fix channel role 2025-09-23 15:50:55 +07:00
devlikepro b0c9232153 [core] Update WEBJS 2025-09-23 15:50:55 +07:00
devlikepro 0070fb9e5c [core] Update NOWEB, use ESM wrapper 2025-09-23 15:50:53 +07:00
devlikepro aa33ac4b1d [core] NOWEB - handle lid-mapping.update 2025-09-23 15:50:52 +07:00
devlikepro 1c2bc5b395 [core] NOWEB - no clean storage on history sync 2025-09-23 15:50:51 +07:00
devlikepro f2fe18a4c5 [core] NOWEB Handle lottie stickers using .zip 2025-09-23 15:50:51 +07:00
devlikepro 87fdcbebf6 [core] NOWEB Use directPath for newsletters 2025-09-23 15:50:51 +07:00
devlikepro 68a4f651ea [core] pin postgres:17.6
fix #1380
2025-09-23 15:50:50 +07:00
devlikepro 243caa9626 [core] ChatWoot - group participant at the bottom
fix #1369
2025-09-23 15:50:50 +07:00
158 changed files with 6110 additions and 1053 deletions

No files matched your search

+17 -13
View File
@@ -3,31 +3,35 @@
# https://waha.devlike.pro/docs/how-to/config/
#
# ====================
# ===== SECURITY =====
# ====================
WAHA_API_KEY=00000000000000000000000000000000
WAHA_DASHBOARD_USERNAME=admin
WAHA_DASHBOARD_PASSWORD=11111111111111111111111111111111
WHATSAPP_SWAGGER_USERNAME=admin
WHATSAPP_SWAGGER_PASSWORD=11111111111111111111111111111111
# Disable Dashboard or Swagger
WAHA_DASHBOARD_ENABLED=True
WHATSAPP_SWAGGER_ENABLED=True
# ==================
# ===== COMMON =====
# ==================
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
WHATSAPP_DEFAULT_ENGINE=WEBJS
# Base URL for the API (used for webhooks, file URLs, etc.)
WAHA_BASE_URL=http://localhost:3000
# WAHA_PUBLIC_URL=http://localhost:3000
# Server configuration (if you need to customize hostname/port)
# WHATSAPP_API_SCHEMA=http
# WHATSAPP_API_PORT=3000
# WHATSAPP_API_HOSTNAME=localhost
# ====================
# ===== SECURITY =====
# ====================
# "sha512:{SHA}" format - below is "admin" api key
WAHA_API_KEY=sha512:c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec
WAHA_DASHBOARD_ENABLED=True
WAHA_DASHBOARD_USERNAME=admin
WAHA_DASHBOARD_PASSWORD=admin
WHATSAPP_SWAGGER_ENABLED=True
WHATSAPP_SWAGGER_USERNAME=admin
WHATSAPP_SWAGGER_PASSWORD=admin
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
WHATSAPP_DEFAULT_ENGINE=WEBJS
# ===================
# ===== LOGGING =====
+1
View File
@@ -129,6 +129,7 @@ jobs:
docker run -d \
--name smoke \
--rm -p3000:3000 \
-e WAHA_API_KEY=test \
${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
sleep 3
+51
View File
@@ -0,0 +1,51 @@
name: 'github-translate'
on:
issues:
types: [opened]
issue_comment:
types: [created]
discussion:
types: [created]
discussion_comment:
types: [created]
# pull_request_target:
# types: [opened, edited]
# pull_request_review_comment:
# types: [created, edited]
jobs:
en:
permissions:
issues: write
discussions: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: devlikeapro/github-translate-action@main
env:
# GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
IS_MODIFY_TITLE: true
CUSTOM_BOT_NOTE: 'English'
TARGET_LANGUAGE: 'eng'
TRANSLATED_LANGUAGE: 'en'
pt:
permissions:
issues: write
discussions: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: devlikeapro/github-translate-action@main
env:
# GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
IS_MODIFY_TITLE: false
CUSTOM_BOT_NOTE: 'Portuguese'
TARGET_LANGUAGE: 'por'
TRANSLATED_LANGUAGE: 'pt'
+2
View File
@@ -67,9 +67,11 @@ repos:
exclude: |
(?x)^(
entrypoint.sh|
AGENTS.md|
.pre-commit-config.yaml|
^src/core/engines/webjs/session.webjs.core.ts|
^src/core/engines/webjs/_lodash.js|
^tests/perf/.*|
^scripts/.*|
^scripts/init-waha.js|
)$
+124
View File
@@ -0,0 +1,124 @@
# WAHA Agent Playbook
This guide summarizes how to explore, modify, and validate the WhatsApp HTTP API
(WAHA) codebase when assisting as an automation or coding agent.
## Product & Variants
- WAHA ships in **Core** and **Plus** editions. Core lives under `src/core` and
only supports the default session plus minimal media features. Plus extends
core via `src/plus` to add multi-session orchestration, richer media handling,
and external storage integrations.
- Core code must remain free from Plus-only references. A pre-commit hook
rejects the word `plus` inside core files; reuse abstractions exposed through
`@waha/core/**` instead of importing Plus modules from Core.
- Commit subjects are validated: changes that touch `src/plus` require a
`[PLUS] …` prefix and must not include non-Plus files; everything else must
use `[core] …`. Verify against `./.precommit/validate_commit_message.py` if
unsure.
## Tech Stack Snapshot
- **Runtime**: Node.js 22.x, Yarn 3.6 (Berry). Always install packages and run
scripts with Yarn.
- **Framework**: NestJS v11 with dependency injection, modular controllers in
`src/api`, and Pino-based logging via `nestjs-pino`.
- **Engines**: WhatsApp engines are abstracted (`WEBJS`, `GOWS`, `NOWEB`). Core
uses `SessionManagerCore`; Plus swaps to `SessionManagerPlus` with extra
storage backends (Mongo/Postgres/SQLite).
- **ESM Bridge**: ESM-only dependencies (Baileys) load through
`src/vendor/esm.ts`. Add new ESM modules there to ensure they load exactly
once.
- **Utilities**: RxJS streams (`SwitchObservable`, `DefaultMap`) drive webhook
event fan-out. Prefer existing helpers in `src/utils` and `src/core/utils`
before adding bespoke logic.
## Repository Landmarks
- `src/main.ts`: runtime entry point; dynamically loads the correct AppModule
(Core vs Plus) and configures global interceptors/filters.
- `src/api/**`: REST controllers and WebSocket gateway. Keep handlers thin;
delegate to managers/services. HTTP routes follow `/api/{sessionName}/…`;
always thread the session name through request DTOs and guards instead of
hardcoding `default`.
- `src/core/**`: shared abstractions (config services, engine bootstrap,
storage, session management). Core only allows the `default` session and
cleans up storage on boot.
- `src/plus/**`: multi-session orchestration, advanced media services, and
external persistence layers (Mongo, Postgres). Reuse this layer when adding
Plus-only capabilities.
- `src/apps/**`: integrations (e.g., ChatWoot) and application-specific
services.
- `src/structures/**` and `src/utils/**`: DTOs, enums (event names follow
`domain.action`), helper utilities. Maintain naming consistency when
introducing new events.
- `tests/**`: Jest-based suites; do not add new tests unless explicitly asked,
but keep existing tests working.
## Coding Expectations
- Favor composable, long-lived solutions. If a helper already exists (e.g.,
`parseBool`, `DefaultMap`, media factories), extend it instead of reinventing
logic. Lodash ships with the project—prefer its utilities over hand-rolled
helpers. Import lodash as a namespace (`import * as lodash from 'lodash';`) so
helpers like `lodash.camelCase` stay consistent across files. When a new
third-party library seems necessary, confirm with the user, especially if the
package looks stale.
- Stick to NestJS patterns: inject dependencies through constructors, expose
provider tokens from modules, and keep controllers free from business logic.
- Logging goes through injected `PinoLogger` or helpers in
`src/utils/logging.ts`. `console.log` is blocked by pre-commit.
- Respect path aliases (`@waha/...`) defined in `tsconfig.json`; keep imports
consistent (use absolute aliases, not relative `../../../`).
- Avoid naming unused variables with a leading underscore; if a parameter is
required by a signature, explicitly `void` it instead.
- For configs, prefer runtime configurability over constants. Environment keys
follow `WAHA_*` for global values and `WAHA_SESSION_CONFIG_*` /
`session.config.*` for per-session overrides. If both env and config are
supported, honor both (`WAHA_WEBJS_CONFIG_*` vs. `session.webjs.config.*`).
## Language & Localization
- Keep identifiers (classes, methods, variables) and code comments in English so
the codebase stays consistent for the global team.
- Route user-visible strings through the existing i18n structure rather than
hardcoding text. ChatWoot copy belongs in `src/apps/chatwoot/i18n` with
English as the source locale before adding translations.
## ChatWoot Integration Notes
- Avoid introducing synthetic events; map onto existing webhook or engine events
whenever plausible. Always use the official ChatWoot API client located in
`src/apps/chatwoot/client`.
- When adding events, align consumer names with webhook/event identifiers (e.g.,
`message.any`).
## Workflow Checklist
1. Identify whether work targets Core, Plus, or shared layers. If Plus-only,
isolate changes to `src/plus` and ensure the commit prefix matches.
2. Lean on existing services/managers; extend the appropriate session manager
rather than branching logic inline.
3. After edits run:
- `pre-commit run --all-files`
- `yarn build`
- `yarn test` Use Node 22. Address lint or formatting issues before
proceeding.
4. Do **not** start the application yourself; ask the user to run it if runtime
validation is required.
5. Capture any assumptions or open questions for the user, especially when
touching configs, introducing dependencies, or modifying public APIs.
## Additional Tips
- Concurrency-sensitive sections (session start/stop) rely on `async-lock`. When
adding async flows, reuse `SessionManager.withLock` or existing retry
utilities (`promiseTimeout`, `waitUntil`).
- Media features centralize through `MediaManager` and `MediaStorageFactory`.
When altering media behavior, update both Core and Plus variants where
applicable.
- Keep docs and code ASCII unless a file already uses other characters. When
updating documentation, mirror the concise, actionable tone used here.
Following this playbook keeps contributions aligned with WAHA’s structure,
automation hooks, and release process.
+5
View File
@@ -195,6 +195,11 @@ COPY --from=build /git/node_modules ./node_modules
COPY --from=build /git/dist ./dist
COPY --from=dashboard /dashboard ./dist/dashboard
COPY --from=gows /go/gows/bin/gows /app/gows
COPY .env.example ./.env.example
COPY scripts/init-waha.js ./scripts/init-waha.js
RUN chmod +x ./scripts/init-waha.js \
&& printf '%s\n' '#!/bin/sh' 'exec node /app/scripts/init-waha.js "$@"' > /usr/local/bin/init-waha \
&& chmod +x /usr/local/bin/init-waha
ENV WAHA_GOWS_PATH=/app/gows
ENV WAHA_GOWS_SOCKET=/tmp/gows.sock
+3 -3
View File
@@ -35,13 +35,13 @@ for-swagger:
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
up-noweb:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-07-04
yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-10-10
up-noweb-libsignal:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
up-webjs:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09
yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-09-10
start-proxy:
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
+1 -1
View File
@@ -36,7 +36,7 @@ services:
# NOTE: Only if you're using PostgreSQL to save sessions
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
# postgres:
# image: postgres:17
# image: postgres:17.6
# restart: always
# environment:
# POSTGRES_USER: postgres
+4
View File
@@ -1,6 +1,10 @@
# Learn about the various environment variables at
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
# Increase rack default timeout (30s) to avoid timeout errors
# Workaround for https://github.com/devlikeapro/waha/issues/1389
RACK_TIMEOUT_SERVICE_TIMEOUT=60
# Used to verify the integrity of signed cookies. so ensure a secure value is set
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
# Use `rake secret` to generate this variable
+15 -14
View File
@@ -3,31 +3,32 @@
# https://waha.devlike.pro/docs/how-to/config/
#
# ====================
# ===== SECURITY =====
# ====================
WAHA_API_KEY_PLAIN=00000000000000000000000000000000
WAHA_API_KEY=00000000000000000000000000000000
WAHA_DASHBOARD_USERNAME=admin
WAHA_DASHBOARD_PASSWORD=11111111111111111111111111111111
WHATSAPP_SWAGGER_USERNAME=admin
WHATSAPP_SWAGGER_PASSWORD=11111111111111111111111111111111
# Disable Dashboard or Swagger
WAHA_DASHBOARD_ENABLED=True
WHATSAPP_SWAGGER_ENABLED=True
# ==================
# ===== COMMON =====
# ==================
# Base URL for the API (used for webhooks, file URLs, etc.)
WAHA_BASE_URL=http://waha:3000
WAHA_PUBLIC_URL=http://localhost:3000
# Server configuration (if you need to customize hostname/port)
# WHATSAPP_API_SCHEMA=http
# WHATSAPP_API_PORT=3000
# WHATSAPP_API_HOSTNAME=localhost
# ====================
# ===== SECURITY =====
# ====================
# WAHA Apps requires both sha and plain version
WAHA_API_KEY_PLAIN=00000000000000000000000000000000
WAHA_API_KEY=sha512:98b6d128682e280b74b324ca82a6bae6e8a3f7174e0605bfd52eb9948fad8984854ec08f7652f32055c4a9f12b69add4850481d9503a7f2225501671d6124648
WAHA_DASHBOARD_USERNAME=admin
WAHA_DASHBOARD_PASSWORD=11111111111111111111111111111111
WHATSAPP_SWAGGER_USERNAME=admin
WHATSAPP_SWAGGER_PASSWORD=11111111111111111111111111111111
WAHA_DASHBOARD_ENABLED=True
WHATSAPP_SWAGGER_ENABLED=True
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
WHATSAPP_DEFAULT_ENGINE=GOWS
+1 -1
View File
@@ -24,7 +24,7 @@ services:
- 8.8.8.8
base: &base
image: chatwoot/chatwoot:v4.5.2
image: chatwoot/chatwoot:v4.6.0
env_file: .chatwoot.env ## Change this file for customized env variables
volumes:
- chatwoot_storage:/app/storage
-5
View File
@@ -41,11 +41,6 @@ if [ -n "$key" ]; then
# If already hashed, use it as is
export WAHA_API_KEY="$key"
else
# Display warning about using plain text API key
echo "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
echo "WARNING: Plain text API key detected. Converting to hashed format for security."
echo "For better security, use WAHA_API_KEY=sha512:{SHA512_HASH_FOR_YOUR_API_KEY}"
echo "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
# Hash the key using sha512sum
HASHED_KEY=$(echo -n "$key" | sha512sum | awk '{print $1}')
export WAHA_API_KEY="sha512:$HASHED_KEY"
+8 -4
View File
@@ -26,7 +26,7 @@
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
},
"dependencies": {
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-07-04",
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-10-10",
"@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-s3": "^3.633.0",
"@aws-sdk/s3-request-presigner": "^3.633.0",
@@ -76,9 +76,10 @@
"joi": "^17.13.3",
"knex": "^3.1.0",
"libphonenumber-js": "^1.10.36",
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
"libsignal": "github:WhiskeySockets/libsignal-node#master",
"link-preview-js": "^3.0.4",
"lodash": "^4.17.21",
"long": "^5.3.2",
"mime-types": "^2.1.27",
"mongodb": "6.9.0",
"mustache": "^4.2.0",
@@ -101,7 +102,8 @@
"sqlite3": "^5.1.7",
"swagger-ui-express": "^4.1.4",
"ulid": "^2.3.0",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09",
"undici": "^7.16.0",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-09-10",
"write-file-atomic": "^6.0.0",
"yaml": "^2.7.1"
},
@@ -113,7 +115,6 @@
"ws": "^8.18.0",
"puppeteer": "^24.19.0",
"whatwg-url": "13.0.0",
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
"axios": "^1.9.0"
},
"devDependencies": {
@@ -148,6 +149,9 @@
"ts"
],
"rootDir": "src",
"moduleNameMapper": {
"^@waha/(.*)$": "<rootDir>/$1"
},
"testRegex": ".test.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env node
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const projectRoot = path.resolve(__dirname, '..');
const args = process.argv.slice(2);
const forceIndex = args.indexOf('--force');
const forceOverwrite = forceIndex !== -1;
if (forceOverwrite) {
args.splice(forceIndex, 1);
}
const folderArg = args[0] || '.';
const sourceArgRaw = args[1];
const targetArg = args[2] || '.env';
const envDir = path.resolve(process.cwd(), folderArg);
const sourceArg = sourceArgRaw || '.env.example';
const templateBaseDir = sourceArgRaw ? envDir : projectRoot;
const templatePath = path.isAbsolute(sourceArg)
? sourceArg
: path.resolve(templateBaseDir, sourceArg);
const targetPath = path.isAbsolute(targetArg)
? targetArg
: path.resolve(envDir, targetArg);
function exitWithMessage(message, code = 1) {
console.error(message);
process.exit(code);
}
if (!fs.existsSync(envDir)) {
exitWithMessage(`Target directory does not exist: ${envDir}`);
}
if (!fs.existsSync(templatePath)) {
exitWithMessage(`Missing template file: ${templatePath}`);
}
if (!forceOverwrite && fs.existsSync(targetPath)) {
const stats = fs.statSync(targetPath);
if (stats.size > 0) {
exitWithMessage(
`${targetPath} already exists and is not empty. Remove it manually or rerun with --force to overwrite.`,
);
}
}
const exampleContent = fs.readFileSync(templatePath, 'utf8');
function generateSecret() {
return crypto.randomUUID({ disableEntropyCache: true }).replace(/-/g, '');
}
const apiKey = generateSecret();
const adminPassword = generateSecret();
const updatedContent = exampleContent
.replace(/^WAHA_API_KEY=.*/m, `WAHA_API_KEY=${apiKey}`)
.replace(/^WAHA_API_KEY_PLAIN=.*/m, `WAHA_API_KEY_PLAIN=${apiKey}`)
.replace(/^WAHA_DASHBOARD_USERNAME=.*/m, 'WAHA_DASHBOARD_USERNAME=admin')
.replace(
/^WAHA_DASHBOARD_PASSWORD=.*/m,
`WAHA_DASHBOARD_PASSWORD=${adminPassword}`,
)
.replace(/^WHATSAPP_SWAGGER_USERNAME=.*/m, 'WHATSAPP_SWAGGER_USERNAME=admin')
.replace(
/^WHATSAPP_SWAGGER_PASSWORD=.*/m,
`WHATSAPP_SWAGGER_PASSWORD=${adminPassword}`,
);
fs.writeFileSync(targetPath, updatedContent, { encoding: 'utf8' });
const lines = [
'Credentials generated.',
'',
'Dashboard and Swagger:',
' - Username: admin',
` - Password: ${adminPassword}`,
'',
'API key: ',
` - ${apiKey}`,
'',
'Use it in the Dashboard connection flow: https://waha.devlike.pro/docs/how-to/dashboard/#api-key',
];
console.log(lines.join('\n'));
-1
View File
@@ -1,4 +1,3 @@
import { isJidGroup } from '@adiwajshing/baileys';
import {
Body,
Controller,
+1 -1
View File
@@ -1,4 +1,3 @@
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import {
Controller,
Get,
@@ -22,6 +21,7 @@ import {
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import { SessionManager } from '../core/abc/manager.abc';
import { isLidUser } from '@waha/core/utils/jids';
@ApiSecurity('api_key')
@Controller('api/:session/lids')
+10 -3
View File
@@ -51,10 +51,17 @@ export function HasBeenRetried(job: Job): boolean {
return attemptsMade > 1;
}
let base =
process.env.WAHA_PUBLIC_URL ||
process.env.WAHA_BASE_URL ||
'http://localhost:3000';
// cut / at the end
base = base.replace(/\/+$/, '');
export function JobLink(job: Job): { text: string; url: string } {
const text = `${job.queueName} => ${job.id}`;
const url = `http://localhost:3000/jobs/queue/${encodeURIComponent(
job.queueName,
)}/${job.id}`;
const url = `${base}/jobs/queue/${encodeURIComponent(job.queueName)}/${
job.id
}`;
return { text: text, url: url };
}
+5 -4
View File
@@ -2,14 +2,15 @@ import * as process from 'node:process';
import { parseBool } from '@waha/helpers';
import * as basicAuth from 'express-basic-auth';
import { Auth } from '@waha/core/auth/config';
export function BullAuthMiddleware() {
let username = process.env.WAHA_DASHBOARD_USERNAME;
let password = process.env.WAHA_DASHBOARD_PASSWORD;
let username = Auth.dashboard.username.value || '';
let password = Auth.dashboard.password.value || '';
if (process.env.WAHA_DASHBOARD_ENABLED) {
const enabled = parseBool(process.env.WAHA_DASHBOARD_ENABLED);
if (!enabled) {
// Generate a random uuid4 username /password to prevent access
// Generate a random uuid4 username/password to effectively disable access
username = 'admin';
password = crypto.randomUUID();
}
@@ -18,7 +19,7 @@ export function BullAuthMiddleware() {
return basicAuth({
challenge: true,
users: {
[username]: password,
[String(username)]: String(password),
},
});
}
+2 -1
View File
@@ -7,7 +7,7 @@ import {
IsString,
ValidateNested,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { ApiExtraModels, ApiProperty } from '@nestjs/swagger';
export type AllowedAppConfig = ChatWootAppConfig;
@@ -15,6 +15,7 @@ export enum AppName {
chatwoot = 'chatwoot',
}
@ApiExtraModels(ChatWootAppConfig)
export class App<T extends AllowedAppConfig = any> {
@IsString()
id: string;
+12
View File
@@ -0,0 +1,12 @@
import { ILogger } from '@waha/apps/app_sdk/ILogger';
export function JestLogger(): ILogger {
return {
fatal: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
trace: jest.fn(),
};
}
@@ -44,4 +44,8 @@ export class AppsDisabledService implements IAppsService {
async beforeSessionStart(session: WhatsappSession, store: DataStore) {
return;
}
async afterSessionStart(session: WhatsappSession, store: DataStore) {
return;
}
}
@@ -145,7 +145,17 @@ export class AppsEnabledService implements IAppsService {
const apps = await repo.getEnabledBySession(session.name);
for (const app of apps) {
const service = this.getAppService(app);
await service.beforeSessionStart(app, session);
service.beforeSessionStart(app, session);
}
}
async afterSessionStart(session: WhatsappSession, store: DataStore) {
const knex = store.getWAHADatabase();
const repo = new AppRepository(knex);
const apps = await repo.getEnabledBySession(session.name);
for (const app of apps) {
const service = this.getAppService(app);
service.afterSessionStart(app, session);
}
}
+3 -1
View File
@@ -23,5 +23,7 @@ export interface IAppService {
beforeDeleted(app: App): Promise<void>;
beforeSessionStart(app: App, session: WhatsappSession): Promise<void>;
beforeSessionStart(app: App, session: WhatsappSession): void;
afterSessionStart(app: App, session: WhatsappSession): void;
}
@@ -17,6 +17,8 @@ export interface IAppsService {
beforeSessionStart(session: WhatsappSession, store: DataStore): Promise<void>;
afterSessionStart(session: WhatsappSession, store: DataStore): Promise<void>;
migrate(knex: Knex): Promise<void>;
}
+3 -2
View File
@@ -13,6 +13,7 @@ import { ChatwootLocalesController } from '@waha/apps/chatwoot/api/chatwoot.loca
import { ChatWootExports } from '@waha/apps/chatwoot/chatwoot.module';
import { parseBool } from '@waha/helpers';
import { RMutexModule } from '@waha/modules/rmutex';
import { Auth } from '@waha/core/auth/config';
const IMPORTS = [
BullModule.forRoot({
@@ -93,11 +94,11 @@ const AppsDisabled = {
};
function checkApiKey() {
const key = process.env.WHATSAPP_API_KEY || process.env.WAHA_API_KEY;
const key = Auth.key.value;
if (!key) {
return;
}
const plain = process.env.WAHA_API_KEY_PLAIN;
const plain = Auth.keyplain.value;
if (!plain) {
throw Error(
'WAHA_API_KEY set, please provide WAHA_API_KEY_PLAIN when WAHA_APPS_ENABLED',
@@ -81,6 +81,19 @@ export class ChatwootWebhookController {
}
return { success: true };
case EventName.MESSAGE_UPDATED:
// We handle only "retries" on message_update
// This is the attribute ChatWoot send
// There's NO other way to identify "status: read" updates right now
// There's no "body.status" in message_updated webhook :(
const isRetryNull = body.content_attributes?.external_error === null;
const isRetrySomething = Boolean(
body.content_attributes?.external_error,
);
const isRetry = isRetryNull || isRetrySomething;
if (!isRetry) {
return { success: true };
}
if (!isCommandsChat) {
await this.chatWootQueueService.addMessageUpdatedJob(data);
} else {
@@ -89,6 +102,7 @@ export class ChatwootWebhookController {
return { success: true };
default:
// Ignore other events
await this.chatWootQueueService.addJobToQueue(body.event, data);
return { success: true };
}
}
+4 -4
View File
@@ -1,8 +1,8 @@
import { public_conversation } from '@figuro/chatwoot-sdk';
import { ChatWootAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import * as NodeCache from 'node-cache';
import { IConversationCache } from './IConversationCache';
import { ContactIds } from '@waha/apps/chatwoot/client/ConversationService';
const cache: NodeCache = new NodeCache({
stdTTL: 24 * 60 * 60, // 1 day
@@ -16,7 +16,7 @@ export function CacheForConfig(config: ChatWootAPIConfig): ConversationCache {
class ConversationCache implements IConversationCache {
constructor(private prefix: string) {}
fullKey(key: string) {
private fullKey(key: string) {
return `${this.prefix}.${key}`;
}
@@ -25,7 +25,7 @@ class ConversationCache implements IConversationCache {
cache.del(fullKey);
}
get(key: string): public_conversation | null {
get(key: string): ContactIds | null {
const fullKey = this.fullKey(key);
return cache.get(fullKey) || null;
}
@@ -35,7 +35,7 @@ class ConversationCache implements IConversationCache {
return cache.has(fullKey);
}
set(key: string, value: public_conversation): void {
set(key: string, value: ContactIds): void {
const fullKey = this.fullKey(key);
cache.set(fullKey, value);
}
+3 -3
View File
@@ -1,8 +1,8 @@
import { public_conversation } from '@figuro/chatwoot-sdk';
import { ContactIds } from '@waha/apps/chatwoot/client/ConversationService';
export interface IConversationCache {
get(key: string): public_conversation | null;
set(key: string, value: public_conversation): void;
get(key: string): ContactIds | null;
set(key: string, value: ContactIds): void;
has(key: string): boolean;
delete(key: string): void;
clean(): void;
+19
View File
@@ -21,10 +21,13 @@ import { WAHAMessageAnyConsumer } from './consumers/waha/message.any';
import { WAHAMessageEditedConsumer } from './consumers/waha/message.edited';
import { WAHAMessageReactionConsumer } from './consumers/waha/message.reaction';
import { WAHAMessageRevokedConsumer } from './consumers/waha/message.revoked';
import { WAHAMessageAckConsumer } from './consumers/waha/message.ack';
import { WAHASessionStatusConsumer } from './consumers/waha/session.status';
import { ChatWootQueueService } from './services/ChatWootQueueService';
import { ChatWootScheduleService } from './services/ChatWootScheduleService';
import { ChatWootWAHAQueueService } from './services/ChatWootWAHAQueueService';
import { ChatWootConversationCreatedConsumer } from './consumers/inbox/conversation_created';
import { ChatWootConversationStatusChangedConsumer } from '@waha/apps/chatwoot/consumers/inbox/conversation_status_changed';
const CONTROLLERS = [ChatwootWebhookController, ChatwootLocalesController];
@@ -53,6 +56,10 @@ const IMPORTS = lodash.flatten([
name: QueueName.WAHA_MESSAGE_REVOKED,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.WAHA_MESSAGE_ACK,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.WAHA_SESSION_STATUS,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
@@ -65,6 +72,14 @@ const IMPORTS = lodash.flatten([
name: QueueName.INBOX_MESSAGE_UPDATED,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.INBOX_CONVERSATION_CREATED,
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.INBOX_CONVERSATION_STATUS_CHANGED,
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.INBOX_MESSAGE_DELETED,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
@@ -79,12 +94,16 @@ const PROVIDERS = [
ChatWootInboxMessageCreatedConsumer,
ChatWootInboxMessageUpdatedConsumer,
ChatWootInboxMessageDeletedConsumer,
// Conversation events
ChatWootConversationCreatedConsumer,
ChatWootConversationStatusChangedConsumer,
ChatWootInboxCommandsConsumer,
WAHASessionStatusConsumer,
WAHAMessageAnyConsumer,
WAHAMessageReactionConsumer,
WAHAMessageEditedConsumer,
WAHAMessageRevokedConsumer,
WAHAMessageAckConsumer,
MessageCleanupConsumer,
CheckVersionConsumer,
ChatWootWAHAQueueService,
@@ -0,0 +1,26 @@
import axios from 'axios';
export class ChatWootInboxNewAPI {
constructor(
private base: string,
private inboxIdentifier: string,
) {}
async updateLastSeen(sourceId: string, conversationId: number) {
// TODO: Move to the lib (fork?)
// Call the update_last_seen endpoint using a direct HTTP request
// since the SDK doesn't have this method
const path = `/public/api/v1/inboxes/${this.inboxIdentifier}/contacts/${sourceId}/conversations/${conversationId}/update_last_seen`;
const url = new URL(path, this.base).toString();
await axios.post(
url,
{},
{
headers: {
api_access_token: this.inboxIdentifier,
'Content-Type': 'application/json',
},
},
);
}
}
@@ -1,12 +1,14 @@
import ChatwootClient, {
ApiError,
ApiError as ChatWootAPIError,
public_contact_create_update_payload,
public_conversation,
} from '@figuro/chatwoot-sdk';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { ContactAPI } from '@waha/apps/chatwoot/client/ContactAPI';
import { ContactService } from '@waha/apps/chatwoot/client/ContactService';
import { Conversation } from '@waha/apps/chatwoot/client/Conversation';
import { ConversationAPI } from '@waha/apps/chatwoot/client/ConversationAPI';
import {
ContactIds,
ConversationService,
} from '@waha/apps/chatwoot/client/ConversationService';
import { ChatWootAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import { InboxContactInfo } from '@waha/apps/chatwoot/contacts/InboxContactInfo';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
@@ -29,8 +31,8 @@ export class ContactConversationService {
constructor(
private config: ChatWootAPIConfig,
private contactAPI: ContactAPI,
private conversationAPI: ConversationAPI,
private contactService: ContactService,
private conversationService: ConversationService,
private accountAPI: ChatwootClient,
private logger: ILogger,
private l: Locale,
@@ -40,7 +42,7 @@ export class ContactConversationService {
private async upsertByContactInfo(
contactInfo: ContactInfo,
): Promise<public_conversation> {
): Promise<ContactIds> {
const chatId = contactInfo.ChatId();
// Check cache for chat id
@@ -51,61 +53,75 @@ export class ContactConversationService {
//
// Find or create contact
//
let contact = await this.contactAPI.searchByAnyID(chatId);
let contact = await this.contactService.searchByAnyID(chatId);
if (!contact) {
const request = await contactInfo.PublicContactCreate();
contact = await this.contactAPI.create(chatId, request);
contact = await this.contactService.create(chatId, request);
}
// Update custom attributes - always
const attributes = await contactInfo.Attributes();
this.logger.info(
`Updating contact custom attributes for chat.id: ${chatId}, contact.id: ${contact.data.id}`,
`Updating if required contact custom attributes for chat.id: ${chatId}, contact.id: ${contact.data.id}`,
);
await this.contactAPI.updateCustomAttributes(contact.data, attributes);
await this.contactService.upsertCustomAttributes(contact.data, attributes);
// Update Avatar if nothing, but keep the original one if any
if (!contact.data.thumbnail) {
const avatarUrl = await contactInfo.AvatarUrl();
const avatarUrl = await contactInfo.AvatarUrl().catch((err) => {
this.logger.warn(
`Error getting avatar for chat.id from WhatsApp: ${chatId}`,
);
this.logger.warn(err);
return null;
});
if (avatarUrl) {
this.contactAPI.updateAvatarUrlSafe(contact.data.id, avatarUrl);
this.contactService.updateAvatarUrlSafe(contact.data.id, avatarUrl);
}
}
this.logger.info(
`Using contact for chat.id: ${chatId}, contact.id: ${contact.sourceId}`,
`Using contact for chat.id: ${chatId}, contact.id: ${contact.data.id}, contact.sourceId: ${contact.sourceId}`,
);
//
// Get or create a conversation for this inbox
//
const conversation = await this.conversationAPI.upsert(contact.sourceId);
const conversation = await this.conversationService.upsert({
id: contact.data.id,
sourceId: contact.sourceId,
});
this.logger.info(
`Using conversation for chat.id: ${chatId}, conversation.id: ${conversation.id}, contact.id: ${contact.sourceId}`,
);
// Save to cache
this.cache.set(chatId, conversation);
return conversation;
const ids = {
id: conversation.id,
sourceId: contact.sourceId,
};
this.cache.set(chatId, ids);
return ids;
}
public async ConversationByContact(
contactInfo: ContactInfo,
): Promise<Conversation> {
const chatId = contactInfo.ChatId();
const publicConversation = await this.upsertByContactInfo(contactInfo);
const ids = await this.upsertByContactInfo(contactInfo);
const conversation = new Conversation(
this.accountAPI,
this.config.accountId,
publicConversation.id,
ids.id,
ids.sourceId,
);
conversation.onError = (err) => {
if (err instanceof ApiError) {
if (err instanceof ChatWootAPIError) {
// invalidate cache
this.cache.delete(chatId);
this.logger.error(`ApiError: ${err.message}`);
this.logger.error(
`ApiError occurred, invalidating cache for chat.id: ${chatId}, conversation.id: ${publicConversation.id}`,
`ApiError occurred, invalidating cache for chat.id: ${chatId}, conversation.id: ${ids.id}, source.id: ${ids.sourceId}`,
);
}
};
@@ -127,4 +143,33 @@ export class ContactConversationService {
public async InboxNotifications() {
return this.ConversationByContact(new InboxContactInfo(this.l));
}
public ResetCache(chatIds: Array<string>) {
this.logger.info(`Resetting cache chat ids: ${chatIds.join(', ')}`);
for (const chatId of chatIds) {
this.cache.delete(chatId);
}
}
public ResetMismatchedCache(chatIds: Array<string>, contactId: number) {
for (const chatId of chatIds) {
if (!this.cache.has(chatId)) {
continue;
}
const current = this.cache.get(chatId);
if (current.id !== contactId) {
this.logger.info(
`Resetting cache for chat id: ${chatId}, value changed from ${current} to ${contactId}`,
);
this.cache.delete(chatId);
}
}
}
public async markConversationAsRead(
conversationId: number,
sourceId: string,
): Promise<void> {
await this.conversationService.markAsRead(conversationId, sourceId);
}
}
@@ -13,13 +13,14 @@ import { isJidCusFormat } from '@waha/utils/wa';
import * as lodash from 'lodash';
import { AttributeKey } from '../const';
import { E164Parser } from '@waha/core/utils/PhoneJidNormalizer';
export interface ContactResponse {
data: generic_id & contact;
sourceId: string;
}
export class ContactAPI {
export class ContactService {
constructor(
private config: ChatWootAPIConfig,
private accountAPI: ChatwootClient,
@@ -64,8 +65,8 @@ export class ContactAPI {
if (isJidCusFormat(chatId)) {
// Search by phone
let phone_number = chatId.split('@')[0];
phone_number = phone_number.replace('+', '');
const phoneNumberE164 = E164Parser.fromJid(chatId);
const phone_number = phoneNumberE164.replace('+', '');
payload[payload.length - 1].query_operator = 'OR';
payload.push({
attribute_key: 'phone_number',
@@ -96,18 +97,22 @@ export class ContactAPI {
};
}
public updateCustomAttributes(
public async upsertCustomAttributes(
contact: generic_id & contact,
attributes: any,
) {
): Promise<boolean> {
if (lodash.isEqual(attributes, contact.custom_attributes)) {
return false;
}
const update: contact_update = {
custom_attributes: { ...contact.custom_attributes, ...attributes },
};
return this.accountAPI.contacts.update({
await this.accountAPI.contacts.update({
id: contact.id,
accountId: this.config.accountId,
data: update,
});
return true;
}
public async create(
@@ -142,7 +147,7 @@ export class ContactAPI {
})
.catch((e) => {
this.logger.warn(
'Error updating avatar_url for contact.id: ' + contactId,
`Error updating avatar_url for contact.id: ${contactId}`,
);
this.logger.warn(e);
});
+1
View File
@@ -9,6 +9,7 @@ export class Conversation {
private accountAPI: ChatwootClient,
private accountId: number,
public conversationId: number,
public sourceId: string | null = null,
) {}
public async send(data: conversation_message_create) {
@@ -1,52 +0,0 @@
import { public_conversation } from '@figuro/chatwoot-sdk';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import {
ChatWootAPIConfig,
ChatWootInboxAPI,
} from '@waha/apps/chatwoot/client/interfaces';
export class ConversationAPI {
constructor(
private config: ChatWootAPIConfig,
private inboxAPI: ChatWootInboxAPI,
private logger: ILogger,
) {}
private async find(
contactIdentifier: string,
): Promise<public_conversation | null> {
const request = {
inboxIdentifier: this.config.inboxIdentifier,
contactIdentifier: contactIdentifier,
};
const conversations = await this.inboxAPI.conversations.list(request);
if (conversations.length != 0) {
return conversations[0];
}
return null;
}
private async create(
contactIdentifier: string,
): Promise<public_conversation> {
const conversation = await this.inboxAPI.conversations.create({
inboxIdentifier: this.config.inboxIdentifier,
contactIdentifier: contactIdentifier,
});
this.logger.info(
`Created conversation.id: ${conversation.id} for contact.id: ${contactIdentifier}`,
);
return conversation;
}
async upsert(contactIdentifier: string): Promise<public_conversation> {
let conversation = await this.find(contactIdentifier);
if (!conversation) {
conversation = await this.create(contactIdentifier);
}
this.logger.info(
`Using conversation.id: ${conversation.id} for contact.id: ${contactIdentifier}`,
);
return conversation;
}
}
@@ -0,0 +1,78 @@
import ChatwootClient, { contact_conversations } from '@figuro/chatwoot-sdk';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import {
ChatWootAPIConfig,
ChatWootInboxAPI,
} from '@waha/apps/chatwoot/client/interfaces';
import type { conversation } from '@figuro/chatwoot-sdk/dist/models/conversation';
import { ConversationSelector } from '@waha/apps/chatwoot/services/ConversationSelector';
import axios from 'axios';
import { ChatWootInboxNewAPI } from '@waha/apps/chatwoot/client/ChatWootInboxNewAPI';
export type ConversationResult = Pick<conversation, 'id' | 'account_id'>;
export interface ContactIds {
id: number;
sourceId: string;
}
export class ConversationService {
constructor(
private config: ChatWootAPIConfig,
private accountAPI: ChatwootClient,
private inboxAPI: ChatWootInboxAPI,
private selector: ConversationSelector,
private logger: ILogger,
) {}
private async find(contact: ContactIds): Promise<ConversationResult | null> {
const result: { payload: contact_conversations } =
(await this.accountAPI.contacts.listConversations({
accountId: this.config.accountId,
id: contact.id,
})) as any;
const conversations = result.payload;
return this.selector.select(conversations);
}
private async create(contact: ContactIds): Promise<ConversationResult> {
const conversation = await this.inboxAPI.conversations.create({
inboxIdentifier: this.config.inboxIdentifier,
contactIdentifier: contact.sourceId,
});
this.logger.info(
`Created conversation.id: ${conversation.id} for contact.id: ${contact.id}, contact.sourceId: ${contact.sourceId}`,
);
return conversation;
}
async upsert(contact: ContactIds): Promise<ConversationResult> {
let conversation = await this.find(contact);
if (!conversation) {
conversation = await this.create(contact);
}
this.logger.info(
`Using conversation.id: ${conversation.id} for contact.id: ${contact.id}, contact.sourceId: ${contact.sourceId}`,
);
return conversation;
}
async markAsRead(conversationId: number, sourceId: string): Promise<void> {
try {
const inboxNewAPI = new ChatWootInboxNewAPI(
this.config.url,
this.config.inboxIdentifier,
);
await inboxNewAPI.updateLastSeen(sourceId, conversationId);
this.logger.info(
`Marked conversation.id: ${conversationId} as read in inbox: ${this.config.inboxIdentifier} for contact: ${sourceId}`,
);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
this.logger.error(
`Error marking conversation.id: ${conversationId} as read: ${reason}`,
);
throw error;
}
}
}
@@ -1,53 +0,0 @@
import ChatwootClient from '@figuro/chatwoot-sdk';
import { ChatWootAccountAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import {
CustomAttributeModel,
CustomAttributeType,
} from '@waha/apps/chatwoot/client/types';
export interface CustomAttribute {
key: string;
name: string;
type: CustomAttributeType;
description: string;
model: CustomAttributeModel;
}
export class CustomAttributesAPI {
constructor(
private config: ChatWootAccountAPIConfig,
private accountAPI: ChatwootClient,
) {}
async upsert(attribute: CustomAttribute): Promise<void> {
const attributes = await this.accountAPI.customAttributes.list({
accountId: this.config.accountId,
attributeModel: String(attribute.model) as '0' | '1',
});
const existing = attributes.find((a) => a.attribute_key === attribute.key);
if (existing) {
await this.accountAPI.customAttributes.update({
accountId: this.config.accountId,
id: existing.id,
data: {
attribute_key: attribute.key,
attribute_display_name: attribute.name,
attribute_display_type: attribute.type,
attribute_description: attribute.description,
},
});
} else {
await this.accountAPI.customAttributes.create({
accountId: this.config.accountId,
data: {
attribute_key: attribute.key,
attribute_display_name: attribute.name,
attribute_display_type: attribute.type,
attribute_description: attribute.description,
attribute_model: attribute.model,
},
});
}
}
}
@@ -0,0 +1,68 @@
import ChatwootClient from '@figuro/chatwoot-sdk';
import { ChatWootAccountAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import {
CustomAttributeModel,
CustomAttributeType,
} from '@waha/apps/chatwoot/client/types';
import type { custom_attribute } from '@figuro/chatwoot-sdk/dist/models/custom_attribute';
export interface CustomAttribute {
key: string;
name: string;
type: CustomAttributeType;
description: string;
model: CustomAttributeModel;
}
export class CustomAttributesService {
constructor(
private config: ChatWootAccountAPIConfig,
private accountAPI: ChatwootClient,
) {}
async upsert(attributes: Array<CustomAttribute>): Promise<void> {
const current: Record<CustomAttributeModel, Array<custom_attribute>> = {
[CustomAttributeModel.CONVERSATION]: [],
[CustomAttributeModel.CONTACT]: [],
};
const models = [
CustomAttributeModel.CONVERSATION,
CustomAttributeModel.CONTACT,
];
for (const model of models) {
current[model] = await this.accountAPI.customAttributes.list({
accountId: this.config.accountId,
attributeModel: String(model) as '0' | '1',
});
}
for (const attribute of attributes) {
const existing = current[attribute.model].find(
(a) => a.attribute_key === attribute.key,
);
if (existing) {
await this.accountAPI.customAttributes.update({
accountId: this.config.accountId,
id: existing.id,
data: {
attribute_key: attribute.key,
attribute_display_name: attribute.name,
attribute_display_type: attribute.type,
attribute_description: attribute.description,
},
});
} else {
await this.accountAPI.customAttributes.create({
accountId: this.config.accountId,
data: {
attribute_key: attribute.key,
attribute_display_name: attribute.name,
attribute_display_type: attribute.type,
attribute_description: attribute.description,
attribute_model: attribute.model,
},
});
}
}
}
}
+11 -1
View File
@@ -1,7 +1,7 @@
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { AttributeKey } from '@waha/apps/chatwoot/const';
import { WhatsAppMessage } from '@waha/apps/chatwoot/storage';
import { buildMessageId } from '@waha/core/engines/noweb/session.noweb.core';
import { isLidUser } from '@waha/core/utils/jids';
export function GetJID(contact: any): string | null {
return contact?.custom_attributes?.[AttributeKey.WA_JID];
@@ -15,6 +15,16 @@ export function GetChatID(contact: any): string | null {
return contact?.custom_attributes?.[AttributeKey.WA_CHAT_ID];
}
export function GetAllChatIDs(contact: any): Array<string> {
const attrs = contact?.custom_attributes || {};
const ids = [
attrs[AttributeKey.WA_CHAT_ID],
attrs[AttributeKey.WA_JID],
attrs[AttributeKey.WA_LID],
];
return ids.filter(Boolean);
}
export function FindChatID(contact: any): string | null {
if (GetJID(contact)) {
return GetJID(contact);
+7
View File
@@ -51,3 +51,10 @@ export enum CustomAttributeModel {
CONVERSATION = 0,
CONTACT = 1,
}
export enum ConversationStatus {
OPEN = 'open',
PENDING = 'pending',
SNOOZED = 'snoozed',
RESOLVED = 'resolved',
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { CustomAttribute } from '@waha/apps/chatwoot/client/CustomAttributesAPI';
import { CustomAttribute } from '@waha/apps/chatwoot/client/CustomAttributesService';
import { CustomAttributeModel, CustomAttributeType } from './client/types';
+3
View File
@@ -13,11 +13,14 @@ export enum QueueName {
WAHA_MESSAGE_REACTION = 'chatwoot.waha | message.reaction',
WAHA_MESSAGE_EDITED = 'chatwoot.waha | message.edited',
WAHA_MESSAGE_REVOKED = 'chatwoot.waha | message.revoked',
WAHA_MESSAGE_ACK = 'chatwoot.waha | message.ack',
//
// ChatWoot Events - Real
//
INBOX_MESSAGE_CREATED = 'chatwoot.inbox | message_created',
INBOX_MESSAGE_UPDATED = 'chatwoot.inbox | message_updated',
INBOX_CONVERSATION_CREATED = 'chatwoot.inbox | conversation_created',
INBOX_CONVERSATION_STATUS_CHANGED = 'chatwoot.inbox | conversation_status_changed',
//
// ChatWoot Events - Artificial
//
+8 -1
View File
@@ -58,13 +58,20 @@ export abstract class ChatWootInboxMessageConsumer extends AppConsumer {
job: Job,
): Promise<any>;
protected GetConversationID(body) {
return body.conversation.id;
}
/**
* Process the job
* This method is called by the queue processor
*/
async processJob(job: Job<InboxData, any, EventName>): Promise<any> {
const body = job.data.body;
const key = ChatWootConversationKey(job.data.app, body.conversation.id);
const key = ChatWootConversationKey(
job.data.app,
this.GetConversationID(body),
);
return await this.withMutex(job, key, () =>
this.ProcessAndReportStatus(job),
);
@@ -0,0 +1,57 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { ChatWootConversationKey } from '@waha/apps/chatwoot/consumers/mutex';
import { ChatWootInboxMessageConsumer } from '@waha/apps/chatwoot/consumers/inbox/base';
import { InboxData } from '@waha/apps/chatwoot/consumers/types';
import { Job } from 'bullmq';
import { GetAllChatIDs } from '@waha/apps/chatwoot/client/ids';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { PinoLogger } from 'nestjs-pino';
import { RMutexService } from '@waha/modules/rmutex';
@Processor(QueueName.INBOX_CONVERSATION_CREATED, {
concurrency: JOB_CONCURRENCY,
})
export class ChatWootConversationCreatedConsumer extends ChatWootInboxMessageConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'ChatWootConversationCreatedConsumer');
}
protected ErrorHeaderKey(): TKey | null {
return null;
}
protected GetConversationID(body) {
return body.id;
}
protected async Process(
container: DIContainer,
body: any,
job: Job,
): Promise<any> {
const handler = new ConversationCreatedHandler(
container.ContactConversationService(),
);
return handler.handle(body);
}
}
class ConversationCreatedHandler {
constructor(private service: ContactConversationService) {}
async handle(body: any) {
const ids = GetAllChatIDs(body?.meta?.sender);
if (!ids || ids.length === 0) {
return;
}
this.service.ResetMismatchedCache(ids, body.id);
}
}
@@ -0,0 +1,62 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { ChatWootInboxMessageConsumer } from '@waha/apps/chatwoot/consumers/inbox/base';
import { Job } from 'bullmq';
import { DIContainer } from '../../di/DIContainer';
import { TKey } from '../../i18n/templates';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { AttributeKey } from '@waha/apps/chatwoot/const';
import { ConversationSelector } from '@waha/apps/chatwoot/services/ConversationSelector';
import { GetAllChatIDs } from '@waha/apps/chatwoot/client/ids';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { PinoLogger } from 'nestjs-pino';
import { RMutexService } from '@waha/modules/rmutex';
@Processor(QueueName.INBOX_CONVERSATION_STATUS_CHANGED, {
concurrency: JOB_CONCURRENCY,
})
export class ChatWootConversationStatusChangedConsumer extends ChatWootInboxMessageConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'ChatWootConversationStatusChangedConsumer');
}
protected ErrorHeaderKey(): TKey | null {
return null;
}
protected GetConversationID(body) {
return body.id;
}
protected async Process(
container: DIContainer,
body: any,
job: Job,
): Promise<any> {
const handler = new ConversationStatusChangedHandler(
container.ContactConversationService(),
container.ConversationSelector(),
);
return await handler.handle(body);
}
}
class ConversationStatusChangedHandler {
constructor(
private service: ContactConversationService,
private selector: ConversationSelector,
) {}
async handle(body) {
if (!this.selector.hasStatusFilter()) {
return;
}
const ids = GetAllChatIDs(body.meta?.sender);
this.service.ResetCache(ids);
}
}
@@ -12,9 +12,10 @@ import { EngineHelper } from '@waha/apps/chatwoot/session';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import {
ChatwootMessage,
MessageMapping,
MessageMappingService,
} from '@waha/apps/chatwoot/storage';
import { MarkdownToWhatsApp } from '@waha/apps/chatwoot/text';
import { MarkdownToWhatsApp } from '@waha/apps/chatwoot/messages/to/whatsapp/markdown';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import {
@@ -79,7 +80,10 @@ export class MessageHandler {
async handle(body: any) {
const chatId = await LookupAndCheckChatId(this.session, body);
const message = body;
if (message.content_type != 'text') {
if (
message.content_type != 'text' &&
message.content_type != 'input_csat'
) {
this.logger.info(
`Message content type not supported. Content type: ${message.content_type}`,
);
@@ -88,29 +92,48 @@ export class MessageHandler {
const content = MarkdownToWhatsApp(message.content);
const results = [];
let part = 1;
let part = 0; // Start from 0 and increment before each send/check
const replyTo = await this.getReplyTo(message).catch((err) => {
this.logger.error(`Error getting reply to message ID: ${err}`);
this.logger.error(
`ChatWoot => WhatsApp: error getting reply to message ID: ${err}`,
);
return undefined;
});
// Send text
// Send text (Part 1 if present)
const attachments = message.attachments || [];
if (content && attachments.length !== 1) {
const textTemplate = this.l.key(TKey.CW_TO_WA_MESSAGE_TEXT);
const text = textTemplate.render({
content: content,
chatwoot: body,
});
const msg = await this.sendTextMessage(chatId, text, replyTo);
results.push(msg);
part = await this.saveMapping(message, msg, part);
this.logger.info(`Text message sent: ${msg.id}`);
const sendText = content && attachments.length !== 1;
if (sendText) {
part += 1; // Text is the first possible part
const exists = await this.getMapping(message, part);
if (exists) {
this.logger.warn(
`Skip part ${part}: mapping exists for Chatwoot message ${message.id}`,
);
} else {
const textTemplate = this.l.key(TKey.CW_TO_WA_MESSAGE_TEXT);
const text = textTemplate.render({
content: content,
chatwoot: body,
});
const msg = await this.sendTextMessage(chatId, text, replyTo);
results.push(msg);
await this.saveMapping(message, msg, part);
this.logger.info(`Text message sent: ${msg.id}`);
}
}
// Send files
const captionTemplate = this.l.key(TKey.CW_TO_WA_MESSAGE_MEDIA_CAPTION);
for (const file of attachments) {
part += 1; // Increment before each attachment send/check
const exists = await this.getMapping(message, part);
if (exists) {
this.logger.warn(
`Skip part ${part}: mapping exists for Chatwoot message ${message.id}`,
);
continue;
}
const caption = captionTemplate.render({
content: content,
chatwoot: body,
@@ -121,7 +144,7 @@ export class MessageHandler {
`File message sent: ${msg.id} - ${file.data_url} - ${file.file_type}`,
);
results.push(msg);
part = await this.saveMapping(message, msg, part);
await this.saveMapping(message, msg, part);
}
return results;
}
@@ -130,15 +153,27 @@ export class MessageHandler {
chatwootMessage: any,
whatsappMessage: any,
part: number,
): Promise<number> {
): Promise<void> {
const chatwoot: Omit<ChatwootMessage, 'id'> = {
timestamp: new Date(chatwootMessage.created_at),
conversation_id: chatwootMessage.conversation.id,
message_id: chatwootMessage.id,
};
const whatsapp = EngineHelper.WhatsAppMessageKeys(whatsappMessage);
await this.mappingService.map(chatwoot, whatsapp, 1);
return part + 1;
await this.mappingService.map(chatwoot, whatsapp, part);
}
private async getMapping(
message: any,
part: number,
): Promise<MessageMapping | null> {
return this.mappingService.getMappingByChatwootCombinedKeyAndPart(
{
conversation_id: message.conversation.id,
message_id: message.id,
},
part,
);
}
async getReplyTo(message): Promise<string | undefined> {
+14 -6
View File
@@ -1,4 +1,3 @@
import { isJidGroup, isJidStatusBroadcast } from '@adiwajshing/baileys';
import { conversation_message_create } from '@figuro/chatwoot-sdk';
import type { generic_id } from '@figuro/chatwoot-sdk/dist/models/generic_id';
import type { message } from '@figuro/chatwoot-sdk/dist/models/message';
@@ -22,7 +21,6 @@ import {
WhatsAppMessage,
} from '@waha/apps/chatwoot/storage';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents } from '@waha/structures/enums.dto';
@@ -36,7 +34,7 @@ import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { isJidBroadcast, isJidGroup, toCusFormat } from '@waha/core/utils/jids';
export function ListenEventsForChatWoot() {
return [
@@ -44,6 +42,7 @@ export function ListenEventsForChatWoot() {
WAHAEvents.MESSAGE_REACTION,
WAHAEvents.MESSAGE_EDITED,
WAHAEvents.MESSAGE_REVOKED,
WAHAEvents.MESSAGE_ACK,
WAHAEvents.SESSION_STATUS,
];
}
@@ -98,8 +97,15 @@ export abstract class ChatWootWAHABaseConsumer extends AppConsumer {
messageInfo: IMessageInfo,
): Promise<any>;
ShouldProcess(event: any): boolean {
return true;
}
async processJob(job: Job<EventData, any, WAHAEvents>): Promise<any> {
const event: WAHAWebhookMessageRevoked = job.data.event as any;
const event = job.data.event as any;
if (!this.ShouldProcess(event)) {
return;
}
const key = WhatsAppChatIdKey(job.data.app, this.GetChatId(event));
return await this.withMutex(job, key, () =>
this.ProcessAndReportErrors(job),
@@ -301,7 +307,9 @@ export abstract class MessageBaseHandler<Payload extends WAMessageBase> {
}
const replyTo = await this.getReplyToChatWootMessageID(payload).catch(
(err) => {
this.logger.error(`Error getting reply to message ID: ${err}`);
this.logger.error(
`WhatsApp => ChatWoot - error getting reply to message ID: ${err}`,
);
return undefined;
},
);
@@ -329,6 +337,6 @@ export abstract class MessageBaseHandler<Payload extends WAMessageBase> {
chat_id: payload.from,
message_id: replyToWhatsAppID,
});
return chatwoot.message_id;
return chatwoot?.message_id;
}
}
@@ -0,0 +1,132 @@
import { Processor } from '@nestjs/bullmq';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { EventData } from '@waha/apps/chatwoot/consumers/types';
import {
ChatWootWAHABaseConsumer,
IMessageInfo,
} from '@waha/apps/chatwoot/consumers/waha/base';
import { WhatsAppContactInfo } from '@waha/apps/chatwoot/contacts/WhatsAppContactInfo';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents, WAMessageAck } from '@waha/structures/enums.dto';
import { WAHAWebhookMessageAck } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { isJidCusFormat } from '@waha/utils/wa';
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { toCusFormat } from '@waha/core/utils/jids';
@Processor(QueueName.WAHA_MESSAGE_ACK, { concurrency: JOB_CONCURRENCY })
export class WAHAMessageAckConsumer extends ChatWootWAHABaseConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'WAHAMessageAckConsumer');
}
ShouldProcess(event: WAHAWebhookMessageAck): boolean {
// Mark as seen only if it's DM
// Ignore groups and other multiple participants chats
const chatId = this.GetChatId(event);
if (!isJidCusFormat(chatId) && !isLidUser(chatId)) {
return false;
}
const payload = event.payload;
// Only READ and PLAYED
const read = payload.ack === WAMessageAck.READ;
const played = payload.ack == WAMessageAck.PLAYED;
if (!read && !played) {
return true;
}
// Only not from me
const sender = toCusFormat(payload._data?.Sender) || payload.from;
const me = this.manager.getSession(event.session).getSessionMeInfo();
if (sender == me.id || sender === me.lid) {
this.logger.debug('Ignore from me ack');
// Ignore from me
return false;
}
return true;
}
GetChatId(event: WAHAWebhookMessageAck): string {
return event.payload.from;
}
async Process(
job: Job<EventData, any, WAHAEvents>,
info: IMessageInfo,
): Promise<void> {
const container = await this.DIContainer(job, job.data.app);
const event = job.data.event as WAHAWebhookMessageAck;
const session = new WAHASessionAPI(event.session, container.WAHASelf());
const handler = new MessageAckHandler(
container.ContactConversationService(),
container.Logger(),
info,
session,
container.Locale(),
);
try {
await handler.handle(event);
} catch (e) {
// TODO: Investigate errors
// https://github.com/devlikeapro/waha/issues/1492
this.logger.error(e);
}
}
}
class MessageAckHandler {
constructor(
private readonly contactConversationService: ContactConversationService,
private readonly logger: ILogger,
private readonly info: IMessageInfo,
private readonly session: WAHASessionAPI,
private readonly locale: Locale,
) {}
async handle(event: WAHAWebhookMessageAck): Promise<void> {
const payload = event.payload;
const contactInfo = WhatsAppContactInfo(
this.session,
payload.from,
this.locale,
);
const conversation =
await this.contactConversationService.ConversationByContact(contactInfo);
this.info.onConversationId(conversation.conversationId);
const sourceId = conversation.sourceId;
if (!sourceId) {
this.logger.warn(
`Contact not found for chat.id: ${payload.from}. Skipping read update for message ${payload.id}`,
);
return;
}
try {
await this.contactConversationService.markConversationAsRead(
conversation.conversationId,
sourceId,
);
this.logger.info(
`Marked conversation ${conversation.conversationId} as read for chat.id: ${payload.from} (message: ${payload.id}, sourceId: ${sourceId})`,
);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
this.logger.error(
`Error marking conversation ${conversation.conversationId} as read for chat.id: ${payload.from}. Reason: ${reason}`,
);
throw error;
}
}
}
+55 -154
View File
@@ -1,6 +1,5 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { SendAttachment } from '@waha/apps/chatwoot/client/types';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { EventData } from '@waha/apps/chatwoot/consumers/types';
import {
@@ -10,7 +9,6 @@ import {
MessageBaseHandler,
} from '@waha/apps/chatwoot/consumers/waha/base';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/text';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
@@ -19,14 +17,19 @@ import { WAMessage } from '@waha/structures/responses.dto';
import { WAHAWebhookMessageAny } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { JobLink } from '@waha/apps/app_sdk/JobUtils';
import { proto } from '@adiwajshing/baileys';
import * as lodash from 'lodash';
import { parseVCardV3, SimpleVCardInfo } from '@waha/core/vcard';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mime = require('mime-types');
import {
FacebookAdMessage,
EventMessage,
PixMessage,
LocationMessage,
MessageToChatWootConverter,
PollMessage,
ListMessage,
ShareContactMessage,
TextMessage,
UnsupportedMessage,
resolveProtoMessage,
} from '@waha/apps/chatwoot/messages/to/chatwoot';
@Processor(QueueName.WAHA_MESSAGE_ANY, { concurrency: JOB_CONCURRENCY })
export class WAHAMessageAnyConsumer extends ChatWootWAHABaseConsumer {
@@ -63,138 +66,66 @@ export class WAHAMessageAnyConsumer extends ChatWootWAHABaseConsumer {
}
}
function isEmptyString(content: string) {
if (!content) {
return true;
}
return content == '' || content == '\n';
}
class MessageAnyHandler extends MessageBaseHandler<WAMessage> {
protected async getMessage(
payload: WAMessage,
): Promise<ChatWootMessagePartial> {
const protoMessage = this.getProtoMessage(payload);
let msg = await this.getTextMessage(payload, protoMessage);
let converter: MessageToChatWootConverter;
let msg: ChatWootMessagePartial;
const protoMessage = resolveProtoMessage(payload);
// Check for Facebook Ad first - but let it use the normal flow later
converter = new FacebookAdMessage(this.l, this.logger);
msg = await converter.convert(payload, protoMessage);
if (msg) {
return msg;
}
msg = this.getLocationMessage(payload, protoMessage);
converter = new TextMessage(this.l, this.logger, this.waha);
msg = await converter.convert(payload, null);
if (msg) {
return msg;
}
msg = this.getShareContactMessage(payload, protoMessage);
converter = new LocationMessage(this.l);
msg = await converter.convert(payload, protoMessage);
if (msg) {
return msg;
}
return this.getUnsupportedMessage();
}
protected async getTextMessage(
payload: WAMessage,
_: proto.Message | null,
): Promise<ChatWootMessagePartial | null> {
const attachments = await this.getAttachments(payload);
let content = this.l
.key(TKey.WA_TO_CW_MESSAGE)
.render({ payload: payload });
if (isEmptyString(content) && attachments.length == 0) {
return null;
}
if (isEmptyString(content)) {
content = null;
}
return {
content: WhatsappToMarkdown(content),
attachments: attachments,
private: undefined,
};
}
private getLocationMessage(
payload: WAMessage,
message: proto.Message | null,
): ChatWootMessagePartial | null {
const hasLocation = !lodash.isEmpty(message?.locationMessage);
const hasLiveLocation = !lodash.isEmpty(message?.liveLocationMessage);
if (!hasLocation && !hasLiveLocation) {
return null;
}
const location = this.l.key(TKey.WA_TO_CW_MESSAGE_LOCATION).r({
payload,
message,
});
if (isEmptyString(location)) {
return null;
}
return {
content: location,
attachments: [],
private: undefined,
};
}
private getShareContactMessage(
_payload: WAMessage,
message: proto.Message | null,
): ChatWootMessagePartial | null {
let vcards: string[] = [];
if (!lodash.isEmpty(message?.contactsArrayMessage?.contacts)) {
vcards = message!.contactsArrayMessage!.contacts.map((c) => c.vcard);
} else if (!lodash.isEmpty(message?.contactMessage?.vcard)) {
vcards = [message!.contactMessage!.vcard];
}
if (vcards.length === 0) {
return null;
}
const attachments: SendAttachment[] = vcards.map((v, i) => ({
content: Buffer.from(v, 'utf8').toString('base64'),
encoding: 'base64',
filename: `vcard-${i + 1}.vcf`,
}));
let contacts: SimpleVCardInfo[] = [];
try {
contacts = vcards.map(parseVCardV3);
} catch (err) {
this.logger.error(
`Error parsing some vcards: vcards=${vcards}, err=${err}`,
);
}
const msg = this.l.key(TKey.WA_TO_CW_MESSAGE_CONTACTS).r({ contacts });
if (contacts.length === 0 && attachments.length === 0) {
return null;
converter = new ShareContactMessage(this.l, this.logger);
msg = await converter.convert(payload, protoMessage);
if (msg) {
return msg;
}
return {
content: msg,
attachments: attachments,
private: undefined,
};
}
private getUnsupportedMessage(): ChatWootMessagePartial {
const unsupported = this.l
.key(TKey.WA_TO_CW_MESSAGE_UNSUPPORTED)
.render({ details: JobLink(this.job) });
return {
content: unsupported,
attachments: [],
private: true,
};
}
private getProtoMessage(payload: WAMessage): proto.Message | null {
// GOWS
if (payload._data.Message) {
return payload._data.Message;
converter = new PollMessage(this.l);
msg = await converter.convert(payload, protoMessage);
if (msg) {
return msg;
}
// NOWEB
if (payload._data.message) {
return payload._data.message;
converter = new ListMessage(this.l);
msg = await converter.convert(payload, protoMessage);
if (msg) {
return msg;
}
// WEBJS - not available
return null;
converter = new EventMessage(this.l);
msg = await converter.convert(payload, protoMessage);
if (msg) {
return msg;
}
converter = new PixMessage(this.l, this.logger);
msg = await converter.convert(payload, protoMessage);
if (msg) {
return msg;
}
converter = new UnsupportedMessage(this.l, this.job);
msg = await converter.convert(payload, protoMessage);
return msg;
}
getReplyToWhatsAppID(payload: WAMessage): string {
@@ -205,34 +136,4 @@ class MessageAnyHandler extends MessageBaseHandler<WAMessage> {
const key = parseMessageIdSerialized(replyTo.id, true);
return key.id;
}
async getAttachments(payload: WAMessage): Promise<SendAttachment[]> {
//
// WAHA
//
const hasMedia = payload.media?.url;
if (!hasMedia) {
return [];
}
const attachments: SendAttachment[] = [];
const media = payload.media;
this.logger.info(`Downloading media from '${media.url}'...`);
const buffer = await this.waha.fetch(media.url);
const fileContent = buffer.toString('base64');
let filename = media.filename;
if (!filename) {
const extension = mime.extension(media.mimetype);
filename = `no-filename.${extension}`;
}
const attachment: SendAttachment = {
content: fileContent,
filename: filename,
encoding: 'base64',
};
attachments.push(attachment);
this.logger.info(`Downloaded media from '${media.url}' as '${filename}'`);
return attachments;
}
}
@@ -6,9 +6,8 @@ import {
ChatWootMessagePartial,
ChatWootWAHABaseConsumer,
IMessageInfo,
MessageBaseHandler,
} from '@waha/apps/chatwoot/consumers/waha/base';
import { MessageBaseHandler } from '@waha/apps/chatwoot/consumers/waha/base';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/text';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents } from '@waha/structures/enums.dto';
@@ -19,10 +18,12 @@ import {
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { SendAttachment } from '../../client/types';
import { WAHASessionAPI } from '../../session/WAHASelf';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { WAMessage } from '@waha/structures/responses.dto';
import {
MessageEdited,
MessageToChatWootConverter,
resolveProtoMessage,
} from '@waha/apps/chatwoot/messages/to/chatwoot';
@Processor(QueueName.WAHA_MESSAGE_EDITED, { concurrency: JOB_CONCURRENCY })
export class WAHAMessageEditedConsumer extends ChatWootWAHABaseConsumer {
@@ -63,25 +64,12 @@ class MessageEditedHandler extends MessageBaseHandler<WAMessageEditedBody> {
protected async getMessage(
payload: WAMessageEditedBody,
): Promise<ChatWootMessagePartial> {
const body = payload.body;
const formatted = WhatsappToMarkdown(body);
const content = this.l
.key(TKey.MESSAGE_EDITED_IN_WHATSAPP)
.render({ text: formatted });
return {
content: content,
attachments: [],
private: undefined,
};
const protoMessage = resolveProtoMessage(payload);
const converter: MessageToChatWootConverter = new MessageEdited(this.l);
return converter.convert(payload, protoMessage);
}
getReplyToWhatsAppID(payload: WAMessageEditedBody): string {
return payload.editedMessageId;
}
async getAttachments(
payload: WAMessageEditedBody,
): Promise<SendAttachment[]> {
return [];
}
}
@@ -82,8 +82,4 @@ export class MessageReactionHandler extends MessageBaseHandler<WAMessageReaction
const key = parseMessageIdSerialized(messageId, false);
return key.id;
}
async getAttachments(payload: WAMessageReaction): Promise<SendAttachment[]> {
return [];
}
}
@@ -16,13 +16,13 @@ import {
MessageMappingService,
} from '@waha/apps/chatwoot/storage';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { WAHAWebhookMessageRevoked } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { toCusFormat } from '@waha/core/utils/jids';
@Processor(QueueName.WAHA_MESSAGE_REVOKED, { concurrency: JOB_CONCURRENCY })
export class WAHAMessageRevokedConsumer extends ChatWootWAHABaseConsumer {
@@ -20,7 +20,6 @@ import { WAHAWebhookSessionStatus } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { sleep } from '@nestjs/terminus/dist/utils';
import { waitUntil } from '@waha/utils/promiseTimeout';
@Processor(QueueName.WAHA_SESSION_STATUS, { concurrency: JOB_CONCURRENCY })
@@ -103,6 +102,20 @@ export class SessionStatusHandler {
});
break;
case WAHASessionStatus.STOPPED:
try {
// Check the session is yet in STOPPED status.
// In case of server restarts,
// the consumer can process STOPPED after STARTING/WORKING
const response = await this.waha.get(data.session);
if (response.status != WAHASessionStatus.STOPPED) {
return;
}
} catch (_) {}
text = this.l.key(TKey.APP_SESSION_STATUS_ERROR).r();
text += '\n\n';
text += this.l.key(TKey.APP_HELP_REMINDER).r();
text += '\n\n';
break;
case WAHASessionStatus.FAILED:
text = this.l.key(TKey.APP_SESSION_STATUS_ERROR).r();
text += '\n\n';
@@ -1,16 +1,4 @@
import {
isJidGroup,
isJidNewsletter,
isJidStatusBroadcast,
} from '@adiwajshing/baileys';
import {
isJidBroadcast,
isLidUser,
} from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import {
public_contact_create_update_payload,
public_contact_create_update_payload as Contact,
} from '@figuro/chatwoot-sdk';
import { public_contact_create_update_payload as Contact } from '@figuro/chatwoot-sdk';
import { ContactInfo } from '@waha/apps/chatwoot/client/ContactConversationService';
import { AttributeKey } from '@waha/apps/chatwoot/const';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
@@ -18,6 +6,16 @@ import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import { Channel } from '@waha/structures/channels.dto';
import { CacheAsync } from '@waha/utils/Cache';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import {
isJidBroadcast,
isJidGroup,
isJidNewsletter,
isJidStatusBroadcast,
isLidUser,
isPnUser,
} from '@waha/core/utils/jids';
import { UnknownJIDFormat } from '@waha/apps/chatwoot/errors';
import { E164Parser } from '@waha/core/utils/PhoneJidNormalizer';
/**
* Base WhatsApp contact info class
@@ -71,8 +69,7 @@ class JidContactInfo extends ChatContactInfo {
const contact: any = await this.session.getContact(this.chatId);
const name =
contact?.name || contact?.pushName || contact?.pushname || this.chatId;
const phoneNumber = this.chatId.split('@')[0];
const phoneNumberE164 = '+' + phoneNumber;
const phoneNumberE164 = E164Parser.fromJid(this.chatId);
const result: Contact = {
name: name,
@@ -154,6 +151,7 @@ class GroupContactInfo extends ChatContactInfo {
const group: any = await this.session?.getGroup(this.chatId);
if (group) {
name = group.subject || group.name || group.topic || name;
name = group.Name || name;
const suffix = this.locale
.key(TKey.WHATSAPP_CONTACT_GROUP_SUFFIX)
.render();
@@ -271,6 +269,9 @@ export function WhatsAppContactInfo(
return new BroadcastContactInfo(session, chatId, locale);
} else if (isLidUser(chatId)) {
return new LidContactInfo(session, chatId, locale);
} else if (isPnUser(chatId)) {
return new JidContactInfo(session, chatId, locale);
} else {
throw new UnknownJIDFormat(chatId);
}
return new JidContactInfo(session, chatId, locale);
}
+94 -91
View File
@@ -2,10 +2,10 @@ import ChatwootClient from '@figuro/chatwoot-sdk';
import * as lodash from 'lodash';
import { AxiosLogging } from '@waha/apps/app_sdk/AxiosLogging';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { ContactAPI } from '@waha/apps/chatwoot/client/ContactAPI';
import { ContactService } from '@waha/apps/chatwoot/client/ContactService';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { ConversationAPI } from '@waha/apps/chatwoot/client/ConversationAPI';
import { CustomAttributesAPI } from '@waha/apps/chatwoot/client/CustomAttributesAPI';
import { ConversationService } from '@waha/apps/chatwoot/client/ConversationService';
import { CustomAttributesService } from '@waha/apps/chatwoot/client/CustomAttributesService';
import { ChatWootInboxAPI } from '@waha/apps/chatwoot/client/interfaces';
import {
ChatWootAppConfig,
@@ -25,21 +25,17 @@ import {
import { Job } from 'bullmq';
import { Knex } from 'knex';
import { i18n } from '@waha/apps/chatwoot/i18n';
import { CacheSync } from '@waha/utils/Cache';
import {
ConversationSelector,
ConversationSort,
} from '@waha/apps/chatwoot/services/ConversationSelector';
/**
* Dependency Injection Container for ChatWoot
* Manages the creation and caching of various clients and repositories
*/
export class DIContainer {
private accountAPI: ChatwootClient;
private inboxAPI: ChatWootInboxAPI;
private contactAPI: ContactAPI;
private conversationAPI: ConversationAPI;
private contactConversationService: ContactConversationService;
private locale: Locale;
private messageMappingService: MessageMappingService;
private wahaSelf: WAHASelf;
/**
* Creates a new DIContainer with the given configuration
*/
@@ -68,122 +64,124 @@ export class DIContainer {
return this.knex;
}
@CacheSync()
public Locale(): Locale {
if (!this.locale) {
this.locale = i18n.locale(this.config.locale || DEFAULT_LOCALE);
this.locale = this.locale.override(this.ChatWootConfig().templates);
}
return this.locale;
let locale = i18n.locale(this.config.locale || DEFAULT_LOCALE);
locale = locale.override(this.ChatWootConfig().templates);
return locale;
}
/**
* Gets the AccountAPI client
* @returns ChatwootClient instance
*/
@CacheSync()
public AccountAPI(): ChatwootClient {
if (!this.accountAPI) {
this.accountAPI = new ChatwootClient({
config: {
basePath: this.config.url,
with_credentials: true,
credentials: 'include',
token: this.config.accountToken,
},
});
}
return this.accountAPI;
return new ChatwootClient({
config: {
basePath: this.config.url,
with_credentials: true,
credentials: 'include',
token: this.config.accountToken,
},
});
}
/**
* Gets the InboxAPI client
* @returns ChatWootInboxAPI instance
*/
@CacheSync()
public InboxAPI(): ChatWootInboxAPI {
if (!this.inboxAPI) {
const chatwootClientAPI = new ChatwootClient({
config: {
basePath: this.config.url,
with_credentials: true,
credentials: 'include',
token: this.config.inboxIdentifier,
},
});
this.inboxAPI = chatwootClientAPI.client as ChatWootInboxAPI;
}
return this.inboxAPI;
const chatwootClientAPI = new ChatwootClient({
config: {
basePath: this.config.url,
with_credentials: true,
credentials: 'include',
token: this.config.inboxIdentifier,
},
});
return chatwootClientAPI.client as ChatWootInboxAPI;
}
/**
* Gets the ContactAPI
* @returns ContactAPI instance
* Gets the ContactService
* @returns ContactService instance
*/
private ContactAPI(): ContactAPI {
if (!this.contactAPI) {
this.contactAPI = new ContactAPI(
this.config,
this.AccountAPI(),
this.InboxAPI(),
this.logger,
);
}
return this.contactAPI;
@CacheSync()
private ContactService(): ContactService {
return new ContactService(
this.config,
this.AccountAPI(),
this.InboxAPI(),
this.logger,
);
}
@CacheSync()
public ConversationSelector() {
const config = this.ChatWootConfig();
return new ConversationSelector({
sort: config.conversations.sort,
status: config.conversations.status,
inboxId: this.config.inboxId,
});
}
/**
* Gets the ConversationAPI
* @returns ConversationAPI instance
* Gets the ConversationService
* @returns ConversationService instance
*/
private ConversationAPI(): ConversationAPI {
if (!this.conversationAPI) {
this.conversationAPI = new ConversationAPI(
this.config,
this.InboxAPI(),
this.logger,
);
}
return this.conversationAPI;
@CacheSync()
private ConversationService(): ConversationService {
return new ConversationService(
this.config,
this.AccountAPI(),
this.InboxAPI(),
this.ConversationSelector(),
this.logger,
);
}
/**
* Gets the ContactConversationService
* @returns ContactConversationService instance
*/
@CacheSync()
public ContactConversationService(): ContactConversationService {
if (!this.contactConversationService) {
this.contactConversationService = new ContactConversationService(
this.config,
this.ContactAPI(),
this.ConversationAPI(),
this.AccountAPI(),
this.logger,
this.Locale(),
);
}
return this.contactConversationService;
return new ContactConversationService(
this.config,
this.ContactService(),
this.ConversationService(),
this.AccountAPI(),
this.logger,
this.Locale(),
);
}
@CacheSync()
private ChatwootMessageRepository(): ChatwootMessageRepository {
return new ChatwootMessageRepository(this.Knex(), this.AppPk());
}
@CacheSync()
private WhatsAppMessageRepository(): WhatsAppMessageRepository {
return new WhatsAppMessageRepository(this.Knex(), this.AppPk());
}
@CacheSync()
private MessageMappingRepository(): MessageMappingRepository {
return new MessageMappingRepository(this.Knex(), this.AppPk());
}
@CacheSync()
public MessageMappingService(): MessageMappingService {
if (!this.messageMappingService) {
this.messageMappingService = new MessageMappingService(
this.Knex(),
this.WhatsAppMessageRepository(),
this.ChatwootMessageRepository(),
this.MessageMappingRepository(),
);
}
return this.messageMappingService;
return new MessageMappingService(
this.Knex(),
this.WhatsAppMessageRepository(),
this.ChatwootMessageRepository(),
this.MessageMappingRepository(),
);
}
public ChatWootErrorReporter(job: Job): ChatWootErrorReporter {
@@ -194,19 +192,20 @@ export class DIContainer {
* Gets the WAHASelf instance
* @returns WAHASelf instance
*/
@CacheSync()
public WAHASelf(): WAHASelf {
if (!this.wahaSelf) {
this.wahaSelf = new WAHASelf();
const logging = new AxiosLogging(this.Logger());
logging.applyTo(this.wahaSelf.client);
}
return this.wahaSelf;
const self = new WAHASelf();
const logging = new AxiosLogging(this.Logger());
logging.applyTo(self.client);
return self;
}
public CustomAttributesAPI() {
return new CustomAttributesAPI(this.config, this.AccountAPI());
@CacheSync()
public CustomAttributesService() {
return new CustomAttributesService(this.config, this.AccountAPI());
}
@CacheSync()
public ChatWootConfig(): ChatWootConfig {
const defaults: ChatWootConfig = {
templates: {},
@@ -214,6 +213,10 @@ export class DIContainer {
commands: {
server: true,
},
conversations: {
sort: ConversationSort.created_newest,
status: null,
},
};
return lodash.defaults({}, this.config, defaults);
}
+21
View File
@@ -9,6 +9,11 @@ import {
} from 'class-validator';
import { Type } from 'class-transformer';
import { IsDynamicObject } from '@waha/nestjs/validation/IsDynamicObject';
import {
ConversationSelectorConfig,
ConversationSort,
} from '@waha/apps/chatwoot/services/ConversationSelector';
import { ConversationStatus } from '@waha/apps/chatwoot/client/types';
export const DEFAULT_LOCALE = 'en-US';
@@ -23,10 +28,20 @@ export enum LinkPreview {
HQ = 'HG',
}
export class ChatWootConversationsConfig {
@IsEnum(ConversationSort)
sort: ConversationSort;
@IsOptional()
@IsEnum(ConversationStatus, { each: true })
status: Array<ConversationStatus> | null;
}
export interface ChatWootConfig {
templates: Record<string, string>;
linkPreview: LinkPreview;
commands: ChatWootCommandsConfig;
conversations: ChatWootConversationsConfig;
}
export class ChatWootAppConfig implements ChatWootAPIConfig {
@@ -56,7 +71,13 @@ export class ChatWootAppConfig implements ChatWootAPIConfig {
@IsDynamicObject()
templates?: Record<string, string>;
@IsOptional()
@ValidateNested()
@Type(() => ChatWootCommandsConfig)
commands?: ChatWootCommandsConfig;
@IsOptional()
@ValidateNested()
@Type(() => ChatWootConversationsConfig)
conversations?: ChatWootConversationsConfig;
}
@@ -11,8 +11,10 @@ import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { Job } from 'bullmq';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
const renderer: ErrorRenderer = new ErrorRenderer();
export class ChatWootErrorReporter {
private errorRenderer: ErrorRenderer = new ErrorRenderer();
private errorRenderer: ErrorRenderer = renderer;
constructor(
private logger: ILogger,
@@ -27,10 +29,22 @@ export class ChatWootErrorReporter {
error: any,
replyTo?: number,
) {
const errorText = this.errorRenderer.renderError(error);
const errorText = this.errorRenderer.text(error);
this.logger.error(errorText);
try {
const data = this.errorRenderer.data(error);
this.logger.error(JSON.stringify(data, null, 2));
} catch (err) {
this.logger.error(`Error occurred while login details for error: ${err}`);
}
const template = this.l.key(TKey.JOB_REPORT_ERROR);
const nextDelay = NextAttemptDelayInWholeSeconds(this.job);
if (nextDelay) {
// TODO: Add a way to enable it back in config
// https://github.com/devlikeapro/waha/issues/1395
// There's retries more left - ignore it for now
return;
}
const attempts = {
current: this.job.attemptsMade + 1,
max: this.job.opts?.attempts || 1,
@@ -69,9 +83,10 @@ export class ChatWootErrorReporter {
type: MessageType,
replyTo?: number,
): Promise<void> {
const jobUrl = `http://localhost:3000/jobs/queue/${encodeURIComponent(
this.job.queueName,
)}/${this.job.id}`;
// TODO: Add a way to enable it back in config
// https://github.com/devlikeapro/waha/issues/1395
return null;
const template = this.l.key(TKey.JOB_REPORT_SUCCEEDED);
const attempts = {
current: this.job.attemptsMade + 1,
@@ -79,10 +94,7 @@ export class ChatWootErrorReporter {
};
const content = template.render({
details: {
text: `${this.job.queueName} => ${this.job.id}`,
url: jobUrl,
},
details: JobLink(this.job),
attempts: attempts,
});
+105 -40
View File
@@ -1,45 +1,28 @@
import { ApiError } from '@figuro/chatwoot-sdk/dist/core/ApiError';
import { AxiosError } from 'axios';
import { ApiError as ChatWootAPIError } from '@figuro/chatwoot-sdk/dist/core/ApiError';
import type { AxiosError } from 'axios';
/**
* Class responsible for rendering error information based on error type
*/
export class ErrorRenderer {
interface IErrorRender<T> {
/**
* Renders error information based on error type
* @param error The error to render
* @returns Formatted error text
* Get an error text to show to the user
*/
public renderError(error: any): string {
// Handle Axios errors
if (error.isAxiosError) {
return this.renderAxiosError(error as AxiosError);
}
// Handle Chatwoot SDK ApiError
if (error instanceof ApiError) {
return this.renderApiError(error);
}
// For all other errors
return this.renderGenericError(error);
}
text(error: T): string;
/**
* Renders Axios error information
* @param error The Axios error to render
* @returns Formatted error text
* Structured error data for logging/inspection
*/
private renderAxiosError(error: AxiosError): string {
let errorText = `Axios Error: ${error.message}`;
data(error: T): Record<string, any>;
}
class AxiosErrorRenderer implements IErrorRender<AxiosError> {
text(error: AxiosError): string {
let errorText = `API Error: ${error.message}`;
if (!error.response?.data) {
return errorText;
}
// Add response data if it exists and is JSON
try {
const data = error.response.data as any;
let json: undefined;
let json: any;
if (Buffer.isBuffer(data)) {
json = JSON.parse(data.toString());
} else if (typeof data === 'object') {
@@ -55,16 +38,35 @@ export class ErrorRenderer {
return errorText;
}
/**
* Renders Chatwoot SDK ApiError information
* @param error The ApiError to render
* @returns Formatted error text
*/
private renderApiError(error: ApiError): string {
data(error: AxiosError): Record<string, any> {
const { config, response, request } = error as any;
return {
class: 'APIError',
request: {
method: request?.method || config?.method,
url: request?.path || config?.url,
params: config?.params,
body: request?.body || config?.data,
},
response: {
status: response?.status,
statusText: response?.statusText,
headers: response?.headers,
body: response?.data,
},
message: error.message,
code: (error as any)?.code,
stack: error.stack,
};
}
}
class ChatWootAPIErrorRenderer implements IErrorRender<ChatWootAPIError> {
text(error: ChatWootAPIError): string {
let errorText = `ChatWoot API Error: ${error.message}`;
errorText += `\nStatus: ${error.status}`;
// Add body if it exists
if (error.body) {
try {
const body =
@@ -78,12 +80,75 @@ export class ErrorRenderer {
return errorText;
}
data(error: ChatWootAPIError): Record<string, any> {
return {
class: 'ChatWootAPIError',
request: {
method: error.request?.method,
url: error.request?.url,
query: error.request?.query,
path: error.request?.path,
body: error.request?.body,
},
response: {
status: error.status,
statusText: error.statusText,
body: error.body,
},
message: error.message,
stack: error.stack,
};
}
}
class GenericErrorRenderer implements IErrorRender<any> {
text(error: any): string {
return error?.toString?.() ?? String(error);
}
data(error: any): Record<string, any> {
return {
class: error?.name ?? error?.constructor?.name ?? typeof error,
message: error?.message ?? String(error),
stack: error.stack,
};
}
}
/**
* Class responsible for rendering error information based on error type
*/
export class ErrorRenderer implements IErrorRender<any> {
private readonly axiosRenderer = new AxiosErrorRenderer();
private readonly chatwootRenderer = new ChatWootAPIErrorRenderer();
private readonly genericRenderer = new GenericErrorRenderer();
/**
* Renders generic error information
* Renders error information based on error type
* @param error The error to render
* @returns Formatted error text
*/
private renderGenericError(error: any): string {
return error.toString();
public text(error: any): string {
if (error?.isAxiosError) {
return this.axiosRenderer.text(error as AxiosError);
}
if (error instanceof ChatWootAPIError) {
return this.chatwootRenderer.text(error);
}
return this.genericRenderer.text(error);
}
public data(error: any): Record<string, any> {
if (error?.isAxiosError) {
return this.axiosRenderer.data(error as AxiosError);
}
if (error instanceof ChatWootAPIError) {
return this.chatwootRenderer.data(error);
}
return this.genericRenderer.data(error);
}
}
+6
View File
@@ -9,3 +9,9 @@ export class PhoneNumberNotFoundInWhatsAppError extends Error {
super(`Phone number not found in WhatsApp: ${phone}`);
}
}
export class UnknownJIDFormat extends Error {
constructor(public jid: string) {
super(`WhatsApp Chat ID is not recognized: ${jid}`);
}
}
+62 -1
View File
@@ -1,9 +1,19 @@
import * as Mustache from 'mustache';
import { TemplatePayloads, TKey } from '@waha/apps/chatwoot/i18n/templates';
import Long from 'long';
import { ensureNumber } from '@waha/core/engines/noweb/utils';
export class Locale {
constructor(private readonly strings: Record<string, string>) {}
/**
* Return Base Locale
* For number, datetime, currency formats
*/
get locale(): string {
return this.strings['locale.base'] || 'en';
}
key<K extends TKey>(key: K): Template<K> {
return new Template(this.strings[key] || key);
}
@@ -15,13 +25,64 @@ export class Locale {
override(strings: Record<string, string>): Locale {
return new Locale({ ...this.strings, ...strings });
}
FormatCurrency(
currency: string,
value: number | null,
offset: number = 1,
): string | null {
if (value == null) {
return null;
}
if (!currency) {
return null;
}
offset = offset || 1;
try {
const fmt = new Intl.NumberFormat(this.locale, {
style: 'currency',
currency: currency,
});
return fmt.format(value / offset);
} catch (err) {
return `${currency} ${value}`;
}
}
FormatDatetime(date: Date | null): string | null {
if (!date) {
return null;
}
const options: any = this.strings['datetime'] || {};
options.timeZone = options.timeZone || options.timezone || process.env.TZ;
return date.toLocaleString(this.locale, options);
}
FormatTimestamp(timestamp: Long | string | number | null): string | null {
const value = ensureNumber(timestamp);
if (!value) {
return undefined;
}
if (!Number.isFinite(value)) {
return undefined;
}
const milliseconds = value >= 1e12 ? value : value * 1000;
const date = new Date(milliseconds);
if (Number.isNaN(date.getTime())) {
return undefined;
}
return this.FormatDatetime(date);
}
}
export class Template<K extends TKey> {
constructor(private readonly template: string) {}
render(data: TemplatePayloads[K]): string {
return Mustache.render(this.template, data);
const text = Mustache.render(this.template, data);
// Remove /n at the end if any
return text.replace(/\n$/, '');
}
r(data: TemplatePayloads[K]): string {
+151 -5
View File
@@ -1,8 +1,28 @@
# Arabic translations for ChatWoot integration
locale.name: '🇦🇪 العربية (Arabic)'
app.inbox.contact.name: 'تكامل واتساب (WAHA)'
# رمز اللغة الأساسي - يجب أن يكون محليًا صالحًا وفق BCP 47
# https://en.wikipedia.org/wiki/IETF_language_tag
# تستخدمه WAHA لعرض الأرقام والعملات والتاريخ/الوقت
locale.base: 'ar-AE'
app.inbox.contact.name: 'تكامل واتساب'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# "options" من date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: ar-AE
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Asia/Dubai'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *تم تعديلها في واتساب*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*تفاعل* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **الموقع المشترك**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **العنوان:** {{address}}
{{/address}}
{{#url}}
🌐 **الموقع الإلكتروني:** {{url}}
{{/url}}
🔗 **عرض على الخريطة:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **مشاركة الموقع المباشر**
افتح **WhatsApp** لعرض التحديثات اللحظية.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **العنوان:** {{address}}
{{/address}}
{{#url}}
🌐 **الموقع الإلكتروني:** {{url}}
{{/url}}
🔗 **عرض على الخريطة:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **استطلاع**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **حدث**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **الموقع:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **البدء:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **الانتهاء:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **رابط الانضمام:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - نسخ ولصق**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**مفتاح PIX:** {{key}}
{{/key}}
{{#keyType}}
**النوع:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**المبلغ:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**المرجع:** {{referenceId}}
{{/referenceId}}
💳 **تم إرسال PIX نسخ ولصق**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **هذا النوع من الرسائل غير مدعوم في ChatWoot.**
📱 يرجى فتح **WhatsApp** لعرضه.
التفاصيل: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **إعلان فيسبوك**
**العنوان:** {{{adData.title}}}
**الوصف:**
{{{adData.body}}}
**رابط الإعلان:** [عرض على فيسبوك]({{{adData.sourceUrl}}})
**معرّف الإعلان:** {{{adData.sourceId}}}
---
💬 **رسالة العميل:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# Bengali translations for ChatWoot integration
locale.name: '🇧🇩 বাংলা (Bengali)'
app.inbox.contact.name: 'হোয়াটসঅ্যাপ ইন্টিগ্রেশন (WAHA)'
# বেস লোকেল কোড - এটি একটি বৈধ BCP 47 লোকেল হতে হবে
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA এটি সংখ্যা, মুদ্রা এবং তারিখ/সময়ের ফরম্যাটিংয়ে ব্যবহার করে।
locale.base: 'bn-BD'
app.inbox.contact.name: 'হোয়াটসঅ্যাপ ইন্টিগ্রেশন'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# date.toLocaleString এর "options"
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: bn-BD
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Asia/Dhaka'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *হোয়াটসঅ্যাপে সম্পাদিত*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*প্রতিক্রিয়া দিয়েছেন* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **শেয়ার করা লোকেশন**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **ঠিকানা:** {{address}}
{{/address}}
{{#url}}
🌐 **ওয়েবসাইট:** {{url}}
{{/url}}
🔗 **মানচিত্রে দেখুন:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **লাইভ লোকেশন শেয়ার করা হচ্ছে**
রিয়েল-টাইম আপডেট দেখতে **WhatsApp** খুলুন।
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **ঠিকানা:** {{address}}
{{/address}}
{{#url}}
🌐 **ওয়েবসাইট:** {{url}}
{{/url}}
🔗 **মানচিত্রে দেখুন:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **জরিপ**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **ইভেন্ট**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **স্থান:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **শুরুর সময়:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **শেষের সময়:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **যোগদানের লিঙ্ক:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - কপি এবং পেস্ট**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**PIX কী:** {{key}}
{{/key}}
{{#keyType}}
**ধরন:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**পরিমাণ:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**রেফারেন্স:** {{referenceId}}
{{/referenceId}}
💳 **PIX কপি এবং পেস্ট পাঠানো হয়েছে**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **এই ধরনের বার্তা ChatWoot-এ সমর্থিত নয়।**
📱 এটি দেখতে অনুগ্রহ করে **WhatsApp** খুলুন।
বিস্তারিত: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **ফেসবুক বিজ্ঞাপন**
**শিরোনাম:** {{{adData.title}}}
**বর্ণনা:**
{{{adData.body}}}
**বিজ্ঞাপনের লিংক:** [ফেসবুকে দেখুন]({{{adData.sourceUrl}}})
**বিজ্ঞাপনের আইডি:** {{{adData.sourceId}}}
---
💬 **গ্রাহকের বার্তা:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# German translations for ChatWoot integration
locale.name: '🇩🇪 Deutsch (German)'
app.inbox.contact.name: 'WhatsApp Integration (WAHA)'
# Basis-Gebietsschema-Code - muss eine gültige BCP 47 Locale sein
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA nutzt ihn zur Darstellung von Zahlen, Währungen sowie Datum und Uhrzeit
locale.base: 'de-DE'
app.inbox.contact.name: 'WhatsApp Integration'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# "Optionen" von date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: de-DE
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Europe/Berlin'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -52,10 +72,10 @@ message.edited.in.whatsapp: |-
✏️ *In WhatsApp bearbeitet*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Reagiert mit* {{{emoji}}}
@@ -219,7 +239,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **Geteilter Standort**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Adresse:** {{address}}
{{/address}}
{{#url}}
🌐 **Webseite:** {{url}}
{{/url}}
🔗 **Auf Karte ansehen:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -227,13 +259,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **Live-Standort wird geteilt**
Öffne **WhatsApp**, um Aktualisierungen in Echtzeit zu sehen.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Adresse:** {{address}}
{{/address}}
{{#url}}
🌐 **Webseite:** {{url}}
{{/url}}
🔗 **Auf Karte ansehen:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **Umfrage**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **Veranstaltung**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **Ort:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **Beginn:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **Ende:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **Teilnahmelink:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - Kopieren und Einfügen**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**PIX-Schlüssel:** {{key}}
{{/key}}
{{#keyType}}
**Typ:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**Betrag:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**Referenz:** {{referenceId}}
{{/referenceId}}
💳 **PIX Kopieren und Einfügen gesendet**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **Dieser Nachrichtentyp wird in ChatWoot nicht unterstützt.**
📱 Bitte öffne **WhatsApp**, um ihn anzusehen.
Details: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **FACEBOOK-ANZEIGE**
**Titel:** {{{adData.title}}}
**Beschreibung:**
{{{adData.body}}}
**Anzeigenlink:** [Auf Facebook ansehen]({{{adData.sourceUrl}}})
**Anzeigen-ID:** {{{adData.sourceId}}}
---
💬 **Kundennachricht:**
{{{payload.body}}}
+149 -5
View File
@@ -1,9 +1,27 @@
# English translations for ChatWoot integration
locale.name: '🇺🇸 English (English)'
app.inbox.contact.name: 'WhatsApp Integration (WAHA)'
# Base locale code - must be a valid BCP 47 locale
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA uses it for number, currency and datetime representation
locale.base: 'en-US'
app.inbox.contact.name: 'WhatsApp Integration'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# "options" from date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'America/New_York'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -50,10 +68,10 @@ message.edited.in.whatsapp: |-
✏️ *Edited in WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Reacted* {{{emoji}}}
@@ -95,7 +113,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **Shared Location**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Address:** {{address}}
{{/address}}
{{#url}}
🌐 **Website:** {{url}}
{{/url}}
🔗 **View on map:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -103,17 +133,131 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **Sharing Live Location**
Open **WhatsApp** to see real-time updates.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Address:** {{address}}
{{/address}}
{{#url}}
🌐 **Website:** {{url}}
{{/url}}
🔗 **View on map:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **Poll**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **Event**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **Location:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **Start:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **End:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **Join Link:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - Copy and Paste**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**PIX Key:** {{key}}
{{/key}}
{{#keyType}}
**Type:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**Amount:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**Reference:** {{referenceId}}
{{/referenceId}}
💳 **PIX Copy and Paste sent**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **This message type is not supported in ChatWoot.**
📱 Please open **WhatsApp** to view it.
Details: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **FACEBOOK AD**
**Title:** {{{adData.title}}}
**Description:**
{{{adData.body}}}
**Ad Link:** [View on Facebook]({{{adData.sourceUrl}}})
**Ad ID:** {{{adData.sourceId}}}
---
💬 **CUSTOMER MESSAGE:**
{{{payload.body}}}
app.connected.message: |-
🔗 **CONNECTED: WhatsApp Session to ChatWoot Inbox**
+151 -5
View File
@@ -1,8 +1,28 @@
# Spanish translations for ChatWoot integration
locale.name: '🇪🇸 Español (Spanish)'
app.inbox.contact.name: 'Integración de WhatsApp (WAHA)'
# Código de locale base - debe ser un locale BCP 47 válido
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA lo utiliza para formatear números, moneda y fecha/hora
locale.base: 'es-ES'
app.inbox.contact.name: 'Integración de WhatsApp'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# "opciones" de date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: es-ES
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Europe/Madrid'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *Editado en WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Reaccionó con* {{{emoji}}}
@@ -219,7 +239,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **Ubicación compartida**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Dirección:** {{address}}
{{/address}}
{{#url}}
🌐 **Sitio web:** {{url}}
{{/url}}
🔗 **Ver en el mapa:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -227,13 +259,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **Compartiendo ubicación en tiempo real**
Abre **WhatsApp** para ver las actualizaciones en tiempo real.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Dirección:** {{address}}
{{/address}}
{{#url}}
🌐 **Sitio web:** {{url}}
{{/url}}
🔗 **Ver en el mapa:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **Encuesta**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **Evento**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **Ubicación:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **Inicio:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **Fin:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **Enlace para unirse:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - Copiar y Pegar**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**Clave PIX:** {{key}}
{{/key}}
{{#keyType}}
**Tipo:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**Importe:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**Referencia:** {{referenceId}}
{{/referenceId}}
💳 **PIX Copiar y Pegar enviado**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **Este tipo de mensaje no es compatible con ChatWoot.**
📱 Por favor, abre **WhatsApp** para verlo.
Detalles: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **ANUNCIO DE FACEBOOK**
**Título:** {{{adData.title}}}
**Descripción:**
{{{adData.body}}}
**Enlace del anuncio:** [Ver en Facebook]({{{adData.sourceUrl}}})
**ID del anuncio:** {{{adData.sourceId}}}
---
💬 **Mensaje del cliente:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# Persian translations for ChatWoot integration
locale.name: '🇮🇷 فارسی (Persian)'
app.inbox.contact.name: 'ادغام واتساپ (WAHA)'
# کد محلی پایه - باید یک محلی معتبر BCP 47 باشد
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA از آن برای نمایش اعداد، ارز و تاریخ/زمان استفاده می‌کند
locale.base: 'fa-IR'
app.inbox.contact.name: 'ادغام واتساپ'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# گزینه‌های "options" از date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: fa-IR
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Asia/Tehran'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *ویرایش شده در واتساپ*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*واکنش* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **موقعیت مکانی به‌اشتراک‌گذاشته‌شده**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **آدرس:** {{address}}
{{/address}}
{{#url}}
🌐 **وب‌سایت:** {{url}}
{{/url}}
🔗 **مشاهده روی نقشه:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **اشتراک‌گذاری موقعیت مکانی زنده**
برای دیدن به‌روزرسانی‌های لحظه‌ای **WhatsApp** را باز کنید.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **آدرس:** {{address}}
{{/address}}
{{#url}}
🌐 **وب‌سایت:** {{url}}
{{/url}}
🔗 **مشاهده روی نقشه:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **نظرسنجی**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **رویداد**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **مکان:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **شروع:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **پایان:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **لینک پیوستن:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - کپی و چسباندن**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**کلید PIX:** {{key}}
{{/key}}
{{#keyType}}
**نوع:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**مبلغ:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**شناسه مرجع:** {{referenceId}}
{{/referenceId}}
💳 **PIX کپی و چسباندن ارسال شد**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **این نوع پیام در ChatWoot پشتیبانی نمی‌شود.**
📱 لطفاً برای مشاهده آن **WhatsApp** را باز کنید.
جزئیات: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **آگهی فیس‌بوک**
**عنوان:** {{{adData.title}}}
**توضیحات:**
{{{adData.body}}}
**لینک آگهی:** [مشاهده در فیس‌بوک]({{{adData.sourceUrl}}})
**شناسه آگهی:** {{{adData.sourceId}}}
---
💬 **پیام مشتری:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# French translations for ChatWoot integration
locale.name: '🇫🇷 Français (French)'
app.inbox.contact.name: 'Intégration WhatsApp (WAHA)'
# Code de locale de base - doit être une locale BCP 47 valide
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA l'utilise pour le formatage des nombres, devises et dates/heures
locale.base: 'fr-FR'
app.inbox.contact.name: 'Intégration WhatsApp'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# "options" de date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: fr-FR
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Europe/Paris'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *Modifié dans WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*A réagi avec* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **Position partagée**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Adresse :** {{address}}
{{/address}}
{{#url}}
🌐 **Site web :** {{url}}
{{/url}}
🔗 **Voir sur la carte :** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **Partage de position en direct**
Ouvrez **WhatsApp** pour voir les mises à jour en temps réel.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Adresse :** {{address}}
{{/address}}
{{#url}}
🌐 **Site web :** {{url}}
{{/url}}
🔗 **Voir sur la carte :** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **Sondage**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **Événement**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **Lieu :** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **Début :** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **Fin :** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **Lien de participation :** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - Copier et Coller**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**Clé PIX :** {{key}}
{{/key}}
{{#keyType}}
**Type :** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**Montant :** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**Référence :** {{referenceId}}
{{/referenceId}}
💳 **PIX Copier et Coller envoyé**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **Ce type de message n’est pas pris en charge dans ChatWoot.**
📱 Veuillez ouvrir **WhatsApp** pour l’afficher.
Détails: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **PUBLICITÉ FACEBOOK**
**Titre:** {{{adData.title}}}
**Description:**
{{{adData.body}}}
**Lien de l'annonce :** [Voir sur Facebook]({{{adData.sourceUrl}}})
**ID de l'annonce :** {{{adData.sourceId}}}
---
💬 **Message du client :**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# Hebrew translations for ChatWoot integration
locale.name: '🇮🇱 עברית (Hebrew)'
app.inbox.contact.name: 'אינטגרציית וואטסאפ (WAHA)'
# קוד הלוקאל הבסיסי - חייב להיות לוקאל תקין לפי BCP 47
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA משתמש בכך להצגת מספרים, מטבע ותאריך/שעה
locale.base: 'he-IL'
app.inbox.contact.name: 'אינטגרציית וואטסאפ'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# "אפשרויות" של date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: he-IL
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Asia/Jerusalem'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *נערך בוואטסאפ*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*הגיב* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **מיקום משותף**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **כתובת:** {{address}}
{{/address}}
{{#url}}
🌐 **אתר אינטרנט:** {{url}}
{{/url}}
🔗 **הצג במפה:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **משתף מיקום חי**
פתח/י את **WhatsApp** כדי לראות עדכונים בזמן אמת.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **כתובת:** {{address}}
{{/address}}
{{#url}}
🌐 **אתר אינטרנט:** {{url}}
{{/url}}
🔗 **הצג במפה:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **סקר**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **אירוע**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **מיקום:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **התחלה:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **סיום:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **קישור הצטרפות:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - העתק והדבק**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**מפתח PIX:** {{key}}
{{/key}}
{{#keyType}}
**סוג:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**סכום:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**אסמכתא:** {{referenceId}}
{{/referenceId}}
💳 **PIX העתק והדבק נשלח**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **סוג הודעה זה אינו נתמך ב-ChatWoot.**
📱 אנא פתח/י את **WhatsApp** כדי לצפות בזה.
פרטים: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **מודעת פייסבוק**
**כותרת:** {{{adData.title}}}
**תיאור:**
{{{adData.body}}}
**קישור למודעה:** [לצפייה בפייסבוק]({{{adData.sourceUrl}}})
**מזהה המודעה:** {{{adData.sourceId}}}
---
💬 **הודעת הלקוח:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# Hindi translations for ChatWoot integration
locale.name: '🇮🇳 हिंदी (Hindi)'
app.inbox.contact.name: 'व्हाट्सएप इंटीग्रेशन (WAHA)'
# बेस लोकैल कोड - यह एक मान्य BCP 47 लोकैल होना चाहिए
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA इसे संख्याएँ, मुद्रा और दिनांक/समय प्रदर्शित करने के लिए उपयोग करता है।
locale.base: 'hi-IN'
app.inbox.contact.name: 'व्हाट्सएप इंटीग्रेशन'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# date.toLocaleString की "options"
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: hi-IN
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Asia/Kolkata'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *व्हाट्सएप में संपादित*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*प्रतिक्रिया दी* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **साझा स्थान**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **पता:** {{address}}
{{/address}}
{{#url}}
🌐 **वेबसाइट:** {{url}}
{{/url}}
🔗 **मानचित्र पर देखें:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **लाइव लोकेशन साझा की जा रही है**
रीयल-टाइम अपडेट देखने के लिए **WhatsApp** खोलें।
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **पता:** {{address}}
{{/address}}
{{#url}}
🌐 **वेबसाइट:** {{url}}
{{/url}}
🔗 **मानचित्र पर देखें:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **मतदान**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **कार्यक्रम**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **स्थान:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **आरंभ:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **समाप्ति:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **जुड़ने का लिंक:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - कॉपी और पेस्ट**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**PIX कुंजी:** {{key}}
{{/key}}
{{#keyType}}
**प्रकार:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**राशि:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**संदर्भ:** {{referenceId}}
{{/referenceId}}
💳 **PIX कॉपी और पेस्ट भेजा गया**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **इस प्रकार का संदेश ChatWoot में समर्थित नहीं है।**
📱 इसे देखने के लिए कृपया **WhatsApp** खोलें।
विवरण: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **फेसबुक विज्ञापन**
**शीर्षक:** {{{adData.title}}}
**विवरण:**
{{{adData.body}}}
**विज्ञापन लिंक:** [फेसबुक पर देखें]({{{adData.sourceUrl}}})
**विज्ञापन आईडी:** {{{adData.sourceId}}}
---
💬 **ग्राहक संदेश:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# Indonesian translations for ChatWoot integration
locale.name: '🇮🇩 Bahasa Indonesia (Indonesian)'
app.inbox.contact.name: 'Integrasi WhatsApp (WAHA)'
# Kode locale dasar - harus berupa locale BCP 47 yang valid
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA menggunakannya untuk format angka, mata uang, dan tanggal/waktu
locale.base: 'id-ID'
app.inbox.contact.name: 'Integrasi WhatsApp'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# "opsi" dari date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: id-ID
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Asia/Jakarta'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *Diedit di WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Bereaksi* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **Lokasi yang dibagikan**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Alamat:** {{address}}
{{/address}}
{{#url}}
🌐 **Situs web:** {{url}}
{{/url}}
🔗 **Lihat di peta:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **Berbagi Lokasi Langsung**
Buka **WhatsApp** untuk melihat pembaruan waktu nyata.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Alamat:** {{address}}
{{/address}}
{{#url}}
🌐 **Situs web:** {{url}}
{{/url}}
🔗 **Lihat di peta:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **Jajak Pendapat**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **Acara**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **Lokasi:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **Mulai:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **Selesai:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **Tautan bergabung:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - Salin dan Tempel**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**Kunci PIX:** {{key}}
{{/key}}
{{#keyType}}
**Jenis:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**Jumlah:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**Referensi:** {{referenceId}}
{{/referenceId}}
💳 **PIX Salin dan Tempel terkirim**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **Jenis pesan ini tidak didukung di ChatWoot.**
📱 Silakan buka **WhatsApp** untuk melihatnya.
Detail: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **IKLAN FACEBOOK**
**Judul:** {{{adData.title}}}
**Deskripsi:**
{{{adData.body}}}
**Tautan Iklan:** [Lihat di Facebook]({{{adData.sourceUrl}}})
**ID Iklan:** {{{adData.sourceId}}}
---
💬 **Pesan Pelanggan:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# Punjabi translations for ChatWoot integration
locale.name: '🇵🇰 ਪੰਜਾਬੀ (Punjabi)'
app.inbox.contact.name: 'ਵਟਸਐਪ ਇੰਟੀਗ੍ਰੇਸ਼ਨ (WAHA)'
# ਅਧਾਰਭੂਤ ਲੋਕੇਲ ਕੋਡ - ਇਹ ਇੱਕ ਵੈਧ BCP 47 ਲੋਕੇਲ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA ਇਸਨੂੰ ਅੰਕ, ਕਰੰਸੀ ਅਤੇ ਤਾਰੀਖ/ਸਮੇਂ ਦੀ ਨਮਾਇਸ਼ ਲਈ ਵਰਤਦਾ ਹੈ
locale.base: 'pa-PK'
app.inbox.contact.name: 'ਵਟਸਐਪ ਇੰਟੀਗ੍ਰੇਸ਼ਨ'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# date.toLocaleString ਦੀਆਂ "options"
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: pa-PK
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Asia/Karachi'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *ਵਟਸਐਪ ਵਿੱਚ ਸੰਪਾਦਿਤ ਕੀਤਾ ਗਿਆ*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*ਪ੍ਰਤੀਕਿਰਿਆ ਕੀਤੀ* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **ਸਾਂਝੀ ਸਥਿਤੀ**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **ਪਤਾ:** {{address}}
{{/address}}
{{#url}}
🌐 **ਵੈਬਸਾਈਟ:** {{url}}
{{/url}}
🔗 **ਨਕਸ਼ੇ 'ਤੇ ਵੇਖੋ:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **ਲਾਈਵ ਸਥਿਤੀ ਸਾਂਝੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ**
ਰੀਅਲ-ਟਾਈਮ ਅੱਪਡੇਟ ਦੇਖਣ ਲਈ **WhatsApp** ਖੋਲ੍ਹੋ।
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **ਪਤਾ:** {{address}}
{{/address}}
{{#url}}
🌐 **ਵੈਬਸਾਈਟ:** {{url}}
{{/url}}
🔗 **ਨਕਸ਼ੇ 'ਤੇ ਵੇਖੋ:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **ਸਰਵੇਖਣ**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **ਇਵੈਂਟ**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **ਸਥਾਨ:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **ਸ਼ੁਰੂਆਤ:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **ਖਾਤਮਾ:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **ਜੁੜਨ ਲਈ ਲਿੰਕ:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - ਕਾਪੀ ਅਤੇ ਪੇਸਟ**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**PIX ਕੁੰਜੀ:** {{key}}
{{/key}}
{{#keyType}}
**ਕਿਸਮ:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**ਰਕਮ:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**ਹਵਾਲਾ:** {{referenceId}}
{{/referenceId}}
💳 **PIX ਕਾਪੀ ਅਤੇ ਪੇਸਟ ਭੇਜਿਆ ਗਿਆ**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **ਇਹ ਸੁਨੇਹੇ ਦੀ ਕਿਸਮ ChatWoot ਵਿੱਚ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ।**
📱 ਇਸਨੂੰ ਦੇਖਣ ਲਈ ਕਿਰਪਾ ਕਰਕੇ **WhatsApp** ਖੋਲ੍ਹੋ।
ਵੇਰਵੇ: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **ਫੇਸਬੁੱਕ ਵਿਗਿਆਪਨ**
**ਸਿਰਲੇਖ:** {{{adData.title}}}
**ਵੇਰਵਾ:**
{{{adData.body}}}
**ਵਿਗਿਆਪਨ ਲਿੰਕ:** [ਫੇਸਬੁੱਕ 'ਤੇ ਵੇਖੋ]({{{adData.sourceUrl}}})
**ਵਿਗਿਆਪਨ ਆਈਡੀ:** {{{adData.sourceId}}}
---
💬 **ਗਾਹਕ ਦਾ ਸੁਨੇਹਾ:**
{{{payload.body}}}
+155 -5
View File
@@ -1,8 +1,28 @@
# Portuguese translations for ChatWoot integration
locale.name: '🇧🇷🇵🇹 Português (Portuguese)'
app.inbox.contact.name: 'Integração WhatsApp (WAHA)'
# Código de locale base - deve ser um locale BCP 47 válido
# https://en.wikipedia.org/wiki/IETF_language_tag
# A WAHA usa isso para formatar números, moeda e data/hora
locale.base: 'pt-BR'
app.inbox.contact.name: 'Integração WhatsApp'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# "opções" de date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: pt-BR
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
timeZone: 'America/Sao_Paulo'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -52,10 +72,10 @@ message.edited.in.whatsapp: |-
✏️ *Editado no WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Reagiu com* {{{emoji}}}
@@ -219,7 +239,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **Localização compartilhada**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Endereço:** {{address}}
{{/address}}
{{#url}}
🌐 **Site:** {{url}}
{{/url}}
🔗 **Ver no mapa:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -227,13 +259,131 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **Compartilhando localização ao vivo**
Abra o **WhatsApp** para ver as atualizações em tempo real.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Endereço:** {{address}}
{{/address}}
{{#url}}
🌐 **Site:** {{url}}
{{/url}}
🔗 **Ver no mapa:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **Enquete**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **Evento**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **Local:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **Início:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **Fim:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **Link de participação:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - Copia e Cola**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**Chave PIX:** {{key}}
{{/key}}
{{#keyType}}
**Tipo:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**Valor:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**Referência:** {{referenceId}}
{{/referenceId}}
💳 **PIX Copia e Cola enviado**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **Esse tipo de mensagem não é suportado no ChatWoot.**
📱 Por favor, abra o **WhatsApp** para visualizá-la.
Detalhes: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **ANÚNCIO DO FACEBOOK**
**Título:** {{{adData.title}}}
**Descrição:**
{{{adData.body}}}
**Link do Anúncio:** [Ver no Facebook]({{{adData.sourceUrl}}})
**ID do Anúncio:** {{{adData.sourceId}}}
---
💬 **MENSAGEM DO CLIENTE:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# Russian translations for ChatWoot integration
locale.name: '🇷🇺 Русский (Russian)'
app.inbox.contact.name: 'WhatsApp Интеграция (WAHA)'
# Базовый код локали - должен быть корректной локалью BCP 47
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA использует его для форматирования чисел, валют и даты/времени
locale.base: 'ru-RU'
app.inbox.contact.name: 'WhatsApp Интеграция'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# "параметры" из date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: ru-RU
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Europe/Moscow'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *Отредактировано в WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Реакция* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **Переданное местоположение**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Адрес:** {{address}}
{{/address}}
{{#url}}
🌐 **Сайт:** {{url}}
{{/url}}
🔗 **Открыть на карте:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **Делится геопозицией в реальном времени**
Откройте **WhatsApp**, чтобы увидеть обновления в реальном времени.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Адрес:** {{address}}
{{/address}}
{{#url}}
🌐 **Сайт:** {{url}}
{{/url}}
🔗 **Открыть на карте:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **Опрос**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **Событие**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **Место:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **Начало:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **Окончание:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **Ссылка для участия:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - Копировать и вставить**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**Ключ PIX:** {{key}}
{{/key}}
{{#keyType}}
**Тип:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**Сумма:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**Референс:** {{referenceId}}
{{/referenceId}}
💳 **PIX Копировать и вставить отправлен**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **Этот тип сообщения не поддерживается в ChatWoot.**
📱 Пожалуйста, откройте **WhatsApp**, чтобы просмотреть.
Подробности: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **РЕКЛАМА FACEBOOK**
**Заголовок:** {{{adData.title}}}
**Описание:**
{{{adData.body}}}
**Ссылка на объявление:** [Посмотреть в Facebook]({{{adData.sourceUrl}}})
**Идентификатор объявления:** {{{adData.sourceId}}}
---
💬 **Сообщение клиента:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# Turkish translations for ChatWoot integration
locale.name: '🇹🇷 Türkçe (Turkish)'
app.inbox.contact.name: 'WhatsApp Entegrasyonu (WAHA)'
# Temel yerel kodu - geçerli bir BCP 47 yerel kodu olmalı
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA bunu sayı, para birimi ve tarih/saat gösterimi için kullanır
locale.base: 'tr-TR'
app.inbox.contact.name: 'WhatsApp Entegrasyonu'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# date.toLocaleString için "options"
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: tr-TR
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Europe/Istanbul'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *WhatsApp'ta düzenlendi*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Tepki verdi* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **Paylaşılan Konum**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Adres:** {{address}}
{{/address}}
{{#url}}
🌐 **Web sitesi:** {{url}}
{{/url}}
🔗 **Haritada görüntüle:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **Canlı Konum Paylaşılıyor**
Gerçek zamanlı güncellemeleri görmek için **WhatsApp’ı** açın.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Adres:** {{address}}
{{/address}}
{{#url}}
🌐 **Web sitesi:** {{url}}
{{/url}}
🔗 **Haritada görüntüle:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **Anket**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **Etkinlik**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **Konum:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **Başlangıç:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **Bitiş:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **Katılma bağlantısı:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - Kopyala ve Yapıştır**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**PIX Anahtarı:** {{key}}
{{/key}}
{{#keyType}}
**Tür:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**Tutar:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**Referans:** {{referenceId}}
{{/referenceId}}
💳 **PIX Kopyala ve Yapıştır gönderildi**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **Bu mesaj türü ChatWoot’ta desteklenmiyor.**
📱 Görüntülemek için lütfen **WhatsApp’ı** açın.
Ayrıntılar: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **FACEBOOK İLANI**
**Başlık:** {{{adData.title}}}
**Açıklama:**
{{{adData.body}}}
**İlan Bağlantısı:** [Facebook’ta Görüntüle]({{{adData.sourceUrl}}})
**İlan Kimliği:** {{{adData.sourceId}}}
---
💬 **Müşteri Mesajı:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# Ukrainian translations for ChatWoot integration
locale.name: '🇺🇦 Українська (Ukrainian)'
app.inbox.contact.name: 'Інтеграція WhatsApp (WAHA)'
# Базовий код локалі - має бути чинною локаллю BCP 47
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA використовує його для форматування чисел, валют і дати/часу
locale.base: 'uk-UA'
app.inbox.contact.name: 'Інтеграція WhatsApp'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# "параметри" з date.toLocaleString
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: uk-UA
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Europe/Kyiv'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *Відредаговано у WhatsApp*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*Реакція* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **Надіслана локація**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Адреса:** {{address}}
{{/address}}
{{#url}}
🌐 **Вебсайт:** {{url}}
{{/url}}
🔗 **Переглянути на карті:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **Надсилання геолокації в реальному часі**
Відкрийте **WhatsApp**, щоб побачити оновлення в реальному часі.
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **Адреса:** {{address}}
{{/address}}
{{#url}}
🌐 **Вебсайт:** {{url}}
{{/url}}
🔗 **Переглянути на карті:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **Опитування**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **Подія**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **Місце:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **Початок:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **Завершення:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **Посилання для приєднання:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - Копіювати та Вставити**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**Ключ PIX:** {{key}}
{{/key}}
{{#keyType}}
**Тип:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**Сума:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**Референс:** {{referenceId}}
{{/referenceId}}
💳 **PIX Копіювати та Вставити надіслано**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **Цей тип повідомлення не підтримується в ChatWoot.**
📱 Будь ласка, відкрийте **WhatsApp**, щоб переглянути.
Подробиці: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **РЕКЛАМА FACEBOOK**
**Заголовок:** {{{adData.title}}}
**Опис:**
{{{adData.body}}}
**Посилання на рекламу:** [Переглянути у Facebook]({{{adData.sourceUrl}}})
**Ідентифікатор реклами:** {{{adData.sourceId}}}
---
💬 **Повідомлення клієнта:**
{{{payload.body}}}
+151 -5
View File
@@ -1,8 +1,28 @@
# Urdu translations for ChatWoot integration
locale.name: '🇵🇰 اردو (Urdu)'
app.inbox.contact.name: 'واٹس ایپ انٹیگریشن (WAHA)'
# بنیادی لوکل کوڈ - یہ ایک معتبر BCP 47 لوکل ہونا چاہیے
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA اسے نمبرز، کرنسی اور تاریخ/وقت کی نمائندگی کے لیے استعمال کرتا ہے۔
locale.base: 'ur-PK'
app.inbox.contact.name: 'واٹس ایپ انٹیگریشن'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# date.toLocaleString کی "options"
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: ur-PK
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Asia/Karachi'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *واٹس ایپ میں ترمیم کی گئی*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*ردعمل دیا* {{{emoji}}}
@@ -216,7 +236,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **شیئر کی گئی لوکیشن**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **پتہ:** {{address}}
{{/address}}
{{#url}}
🌐 **ویب سائٹ:** {{url}}
{{/url}}
🔗 **نقشے پر دیکھیں:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -224,13 +256,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **لائیو لوکیشن شیئر کی جا رہی ہے**
حقیقی وقت کی تازہ کاریوں کے لیے **WhatsApp** کھولیں۔
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **پتہ:** {{address}}
{{/address}}
{{#url}}
🌐 **ویب سائٹ:** {{url}}
{{/url}}
🔗 **نقشے پر دیکھیں:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **رائے شماری**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **ایونٹ**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **مقام:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **آغاز:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **اختتام:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **شمولیت کا لنک:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - کاپی اور پیسٹ**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**PIX کلید:** {{key}}
{{/key}}
{{#keyType}}
**قسم:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**رقم:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**حوالہ:** {{referenceId}}
{{/referenceId}}
💳 **PIX کاپی اور پیسٹ بھیج دیا گیا**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **ChatWoot میں اس قسم کا پیغام معاونت یافتہ نہیں ہے۔**
📱 اسے دیکھنے کے لیے براہ کرم **WhatsApp** کھولیں۔
تفصیلات: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **فیس بک اشتہار**
**عنوان:** {{{adData.title}}}
**تفصیل:**
{{{adData.body}}}
**اشتہار کا لنک:** [فیس بک پر دیکھیں]({{{adData.sourceUrl}}})
**اشتہار کا شناختی نمبر:** {{{adData.sourceId}}}
---
💬 **گاہک کا پیغام:**
{{{payload.body}}}
+150 -4
View File
@@ -1,8 +1,28 @@
# Simplified Chinese translations for ChatWoot integration
locale.name: '🇨🇳 中文(Simplified Chinese)'
# 基础语言代码 - 必须是有效的 BCP 47 语言区域
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA 使用它来格式化数字、货币和日期时间。
locale.base: 'zh-CN'
app.inbox.contact.name: 'WhatsApp 集成(WAHA)'
app.inbox.contact.avatar.url: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
# date.toLocaleString 的 "options"
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options
datetime:
locales: zh-CN
day: 2-digit
month: 2-digit
year: numeric
hour: 2-digit
minute: 2-digit
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#timezonename
timeZoneName: shortGeneric
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
# timeZone: 'Asia/Shanghai'
job.report.error: |-
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
@@ -49,10 +69,10 @@ message.edited.in.whatsapp: |-
✏️ *已在 WhatsApp 中编辑*
whatsapp.group.message: |-
{{{text}}}
👥 *{{{participant}}}*
{{{text}}}
whatsapp.reaction.added: |-
*已反应* {{{emoji}}}
@@ -218,7 +238,19 @@ whatsapp.to.chatwoot.message.location: |-
{{#locationMessage}}
📍 **共享位置**
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **地址:** {{address}}
{{/address}}
{{#url}}
🌐 **网站:** {{url}}
{{/url}}
🔗 **在地图上查看:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/locationMessage}}
@@ -226,13 +258,127 @@ whatsapp.to.chatwoot.message.location: |-
📍📡 **正在共享实时位置**
打开 **WhatsApp** 查看实时更新。
https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#name}}
**{{name}}**
{{/name}}
{{#address}}
📍 **地址:** {{address}}
{{/address}}
{{#url}}
🌐 **网站:** {{url}}
{{/url}}
🔗 **在地图上查看:** https://www.google.com/maps?q={{degreesLatitude}},{{degreesLongitude}}
{{#caption}}{{caption}}{{/caption}}
{{/liveLocationMessage}}
{{/message}}
whatsapp.to.chatwoot.message.poll: |-
{{#poll}}
📊 **投票**
**{{name}}**
{{#options}}
- {{optionName}}
{{/options}}
{{/poll}}
whatsapp.to.chatwoot.message.list: |-
{{#message.listMessage}}
{{#title}}
*{{{title}}}*
{{/title}}
{{#description}}
{{{description}}}
{{/description}}
{{#footerText}}
{{{footerText}}}
{{/footerText}}
{{#buttonText}}
{{{buttonText}}}
{{/buttonText}}
{{#sections}}
{{#title}}
{{{title}}}:
{{/title}}
{{#rows}}
{{#title}}
- {{{title}}}
{{/title}}
{{/rows}}
{{/sections}}
{{/message.listMessage}}
whatsapp.to.chatwoot.message.event: |-
{{#message}}
{{#eventMessage}}
📅 **活动**
**{{name}}**
{{#description}}
{{description}}
{{/description}}
{{#location.name}}
📍 **地点:** {{location.name}}
{{/location.name}}
{{#startTime}}
🕐 **开始时间:** {{startTime}}
{{/startTime}}
{{#endTime}}
🕐 **结束时间:** {{endTime}}
{{/endTime}}
{{#joinLink}}
🔗 **加入链接:** [{{joinLink}}]({{joinLink}})
{{/joinLink}}
{{/eventMessage}}
{{/message}}
whatsapp.to.chatwoot.message.pix: |-
💳 **PIX - 复制粘贴**
{{#pixData}}
{{#merchantName}}
**{{merchantName}}**
{{/merchantName}}
{{#key}}
**PIX 密钥:** {{key}}
{{/key}}
{{#keyType}}
**类型:** {{keyType}}
{{/keyType}}
{{#totalAmount}}
**金额:** {{totalAmountFormatted}}
{{/totalAmount}}
{{#referenceId}}
**参考:** {{referenceId}}
{{/referenceId}}
💳 **PIX 复制粘贴已发送**
{{/pixData}}
whatsapp.to.chatwoot.message.unsupported: |-
⚠️ **ChatWoot 不支持此消息类型。**
📱 请打开 **WhatsApp** 查看。
详情: [{{{details.text}}}]({{{details.url}}})
whatsapp.to.chatwoot.message.facebook.ad: |-
📢 **Facebook 广告**
**标题:** {{{adData.title}}}
**描述:**
{{{adData.body}}}
**广告链接:** [在 Facebook 查看]({{{adData.sourceUrl}}})
**广告 ID:** {{{adData.sourceId}}}
---
💬 **客户消息:**
{{{payload.body}}}
+58 -1
View File
@@ -1,6 +1,6 @@
import type { proto } from '@adiwajshing/baileys';
import { ChatWootCommandsConfig } from '@waha/apps/chatwoot/dto/config.dto';
import { WAMessage } from '@waha/structures/responses.dto';
import { proto } from '@adiwajshing/baileys';
import { SimpleVCardInfo } from '@waha/core/vcard';
export enum TKey {
@@ -51,7 +51,12 @@ export enum TKey {
WA_TO_CW_MESSAGE = 'whatsapp.to.chatwoot.message',
WA_TO_CW_MESSAGE_CONTACTS = 'whatsapp.to.chatwoot.message.contacts',
WA_TO_CW_MESSAGE_LOCATION = 'whatsapp.to.chatwoot.message.location',
WA_TO_CW_MESSAGE_POLL = 'whatsapp.to.chatwoot.message.poll',
WA_TO_CW_MESSAGE_EVENT = 'whatsapp.to.chatwoot.message.event',
WA_TO_CW_MESSAGE_PIX = 'whatsapp.to.chatwoot.message.pix',
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_LIST = 'whatsapp.to.chatwoot.message.list',
//
// App Inbox
@@ -99,6 +104,34 @@ interface Link {
url: string;
}
export interface FacebookAdTemplateData {
title: string;
body: string;
thumbnailUrl: string;
originalImageUrl: string;
sourceUrl: string;
sourceId: string;
}
export interface PollOption {
optionName?: string;
}
export interface PollCreationMessage {
name?: string;
options?: Array<PollOption>;
}
export interface PixTemplatePayload {
merchantName?: string;
key?: string;
keyType?: string;
currency?: string;
totalAmount?: number;
totalAmountFormatted?: string;
referenceId?: string;
}
export type TemplatePayloads = {
[TKey.LOCALE_NAME]: void;
[TKey.APP_INBOX_CONTACT_NAME]: void;
@@ -131,7 +164,31 @@ export type TemplatePayloads = {
};
[TKey.WA_TO_CW_MESSAGE_CONTACTS]: { contacts: SimpleVCardInfo[] };
[TKey.WA_TO_CW_MESSAGE_LOCATION]: { payload: any; message: proto.Message };
[TKey.WA_TO_CW_MESSAGE_POLL]: {
payload: WAMessage;
poll: PollCreationMessage;
message: proto.Message;
};
[TKey.WA_TO_CW_MESSAGE_EVENT]: {
payload: WAMessage;
message: {
eventMessage: Record<string, unknown>;
};
};
[TKey.WA_TO_CW_MESSAGE_LIST]: {
payload: WAMessage;
message: proto.Message;
};
[TKey.WA_TO_CW_MESSAGE_PIX]: {
payload: WAMessage;
message: proto.Message | null;
pixData: PixTemplatePayload;
};
[TKey.WA_TO_CW_MESSAGE_UNSUPPORTED]: { details: Link };
[TKey.WA_TO_CW_MESSAGE_FACEBOOK_AD]: {
payload: WAMessage;
adData: FacebookAdTemplateData;
};
[TKey.JOB_SCHEDULED_ERROR_HEADER]: void;
[TKey.JOB_REPORT_ERROR]: {
header: string;
@@ -0,0 +1,45 @@
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/proto';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
import type { proto } from '@adiwajshing/baileys';
import { WAMessage } from '@waha/structures/responses.dto';
export class EventMessage implements MessageToChatWootConverter {
constructor(private readonly l: Locale) {}
convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): ChatWootMessagePartial | null {
const eventMessage = protoMessage?.eventMessage;
if (!eventMessage) {
return null;
}
const formattedEventMessage = {
...eventMessage,
startTime: this.l.FormatTimestamp(eventMessage.startTime),
endTime: this.l.FormatTimestamp(eventMessage.endTime),
};
const content = this.l.key(TKey.WA_TO_CW_MESSAGE_EVENT).r({
payload,
message: {
eventMessage: formattedEventMessage,
},
});
if (isEmptyString(content)) {
return null;
}
return {
content: WhatsappToMarkdown(content),
attachments: [],
private: undefined,
};
}
}
@@ -0,0 +1,91 @@
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { SendAttachment } from '@waha/apps/chatwoot/client/types';
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import {
FacebookAdTemplateData,
TKey,
} from '@waha/apps/chatwoot/i18n/templates';
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
import { WAMessage } from '@waha/structures/responses.dto';
import type { proto } from '@adiwajshing/baileys';
import * as lodash from 'lodash';
import { fetchBuffer } from '@waha/core/abc/session.abc';
export class FacebookAdMessage implements MessageToChatWootConverter {
constructor(
private readonly locale: Locale,
private readonly logger: ILogger,
) {}
async convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): Promise<ChatWootMessagePartial | null> {
const adData = this.extractAdData(protoMessage);
if (!adData) {
return null;
}
const content = this.locale.key(TKey.WA_TO_CW_MESSAGE_FACEBOOK_AD).render({
payload,
adData,
});
const attachments = await this.getAttachments(adData);
return {
content: WhatsappToMarkdown(content),
attachments,
private: undefined,
};
}
private extractAdData(protoMessage: proto.Message): FacebookAdTemplateData {
if (
lodash.isEmpty(
protoMessage?.extendedTextMessage?.contextInfo?.externalAdReply,
)
) {
return null;
}
const adReply =
protoMessage?.extendedTextMessage?.contextInfo?.externalAdReply;
return {
title: adReply.title ?? '',
body: adReply.body ?? '',
thumbnailUrl: adReply.thumbnailUrl ?? '',
originalImageUrl: adReply.originalImageUrl ?? '',
sourceUrl: adReply.sourceUrl ?? '',
sourceId: adReply.sourceId ?? '',
};
}
private async getAttachments(
adData: FacebookAdTemplateData,
): Promise<SendAttachment[]> {
const imageUrl = adData.originalImageUrl || adData.thumbnailUrl;
if (!imageUrl) {
return [];
}
try {
this.logger.info(`Downloading Facebook Ad image from '${imageUrl}'...`);
const buffer = await fetchBuffer(imageUrl);
const attachment: SendAttachment = {
content: buffer.toString('base64'),
filename: 'facebook-ad-image.jpg',
encoding: 'base64',
};
this.logger.info(
`Downloaded Facebook Ad image from '${imageUrl}' as '${attachment.filename}'`,
);
return [attachment];
} catch (error) {
const reason = error instanceof Error ? error.message : `${error}`;
this.logger.error(
`Failed to download Facebook Ad image from '${imageUrl}': ${reason}`,
);
return [];
}
}
}
@@ -0,0 +1,37 @@
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/proto';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
import * as lodash from 'lodash';
import type { proto } from '@adiwajshing/baileys';
import { WAMessage } from '@waha/structures/responses.dto';
export class ListMessage implements MessageToChatWootConverter {
constructor(private readonly locale: Locale) {}
convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): ChatWootMessagePartial | null {
const listMessage = protoMessage?.listMessage;
if (lodash.isEmpty(listMessage)) {
return null;
}
const content = this.locale
.key(TKey.WA_TO_CW_MESSAGE_LIST)
.r({ payload, message: protoMessage as proto.Message });
if (isEmptyString(content)) {
return null;
}
return {
content: WhatsappToMarkdown(content),
attachments: [],
private: undefined,
};
}
}
@@ -0,0 +1,37 @@
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
import { isEmptyString } from './utils/proto';
import * as lodash from 'lodash';
import type { proto } from '@adiwajshing/baileys';
import { WAMessage } from '@waha/structures/responses.dto';
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
export class LocationMessage implements MessageToChatWootConverter {
constructor(private readonly locale: Locale) {}
convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): ChatWootMessagePartial | null {
const hasLocation = !lodash.isEmpty(protoMessage?.locationMessage);
const hasLiveLocation = !lodash.isEmpty(protoMessage?.liveLocationMessage);
if (!hasLocation && !hasLiveLocation) {
return null;
}
const content = this.locale
.key(TKey.WA_TO_CW_MESSAGE_LOCATION)
.r({ payload, message: protoMessage });
if (isEmptyString(content)) {
return null;
}
return {
content,
attachments: [],
private: undefined,
};
}
}
@@ -0,0 +1,28 @@
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
import { WAMessage } from '@waha/structures/responses.dto';
import type { proto } from '@adiwajshing/baileys';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
export class MessageEdited implements MessageToChatWootConverter {
constructor(private readonly locale: Locale) {}
convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): ChatWootMessagePartial | null {
void protoMessage;
const formatted = WhatsappToMarkdown(payload.body);
const content = this.locale
.key(TKey.MESSAGE_EDITED_IN_WHATSAPP)
.render({ text: formatted });
return {
content,
attachments: [],
private: undefined,
};
}
}
@@ -0,0 +1,250 @@
import { PixMessage } from '@waha/apps/chatwoot/messages/to/chatwoot/PixMessage';
import { resolveProtoMessage } from '@waha/apps/chatwoot/messages/to/chatwoot';
import { WAMessage } from '@waha/structures/responses.dto';
import { JestLogger } from '@waha/apps/app_sdk/jest/JestLogger';
import { i18n } from '@waha/apps/chatwoot/i18n';
describe('PixMessage - pt-BR', () => {
const locale = i18n.locale('pt-BR');
it('GOWS - no total amount', () => {
// Setup
const buttonParamsJson = JSON.stringify({
currency: 'BRL',
total_amount: {
value: 0,
offset: 100,
},
reference_id: '4TB11111111',
type: 'physical-goods',
order: {
status: 'pending',
subtotal: {
value: 0,
offset: 100,
},
order_type: 'ORDER',
items: [
{
name: '',
amount: {
value: 0,
offset: 100,
},
quantity: 0,
sale_amount: {
value: 0,
offset: 100,
},
},
],
},
payment_settings: [
{
type: 'pix_static_code',
pix_static_code: {
merchant_name: 'Merchant Name Here',
key: '+5000000000000',
key_type: 'PHONE',
},
},
],
share_payment_status: false,
referral: 'chat_attachment',
});
const payload = {
id: 'false_559900000001@c.us_FAKEPIXORDER1',
timestamp: 1899003201,
from: '559900000001@c.us',
fromMe: false,
source: 'app',
to: null,
participant: null,
hasMedia: false,
ack: 2,
ackName: 'DEVICE',
body: '',
_data: {
Message: {
messageContextInfo: {
deviceListMetadata: {
senderKeyHash: 'aFakeSenderKeyHash==',
senderTimestamp: 1880001234,
recipientKeyHash: 'anotherFakeRecipientKeyHash==',
recipientTimestamp: 1880005678,
},
deviceListMetadataVersion: 2,
messageSecret: 'fakeMessageSecret==',
},
interactiveMessage: {
InteractiveMessage: {
NativeFlowMessage: {
buttons: [
{
name: 'payment_info',
buttonParamsJSON: buttonParamsJson,
},
],
},
},
},
contextInfo: {
expiration: 0,
ephemeralSettingTimestamp: 1880006789,
disappearingMode: {
initiator: 0,
trigger: 0,
},
},
},
},
} as unknown as WAMessage;
// Test
const logger = JestLogger();
const protoMessage = resolveProtoMessage(payload);
const converter = new PixMessage(locale, logger);
const result = converter.convert(payload, protoMessage);
// Assert
const expected = `💳 ****PIX - Copia e Cola****
****Merchant Name Here****
****Chave PIX:**** +5000000000000
****Tipo:**** PHONE
****Referência:**** 4TB11111111
💳 ****PIX Copia e Cola enviado****
`;
expect(protoMessage).not.toBeNull();
expect(result).not.toBeNull();
expect(result.content).toEqual(expected);
expect(logger.fatal).not.toHaveBeenCalled();
expect(logger.error).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});
it('GOWS - with total amount', () => {
// Setup
const buttonParamsJson = JSON.stringify({
currency: 'BRL',
total_amount: {
value: 2512,
offset: 100,
},
reference_id: '4TB11111111',
type: 'physical-goods',
order: {
status: 'pending',
subtotal: {
value: 2512,
offset: 100,
},
order_type: 'ORDER',
items: [
{
name: '',
amount: {
value: 2512,
offset: 100,
},
quantity: 1,
sale_amount: {
value: 2512,
offset: 100,
},
},
],
},
payment_settings: [
{
type: 'pix_static_code',
pix_static_code: {
merchant_name: 'Merchant Name Here',
key: '+5000000000000',
key_type: 'PHONE',
},
},
],
share_payment_status: false,
referral: 'chat_attachment',
});
const payload = {
id: 'false_559900000001@c.us_FAKEPIXORDER1',
timestamp: 1899003201,
from: '559900000001@c.us',
fromMe: false,
source: 'app',
to: null,
participant: null,
hasMedia: false,
ack: 2,
ackName: 'DEVICE',
body: '',
_data: {
Message: {
messageContextInfo: {
deviceListMetadata: {
senderKeyHash: 'aFakeSenderKeyHash==',
senderTimestamp: 1880001234,
recipientKeyHash: 'anotherFakeRecipientKeyHash==',
recipientTimestamp: 1880005678,
},
deviceListMetadataVersion: 2,
messageSecret: 'fakeMessageSecret==',
},
interactiveMessage: {
InteractiveMessage: {
NativeFlowMessage: {
buttons: [
{
name: 'payment_info',
buttonParamsJSON: buttonParamsJson,
},
],
},
},
},
contextInfo: {
expiration: 0,
ephemeralSettingTimestamp: 1880006789,
disappearingMode: {
initiator: 0,
trigger: 0,
},
},
},
},
} as unknown as WAMessage;
// Test
const logger = JestLogger();
const protoMessage = resolveProtoMessage(payload);
const converter = new PixMessage(locale, logger);
const result = converter.convert(payload, protoMessage);
// Assert
const expected = `💳 ****PIX - Copia e Cola****
****Merchant Name Here****
****Chave PIX:**** +5000000000000
****Tipo:**** PHONE
****Valor:**** R$\u00A025,12
****Referência:**** 4TB11111111
💳 ****PIX Copia e Cola enviado****
`;
expect(protoMessage).not.toBeNull();
expect(result).not.toBeNull();
expect(result.content).toEqual(expected);
expect(logger.fatal).not.toHaveBeenCalled();
expect(logger.error).not.toHaveBeenCalled();
expect(logger.warn).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,124 @@
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { PixTemplatePayload, TKey } from '@waha/apps/chatwoot/i18n/templates';
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/proto';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
import * as lodash from 'lodash';
import type { proto } from '@adiwajshing/baileys';
import { WAMessage } from '@waha/structures/responses.dto';
export class PixMessage implements MessageToChatWootConverter {
constructor(
private readonly l: Locale,
private readonly logger: ILogger,
) {}
convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): ChatWootMessagePartial | null {
if (!protoMessage) {
return null;
}
const pixData = this.extractPixData(protoMessage);
if (!pixData) {
return null;
}
const content = this.l.key(TKey.WA_TO_CW_MESSAGE_PIX).r({
payload: payload,
message: protoMessage,
pixData: pixData,
});
if (isEmptyString(content)) {
return null;
}
return {
content: WhatsappToMarkdown(content),
attachments: [],
private: undefined,
};
}
private extractPixData(
protoMessage: proto.Message,
): PixTemplatePayload | null {
const buttons = this.resolveNativeFlowButtons(protoMessage);
if (!buttons || buttons.length === 0) {
return null;
}
const paymentButton = buttons.find(
(button) => button?.name === 'payment_info' && button?.buttonParamsJson,
);
if (!paymentButton?.buttonParamsJson) {
return null;
}
let params: any;
try {
params = JSON.parse(paymentButton.buttonParamsJson);
} catch (error) {
this.logger.warn(`Failed to parse PIX button params: ${error}`);
return null;
}
const pixSettings = params?.payment_settings?.find(
(setting: any) => setting?.type === 'pix_static_code',
);
const pixCode = pixSettings?.pix_static_code;
if (!pixCode) {
return null;
}
const totalAmountFormatted = this.l.FormatCurrency(
params?.currency,
params?.total_amount?.value,
params?.total_amount?.offset,
);
const data: PixTemplatePayload = {
merchantName: pixCode.merchant_name,
key: pixCode.key,
keyType: pixCode.key_type,
currency: params?.currency,
totalAmount: params?.total_amount?.value,
totalAmountFormatted: totalAmountFormatted,
referenceId: params?.reference_id,
};
if (lodash.isEmpty(data)) {
return null;
}
return data;
}
private resolveNativeFlowButtons(
protoMessage: proto.Message,
):
| proto.Message.InteractiveMessage.NativeFlowMessage.INativeFlowButton[]
| null {
let interactiveMessage: proto.Message.IInteractiveMessage =
protoMessage?.interactiveMessage;
if (lodash.isEmpty(interactiveMessage)) {
return null;
}
// GOWS has weird oneOf protobuf behaviour,
// so we need to double interactiveMessage
if (
!lodash.isEmpty(
(interactiveMessage as proto.Message.InteractiveMessage)
.interactiveMessage,
)
) {
interactiveMessage = (
interactiveMessage as proto.Message.InteractiveMessage
).interactiveMessage as proto.Message.IInteractiveMessage;
}
return interactiveMessage?.nativeFlowMessage?.buttons;
}
}
@@ -0,0 +1,46 @@
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { PollCreationMessage, TKey } from '@waha/apps/chatwoot/i18n/templates';
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
import { isEmptyString } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/proto';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
import type { proto } from '@adiwajshing/baileys';
import { WAMessage } from '@waha/structures/responses.dto';
import * as lodash from 'lodash';
export class PollMessage implements MessageToChatWootConverter {
constructor(private readonly locale: Locale) {}
convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): ChatWootMessagePartial | null {
let poll: PollCreationMessage = null;
if (!lodash.isEmpty(protoMessage?.pollCreationMessage)) {
poll = protoMessage.pollCreationMessage;
}
if (!lodash.isEmpty(protoMessage?.pollCreationMessageV2)) {
poll = protoMessage.pollCreationMessageV2;
}
if (!lodash.isEmpty(protoMessage?.pollCreationMessageV3)) {
poll = protoMessage.pollCreationMessageV3;
}
if (!poll) {
return null;
}
const content = this.locale
.key(TKey.WA_TO_CW_MESSAGE_POLL)
.r({ payload: payload, poll: poll, message: protoMessage });
if (isEmptyString(content)) {
return null;
}
return {
content: WhatsappToMarkdown(content),
attachments: [],
private: undefined,
};
}
}
@@ -0,0 +1,70 @@
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { SendAttachment } from '@waha/apps/chatwoot/client/types';
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 { parseVCardV3, SimpleVCardInfo } from '@waha/core/vcard';
import * as lodash from 'lodash';
import type { proto } from '@adiwajshing/baileys';
import { WAMessage } from '@waha/structures/responses.dto';
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
export class ShareContactMessage implements MessageToChatWootConverter {
constructor(
private readonly locale: Locale,
private readonly logger: ILogger,
) {}
convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): ChatWootMessagePartial | null {
void payload;
const vcards = collectVcards(protoMessage);
if (vcards.length === 0) {
return null;
}
const attachments: SendAttachment[] = vcards.map((vcard, index) => ({
content: Buffer.from(vcard, 'utf8').toString('base64'),
encoding: 'base64',
filename: `vcard-${index + 1}.vcf`,
}));
let contacts: SimpleVCardInfo[] = [];
try {
contacts = vcards.map(parseVCardV3);
} catch (err) {
this.logger.error(
`Error parsing some vcards: vcards=${vcards}, err=${err}`,
);
}
if (contacts.length === 0 && attachments.length === 0) {
return null;
}
const content = this.locale
.key(TKey.WA_TO_CW_MESSAGE_CONTACTS)
.r({ contacts });
return {
content,
attachments,
private: undefined,
};
}
}
function collectVcards(message: proto.Message | null): string[] {
if (lodash.isEmpty(message?.contactsArrayMessage?.contacts)) {
if (!lodash.isEmpty(message?.contactMessage?.vcard)) {
return [message!.contactMessage!.vcard];
}
return [];
}
return message!.contactsArrayMessage!.contacts.map(
(contact) => contact.vcard,
);
}
@@ -0,0 +1,67 @@
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { SendAttachment } from '@waha/apps/chatwoot/client/types';
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { WAHASelf } from '@waha/apps/chatwoot/session/WAHASelf';
import { isEmptyString } from './utils/proto';
import type { proto } from '@adiwajshing/baileys';
import { WAMessage } from '@waha/structures/responses.dto';
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mime = require('mime-types');
export class TextMessage implements MessageToChatWootConverter {
constructor(
private readonly locale: Locale,
private readonly logger: ILogger,
private readonly waha: WAHASelf,
) {}
async convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): Promise<ChatWootMessagePartial | null> {
void protoMessage;
const attachments = await this.getAttachments(payload);
let content = this.locale.key(TKey.WA_TO_CW_MESSAGE).render({ payload });
if (isEmptyString(content) && attachments.length === 0) {
return null;
}
if (isEmptyString(content)) {
content = null;
}
return {
content: WhatsappToMarkdown(content),
attachments,
private: undefined,
};
}
private async getAttachments(payload: WAMessage): Promise<SendAttachment[]> {
const hasMedia = payload.media?.url;
if (!hasMedia) {
return [];
}
const media = payload.media!;
this.logger.info(`Downloading media from '${media.url}'...`);
const buffer = await this.waha.fetch(media.url);
const fileContent = buffer.toString('base64');
let filename = media.filename;
if (!filename) {
const extension = mime.extension(media.mimetype);
filename = `no-filename.${extension}`;
}
const attachment: SendAttachment = {
content: fileContent,
filename,
encoding: 'base64',
};
this.logger.info(`Downloaded media from '${media.url}' as '${filename}'`);
return [attachment];
}
}
@@ -0,0 +1,32 @@
import { JobLink } from '@waha/apps/app_sdk/JobUtils';
import { Locale } from '@waha/apps/chatwoot/i18n/locale';
import { TKey } from '@waha/apps/chatwoot/i18n/templates';
import { Job } from 'bullmq';
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
import type { proto } from '@adiwajshing/baileys';
import { WAMessage } from '@waha/structures/responses.dto';
import { MessageToChatWootConverter } from '@waha/apps/chatwoot/messages/to/chatwoot';
export class UnsupportedMessage implements MessageToChatWootConverter {
constructor(
private readonly locale: Locale,
private readonly job: Job,
) {}
convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): ChatWootMessagePartial {
void payload;
void protoMessage;
const content = this.locale
.key(TKey.WA_TO_CW_MESSAGE_UNSUPPORTED)
.render({ details: JobLink(this.job) });
return {
content,
attachments: [],
private: true,
};
}
}
@@ -0,0 +1,24 @@
import { WAMessage } from '@waha/structures/responses.dto';
import type { proto } from '@adiwajshing/baileys';
import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base';
export { resolveProtoMessage } from './utils/proto';
export { TextMessage } from './TextMessage';
export { LocationMessage } from './LocationMessage';
export { ShareContactMessage } from './ShareContactMessage';
export { UnsupportedMessage } from './UnsupportedMessage';
export { MessageEdited } from './MessageEdited';
export { FacebookAdMessage } from './FacebookAdMessage';
export { PollMessage } from './PollMessage';
export { EventMessage } from './EventMessage';
export { PixMessage } from './PixMessage';
export { ListMessage } from './ListMessage';
export type Awaitable<T> = T | Promise<T>;
export interface MessageToChatWootConverter {
convert(
payload: WAMessage,
protoMessage: proto.Message | null,
): Awaitable<ChatWootMessagePartial | null>;
}
@@ -0,0 +1,17 @@
import { WhatsappToMarkdown } from './markdown';
describe('WhatsappToMarkdown', () => {
it('converts asterisks to bold', () => {
const input = '*bold*';
expect(WhatsappToMarkdown(input)).toBe('**bold**');
});
it('converts underscores to italic', () => {
const input = '_italic_';
expect(WhatsappToMarkdown(input)).toBe('*italic*');
});
it('handles multiple transformations at once', () => {
const input = `*bold* | _italic_ | ~strike~ | \`one line code\` | \`\`\`\nmultiple lines\`\`\` `;
const expected = `**bold** | *italic* | ~~strike~~ | \`one line code\` | \`\`\`\nmultiple lines\`\`\` `;
expect(WhatsappToMarkdown(input)).toBe(expected);
});
});
@@ -0,0 +1,17 @@
export function WhatsappToMarkdown(text: string): string {
if (!text) {
return text;
}
if (text == '') {
return '';
}
return (
text
// Bold: *bold* → **bold**
.replace(/\*(.*?)\*/g, '**$1**')
// Strikethrough: ~strike~ → ~~strike~~
.replace(/~(.*?)~/g, '~~$1~~')
// Italic: _italic_ → *italic*
.replace(/_(.*?)_/g, '*$1*')
);
}
@@ -0,0 +1,55 @@
import { camelCaseKeysDeep } from './proto';
describe('camelCaseKeysDeep', () => {
it('URL to url', () => {
const input = { URL: 'https://example.com/image.png' };
const result = camelCaseKeysDeep(input);
expect(result).toEqual({
url: 'https://example.com/image.png',
});
});
it('converts originalImageURL to originalImageUrl', () => {
const input = { originalImageURL: 'https://example.com/image.png' };
const result = camelCaseKeysDeep(input);
expect(result).toEqual({
originalImageUrl: 'https://example.com/image.png',
});
});
it('keeps externalAdReply as externalAdReply', () => {
const input = { externalAdReply: { title: 'Sample' } };
const result = camelCaseKeysDeep(input);
expect(result).toEqual({ externalAdReply: { title: 'Sample' } });
});
it('converts PascalCase keys to pascalCase', () => {
const input = { PascalCase: 'value' };
const result = camelCaseKeysDeep(input);
expect(result).toEqual({ pascalCase: 'value' });
});
it('camel cases keys nested inside arrays while keeping primitive entries intact', () => {
const input = [
{ MixedCaseKey: 'value', inner_object: { AnotherNestedKey: 42 } },
'STRING_VALUE',
10,
];
const result = camelCaseKeysDeep(input);
expect(result).toEqual([
{ mixedCaseKey: 'value', innerObject: { anotherNestedKey: 42 } },
'STRING_VALUE',
10,
]);
});
});
@@ -0,0 +1,38 @@
import { WAMessage } from '@waha/structures/responses.dto';
import type { proto } from '@adiwajshing/baileys';
import * as lodash from 'lodash';
export function camelCaseKeysDeep<T = any>(input: unknown): T {
if (Array.isArray(input)) return input.map(camelCaseKeysDeep) as unknown as T;
if (input && typeof input === 'object') {
const mapped = lodash.mapKeys(input as Record<string, unknown>, (_v, k) =>
lodash.camelCase(k),
);
return lodash.mapValues(mapped, camelCaseKeysDeep) as unknown as T;
}
return input as T;
}
export function resolveProtoMessage(payload: WAMessage): proto.Message | null {
// GOWS
if (payload._data.Message) {
const protoMessage = payload._data.Message;
// mediaURL => mediaUrl
// otherAttributes => otherAttributes
return camelCaseKeysDeep(protoMessage);
}
// NOWEB
if (payload._data.message) {
return payload._data.message;
}
// WEBJS - not available
return null;
}
export function isEmptyString(content: string) {
if (!content) {
return true;
}
return content === '' || content === '\n';
}
@@ -1,4 +1,4 @@
import { MarkdownToWhatsApp, WhatsappToMarkdown } from './text';
import { MarkdownToWhatsApp } from './markdown';
describe('MarkdownToWhatsApp', () => {
it('converts triple-backtick blocks', () => {
@@ -15,19 +15,3 @@ describe('MarkdownToWhatsApp', () => {
expect(MarkdownToWhatsApp(input)).toBe(expected);
});
});
describe('WhatsappToMarkdown', () => {
it('converts asterisks to bold', () => {
const input = '*bold*';
expect(WhatsappToMarkdown(input)).toBe('**bold**');
});
it('converts underscores to italic', () => {
const input = '_italic_';
expect(WhatsappToMarkdown(input)).toBe('*italic*');
});
it('handles multiple transformations at once', () => {
const input = `*bold* | _italic_ | ~strike~ | \`one line code\` | \`\`\`\nmultiple lines\`\`\` `;
const expected = `**bold** | *italic* | ~~strike~~ | \`one line code\` | \`\`\`\nmultiple lines\`\`\` `;
expect(WhatsappToMarkdown(input)).toBe(expected);
});
});
@@ -23,21 +23,3 @@ export function MarkdownToWhatsApp(text: string): string {
.replace(/^[-+*] (.*)/gm, '* $1')
);
}
export function WhatsappToMarkdown(text: string): string {
if (!text) {
return text;
}
if (text == '') {
return '';
}
return (
text
// Bold: *bold* → **bold**
.replace(/\*(.*?)\*/g, '**$1**')
// Strikethrough: ~strike~ → ~~strike~~
.replace(/~(.*?)~/g, '~~$1~~')
// Italic: _italic_ → *italic*
.replace(/_(.*?)_/g, '*$1*')
);
}
@@ -119,20 +119,21 @@ export class ChatWootAppService implements IAppService {
await conversation.incoming(updated);
}
async beforeSessionStart(
app: App<ChatWootAppConfig>,
session: WhatsappSession,
) {
beforeSessionStart(app: App<ChatWootAppConfig>, session: WhatsappSession) {
this.chatWootWAHAQueueService.listenEvents(app.id, session);
await this.chatWootScheduleService.schedule(app.id, session.name);
}
afterSessionStart(app: App<ChatWootAppConfig>, session: WhatsappSession) {
this.chatWootScheduleService.schedule(app.id, session.name).catch((err) => {
this.logger.error('Error scheduling session for ChatWoot');
this.logger.error(err, err.stack);
});
}
private async setupCustomAttributes(app: App<ChatWootAppConfig>) {
const di = new DIContainer(0, app.config, this.logger, null);
const api = di.CustomAttributesAPI();
for (const attr of CHATWOOT_CUSTOM_ATTRIBUTES) {
await api.upsert(attr);
}
const service = di.CustomAttributesService();
await service.upsert(CHATWOOT_CUSTOM_ATTRIBUTES);
}
private cleanCache(app: App<ChatWootAppConfig>) {
@@ -19,6 +19,10 @@ export class ChatWootQueueService {
private readonly messageUpdatedQueue: Queue,
@InjectQueue(QueueName.INBOX_MESSAGE_DELETED)
private readonly messageDeletedQueue: Queue,
@InjectQueue(QueueName.INBOX_CONVERSATION_CREATED)
private readonly conversationCreatedQueue: Queue,
@InjectQueue(QueueName.INBOX_CONVERSATION_STATUS_CHANGED)
private readonly conversationStatusChanged: Queue,
@InjectQueue(QueueName.INBOX_COMMANDS)
private readonly commandsQueue: Queue,
) {}
@@ -41,10 +45,14 @@ export class ChatWootQueueService {
*/
private getQueueForEvent(event: string): Queue | null {
switch (event) {
case EventName.CONVERSATION_CREATED:
return this.conversationCreatedQueue;
case EventName.MESSAGE_CREATED:
return this.messageCreatedQueue;
case EventName.MESSAGE_UPDATED:
return this.messageUpdatedQueue;
case EventName.CONVERSATION_STATUS_CHANGED:
return this.conversationStatusChanged;
case 'message_deleted':
return this.messageDeletedQueue;
case 'commands':
@@ -95,9 +103,9 @@ export class ChatWootQueueService {
*/
async addJobToQueue(event: string, data: InboxData): Promise<any> {
const queue = this.getQueueForEvent(event);
if (queue) {
return await this.add(queue, event, data);
if (!queue) {
return;
}
return { ignored: true, event };
await this.add(queue, event, data);
}
}
@@ -23,6 +23,8 @@ export class ChatWootWAHAQueueService {
private readonly queueMessageEdited: Queue,
@InjectQueue(QueueName.WAHA_MESSAGE_REVOKED)
private readonly queueMessageRevoked: Queue,
@InjectQueue(QueueName.WAHA_MESSAGE_ACK)
private readonly queueMessageAck: Queue,
@InjectQueue(QueueName.WAHA_SESSION_STATUS)
private readonly queueSessionStatus: Queue,
) {}
@@ -42,6 +44,8 @@ export class ChatWootWAHAQueueService {
return this.queueMessageEdited;
case WAHAEvents.MESSAGE_REVOKED:
return this.queueMessageRevoked;
case WAHAEvents.MESSAGE_ACK:
return this.queueMessageAck;
case WAHAEvents.SESSION_STATUS:
return this.queueSessionStatus;
default:
@@ -0,0 +1,72 @@
import { ConversationStatus } from '@waha/apps/chatwoot/client/types';
import { contact_conversations } from '@figuro/chatwoot-sdk/dist/models/contact_conversations';
import * as lodash from 'lodash';
import type { conversation } from '@figuro/chatwoot-sdk/dist/models/conversation';
export enum ConversationSort {
activity_newest = 'activity_newest',
created_newest = 'created_newest',
created_oldest = 'created_oldest',
activity_oldest = 'activity_oldest',
}
export type ConversationSelectorConfig = {
sort: ConversationSort;
status?: Array<ConversationStatus>;
inboxId: number;
};
export type ConversationResult = Pick<conversation, 'id' | 'account_id'>;
export class ConversationSelector {
constructor(private config: ConversationSelectorConfig) {}
hasStatusFilter() {
return this.config.status;
}
select(conversations: contact_conversations): ConversationResult | null {
conversations = this.filter(conversations);
conversations = this.sort(conversations);
return conversations[0] || null;
}
private filter(conversations: contact_conversations): contact_conversations {
// Filter by inbox id
conversations = lodash.filter(conversations, {
inbox_id: this.config.inboxId,
}) as contact_conversations;
// Filter by status
if (this.config.status && this.config.status.length > 0) {
conversations = lodash.filter(conversations, (conversation) => {
return this.config.status.includes(
conversation.status as ConversationStatus,
);
});
}
return conversations;
}
private sort(conversations: contact_conversations): contact_conversations {
let field = null;
let dir = null;
switch (this.config.sort) {
case ConversationSort.activity_newest:
[field, dir] = ['last_activity_at', 'desc'];
break;
case ConversationSort.created_newest:
[field, dir] = ['created_at', 'desc'];
break;
case ConversationSort.created_oldest:
[field, dir] = ['created_at', 'asc'];
break;
case ConversationSort.activity_oldest:
[field, dir] = ['last_activity_at', 'asc'];
}
if (!field || !dir) {
return conversations;
}
return lodash.orderBy(conversations, [field], [dir]);
}
}
+2 -1
View File
@@ -11,13 +11,14 @@ import {
} from '@waha/structures/chatting.dto';
import { SessionInfo } from '@waha/structures/sessions.dto';
import axios, { AxiosInstance } from 'axios';
import { Auth } from '@waha/core/auth/config';
export class WAHASelf {
public client: AxiosInstance;
constructor() {
// Set 'X-Api-Key'
const key = process.env.WAHA_API_KEY_PLAIN;
const key = Auth.keyplain.value;
const port =
parseInt(process.env.PORT) ||
parseInt(process.env.WHATSAPP_API_PORT) ||
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff. Show more