Compare commits

..
68 Commits
Author SHA1 Message Date
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
107 changed files with 4650 additions and 627 deletions

No files matched your search

+1
View File
@@ -8,6 +8,7 @@
# ==================
# 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
+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'
+1
View File
@@ -67,6 +67,7 @@ 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|
+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.
+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-09-21
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-09-10
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
+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
+1
View File
@@ -8,6 +8,7 @@
# ==================
# 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
+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 -1
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-09-21",
"@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",
@@ -79,6 +79,7 @@
"libsignal": "github:devlikeapro/libsignal-node#fork-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",
@@ -148,6 +149,9 @@
"ts"
],
"rootDir": "src",
"moduleNameMapper": {
"^@waha/(.*)$": "<rootDir>/$1"
},
"testRegex": ".test.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
+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 };
}
+4 -4
View File
@@ -4,12 +4,12 @@ import { parseBool } from '@waha/helpers';
import * as basicAuth from 'express-basic-auth';
export function BullAuthMiddleware() {
let username = process.env.WAHA_DASHBOARD_USERNAME;
let password = process.env.WAHA_DASHBOARD_PASSWORD;
let username = process.env.WAHA_DASHBOARD_USERNAME || '';
let password = process.env.WAHA_DASHBOARD_PASSWORD || '';
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 +18,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>;
}
@@ -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,
},
});
}
}
}
}
+10
View File
@@ -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> {
+13 -3
View File
@@ -42,6 +42,7 @@ export function ListenEventsForChatWoot() {
WAHAEvents.MESSAGE_REACTION,
WAHAEvents.MESSAGE_EDITED,
WAHAEvents.MESSAGE_REVOKED,
WAHAEvents.MESSAGE_ACK,
WAHAEvents.SESSION_STATUS,
];
}
@@ -96,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),
@@ -299,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;
},
);
@@ -327,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,113 @@
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';
@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;
}
// Only READ and PLAYED
const payload = event.payload;
return (
payload.ack === WAMessageAck.READ || payload.ack == WAMessageAck.PLAYED
);
}
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(),
);
await handler.handle(event);
}
}
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;
}
}
}
+48 -154
View File
@@ -1,7 +1,5 @@
import type { proto } from '@adiwajshing/baileys';
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 {
@@ -11,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';
@@ -20,13 +17,18 @@ 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 * 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,
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 +65,60 @@ 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 EventMessage(this.l);
msg = await converter.convert(payload, protoMessage);
if (msg) {
return msg;
}
// WEBJS - not available
return null;
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 +129,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 [];
}
}
@@ -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';
@@ -12,7 +12,10 @@ import {
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
@@ -66,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,
@@ -149,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();
@@ -266,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,8 +29,14 @@ 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);
const attempts = {
@@ -69,9 +77,6 @@ export class ChatWootErrorReporter {
type: MessageType,
replyTo?: number,
): Promise<void> {
const jobUrl = `http://localhost:3000/jobs/queue/${encodeURIComponent(
this.job.queueName,
)}/${this.job.id}`;
const template = this.l.key(TKey.JOB_REPORT_SUCCEEDED);
const attempts = {
current: this.job.attemptsMade + 1,
@@ -79,10 +84,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 {
+120 -2
View File
@@ -1,8 +1,28 @@
# Arabic translations for ChatWoot integration
locale.name: '🇦🇪 العربية (Arabic)'
# رمز اللغة الأساسي - يجب أن يكون محليًا صالحًا وفق BCP 47
# https://en.wikipedia.org/wiki/IETF_language_tag
# تستخدمه WAHA لعرض الأرقام والعملات والتاريخ/الوقت
locale.base: 'ar-AE'
app.inbox.contact.name: 'تكامل واتساب (WAHA)'
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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# Bengali translations for ChatWoot integration
locale.name: '🇧🇩 বাংলা (Bengali)'
# বেস লোকেল কোড - এটি একটি বৈধ BCP 47 লোকেল হতে হবে
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA এটি সংখ্যা, মুদ্রা এবং তারিখ/সময়ের ফরম্যাটিংয়ে ব্যবহার করে।
locale.base: 'bn-BD'
app.inbox.contact.name: 'হোয়াটসঅ্যাপ ইন্টিগ্রেশন (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: 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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# German translations for ChatWoot integration
locale.name: '🇩🇪 Deutsch (German)'
# 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 (WAHA)'
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}}
@@ -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,99 @@ 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.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}}}
+118 -2
View File
@@ -1,9 +1,27 @@
# English translations for ChatWoot integration
locale.name: '🇺🇸 English (English)'
# 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 (WAHA)'
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}}
@@ -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,103 @@ 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.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**
+120 -2
View File
@@ -1,8 +1,28 @@
# Spanish translations for ChatWoot integration
locale.name: '🇪🇸 Español (Spanish)'
# 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 (WAHA)'
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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# Persian translations for ChatWoot integration
locale.name: '🇮🇷 فارسی (Persian)'
# کد محلی پایه - باید یک محلی معتبر BCP 47 باشد
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA از آن برای نمایش اعداد، ارز و تاریخ/زمان استفاده می‌کند
locale.base: 'fa-IR'
app.inbox.contact.name: 'ادغام واتساپ (WAHA)'
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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# French translations for ChatWoot integration
locale.name: '🇫🇷 Français (French)'
# 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 (WAHA)'
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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# Hebrew translations for ChatWoot integration
locale.name: '🇮🇱 עברית (Hebrew)'
# קוד הלוקאל הבסיסי - חייב להיות לוקאל תקין לפי BCP 47
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA משתמש בכך להצגת מספרים, מטבע ותאריך/שעה
locale.base: 'he-IL'
app.inbox.contact.name: 'אינטגרציית וואטסאפ (WAHA)'
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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# Hindi translations for ChatWoot integration
locale.name: '🇮🇳 हिंदी (Hindi)'
# बेस लोकैल कोड - यह एक मान्य BCP 47 लोकैल होना चाहिए
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA इसे संख्याएँ, मुद्रा और दिनांक/समय प्रदर्शित करने के लिए उपयोग करता है।
locale.base: 'hi-IN'
app.inbox.contact.name: 'व्हाट्सएप इंटीग्रेशन (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: 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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# Indonesian translations for ChatWoot integration
locale.name: '🇮🇩 Bahasa Indonesia (Indonesian)'
# 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 (WAHA)'
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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# Punjabi translations for ChatWoot integration
locale.name: '🇵🇰 ਪੰਜਾਬੀ (Punjabi)'
# ਅਧਾਰਭੂਤ ਲੋਕੇਲ ਕੋਡ - ਇਹ ਇੱਕ ਵੈਧ BCP 47 ਲੋਕੇਲ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA ਇਸਨੂੰ ਅੰਕ, ਕਰੰਸੀ ਅਤੇ ਤਾਰੀਖ/ਸਮੇਂ ਦੀ ਨਮਾਇਸ਼ ਲਈ ਵਰਤਦਾ ਹੈ
locale.base: 'pa-PK'
app.inbox.contact.name: 'ਵਟਸਐਪ ਇੰਟੀਗ੍ਰੇਸ਼ਨ (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: 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}}
@@ -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,99 @@ 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.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}}}
+124 -2
View File
@@ -1,8 +1,28 @@
# Portuguese translations for ChatWoot integration
locale.name: '🇧🇷🇵🇹 Português (Portuguese)'
# 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 (WAHA)'
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}}
@@ -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,103 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# Russian translations for ChatWoot integration
locale.name: '🇷🇺 Русский (Russian)'
# Базовый код локали - должен быть корректной локалью BCP 47
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA использует его для форматирования чисел, валют и даты/времени
locale.base: 'ru-RU'
app.inbox.contact.name: 'WhatsApp Интеграция (WAHA)'
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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# Turkish translations for ChatWoot integration
locale.name: '🇹🇷 Türkçe (Turkish)'
# 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 (WAHA)'
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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# Ukrainian translations for ChatWoot integration
locale.name: '🇺🇦 Українська (Ukrainian)'
# Базовий код локалі - має бути чинною локаллю BCP 47
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA використовує його для форматування чисел, валют і дати/часу
locale.base: 'uk-UA'
app.inbox.contact.name: 'Інтеграція WhatsApp (WAHA)'
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}}
@@ -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,99 @@ 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.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}}}
+120 -2
View File
@@ -1,8 +1,28 @@
# Urdu translations for ChatWoot integration
locale.name: '🇵🇰 اردو (Urdu)'
# بنیادی لوکل کوڈ - یہ ایک معتبر BCP 47 لوکل ہونا چاہیے
# https://en.wikipedia.org/wiki/IETF_language_tag
# WAHA اسے نمبرز، کرنسی اور تاریخ/وقت کی نمائندگی کے لیے استعمال کرتا ہے۔
locale.base: 'ur-PK'
app.inbox.contact.name: 'واٹس ایپ انٹیگریشن (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: 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}}
@@ -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,99 @@ 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.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}}}
+120 -2
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}}
@@ -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,99 @@ 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.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}}}
+52
View File
@@ -51,7 +51,11 @@ 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',
//
// App Inbox
@@ -99,6 +103,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 +163,27 @@ 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_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 { 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,23 @@
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 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]);
}
}
@@ -74,4 +74,17 @@ export class MessageMappingRepository {
})
.first();
}
async getByChatwootMessageIdAndPart(
id: number,
part: number,
): Promise<MessageMapping | null> {
return this.knex(this.tableName)
.where({
app_pk: this.appPk,
chatwoot_message_id: id,
part: part,
})
.first();
}
}
@@ -123,4 +123,26 @@ export class MessageMappingService {
}
return whatsapp;
}
async getMappingByChatwootCombinedKeyAndPart(
chatwoot: ChatWootCombinedKey,
part: number,
): Promise<MessageMapping | null> {
const chatwootMessages =
await this.chatwootMessageRepository.getByCombinedKey(chatwoot);
if (!chatwootMessages || chatwootMessages.length === 0) {
return null;
}
for (const cw of chatwootMessages) {
const mapping =
await this.messageMappingRepository.getByChatwootMessageIdAndPart(
cw.id,
part,
);
if (mapping) {
return mapping;
}
}
return null;
}
}
-4
View File
@@ -104,10 +104,6 @@ export abstract class SessionManager
);
}
async configureApps(session: WhatsappSession) {
await this.appsService.beforeSessionStart(session, this.store);
}
//
// API Methods
//
+17 -4
View File
@@ -43,6 +43,7 @@ import { complete } from '@waha/utils/reactive/complete';
import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable';
import axios from 'axios';
import axiosRetry from 'axios-retry';
import { Agent as HttpsAgent } from 'https';
import * as fs from 'fs';
import * as lodash from 'lodash';
import * as NodeCache from 'node-cache';
@@ -53,7 +54,6 @@ import {
delay,
filter,
of,
pairwise,
retry,
scan,
share,
@@ -1076,11 +1076,24 @@ export abstract class WhatsappSession {
/**
* Fetches the content from the specified URL and returns it as a Buffer.
*/
public async fetch(url: string): Promise<Buffer> {
return axios.get(url, { responseType: 'arraybuffer' }).then((res) => {
public fetch(url: string): Promise<Buffer> {
return fetchBuffer(url);
}
}
const InsecureHttpsAgent = new HttpsAgent({
rejectUnauthorized: false,
});
export async function fetchBuffer(url: string): Promise<Buffer> {
return axios
.get(url, {
responseType: 'arraybuffer',
httpsAgent: InsecureHttpsAgent,
})
.then((res) => {
return Buffer.from(res.data);
});
}
}
export function getGroupInviteLink(code: string) {
+1 -1
View File
@@ -11,7 +11,7 @@ export function BasicAuthFunction(username, password, exclude: string[] = []) {
const auth = basicAuth({
challenge: true,
users: {
[username]: password,
[String(username)]: String(password),
},
});
auth(req, res, next);
+4 -3
View File
@@ -943,16 +943,17 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
async checkNumberStatus(
request: CheckNumberStatusQuery,
): Promise<WANumberExistResult> {
let phone = request.phone.replace(/\+/g, '');
const req = new messages.CheckPhonesRequest({
session: this.session,
phones: [request.phone],
phones: [phone],
});
const response = await promisify(this.client.CheckPhones)(req);
const data = response.toObject();
const info = data.infos[0];
return {
numberExists: info.registered,
chatId: toCusFormat(info.jid),
numberExists: info?.registered || false,
chatId: toCusFormat(info?.jid || null),
};
}
+27 -7
View File
@@ -293,7 +293,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
async start() {
this.status = WAHASessionStatus.STARTING;
await this.buildClient().catch((err) => {
this.buildClient().catch((err) => {
this.logger.error('Failed to start the client');
this.logger.error(err, err.stack);
this.status = WAHASessionStatus.FAILED;
@@ -641,7 +641,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
const creationMsgKey = content.pollUpdateMessage.pollCreationMessageKey;
// we need to fetch the poll creation message to get the poll enc key
const pollMsg = await this.getMessage(creationMsgKey);
const pkey = { ...creationMsgKey };
pkey.remoteJid = null; // try to find message creation by id only
const pollMsg = await this.getMessage(pkey);
if (!pollMsg) {
this.logger.warn(
{ creationMsgKey },
@@ -696,7 +698,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.sock.ev.on('messages.upsert', ({ messages }) => {
const meId = this.sock?.authState?.creds?.me?.id;
for (const message of messages) {
if (!isRealMessage(message, meId)) {
if (!isRealMessage(message)) {
continue;
}
if (message.key.fromMe) {
@@ -827,7 +829,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
async checkNumberStatus(
request: CheckNumberStatusQuery,
): Promise<WANumberExistResult> {
const phone = request.phone.split('@')[0];
let phone = request.phone.split('@')[0];
phone = phone.replace(/\+/g, '');
const [result] = await this.sock.onWhatsApp(phone);
if (!result || !result.exists) {
return { numberExists: false };
@@ -989,7 +992,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
force: true,
};
const options = await this.getMessageOptions(request);
const result = await this.sock.sendMessage(chatId, message, options);
const result = await this.sock.sendMessage(chatId, message as any, options);
return this.toWAMessage(result);
}
@@ -2212,6 +2215,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
proto.Message.ProtocolMessage.Type.EPHEMERAL_SYNC_RESPONSE
)
return;
if (
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type
.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE
)
return;
const normalizedContent = normalizeMessageContent(message.message);
const contentType = getContentType(normalizedContent);
@@ -2377,7 +2386,16 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
const pollCreationMessageKey = key;
const pollCreationMessage = await this.getMessage(key);
const pkey = { ...key };
pkey.remoteJid = null; // try to find message creation by id only
const pollCreationMessage = await this.getMessage(pkey);
if (!pollCreationMessage) {
this.logger.warn(
{ pollCreationMessageKey },
'poll creation message not found, cannot aggregate votes',
);
return;
}
// Handle updates one by one, so we can get Vote Message for the specific vote
for (const pollUpdate of pollUpdates) {
const votes = getAggregateVotesInPollMessage({
@@ -2416,7 +2434,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return;
}
const pollCreationMessageKey = pollUpdateMessage.pollCreationMessageKey;
const pollCreationMessage = await this.getMessage(pollCreationMessageKey);
const pkey = { ...pollCreationMessageKey };
pkey.remoteJid = null; // try to find message creation by id only
const pollCreationMessage = await this.getMessage(pkey);
if (pollCreationMessage) {
// We found message, so later the engine will issue a message.update message
return;
@@ -315,9 +315,7 @@ export class NowebPersistentStore implements INowebStore {
Object.assign(message, fields);
// In case of revoked messages - remove it
// TODO: May be we should save the flag instead of completely removing the message
const isYetRealMessage =
esm.b.isRealMessage(message, this.socket?.authState?.creds?.me?.id) ||
false;
const isYetRealMessage = esm.b.isRealMessage(message) || false;
if (isYetRealMessage) {
await this.messagesRepo.upsertOne(message);
} else {
@@ -346,8 +346,8 @@ export default (config: BaileysInMemoryStoreConfig) => {
switch (action) {
case 'add':
metadata.participants.push(
...participants.map((id) => ({
id,
...participants.map((participant) => ({
id: participant.id,
isAdmin: false,
isSuperAdmin: false,
})),
@@ -356,7 +356,7 @@ export default (config: BaileysInMemoryStoreConfig) => {
case 'demote':
case 'promote':
for (const participant of metadata.participants) {
if (participants.includes(participant.id)) {
if (participants.find((p) => p.id === participant.id)) {
participant.isAdmin = action === 'promote';
}
}
@@ -364,7 +364,8 @@ export default (config: BaileysInMemoryStoreConfig) => {
break;
case 'remove':
metadata.participants = metadata.participants.filter(
(p) => !participants.includes(p.id),
(participant) =>
!participants.find((p) => p.id === participant.id),
);
break;
}
@@ -414,7 +415,7 @@ export default (config: BaileysInMemoryStoreConfig) => {
for (const jid in json.messages) {
const list = assertMessageList(jid);
for (const msg of json.messages[jid]) {
list.upsert(esm.b.proto.WebMessageInfo.create(msg), 'append');
list.upsert(esm.b.proto.WebMessageInfo.create(msg) as any, 'append');
}
}
};
+9 -2
View File
@@ -79,12 +79,19 @@ const isObjectALong = (value: any): value is Long => {
);
};
export function ensureNumber(value: number | Long): number {
export function ensureNumber(value: number | Long | string | null): number {
if (!value) {
// @ts-ignore
return value;
}
return typeof value === 'number' ? value : toNumber(value);
if (typeof value === 'string') {
return Number.parseInt(value, 10);
}
if (isObjectALong(value)) {
return toNumber(value);
}
// number
return value;
}
const toNumber = (longValue: Long): number => {
+2 -1
View File
@@ -600,7 +600,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
async checkNumberStatus(
request: CheckNumberStatusQuery,
): Promise<WANumberExistResult> {
const phone = request.phone.split('@')[0];
let phone = request.phone.split('@')[0];
phone = phone.replace(/\+/g, '');
const result = await this.whatsapp.getNumberId(phone);
if (!result) {
return {
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more