Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad10ea4e96 | ||
|
|
9de53a972a | ||
|
|
4043efbb9a | ||
|
|
42019644f0 | ||
|
|
bcc25ef42f | ||
|
|
602bbf3b60 | ||
|
|
585ac6074f | ||
|
|
5bd1a43df6 | ||
|
|
c9279a4b59 | ||
|
|
9ff8eed6ba | ||
|
|
96ffdd251f | ||
|
|
35b8b7d01c | ||
|
|
4e2e2b2b3c | ||
|
|
bfff0c8cf4 | ||
|
|
e253f445b9 | ||
|
|
73ed663c9c | ||
|
|
2236150b83 | ||
|
|
da24feb390 | ||
|
|
44a5ada162 | ||
|
|
e57cc2587c | ||
|
|
38bfb9baa5 | ||
|
|
540636d59b | ||
|
|
3cc784353e | ||
|
|
44946ee784 | ||
|
|
a9d77487c6 | ||
|
|
70d4f27abd | ||
|
|
9fca09661d | ||
|
|
2f0efd0223 | ||
|
|
5c31ed53df | ||
|
|
87640ea238 | ||
|
|
eb8f771b06 | ||
|
|
b080b5862e | ||
|
|
df50e7527e | ||
|
|
e2b43575c0 | ||
|
|
2059793562 | ||
|
|
e5df21e165 | ||
|
|
792029b13f | ||
|
|
0ef49d7116 | ||
|
|
600d3291eb | ||
|
|
b78413190c | ||
|
|
c9729af139 | ||
|
|
697f267fc3 | ||
|
|
ac49ce3cc5 | ||
|
|
555daf6888 | ||
|
|
8f0c2994a6 | ||
|
|
873e66db19 | ||
|
|
38b542def4 | ||
|
|
a646e5efc0 | ||
|
|
31590c9229 | ||
|
|
98c2172908 | ||
|
|
314b02daf1 | ||
|
|
d2ab78b65c | ||
|
|
db2c352fd1 | ||
|
|
6e79ad2793 | ||
|
|
77eaa287fe | ||
|
|
a64820048c | ||
|
|
4ff4a5a893 | ||
|
|
5ea5681d2b | ||
|
|
2bb327b747 | ||
|
|
4dcdf60309 | ||
|
|
009fd5b2ec | ||
|
|
091e7e5bc1 | ||
|
|
ea1202bb65 | ||
|
|
cb413febaa | ||
|
|
364e55d477 | ||
|
|
680a35a26d | ||
|
|
08ff59e463 | ||
|
|
9db4c8c333 | ||
|
|
2489c2da46 | ||
|
|
fbc58b0707 | ||
|
|
d5a525789e | ||
|
|
34066921e1 | ||
|
|
901c594311 | ||
|
|
5f619e608b | ||
|
|
49295090ad | ||
|
|
a6e43adb0a | ||
|
|
1f165bb6cb | ||
|
|
8fc9d88d2e | ||
|
|
d7e5a5ce05 | ||
|
|
09d446fdeb | ||
|
|
c0bbd60e15 | ||
|
|
af3fc137be | ||
|
|
0ee5e1bd88 | ||
|
|
f92ea18651 | ||
|
|
3cce2bb35f | ||
|
|
e080ce42ee | ||
|
|
5dbe41da78 | ||
|
|
eef3957d4f | ||
|
|
680f4596b9 | ||
|
|
8de344f220 | ||
|
|
8005846955 | ||
|
|
3edd60448b | ||
|
|
4a2fa5c076 | ||
|
|
96e9c2cc4e | ||
|
|
2089a1bb35 | ||
|
|
023c2718e2 | ||
|
|
f7aacb193a | ||
|
|
e710040904 | ||
|
|
fb12826801 | ||
|
|
833a70c6e0 | ||
|
|
c807f9021c | ||
|
|
46b66bcdf0 | ||
|
|
2ebade817e | ||
|
|
080342e619 | ||
|
|
75b06d3ba1 | ||
|
|
3b6fa3343d | ||
|
|
469cdc9b1d | ||
|
|
8a179e7194 | ||
|
|
c133795753 | ||
|
|
941ace7461 | ||
|
|
8589420f5d | ||
|
|
ece24e182f | ||
|
|
38e5bb7bdf | ||
|
|
d09e931827 | ||
|
|
3f3f1e1c20 | ||
|
|
32f2468ba8 | ||
|
|
1d6540e0d9 | ||
|
|
31da4bfca1 | ||
|
|
41d49894e2 | ||
|
|
6dcdd15b1c | ||
|
|
c2b275fd90 | ||
|
|
e4838ef3ef | ||
|
|
55ca4660d7 | ||
|
|
c16c903ec4 | ||
|
|
937cf38ff6 | ||
|
|
07f76e854d | ||
|
|
cde1d084c6 | ||
|
|
9362c5d6ae | ||
|
|
117a9108c0 | ||
|
|
5214e461f2 | ||
|
|
9bdb178b7d | ||
|
|
9c526ffdba | ||
|
|
f80a74edd1 | ||
|
|
7d8c6c0a62 | ||
|
|
17b26b869b | ||
|
|
dfbe54e92f | ||
|
|
2321de409b | ||
|
|
502287a258 | ||
|
|
052ff0ea6e | ||
|
|
2b76789cdf | ||
|
|
7bbf5df231 | ||
|
|
794a04e7d3 | ||
|
|
645467f13a | ||
|
|
7e87637341 |
No files matched your search
@@ -1,4 +1,5 @@
|
||||
Dockerfile
|
||||
docker-compose
|
||||
src/core/engines/gows/proto
|
||||
.*sessions
|
||||
sessions
|
||||
|
||||
+97
-168
@@ -1,191 +1,120 @@
|
||||
# WAHA Configuration
|
||||
# WAHA - WhatsApp HTTP API
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/
|
||||
#
|
||||
# Common
|
||||
#
|
||||
# If you know the right URL, you can use it here.
|
||||
# Use any port in WHATSAPP_API_PORT and domain in WHATSAPP_API_HOSTNAME
|
||||
# https://waha.devlike.pro/docs/how-to/config/#common
|
||||
|
||||
# ==================
|
||||
# ===== COMMON =====
|
||||
# ==================
|
||||
# Base URL for the API (used for webhooks, file URLs, etc.)
|
||||
WAHA_BASE_URL=http://localhost:3000
|
||||
|
||||
#WHATSAPP_API_SCHEMA=http
|
||||
#WHATSAPP_API_PORT=3000
|
||||
#WHATSAPP_API_HOSTNAME=localhost
|
||||
# Server configuration (if you need to customize hostname/port)
|
||||
# WHATSAPP_API_SCHEMA=http
|
||||
# WHATSAPP_API_PORT=3000
|
||||
# WHATSAPP_API_HOSTNAME=localhost
|
||||
|
||||
#
|
||||
# Set your timezone to see the right time on screenshots
|
||||
# Find your timezone name in the list
|
||||
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
#TZ=Europe/Dublin
|
||||
|
||||
#
|
||||
# Security
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/security/
|
||||
#
|
||||
WHATSAPP_API_KEY=admin
|
||||
# ====================
|
||||
# ===== SECURITY =====
|
||||
# ====================
|
||||
# "sha512:{SHA}" format - below is "admin" api key
|
||||
WAHA_API_KEY=sha512:c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec
|
||||
WAHA_DASHBOARD_ENABLED=True
|
||||
WAHA_DASHBOARD_USERNAME=admin
|
||||
WAHA_DASHBOARD_PASSWORD=admin
|
||||
#WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
|
||||
#
|
||||
# Dashboard
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
|
||||
# Enabled by default
|
||||
#WAHA_DASHBOARD_ENABLED=true
|
||||
#WAHA_DASHBOARD_USERNAME=admin
|
||||
#WAHA_DASHBOARD_PASSWORD=admin
|
||||
|
||||
#
|
||||
# Logging
|
||||
# https://waha.devlike.pro/docs/how-to/observability/#logging
|
||||
#
|
||||
# Set log format to JSON if you consume logs with log management systems
|
||||
WAHA_LOG_FORMAT=JSON
|
||||
WAHA_LOG_LEVEL=info
|
||||
# WAHA_LOG_FORMAT=PRETTY
|
||||
# If you want to see more logs, you can set the log level to debug
|
||||
#WAHA_LOG_LEVEL=debug
|
||||
# DEBUG=1 # shortcut for setting log level to debug
|
||||
|
||||
#
|
||||
# Engine
|
||||
# https://waha.devlike.pro/docs/how-to/engines/
|
||||
#
|
||||
# Choose the right engine for your needs (WEBJS by default)
|
||||
WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
# Use NOWEB engine for the fastest performance
|
||||
#WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
|
||||
# Worker
|
||||
# WAHA_WORKER_ID=waha1
|
||||
|
||||
#
|
||||
# Sessions
|
||||
# https://waha.devlike.pro/docs/how-to/config/#sessions
|
||||
#
|
||||
# Do not print QR codes in logs
|
||||
WAHA_PRINT_QR=False
|
||||
#
|
||||
# OR you can specify sessions by name, if you have handful amount of sessions
|
||||
#WHATSAPP_START_SESSION=session1,session2
|
||||
#
|
||||
# Restart all sessions when container starts
|
||||
# !!! Do not use it with multiple workers against one database !!!
|
||||
# WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
|
||||
#
|
||||
# Swagger
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/
|
||||
#
|
||||
WHATSAPP_SWAGGER_ENABLED=True
|
||||
WHATSAPP_SWAGGER_USERNAME=admin
|
||||
WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
# Disable Swagger
|
||||
#WHATSAPP_SWAGGER_ENABLED=false
|
||||
# Enable advanced Swagger configuration
|
||||
#WHATSAPP_SWAGGER_CONFIG_ADVANCED=true
|
||||
#
|
||||
# Swagger White Label customization
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/#white-label
|
||||
#WHATSAPP_SWAGGER_TITLE=WAHA API
|
||||
#
|
||||
|
||||
#
|
||||
# PostgresSQL Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---postgresql
|
||||
#WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
#
|
||||
# Media - PostgresSQL Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
|
||||
#WAHA_MEDIA_STORAGE=POSTGRESQL
|
||||
#WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
#
|
||||
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
|
||||
WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
|
||||
#
|
||||
# Global Proxy Settings
|
||||
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
|
||||
#
|
||||
# You can also set proxy settings for each session later when starting it
|
||||
# https://waha.devlike.pro/docs/how-to/sessions/#configure-proxy
|
||||
#WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
#WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
#WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
#
|
||||
# ===================
|
||||
# ===== LOGGING =====
|
||||
# ===================
|
||||
# Log format: JSON (for log management systems) or PRETTY (for development)
|
||||
WAHA_LOG_FORMAT=JSON
|
||||
|
||||
#
|
||||
# Media Configuration
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#files
|
||||
#
|
||||
# Disable media (images, videos, files) download for incoming messages
|
||||
#WHATSAPP_DOWNLOAD_MEDIA=false
|
||||
#
|
||||
# Download only specific media types
|
||||
#WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
#
|
||||
# Log level: info, debug, error, warn
|
||||
WAHA_LOG_LEVEL=info
|
||||
|
||||
# Media - Local Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---local
|
||||
#
|
||||
# Set to 0 to keep media files forever
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
# Don't print QR codes in logs
|
||||
WAHA_PRINT_QR=False
|
||||
|
||||
# =========================
|
||||
# ===== MEDIA STORAGE =====
|
||||
# =========================
|
||||
# Local storage (default)
|
||||
WAHA_MEDIA_STORAGE=LOCAL
|
||||
WHATSAPP_FILES_LIFETIME=0
|
||||
WHATSAPP_FILES_FOLDER=/app/.media
|
||||
# Keep media files for 180 seconds (3 minutes)
|
||||
#WHATSAPP_FILES_LIFETIME=180
|
||||
#
|
||||
|
||||
# Media - S3 Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
#WAHA_MEDIA_STORAGE=S3
|
||||
#WAHA_S3_REGION=eu-west-2
|
||||
#WAHA_S3_BUCKET=waha
|
||||
#WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
#WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
#WAHA_S3_ENDPOINT=http://minio:9000 # Not required if you're using AWS S3
|
||||
#WAHA_S3_FORCE_PATH_STYLE=True # Required for Minio
|
||||
#WAHA_S3_PROXY_FILES=True # Required for docker-compose setup
|
||||
# Media download settings
|
||||
# WHATSAPP_DOWNLOAD_MEDIA=true
|
||||
# WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
|
||||
#
|
||||
# Global Webhooks
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#webhooks
|
||||
# https://waha.devlike.pro/docs/how-to/webhooks/
|
||||
#WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
#WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
#
|
||||
# S3 storage (uncomment to use)
|
||||
# WAHA_MEDIA_STORAGE=S3
|
||||
# WAHA_S3_REGION=eu-west-2
|
||||
# WAHA_S3_BUCKET=waha
|
||||
# WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
# WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
# WAHA_S3_ENDPOINT=http://minio:9000
|
||||
# WAHA_S3_FORCE_PATH_STYLE=True
|
||||
# WAHA_S3_PROXY_FILES=True
|
||||
|
||||
#
|
||||
# Local Configuration
|
||||
#
|
||||
# WAHA_LOCAL_STORE_BASE_DIR=/app/sessions
|
||||
# Remember to map the volume to the host machine to the right direction in "volumes" field in docker-compose.yml
|
||||
# volumes:
|
||||
# - './.sessions:/app/sessions'
|
||||
# PostgreSQL storage (uncomment to use)
|
||||
# WAHA_MEDIA_STORAGE=POSTGRESQL
|
||||
# WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# ===========================
|
||||
# ===== SESSION STORAGE =====
|
||||
# ===========================
|
||||
# PostgreSQL for sessions (uncomment to use)
|
||||
# WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# MongoDB for sessions (uncomment to use)
|
||||
# WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
|
||||
|
||||
# =================
|
||||
# ===== APPS ======
|
||||
# =================
|
||||
# WAHA_APPS_ENABLED=True
|
||||
# REDIS_URL=redis://:redis@redis:6379
|
||||
|
||||
# Apps Jobs configuration - *_AGE in seconds
|
||||
# WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_AGE=
|
||||
# WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_COUNT=
|
||||
# WAHA_APPS_JOBS_REMOVE_ON_FAIL_AGE=
|
||||
# WAHA_APPS_JOBS_REMOVE_ON_FAIL_COUNT
|
||||
|
||||
|
||||
#
|
||||
# MongoDB Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
#WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
|
||||
#
|
||||
# ==================================
|
||||
# ===== ADVANCED CONFIGURATION =====
|
||||
# ==================================
|
||||
|
||||
#
|
||||
# HTTPS Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/config/#https
|
||||
#
|
||||
# Consider using certbot for HTTPS
|
||||
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
|
||||
#
|
||||
#WAHA_HTTPS_ENABLED=true
|
||||
#WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
#WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
|
||||
#WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
#WHATSAPP_API_SCHEMA=https
|
||||
#WHATSAPP_API_PORT=3000
|
||||
#WHATSAPP_API_HOSTNAME=waha.example.pro
|
||||
#
|
||||
# Timezone for screenshots and logs
|
||||
# TZ=Europe/Dublin
|
||||
|
||||
# Session management
|
||||
# WHATSAPP_START_SESSION=session1,session2
|
||||
# WHATSAPP_RESTART_ALL_SESSIONS=False
|
||||
|
||||
# Webhooks
|
||||
# WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# WHATSAPP_HOOK_CUSTOM_HEADERS=custom_header_key:custom_header_value
|
||||
# WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
|
||||
# Proxy configuration
|
||||
# WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
# WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
# WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
|
||||
# HTTPS configuration
|
||||
# !DEPRECATED!
|
||||
# Setup nginx reverse proxy to handle TLS connection
|
||||
# using Let's encrypt or self-issued certificate
|
||||
# WAHA_HTTPS_ENABLED=true
|
||||
# WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
# WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem
|
||||
# WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
@@ -126,7 +126,11 @@ jobs:
|
||||
working-directory: tests/smoke
|
||||
timeout-minutes: 2
|
||||
run: |
|
||||
docker run -d --name smoke --rm -p3000:3000 ${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
|
||||
docker run -d \
|
||||
--name smoke \
|
||||
--rm -p3000:3000 \
|
||||
${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
|
||||
|
||||
sleep 3
|
||||
docker logs smoke
|
||||
goss validate --retry-timeout 30s --sleep 1s
|
||||
|
||||
@@ -39,6 +39,7 @@ repos:
|
||||
(?x)^(
|
||||
docs/.*|
|
||||
README.md|
|
||||
tests/smoke/goss.yaml|
|
||||
)$
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
|
||||
rev: v1.1.2
|
||||
|
||||
+44
-8
@@ -1,8 +1,10 @@
|
||||
ARG NODE_IMAGE_TAG=22.16-bookworm-slim
|
||||
ARG GOLANG_IMAGE_TAG=1.23-bookworm
|
||||
|
||||
#
|
||||
# Build
|
||||
#
|
||||
ARG NODE_VERSION=22.8-bullseye
|
||||
FROM node:${NODE_VERSION} AS build
|
||||
FROM node:${NODE_IMAGE_TAG} AS build
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
|
||||
# npm packages
|
||||
@@ -10,6 +12,10 @@ WORKDIR /git
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
ENV YARN_CHECKSUM_BEHAVIOR=update
|
||||
|
||||
# git
|
||||
RUN apt-get update && apt-get install -y git
|
||||
|
||||
RUN npm install -g corepack && corepack enable
|
||||
RUN yarn set version 3.6.3
|
||||
RUN yarn install
|
||||
@@ -23,11 +29,14 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
#
|
||||
# Dashboard
|
||||
#
|
||||
FROM node:${NODE_VERSION} AS dashboard
|
||||
FROM node:${NODE_IMAGE_TAG} AS dashboard
|
||||
|
||||
# jq to parse json
|
||||
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# wget, unzip
|
||||
RUN apt-get update && apt-get install -y wget unzip && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY waha.config.json /tmp/waha.config.json
|
||||
RUN \
|
||||
WAHA_DASHBOARD_GITHUB_REPO=$(jq -r '.waha.dashboard.repo' /tmp/waha.config.json) && \
|
||||
@@ -42,7 +51,7 @@ RUN \
|
||||
#
|
||||
# GOWS
|
||||
#
|
||||
FROM golang:1.23-bullseye AS gows
|
||||
FROM golang:${GOLANG_IMAGE_TAG} AS gows
|
||||
|
||||
# jq to parse json
|
||||
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
|
||||
@@ -73,7 +82,7 @@ RUN \
|
||||
#
|
||||
# Final
|
||||
#
|
||||
FROM node:${NODE_VERSION} AS release
|
||||
FROM node:${NODE_IMAGE_TAG} AS release
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
# Quick fix for memory potential memory leaks
|
||||
# https://github.com/devlikeapro/waha/issues/347
|
||||
@@ -98,6 +107,13 @@ RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install wget - either for chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update \
|
||||
&& apt-get install -y wget \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install fonts if using either chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update \
|
||||
@@ -119,6 +135,21 @@ RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install xvfb, xauth
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
xvfb \
|
||||
xauth \
|
||||
libnss3 \
|
||||
libxss1 \
|
||||
libasound2 \
|
||||
libatk-bridge2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libdrm2 \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install Chromium
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
apt-get update \
|
||||
@@ -131,7 +162,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
# Install Chrome
|
||||
# Available versions:
|
||||
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
|
||||
ARG CHROME_VERSION="135.0.7049.114-1"
|
||||
ARG CHROME_VERSION="137.0.7151.103-1"
|
||||
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
|
||||
&& apt-get update \
|
||||
@@ -140,6 +171,11 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# curl
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# GOWS requirements
|
||||
# libc6
|
||||
RUN apt-get update \
|
||||
@@ -159,8 +195,8 @@ COPY --from=build /git/node_modules ./node_modules
|
||||
COPY --from=build /git/dist ./dist
|
||||
COPY --from=dashboard /dashboard ./dist/dashboard
|
||||
COPY --from=gows /go/gows/bin/gows /app/gows
|
||||
ENV WAHA_GOWS_PATH /app/gows
|
||||
ENV WAHA_GOWS_SOCKET /tmp/gows.sock
|
||||
ENV WAHA_GOWS_PATH=/app/gows
|
||||
ENV WAHA_GOWS_SOCKET=/tmp/gows.sock
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
build-all: build-plus build-chrome build-gows build-noweb
|
||||
|
||||
build:
|
||||
docker build . -t devlikeapro/waha
|
||||
|
||||
build-plus:
|
||||
docker build . -t devlikeapro/waha-plus
|
||||
|
||||
build-chrome:
|
||||
docker build . -t devlikeapro/waha-plus:chrome --build-arg USE_BROWSER=chrome
|
||||
|
||||
build-noweb:
|
||||
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
docker build . -t devlikeapro/waha-plus:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
|
||||
build-plus-gows:
|
||||
build-gows:
|
||||
docker build . -t devlikeapro/waha-plus:gows --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=GOWS
|
||||
|
||||
build-all: build build-chrome build-noweb
|
||||
|
||||
build-plus:
|
||||
docker build . -t devlikeapro/waha-plus
|
||||
|
||||
build-ssh:
|
||||
# check IMAGE provided
|
||||
@[ "${IMAGE}" ] || ( echo "Add APP: make build-ssh image=devlikeapro/waha"; exit 1 );
|
||||
@@ -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-05-01
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-07-04
|
||||
|
||||
up-noweb-libsignal:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec 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-channels
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09
|
||||
|
||||
start-proxy:
|
||||
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
|
||||
|
||||
@@ -146,7 +146,7 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
|
||||
|
||||
## Start the project
|
||||
1. Clone the repository
|
||||
2. Make sure you're using node>=22
|
||||
2. Make sure you're using node>=22 (check [.nvmrc](/.nvmrc) to get the version)
|
||||
3. Run the following commands:
|
||||
```bash
|
||||
# Install dependencies
|
||||
|
||||
+7
-2
@@ -8,6 +8,11 @@ services:
|
||||
# WAHA Core
|
||||
# image: devlikeapro/waha:latest
|
||||
|
||||
# Add "dns" if you have a problem with resolving "web.whatsapp.com"
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 8.8.8.8
|
||||
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
@@ -23,7 +28,7 @@ services:
|
||||
|
||||
# Save media files
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
- './.media:/app/.media'
|
||||
- './media:/app/.media'
|
||||
|
||||
env_file:
|
||||
- .env
|
||||
@@ -38,7 +43,7 @@ services:
|
||||
# POSTGRES_PASSWORD: postgres
|
||||
# POSTGRES_DB: postgres
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
# - "127.0.0.1:5432:5432"
|
||||
# volumes:
|
||||
# - pg_data:/var/lib/postgresql/data
|
||||
# command:
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# Learn about the various environment variables at
|
||||
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
|
||||
|
||||
# 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
|
||||
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||
|
||||
# Replace with the URL you are planning to use for your app
|
||||
FRONTEND_URL=http://chatwoot:3009
|
||||
# To use a dedicated URL for help center pages
|
||||
# HELPCENTER_URL=http://0.0.0.0:3000
|
||||
|
||||
# If the variable is set, all non-authenticated pages would fallback to the default locale.
|
||||
# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en
|
||||
# DEFAULT_LOCALE=en
|
||||
|
||||
# If you plan to use CDN for your assets, set Asset CDN Host
|
||||
ASSET_CDN_HOST=
|
||||
|
||||
# Force all access to the app over SSL, default is set to false
|
||||
FORCE_SSL=false
|
||||
|
||||
# This lets you control new sign ups on your chatwoot installation
|
||||
# true : default option, allows sign ups
|
||||
# false : disables all the end points related to sign ups
|
||||
# api_only: disables the UI for signup, but you can create sign ups via the account apis
|
||||
ENABLE_ACCOUNT_SIGNUP=false
|
||||
|
||||
# Redis config
|
||||
# specify the configs via single URL or individual variables
|
||||
# ref: https://www.iana.org/assignments/uri-schemes/prov/redis
|
||||
# You can also use the following format for the URL: redis://:password@host:port/db_number
|
||||
REDIS_URL=redis://redis:6379
|
||||
# If you are using docker-compose, set this variable's value to be any string,
|
||||
# which will be the password for the redis service running inside the docker-compose
|
||||
# to make it secure
|
||||
REDIS_PASSWORD=redis
|
||||
# Redis Sentinel can be used by passing list of sentinel host and ports e,g. sentinel_host1:port1,sentinel_host2:port2
|
||||
REDIS_SENTINELS=
|
||||
# Redis sentinel master name is required when using sentinel, default value is "mymaster".
|
||||
# You can find list of master using "SENTINEL masters" command
|
||||
REDIS_SENTINEL_MASTER_NAME=
|
||||
|
||||
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
|
||||
# Use the following environment variable to customize passwords for sentinels.
|
||||
# Use empty string if sentinels are configured with out passwords
|
||||
# REDIS_SENTINEL_PASSWORD=
|
||||
|
||||
# Redis premium breakage in heroku fix
|
||||
# enable the following configuration
|
||||
# ref: https://github.com/chatwoot/chatwoot/issues/2420
|
||||
# REDIS_OPENSSL_VERIFY_MODE=none
|
||||
|
||||
# Postgres Database config variables
|
||||
# You can leave POSTGRES_DATABASE blank. The default name of
|
||||
# the database in the production environment is chatwoot_production
|
||||
POSTGRES_DATABASE=chatwoot
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_USERNAME=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
RAILS_ENV=development
|
||||
# Changes the Postgres query timeout limit. The default is 14 seconds. Modify only when required.
|
||||
# POSTGRES_STATEMENT_TIMEOUT=14s
|
||||
RAILS_MAX_THREADS=5
|
||||
|
||||
# The email from which all outgoing emails are sent
|
||||
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
||||
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
|
||||
|
||||
#SMTP domain key is set up for HELO checking
|
||||
SMTP_DOMAIN=chatwoot.com
|
||||
# Set the value to "mailhog" if using docker-compose for development environments,
|
||||
# Set the value as "localhost" or your SMTP address in other environments
|
||||
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix)
|
||||
SMTP_ADDRESS=
|
||||
SMTP_PORT=1025
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
# plain,login,cram_md5
|
||||
SMTP_AUTHENTICATION=
|
||||
SMTP_ENABLE_STARTTLS_AUTO=true
|
||||
# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html
|
||||
SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
# Comment out the following environment variables if required by your SMTP server
|
||||
# SMTP_TLS=
|
||||
# SMTP_SSL=
|
||||
# SMTP_OPEN_TIMEOUT
|
||||
# SMTP_READ_TIMEOUT
|
||||
|
||||
# Mail Incoming
|
||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||
MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
# Set this to the appropriate ingress channel with regards to incoming emails
|
||||
# Possible values are :
|
||||
# relay for Exim, Postfix, Qmail
|
||||
# mailgun for Mailgun
|
||||
# mandrill for Mandrill
|
||||
# postmark for Postmark
|
||||
# sendgrid for Sendgrid
|
||||
RAILS_INBOUND_EMAIL_SERVICE=
|
||||
# Use one of the following based on the email ingress service
|
||||
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||
# Set this to a password of your choice and use it in the Inbound webhook
|
||||
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||
|
||||
MAILGUN_INGRESS_SIGNING_KEY=
|
||||
MANDRILL_INGRESS_API_KEY=
|
||||
|
||||
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
|
||||
# Inbound webhook URL format:
|
||||
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
|
||||
# Note: Replace the values inside the brackets; do not include the brackets themselves.
|
||||
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
|
||||
# For Postmark
|
||||
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
|
||||
|
||||
# Storage
|
||||
ACTIVE_STORAGE_SERVICE=local
|
||||
|
||||
# Amazon S3
|
||||
# documentation: https://www.chatwoot.com/docs/configuring-s3-bucket-as-cloud-storage
|
||||
S3_BUCKET_NAME=
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_REGION=
|
||||
|
||||
# Log settings
|
||||
# Disable if you want to write logs to a file
|
||||
RAILS_LOG_TO_STDOUT=true
|
||||
LOG_LEVEL=info
|
||||
LOG_SIZE=500
|
||||
# Configure this environment variable if you want to use lograge instead of rails logger
|
||||
#LOGRAGE_ENABLED=true
|
||||
|
||||
### This environment variables are only required if you are setting up social media channels
|
||||
|
||||
# Facebook
|
||||
# documentation: https://www.chatwoot.com/docs/facebook-setup
|
||||
FB_VERIFY_TOKEN=
|
||||
FB_APP_SECRET=
|
||||
FB_APP_ID=
|
||||
|
||||
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
|
||||
IG_VERIFY_TOKEN=
|
||||
|
||||
# Twitter
|
||||
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
||||
TWITTER_APP_ID=
|
||||
TWITTER_CONSUMER_KEY=
|
||||
TWITTER_CONSUMER_SECRET=
|
||||
TWITTER_ENVIRONMENT=
|
||||
|
||||
#slack integration
|
||||
SLACK_CLIENT_ID=
|
||||
SLACK_CLIENT_SECRET=
|
||||
|
||||
# Google OAuth
|
||||
GOOGLE_OAUTH_CLIENT_ID=
|
||||
GOOGLE_OAUTH_CLIENT_SECRET=
|
||||
GOOGLE_OAUTH_CALLBACK_URL=
|
||||
|
||||
### Change this env variable only if you are using a custom build mobile app
|
||||
## Mobile app env variables
|
||||
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
|
||||
ANDROID_BUNDLE_ID=com.chatwoot.app
|
||||
|
||||
# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section)
|
||||
ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:D4:5D:D4:53:F8:3B:FB:D3:C6:28:64:1D:AA:08:1E:D8
|
||||
|
||||
### Smart App Banner
|
||||
# https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
|
||||
# You can find your app-id in https://itunesconnect.apple.com
|
||||
#IOS_APP_IDENTIFIER=1495796682
|
||||
|
||||
## Push Notification
|
||||
## generate a new key value here : https://d3v.one/vapid-key-generator/
|
||||
# VAPID_PUBLIC_KEY=
|
||||
# VAPID_PRIVATE_KEY=
|
||||
#
|
||||
# for mobile apps
|
||||
# FCM_SERVER_KEY=
|
||||
|
||||
### APM and Error Monitoring configurations
|
||||
## Elastic APM
|
||||
## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html
|
||||
# ELASTIC_APM_SERVER_URL=
|
||||
# ELASTIC_APM_SECRET_TOKEN=
|
||||
|
||||
## Sentry
|
||||
# SENTRY_DSN=
|
||||
|
||||
|
||||
## Scout
|
||||
## https://scoutapm.com/docs/ruby/configuration
|
||||
# SCOUT_KEY=YOURKEY
|
||||
# SCOUT_NAME=YOURAPPNAME (Production)
|
||||
# SCOUT_MONITOR=true
|
||||
|
||||
## NewRelic
|
||||
# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/
|
||||
# NEW_RELIC_LICENSE_KEY=
|
||||
# Set this to true to allow newrelic apm to send logs.
|
||||
# This is turned off by default.
|
||||
# NEW_RELIC_APPLICATION_LOGGING_ENABLED=
|
||||
|
||||
## Datadog
|
||||
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
|
||||
# DD_TRACE_AGENT_URL=
|
||||
|
||||
# MaxMindDB API key to download GeoLite2 City database
|
||||
# IP_LOOKUP_API_KEY=
|
||||
|
||||
## Rack Attack configuration
|
||||
## To prevent and throttle abusive requests
|
||||
# ENABLE_RACK_ATTACK=true
|
||||
# RACK_ATTACK_LIMIT=300
|
||||
# ENABLE_RACK_ATTACK_WIDGET_API=true
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
## setting this value to true will disable the frontend dashboard endpoints
|
||||
# CW_API_ONLY_SERVER=false
|
||||
|
||||
## Development Only Config
|
||||
# if you want to use letter_opener for local emails
|
||||
# LETTER_OPENER=true
|
||||
# meant to be used in github codespaces
|
||||
# WEBPACKER_DEV_SERVER_PUBLIC=
|
||||
|
||||
# If you want to use official mobile app,
|
||||
# the notifications would be relayed via a Chatwoot server
|
||||
ENABLE_PUSH_RELAY_SERVER=true
|
||||
|
||||
# Stripe API key
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# Set to true if you want to upload files to cloud storage using the signed url
|
||||
# Make sure to follow https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration on the cloud storage after setting this to true.
|
||||
DIRECT_UPLOADS_ENABLED=
|
||||
|
||||
#MS OAUTH creds
|
||||
AZURE_APP_ID=
|
||||
AZURE_APP_SECRET=
|
||||
|
||||
## Advanced configurations
|
||||
## Change these values to fine tune performance
|
||||
# control the concurrency setting of sidekiq
|
||||
# SIDEKIQ_CONCURRENCY=10
|
||||
|
||||
|
||||
# AI powered features
|
||||
## OpenAI key
|
||||
# OPENAI_API_KEY=
|
||||
|
||||
# Housekeeping/Performance related configurations
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# WAHA - WhatsApp HTTP API
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/
|
||||
#
|
||||
|
||||
# ==================
|
||||
# ===== COMMON =====
|
||||
# ==================
|
||||
# Base URL for the API (used for webhooks, file URLs, etc.)
|
||||
WAHA_BASE_URL=http://waha:3000
|
||||
|
||||
# Server configuration (if you need to customize hostname/port)
|
||||
# WHATSAPP_API_SCHEMA=http
|
||||
# WHATSAPP_API_PORT=3000
|
||||
# WHATSAPP_API_HOSTNAME=localhost
|
||||
|
||||
# ====================
|
||||
# ===== SECURITY =====
|
||||
# ====================
|
||||
# WAHA Apps requires both sha and plain version
|
||||
WAHA_API_KEY_PLAIN=00000000000000000000000000000000
|
||||
WAHA_API_KEY=sha512:98b6d128682e280b74b324ca82a6bae6e8a3f7174e0605bfd52eb9948fad8984854ec08f7652f32055c4a9f12b69add4850481d9503a7f2225501671d6124648
|
||||
WAHA_DASHBOARD_USERNAME=admin
|
||||
WAHA_DASHBOARD_PASSWORD=11111111111111111111111111111111
|
||||
WHATSAPP_SWAGGER_USERNAME=admin
|
||||
WHATSAPP_SWAGGER_PASSWORD=11111111111111111111111111111111
|
||||
|
||||
WAHA_DASHBOARD_ENABLED=True
|
||||
WHATSAPP_SWAGGER_ENABLED=True
|
||||
|
||||
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
|
||||
WHATSAPP_DEFAULT_ENGINE=GOWS
|
||||
|
||||
# =================
|
||||
# ===== APPS ======
|
||||
# =================
|
||||
WAHA_APPS_ENABLED=True
|
||||
REDIS_URL=redis://:redis@redis:6379
|
||||
|
||||
# ===================
|
||||
# ===== LOGGING =====
|
||||
# ===================
|
||||
# Log format: JSON (for log management systems) or PRETTY (for development)
|
||||
WAHA_LOG_FORMAT=JSON
|
||||
|
||||
# Log level: info, debug, error, warn
|
||||
WAHA_LOG_LEVEL=info
|
||||
|
||||
# Don't print QR codes in logs
|
||||
WAHA_PRINT_QR=False
|
||||
|
||||
# =========================
|
||||
# ===== MEDIA STORAGE =====
|
||||
# =========================
|
||||
# Local storage (default)
|
||||
WAHA_MEDIA_STORAGE=LOCAL
|
||||
WHATSAPP_FILES_LIFETIME=1800
|
||||
WHATSAPP_FILES_FOLDER=/app/.media
|
||||
|
||||
# Media download settings
|
||||
# WHATSAPP_DOWNLOAD_MEDIA=true
|
||||
# WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
|
||||
# S3 storage (uncomment to use)
|
||||
# WAHA_MEDIA_STORAGE=S3
|
||||
# WAHA_S3_REGION=eu-west-2
|
||||
# WAHA_S3_BUCKET=waha
|
||||
# WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
# WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
# WAHA_S3_ENDPOINT=http://minio:9000
|
||||
# WAHA_S3_FORCE_PATH_STYLE=True
|
||||
# WAHA_S3_PROXY_FILES=True
|
||||
|
||||
# PostgreSQL storage (uncomment to use)
|
||||
# WAHA_MEDIA_STORAGE=POSTGRESQL
|
||||
# WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# ===========================
|
||||
# ===== SESSION STORAGE =====
|
||||
# ===========================
|
||||
# PostgreSQL for sessions (uncomment to use)
|
||||
WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# MongoDB for sessions (uncomment to use)
|
||||
# WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
|
||||
|
||||
|
||||
# ==================================
|
||||
# ===== ADVANCED CONFIGURATION =====
|
||||
# ==================================
|
||||
|
||||
# Timezone for screenshots and logs
|
||||
# TZ=Europe/Dublin
|
||||
|
||||
# Session management
|
||||
# WHATSAPP_START_SESSION=session1,session2
|
||||
# WHATSAPP_RESTART_ALL_SESSIONS=False
|
||||
|
||||
# Webhooks
|
||||
# WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
|
||||
# Proxy configuration
|
||||
# WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
# WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
# WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
|
||||
# HTTPS configuration
|
||||
# !DEPRECATED!
|
||||
# Setup nginx reverse proxy to handle TLS connection
|
||||
# using Let's encrypt or self-issued certificate
|
||||
# WAHA_HTTPS_ENABLED=true
|
||||
# WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
# WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem
|
||||
# WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
@@ -0,0 +1,97 @@
|
||||
services:
|
||||
waha:
|
||||
restart: always
|
||||
image: devlikeapro/waha-plus
|
||||
ports:
|
||||
- '127.0.0.1:3000:3000/tcp'
|
||||
volumes:
|
||||
- './sessions:/app/.sessions' # required for WAHA Core only
|
||||
- './media:/app/.media'
|
||||
env_file:
|
||||
- .waha.env
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- chatwoot
|
||||
- chatwoot-sidekiq
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '100m'
|
||||
max-file: '10'
|
||||
dns:
|
||||
- 1.1.1.1
|
||||
- 8.8.8.8
|
||||
|
||||
base: &base
|
||||
image: chatwoot/chatwoot:v4.3.0
|
||||
env_file: .chatwoot.env ## Change this file for customized env variables
|
||||
volumes:
|
||||
- chatwoot_storage:/app/storage
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '100m'
|
||||
max-file: '10'
|
||||
|
||||
chatwoot:
|
||||
<<: *base
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
ports:
|
||||
- '127.0.0.1:3009:3009'
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- RAILS_ENV=production
|
||||
- INSTALLATION_ENV=docker
|
||||
entrypoint: docker/entrypoints/rails.sh
|
||||
command: ['bundle', 'exec', 'rails', 's', '-p', '3009', '-b', '0.0.0.0']
|
||||
restart: always
|
||||
|
||||
chatwoot-sidekiq:
|
||||
<<: *base
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- RAILS_ENV=production
|
||||
- INSTALLATION_ENV=docker
|
||||
command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml']
|
||||
restart: always
|
||||
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
restart: always
|
||||
ports:
|
||||
- '127.0.0.1:5432:5432'
|
||||
volumes:
|
||||
- chatwoot_pg_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=chatwoot
|
||||
- POSTGRES_USER=postgres
|
||||
# Please provide your own password.
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
restart: always
|
||||
command:
|
||||
- 'sh'
|
||||
- '-c'
|
||||
- >
|
||||
redis-server --bind 0.0.0.0 --port 6379 --requirepass
|
||||
${REDIS_PASSWORD:-redis} --timeout 300 --tcp-keepalive 60 --save 900 1
|
||||
--save 300 10 --save 60 10000 --appendonly yes --appendfsync everysec
|
||||
env_file: .chatwoot.env
|
||||
volumes:
|
||||
- chatwoot_redis:/data
|
||||
ports:
|
||||
- '127.0.0.1:6379:6379'
|
||||
|
||||
volumes:
|
||||
chatwoot_pg_data: {}
|
||||
chatwoot_redis: {}
|
||||
chatwoot_storage: {}
|
||||
@@ -17,7 +17,7 @@ services:
|
||||
ports:
|
||||
- '127.0.0.1:3001:3000/tcp'
|
||||
volumes:
|
||||
- './.media:/app/.media'
|
||||
- './media:/app/.media'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@@ -42,7 +42,7 @@ services:
|
||||
ports:
|
||||
- '127.0.0.1:3002:3000/tcp'
|
||||
volumes:
|
||||
- './.media:/app/.media'
|
||||
- './media:/app/.media'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -7,10 +7,10 @@ services:
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
volumes:
|
||||
- './.sessions:/app/.sessions'
|
||||
- './.media:/app/.media'
|
||||
- './sessions:/app/.sessions'
|
||||
- './media:/app/.media'
|
||||
environment:
|
||||
- WHATSAPP_API_KEY=321
|
||||
- WAHA_API_KEY=321
|
||||
- WAHA_DASHBOARD_USERNAME=admin
|
||||
- WAHA_DASHBOARD_PASSWORD=admin
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_HOSTNAME=localhost
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
- WHATSAPP_API_KEY=321
|
||||
- WAHA_API_KEY=321
|
||||
# Username and password for Swagger
|
||||
- WHATSAPP_SWAGGER_USERNAME=swagger
|
||||
- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
@@ -55,7 +55,7 @@ services:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_KEY=321
|
||||
# - WAHA_API_KEY=321
|
||||
# - WHATSAPP_SWAGGER_USERNAME=admin
|
||||
# - WHATSAPP_SWAGGER_PASSWORD=123
|
||||
# - WAHA_DASHBOARD_USERNAME=admin
|
||||
|
||||
+79
-3
@@ -1,11 +1,87 @@
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
#
|
||||
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
|
||||
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
|
||||
uv_threadpool_size=$(($cpus * 1))
|
||||
#
|
||||
# Try to get CPU count using Node.js, fallback to 1 if it fails
|
||||
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);" 2>/dev/null) || cpus=1
|
||||
# Make sure cpus is a number, default to 1 if not
|
||||
case $cpus in
|
||||
''|*[!0-9]*) cpus=1 ;;
|
||||
esac
|
||||
uv_threadpool_size=$(($cpus * 2))
|
||||
|
||||
# Check if uv_threadpool_size is less than 4 (default), set it to 4 if it is
|
||||
if [ "$uv_threadpool_size" -lt 4 ]; then
|
||||
uv_threadpool_size=4
|
||||
fi
|
||||
|
||||
# Set UV_THREADPOOL_SIZE as an environment variable
|
||||
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
|
||||
|
||||
#
|
||||
# Handle API key hashing
|
||||
#
|
||||
# Save WHATSAPP_API_KEY or WAHA_API_KEY in a variable (WHATSAPP_API_KEY has priority)
|
||||
if [ -n "$WHATSAPP_API_KEY" ]; then
|
||||
key="$WHATSAPP_API_KEY"
|
||||
elif [ -n "$WAHA_API_KEY" ]; then
|
||||
key="$WAHA_API_KEY"
|
||||
fi
|
||||
|
||||
# Unset both environment variables
|
||||
unset WHATSAPP_API_KEY
|
||||
unset WAHA_API_KEY
|
||||
|
||||
# Process the key if it exists
|
||||
if [ -n "$key" ]; then
|
||||
# Check if key is already hashed
|
||||
if echo "$key" | grep -q "^sha512:"; then
|
||||
# If already hashed, use it as is
|
||||
export WAHA_API_KEY="$key"
|
||||
else
|
||||
# Display warning about using plain text API key
|
||||
echo "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
|
||||
echo "WARNING: Plain text API key detected. Converting to hashed format for security."
|
||||
echo "For better security, use WAHA_API_KEY=sha512:{SHA512_HASH_FOR_YOUR_API_KEY}"
|
||||
echo "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
|
||||
# Hash the key using sha512sum
|
||||
HASHED_KEY=$(echo -n "$key" | sha512sum | awk '{print $1}')
|
||||
export WAHA_API_KEY="sha512:$HASHED_KEY"
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# xvfb-run
|
||||
#
|
||||
USE_XVFB=false
|
||||
|
||||
# Check WAHA_RUN_XVFB parameter - only test for "false" case, treat all others as True
|
||||
if [ "$WAHA_RUN_XVFB" = "false" ] || [ "$WAHA_RUN_XVFB" = "False" ] || [ "$WAHA_RUN_XVFB" = "0" ]; then
|
||||
# Explicitly disabled by user
|
||||
echo "WAHA_RUN_XVFB value: $WAHA_RUN_XVFB - xvfb is disabled"
|
||||
USE_XVFB=false
|
||||
else
|
||||
# Check engine and run test if it's WEBJS or not specified
|
||||
if [ -z "$WHATSAPP_DEFAULT_ENGINE" ] || [ "$WHATSAPP_DEFAULT_ENGINE" = "WEBJS" ]; then
|
||||
# Try to run xvfb-run with a test command
|
||||
if xvfb-run --auto-servernum echo "xvfb-run is working!"; then
|
||||
USE_XVFB=true
|
||||
else
|
||||
echo "xvfb-run test failed, do not run it"
|
||||
USE_XVFB=false
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# Start your application using node with exec to ensure proper signal handling
|
||||
exec node dist/main
|
||||
#
|
||||
if [ "$USE_XVFB" = "true" ]; then
|
||||
echo "Executing node with xvfb-run..."
|
||||
exec xvfb-run --auto-servernum node dist/main
|
||||
else
|
||||
echo "Executing node without xvfb-run..."
|
||||
exec node dist/main
|
||||
fi
|
||||
Binary file not shown.
Binary file not shown.
+6
-1
@@ -3,7 +3,12 @@
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"plugins": ["@nestjs/swagger"],
|
||||
"assets": ["dashboard/**", "core/engines/webjs/*", "plus/engines/webjs/*"],
|
||||
"assets": [
|
||||
"dashboard/**",
|
||||
"core/engines/webjs/*",
|
||||
"plus/engines/webjs/*",
|
||||
"apps/chatwoot/i18n/*.yaml"
|
||||
],
|
||||
"watchAssets": true
|
||||
}
|
||||
}
|
||||
+24
-12
@@ -26,16 +26,22 @@
|
||||
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-05-01",
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-07-04",
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@aws-sdk/client-s3": "^3.633.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.633.0",
|
||||
"@bull-board/api": "^6.9.1",
|
||||
"@bull-board/express": "^6.9.1",
|
||||
"@bull-board/nestjs": "^6.9.1",
|
||||
"@figuro/chatwoot-sdk": "^1.1.17",
|
||||
"@liaoliaots/nestjs-redis": "^9",
|
||||
"@nestjs/axios": "^3.0.2",
|
||||
"@nestjs/bullmq": "^11.0.2",
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/config": "^3.2.3",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/passport": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.1.5",
|
||||
"@nestjs/platform-ws": "^11.0.0",
|
||||
"@nestjs/serve-static": "^5.0.0",
|
||||
"@nestjs/swagger": "^7.1.11",
|
||||
@@ -43,25 +49,28 @@
|
||||
"@nestjs/websockets": "^11.0.0",
|
||||
"@types/better-sqlite3": "^7.6.10",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/ws": "^8.5.4",
|
||||
"adm-zip": "0.5.10",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"async-lock": "^1.4.1",
|
||||
"audio-decode": "^2.2.2",
|
||||
"axios": "^1.7.7",
|
||||
"axios": "^1.9.0",
|
||||
"axios-retry": "^4.5.0",
|
||||
"better-sqlite3": "11.3.0",
|
||||
"bullmq": "^5.48.1",
|
||||
"check-disk-space": "^3.4.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "0.14.0",
|
||||
"del": "^6.0.0",
|
||||
"del": "^8.0.0",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"google-protobuf": "^3.21.4",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"ioredis": "^5",
|
||||
"joi": "^17.13.3",
|
||||
"knex": "^3.1.0",
|
||||
"libphonenumber-js": "^1.10.36",
|
||||
@@ -70,6 +79,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.27",
|
||||
"mongodb": "6.9.0",
|
||||
"mustache": "^4.2.0",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"node-cache": "5.1.2",
|
||||
"passport": "^0.7.0",
|
||||
@@ -79,17 +89,18 @@
|
||||
"pino-pretty": "^11.2.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"promise-retry": "^2.0.1",
|
||||
"puppeteer": "^23.6.0",
|
||||
"puppeteer": "^24.10.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rimraf": "^4.3.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.4",
|
||||
"swagger-ui-express": "^4.1.4",
|
||||
"ulid": "^2.3.0",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-channels",
|
||||
"write-file-atomic": "^6.0.0"
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09",
|
||||
"write-file-atomic": "^6.0.0",
|
||||
"yaml": "^2.7.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.8"
|
||||
@@ -97,12 +108,13 @@
|
||||
"resolutions": {
|
||||
"bufferutil": "^4.0.8",
|
||||
"ws": "^8.18.0",
|
||||
"puppeteer": "^24.7.2",
|
||||
"puppeteer": "^24.10.0",
|
||||
"whatwg-url": "13.0.0",
|
||||
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
|
||||
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
|
||||
"axios": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grpc/grpc-js": "^1.12.4",
|
||||
"@grpc/grpc-js": "^1.13.4",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
@@ -117,7 +129,7 @@
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"grpc-tools": "^1.12.4",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^1.19.1",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
|
||||
+2
-2
@@ -12,9 +12,9 @@ touch "$OUTPUT_FILE"
|
||||
# Find all .md files in the source directory and combine them
|
||||
find "$SOURCE_DIR" -type f -name "*.ts" | while read -r file; do
|
||||
# Add file name to OUTPUT_FILE
|
||||
echo -e "\n-------" >> "$OUTPUT_FILE"
|
||||
echo "\n-------" >> "$OUTPUT_FILE"
|
||||
echo "File: $file" >> "$OUTPUT_FILE"
|
||||
echo -e "-------\n" >> "$OUTPUT_FILE"
|
||||
echo "-------\n" >> "$OUTPUT_FILE"
|
||||
# Append the content of the file to the output file
|
||||
cat "$file" >> "$OUTPUT_FILE"
|
||||
done
|
||||
|
||||
@@ -36,8 +36,8 @@ class AuthController {
|
||||
summary: 'Get QR code for pairing WhatsApp API.',
|
||||
})
|
||||
@SessionApiParam
|
||||
@ApiFileAcceptHeader(Base64File, QRCodeValue)
|
||||
@UseInterceptors(new BufferResponseInterceptor())
|
||||
@ApiFileAcceptHeader('image/png', Base64File, QRCodeValue)
|
||||
@UseInterceptors(new BufferResponseInterceptor('image/png'))
|
||||
async getQR(
|
||||
@QRCodeSessionParam session: WhatsappSession,
|
||||
@Query() query: QRCodeQuery,
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Post,
|
||||
UnprocessableEntityException,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WAMimeType } from '@waha/core/media/WAMimeType';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import {
|
||||
FileDTO,
|
||||
VideoFileDTO,
|
||||
VoiceFileDTO,
|
||||
} from '@waha/structures/media.dto';
|
||||
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/media')
|
||||
@ApiTags('🖼️ Media')
|
||||
class MediaController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post('convert/voice')
|
||||
@ApiOperation({
|
||||
summary: 'Convert voice to WhatsApp format (opus)',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UseInterceptors(
|
||||
new BufferResponseInterceptor(WAMimeType.VOICE, 'output.opus'),
|
||||
)
|
||||
@ApiFileAcceptHeader(WAMimeType.VOICE)
|
||||
async convertVoice(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() file: VoiceFileDTO,
|
||||
): Promise<Buffer> {
|
||||
const data = await this.buffer(session, file);
|
||||
const content = await session.mediaConverter.voice(data);
|
||||
return content;
|
||||
}
|
||||
|
||||
@Post('convert/video')
|
||||
@ApiOperation({
|
||||
summary: 'Convert video to WhatsApp format (mp4)',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UseInterceptors(
|
||||
new BufferResponseInterceptor(WAMimeType.VIDEO, 'output.mp4'),
|
||||
)
|
||||
@ApiFileAcceptHeader(WAMimeType.VIDEO)
|
||||
async convertVideo(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() file: VideoFileDTO,
|
||||
): Promise<Buffer> {
|
||||
const data = await this.buffer(session, file);
|
||||
const content = await session.mediaConverter.video(data);
|
||||
return content;
|
||||
}
|
||||
|
||||
private async buffer(
|
||||
session: WhatsappSession,
|
||||
file: FileDTO,
|
||||
): Promise<Buffer> {
|
||||
if ('url' in file) {
|
||||
return session.fetch(file.url);
|
||||
} else if ('data' in file) {
|
||||
return Buffer.from(file.data, 'base64');
|
||||
} else {
|
||||
throw new UnprocessableEntityException(
|
||||
'Either "url" or "data" must be specified.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { MediaController };
|
||||
@@ -21,8 +21,8 @@ export class ScreenshotController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/screenshot')
|
||||
@UseInterceptors(new BufferResponseInterceptor())
|
||||
@ApiFileAcceptHeader()
|
||||
@UseInterceptors(new BufferResponseInterceptor('image/jpeg'))
|
||||
@ApiFileAcceptHeader('image/jpeg')
|
||||
async screenshot(
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@Query() sessionQuery: SessionQuery,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import * as process from 'node:process';
|
||||
import * as v8 from 'node:v8';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
Post,
|
||||
Query,
|
||||
StreamableFile,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
@@ -112,34 +109,3 @@ export class ServerController {
|
||||
return { stopping: true };
|
||||
}
|
||||
}
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/server/debug')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class ServerDebugController {
|
||||
private logger: Logger;
|
||||
private readonly enabled: boolean;
|
||||
|
||||
constructor(protected config: WhatsappConfigService) {
|
||||
this.logger = new Logger('ServerDebugController');
|
||||
this.enabled = this.config.debugModeEnabled;
|
||||
}
|
||||
|
||||
@Get('heapsnapshot')
|
||||
@ApiOperation({
|
||||
summary: 'Return a heapsnapshot',
|
||||
description: "Return a heapsnapshot of the server's memory",
|
||||
})
|
||||
async heapsnapshot() {
|
||||
if (!this.enabled) {
|
||||
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
|
||||
}
|
||||
this.logger.log('Creating a heap snapshot...');
|
||||
const heap = v8.getHeapSnapshot();
|
||||
const fileName = `${Date.now()}.heapsnapshot`;
|
||||
return new StreamableFile(heap, {
|
||||
type: 'application/octet-stream',
|
||||
disposition: `attachment; filename=${fileName}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as v8 from 'node:v8';
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
Query,
|
||||
StreamableFile,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
|
||||
import { createReadStream } from 'fs';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/server/debug')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class ServerDebugController {
|
||||
private logger: Logger;
|
||||
private readonly enabled: boolean;
|
||||
|
||||
constructor(
|
||||
private config: WhatsappConfigService,
|
||||
private manager: SessionManager,
|
||||
) {
|
||||
this.logger = new Logger('ServerDebugController');
|
||||
this.enabled = this.config.debugModeEnabled;
|
||||
}
|
||||
|
||||
@Get('heapsnapshot')
|
||||
@ApiOperation({
|
||||
summary: 'Return a heapsnapshot for the current nodejs process',
|
||||
description: "Return a heapsnapshot of the server's memory",
|
||||
})
|
||||
async heapsnapshot() {
|
||||
if (!this.enabled) {
|
||||
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
|
||||
}
|
||||
this.logger.log('Creating a heap snapshot...');
|
||||
const heap = v8.getHeapSnapshot();
|
||||
const fileName = `${Date.now()}.heapsnapshot`;
|
||||
return new StreamableFile(heap, {
|
||||
type: 'application/octet-stream',
|
||||
disposition: `attachment; filename=${fileName}`,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('browser/trace/:session')
|
||||
@ApiOperation({
|
||||
summary: 'Collect and get a trace.json for Chrome DevTools ',
|
||||
description: 'Uses https://pptr.dev/api/puppeteer.tracing',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async browserTrace(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() query: BrowserTraceQuery,
|
||||
) {
|
||||
if (!this.enabled) {
|
||||
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
|
||||
}
|
||||
const filepath = await session.browserTrace(query);
|
||||
const stream = createReadStream(filepath);
|
||||
const filename = `trace - ${session.name} - ${new Date()}.json`;
|
||||
return new StreamableFile(stream, {
|
||||
type: 'application/octet-stream',
|
||||
disposition: `attachment; filename=${filename}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -209,16 +209,7 @@ class SessionsController {
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async restart(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
await this.manager.assign(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.start(name);
|
||||
});
|
||||
|
||||
await this.manager.restart(name);
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
|
||||
import { WebsocketHeartbeatJob } from '@waha/nestjs/ws/WebsocketHeartbeatJob';
|
||||
import { WebSocket } from '@waha/nestjs/ws/ws';
|
||||
import { WAHAEvents, WAHAEventsWild } from '@waha/structures/enums.dto';
|
||||
@@ -21,6 +22,15 @@ import { IncomingMessage } from 'http';
|
||||
import * as url from 'url';
|
||||
import { Server } from 'ws';
|
||||
|
||||
export enum WebSocketCloseCode {
|
||||
NORMAL = 1000,
|
||||
GOING_AWAY = 1001,
|
||||
PROTOCOL_ERROR = 1002,
|
||||
UNSUPPORTED_DATA = 1003,
|
||||
POLICY_VIOLATION = 1008,
|
||||
INTERNAL_ERROR = 1011,
|
||||
}
|
||||
|
||||
@WebSocketGateway({
|
||||
path: '/ws',
|
||||
cors: true,
|
||||
@@ -41,7 +51,10 @@ export class WebsocketGatewayCore
|
||||
private heartbeat: WebsocketHeartbeatJob;
|
||||
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
|
||||
|
||||
constructor(private manager: SessionManager) {
|
||||
constructor(
|
||||
private manager: SessionManager,
|
||||
private auth: WebSocketAuth,
|
||||
) {
|
||||
this.logger = new Logger('WebsocketGateway');
|
||||
this.heartbeat = new WebsocketHeartbeatJob(
|
||||
this.logger,
|
||||
@@ -51,14 +64,23 @@ export class WebsocketGatewayCore
|
||||
|
||||
handleConnection(socket: WebSocket, request: IncomingMessage, ...args): any {
|
||||
// wsc - websocket client
|
||||
const id = generatePrefixedId('wsc');
|
||||
socket.id = id;
|
||||
this.logger.debug(`New client connected: ${request.url}`);
|
||||
socket.id = generatePrefixedId('wsc');
|
||||
|
||||
if (!this.auth.validateRequest(request)) {
|
||||
// Not authorized - close connection
|
||||
socket.close(WebSocketCloseCode.POLICY_VIOLATION, 'Unauthorized');
|
||||
this.logger.debug(
|
||||
`Unauthorized websocket connection attempt: ${request.url} - ${socket.id}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(`New client connected: ${request.url} - ${socket.id}`);
|
||||
const params = this.getParams(request);
|
||||
const session: string = params.session;
|
||||
const events: WAHAEvents[] = params.events;
|
||||
this.logger.debug(
|
||||
`Client connected to session: '${session}', events: ${events}, ${id}`,
|
||||
`Client connected to session: '${session}', events: ${events}, ${socket.id}`,
|
||||
);
|
||||
|
||||
const sub = this.manager
|
||||
@@ -97,26 +119,11 @@ export class WebsocketGatewayCore
|
||||
async beforeApplicationShutdown(signal?: string) {
|
||||
this.logger.log('Shutting down websocket server');
|
||||
this.heartbeat?.stop();
|
||||
|
||||
// Allow pending messages to be sent, it can be even 1ms, just to release the event loop
|
||||
await sleep(100);
|
||||
// Close clients and server
|
||||
await this.close(this.server);
|
||||
this.logger.log('Websocket server is down');
|
||||
}
|
||||
|
||||
// Cherry-pick from nestjs new version
|
||||
// https://github.com/nestjs/nest/pull/13531/files
|
||||
private async close(server: any) {
|
||||
const closeEventSignal = new Promise((resolve, reject) =>
|
||||
server.close((err) => (err ? reject(err) : resolve(undefined))),
|
||||
);
|
||||
for (const ws of server.clients) {
|
||||
ws.terminate();
|
||||
}
|
||||
await closeEventSignal;
|
||||
}
|
||||
|
||||
afterInit(server: Server) {
|
||||
this.logger.debug('Websocket server initialized');
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { JOB_DELAY, JOB_LOCK_TTL } from '@waha/apps/app_sdk/constants';
|
||||
import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { BaseWorkerHost } from '@waha/utils/bull/BaseWorkerHost';
|
||||
import { Job } from 'bullmq';
|
||||
import { DelayedError } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
/**
|
||||
* Base class for app consumers that provides common functionality
|
||||
* like mutex locking, logging setup, and error handling.
|
||||
*/
|
||||
export abstract class AppConsumer extends BaseWorkerHost {
|
||||
protected readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
appName: string,
|
||||
componentName: string,
|
||||
log: PinoLogger,
|
||||
protected readonly rmutex: RMutexService,
|
||||
) {
|
||||
super();
|
||||
this.logger = log.logger.child({
|
||||
app: appName,
|
||||
component: componentName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a job with mutex locking to ensure only one job processes a specific resource at a time.
|
||||
* @param job The job to process
|
||||
* @param mutexKey The key to use for mutex locking
|
||||
* @param processor The function that processes the job
|
||||
* @returns The result of the processor function
|
||||
*/
|
||||
protected async withMutex<T, R>(
|
||||
job: Job<T, R, any>,
|
||||
mutexKey: string,
|
||||
processor: () => Promise<R>,
|
||||
): Promise<R> {
|
||||
const mutex = this.rmutex.get(mutexKey, JOB_LOCK_TTL);
|
||||
const lock = await mutex.lock();
|
||||
|
||||
if (!lock) {
|
||||
const logger = new JobLoggerWrapper(job, this.logger);
|
||||
logger.debug(
|
||||
`Postponing job '${job.id}' for ${JOB_DELAY}ms, another job is already running the mutex.key='${mutexKey}'`,
|
||||
);
|
||||
await job.moveToDelayed(Date.now() + JOB_DELAY);
|
||||
throw new DelayedError();
|
||||
}
|
||||
|
||||
try {
|
||||
return await processor();
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
export class AxiosLogging {
|
||||
constructor(private readonly logger: ILogger) {}
|
||||
|
||||
applyTo(instance: AxiosInstance) {
|
||||
instance.interceptors.request.use(this.onRequest.bind(this));
|
||||
instance.interceptors.response.use(
|
||||
this.onResponse.bind(this),
|
||||
this.onError.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
private onRequest(config: AxiosRequestConfig) {
|
||||
const method = config.method?.toUpperCase() ?? 'GET';
|
||||
const url = config.url ?? '';
|
||||
const query = config.params
|
||||
? `?${new URLSearchParams(config.params).toString()}`
|
||||
: '';
|
||||
const body = config.data ? JSON.stringify(config.data) : '';
|
||||
|
||||
this.logger.info(`${method} ${url}${query} ${body}`);
|
||||
return config;
|
||||
}
|
||||
|
||||
private onResponse(response: AxiosResponse) {
|
||||
const { method, url } = response.config;
|
||||
const status = response.status;
|
||||
const methodStr = method?.toUpperCase() ?? 'GET';
|
||||
const responseData = JSON.stringify(response.data);
|
||||
|
||||
if (status >= 200 && status < 400) {
|
||||
this.logger.info(`${methodStr} ${status}:OK ${url}`);
|
||||
this.logger.debug(`${methodStr} ${status}:OK ${url} ${responseData}`);
|
||||
} else {
|
||||
this.logger.warn(`${methodStr} ${status} ${url} ${responseData}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private onError(error: any) {
|
||||
if (error.response) {
|
||||
const { method, url } = error.config;
|
||||
const status = error.response.status;
|
||||
const message = `${
|
||||
method?.toUpperCase() ?? 'GET'
|
||||
} ${status} ${url} ${JSON.stringify(error.response.data)}`;
|
||||
this.logger.error(message);
|
||||
} else {
|
||||
this.logger.error(`Axios error: ${error.message}`);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { RegisterQueueOptions } from '@nestjs/bullmq/dist/interfaces/register-queue-options.interface';
|
||||
import { DynamicModule } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* Registers a queue with both BullModule and BullBoardModule
|
||||
* This ensures that whenever a queue is registered, it's also added to the Bull Board
|
||||
*
|
||||
* @param options Queue options including name
|
||||
* @returns An array of dynamic modules for both BullModule and BullBoardModule
|
||||
*/
|
||||
export function RegisterAppQueue(
|
||||
options: RegisterQueueOptions,
|
||||
): DynamicModule[] {
|
||||
return [
|
||||
BullModule.registerQueue({
|
||||
...options,
|
||||
}),
|
||||
BullBoardModule.forFeature({
|
||||
name: options.name,
|
||||
adapter: BullMQAdapter,
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface ILogger {
|
||||
fatal(log: string): void;
|
||||
error(log: string): void;
|
||||
warn(log: string): void;
|
||||
info(log: string): void;
|
||||
debug(log: string): void;
|
||||
trace(log: string): void;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Job } from 'bullmq';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
import { ILogger } from './ILogger';
|
||||
|
||||
/**
|
||||
* JobLoggerWrapper is a wrapper around the BullMQ Job class that provides
|
||||
* a logger with the job ID as a child context.
|
||||
* It allows logging messages to the job's log
|
||||
* and also to the logger itself.
|
||||
*/
|
||||
export class JobLoggerWrapper implements ILogger {
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
private job: Job,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.logger = logger.child({ jobId: job.id });
|
||||
}
|
||||
|
||||
fatal(log: string): void {
|
||||
this.log(log, 'fatal');
|
||||
}
|
||||
|
||||
error(log: string): void {
|
||||
this.log(log, 'error');
|
||||
}
|
||||
|
||||
warn(log: string): void {
|
||||
this.log(log, 'warn');
|
||||
}
|
||||
|
||||
info(log: string): void {
|
||||
this.log(log, 'info');
|
||||
}
|
||||
|
||||
debug(log: string): void {
|
||||
this.log(log, 'debug');
|
||||
}
|
||||
|
||||
trace(log: string): void {
|
||||
this.log(log, 'trace');
|
||||
}
|
||||
|
||||
private log(msg: string, level: string) {
|
||||
if (!this.logger.isLevelEnabled(level)) {
|
||||
return;
|
||||
}
|
||||
this.logger[level](msg);
|
||||
const timestamp = new Date().toISOString();
|
||||
this.job
|
||||
.log(`[${timestamp}] ${level.toUpperCase()}: ${msg}`)
|
||||
.catch((err) => {
|
||||
this.logger.error('Error logging message to job', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Job } from 'bullmq';
|
||||
import { Backoffs } from 'bullmq';
|
||||
|
||||
/**
|
||||
* Calculates the delay until the next attempt for a job.
|
||||
*
|
||||
* @param job The job to calculate the next attempt delay for
|
||||
* @returns The delay in milliseconds, or null if there is no next attempt or backoff is not set
|
||||
*/
|
||||
export function NextAttemptDelayInMs(job: Job): number | null {
|
||||
const attemptsMade = job.attemptsMade + 1;
|
||||
const maxAttempts = job.opts?.attempts || 1;
|
||||
// If this is the last attempt, return null
|
||||
if (attemptsMade >= maxAttempts) {
|
||||
return null;
|
||||
}
|
||||
// Normalize the backoff options
|
||||
const backoff = Backoffs.normalize(job.opts.backoff);
|
||||
if (!backoff) {
|
||||
return null;
|
||||
}
|
||||
// Calculate the delay using Backoffs.calculate
|
||||
const delay = Backoffs.calculate(backoff, attemptsMade, new Error(), job);
|
||||
// The delay can be a Promise, a number, or undefined
|
||||
if (delay === undefined) {
|
||||
return null;
|
||||
}
|
||||
// If it's a Promise, we can't handle it synchronously, so return null
|
||||
if (delay instanceof Promise) {
|
||||
return null;
|
||||
}
|
||||
return delay;
|
||||
}
|
||||
|
||||
export function NextAttemptDelayInWholeSeconds(job: Job) {
|
||||
const delay = NextAttemptDelayInMs(job);
|
||||
if (delay == null) {
|
||||
return null;
|
||||
}
|
||||
return Math.round(delay / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a job has been retried (not on its first attempt).
|
||||
*
|
||||
* @param job The job to check
|
||||
* @returns True if the job has been retried (attemptsMade > 1), false otherwise
|
||||
*/
|
||||
export function HasBeenRetried(job: Job): boolean {
|
||||
const attemptsMade = job.attemptsMade + 1;
|
||||
return attemptsMade > 1;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
AppsService,
|
||||
IAppsService,
|
||||
} from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
|
||||
import { App } from '../dto/app.dto';
|
||||
import { ListAppsQuery } from '../dto/query.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/apps')
|
||||
@ApiTags('🧩 Apps')
|
||||
export class AppsController {
|
||||
constructor(
|
||||
@Inject(AppsService)
|
||||
private appsService: IAppsService,
|
||||
private manager: SessionManager,
|
||||
) {}
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({ summary: 'List all apps for a session' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async list(
|
||||
@Query(new WAHAValidationPipe()) query: ListAppsQuery,
|
||||
): Promise<App[]> {
|
||||
return this.appsService.list(this.manager, query.session);
|
||||
}
|
||||
|
||||
@Post('/')
|
||||
@ApiOperation({ summary: 'Create a new app' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() app: App): Promise<App> {
|
||||
return await this.appsService.create(this.manager, app);
|
||||
}
|
||||
|
||||
@Put('/:id')
|
||||
@ApiOperation({ summary: 'Update an existing app' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async update(@Param('id') id: string, @Body() app: App): Promise<void> {
|
||||
if (!app.id) {
|
||||
app.id = id;
|
||||
} else if (app.id !== id) {
|
||||
throw new NotFoundException(
|
||||
`App ID in path (${id}) does not match ID in body (${app.id})`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.appsService.update(this.manager, app);
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@ApiOperation({ summary: 'Delete an app' })
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async delete(@Param('id') id: string): Promise<void> {
|
||||
await this.appsService.delete(this.manager, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as process from 'node:process';
|
||||
|
||||
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;
|
||||
const enabled = parseBool(process.env.WAHA_DASHBOARD_ENABLED);
|
||||
if (!enabled) {
|
||||
// Generate a random uuid4 username /password to prevent access
|
||||
username = 'admin';
|
||||
password = crypto.randomUUID();
|
||||
}
|
||||
|
||||
return basicAuth({
|
||||
challenge: true,
|
||||
users: {
|
||||
[username]: password,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { DefaultJobOptions } from 'bullmq';
|
||||
|
||||
export const JOB_DELAY = 1_000;
|
||||
export const JOB_LOCK_TTL = 20_000;
|
||||
export const JOB_CONCURRENCY =
|
||||
parseInt(process.env.WAHA_APPS_JOBS_CONCURRENCY) || 50;
|
||||
|
||||
function jobRemoveOptions() {
|
||||
// in seconds
|
||||
const HOUR = 60 * 60;
|
||||
const DAY = 24 * HOUR;
|
||||
return {
|
||||
removeOnComplete: {
|
||||
age:
|
||||
parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_AGE) || 3 * DAY,
|
||||
count:
|
||||
parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_COUNT) || 1000,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_FAIL_AGE) || 31 * DAY,
|
||||
count: parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_FAIL_COUNT) || 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const JobRemoveOptions = jobRemoveOptions();
|
||||
|
||||
export const ExponentialRetriesJobOptions: DefaultJobOptions = {
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 1000,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoRetriesJobOptions: DefaultJobOptions = {
|
||||
attempts: 1,
|
||||
};
|
||||
|
||||
export function merge(...args: any[]) {
|
||||
return Object.assign({}, ...args);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
export type AllowedAppConfig = ChatWootAppConfig;
|
||||
|
||||
export enum AppName {
|
||||
chatwoot = 'chatwoot',
|
||||
}
|
||||
|
||||
export class App<T extends AllowedAppConfig = any> {
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
session: string;
|
||||
|
||||
// App name (aka type)
|
||||
@IsEnum(AppName)
|
||||
app: AppName;
|
||||
|
||||
@ValidateNested()
|
||||
@Type((options) => {
|
||||
if (options && options.object && options.object.app) {
|
||||
switch (options.object.app) {
|
||||
case AppName.chatwoot:
|
||||
return ChatWootAppConfig;
|
||||
default:
|
||||
return Object;
|
||||
}
|
||||
}
|
||||
return Object;
|
||||
})
|
||||
config: T;
|
||||
}
|
||||
|
||||
export class ChatWootAppDto extends App<ChatWootAppConfig> {
|
||||
@Type(() => ChatWootAppConfig)
|
||||
config: ChatWootAppConfig;
|
||||
}
|
||||
|
||||
export type AppDto = ChatWootAppDto;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class ListAppsQuery {
|
||||
@ApiProperty({
|
||||
example: 'default',
|
||||
required: true,
|
||||
description: 'Session name to list apps for',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
session: string;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { Knex } from 'knex';
|
||||
|
||||
function migrateSDK(knex: Knex): Promise<any> {
|
||||
const config = {
|
||||
directory: `${__dirname}/migrations`,
|
||||
tableName: 'apps_migrations',
|
||||
loadExtensions: ['.js'],
|
||||
};
|
||||
return knex.migrate.latest(config);
|
||||
}
|
||||
|
||||
function migrateApp(knex: Knex, app: string): Promise<any> {
|
||||
// one level upp
|
||||
const appsDirectory = path.join(__dirname, '..', '..', 'apps');
|
||||
const directory = path.join(appsDirectory, app, 'migrations');
|
||||
const config = {
|
||||
directory: directory,
|
||||
tableName: `app_${app}_migrations`,
|
||||
loadExtensions: ['.js'],
|
||||
};
|
||||
return knex.migrate.latest(config);
|
||||
}
|
||||
|
||||
export async function migrate(knex: Knex) {
|
||||
await migrateSDK(knex);
|
||||
await migrateApp(knex, 'chatwoot');
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
exports.up = function (knex: Knex) {
|
||||
return knex.schema
|
||||
.createTable('apps', function (table) {
|
||||
table.increments('pk');
|
||||
table.string('id', 64);
|
||||
table.string('session', 64);
|
||||
table.string('app', 64);
|
||||
table.json('config');
|
||||
})
|
||||
.table('apps', function (table) {
|
||||
table.unique(['pk']);
|
||||
table.index(['session']);
|
||||
table.index(['id']);
|
||||
table.unique(['id']);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex: Knex) {
|
||||
return knex.schema.dropTable('apps');
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
|
||||
import { IAppsService } from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { DataStore } from '@waha/core/abc/DataStore';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
import { App } from '../dto/app.dto';
|
||||
|
||||
class AppsIsDisabledError extends UnprocessableEntityException {
|
||||
constructor() {
|
||||
super(
|
||||
"Apps are disabled. Enable it by setting 'WAHA_APPS_ENABLED=True' environment variable.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AppsDisabledService implements IAppsService {
|
||||
async list(manager: SessionManager, session: string): Promise<App[]> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async create(manager: SessionManager, app: App): Promise<App> {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async update(manager: SessionManager, app: App) {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
async delete(manager: SessionManager, appId: string) {
|
||||
throw new AppsIsDisabledError();
|
||||
}
|
||||
|
||||
migrate(knex: Knex<any, any[]>): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async beforeSessionStart(session: WhatsappSession, store: DataStore) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { migrate } from '@waha/apps/app_sdk/migrations';
|
||||
import { IAppService } from '@waha/apps/app_sdk/services/IAppService';
|
||||
import { IAppsService } from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { ChatWootAppService } from '@waha/apps/chatwoot/services/ChatWootAppService';
|
||||
import { DataStore } from '@waha/core/abc/DataStore';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { Knex } from 'knex';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { App, AppName } from '../dto/app.dto';
|
||||
import { AppRepository } from '../storage/AppRepository';
|
||||
|
||||
@Injectable()
|
||||
export class AppsEnabledService implements IAppsService {
|
||||
constructor(
|
||||
protected readonly chatwootService: ChatWootAppService,
|
||||
@InjectPinoLogger('AppsService')
|
||||
protected logger: PinoLogger,
|
||||
) {}
|
||||
|
||||
async list(manager: SessionManager, session: string): Promise<App[]> {
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const apps = await repo.getAllBySession(session);
|
||||
apps.forEach((app) => {
|
||||
delete app.pk;
|
||||
});
|
||||
return apps;
|
||||
}
|
||||
|
||||
async create(manager: SessionManager, app: App): Promise<App> {
|
||||
await this.checkSessionExists(manager, app.session);
|
||||
app.id = app.id || generatePrefixedId('app');
|
||||
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
|
||||
const existingApp = await repo.getById(app.id);
|
||||
if (existingApp) {
|
||||
throw new Error(`App with ID '${app.id}' already exists.`);
|
||||
}
|
||||
|
||||
// Validate only one Chatwoot app per session
|
||||
if (app.app === AppName.chatwoot) {
|
||||
const existingApps = await repo.getAllBySession(app.session);
|
||||
const existingChatwootApp = existingApps.find(
|
||||
(existingApp) => existingApp.app === AppName.chatwoot,
|
||||
);
|
||||
|
||||
if (existingChatwootApp) {
|
||||
throw new Error(
|
||||
`Only one Chatwoot app is allowed per session. Session '${app.session}' already has a Chatwoot app with ID '${existingChatwootApp.id}'.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const service = this.getAppService(app);
|
||||
await service.beforeCreated(app);
|
||||
|
||||
const result = await repo.save(app);
|
||||
await this.restartIfRunning(manager, app.session);
|
||||
delete result.pk;
|
||||
return result;
|
||||
}
|
||||
|
||||
async update(manager: SessionManager, app: App) {
|
||||
await this.checkSessionExists(manager, app.session);
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const savedApp = await repo.getById(app.id);
|
||||
if (!savedApp) {
|
||||
throw new NotFoundException(`App '${app.id}' not found`);
|
||||
}
|
||||
if (savedApp.app != app.app) {
|
||||
throw new Error(
|
||||
`Can not change app type. Delete and create a new app. Before type: '${savedApp.app}' After type: '${app.app}'`,
|
||||
);
|
||||
}
|
||||
if (savedApp.session != app.session) {
|
||||
throw new Error(
|
||||
`Can not change session. Delete and create a new app. Before session: '${savedApp.session}' After session: '${app.session}'`,
|
||||
);
|
||||
}
|
||||
|
||||
const service = this.getAppService(app);
|
||||
await service.beforeUpdated(savedApp, app);
|
||||
|
||||
await repo.update(app.id, app);
|
||||
await this.restartIfRunning(manager, app.session);
|
||||
return;
|
||||
}
|
||||
|
||||
async delete(manager: SessionManager, appId: string) {
|
||||
const knex = manager.store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const app = await repo.getById(appId);
|
||||
if (!app) {
|
||||
throw new NotFoundException(`App '${appId}' not found`);
|
||||
}
|
||||
const service = this.getAppService(app);
|
||||
await service.beforeDeleted(app);
|
||||
await repo.delete(app.id);
|
||||
await this.restartIfRunning(manager, app.session);
|
||||
return;
|
||||
}
|
||||
|
||||
async beforeSessionStart(session: WhatsappSession, store: DataStore) {
|
||||
const knex = store.getWAHADatabase();
|
||||
const repo = new AppRepository(knex);
|
||||
const apps = await repo.getAllBySession(session.name);
|
||||
for (const app of apps) {
|
||||
const service = this.getAppService(app);
|
||||
await service.beforeSessionStart(app, session);
|
||||
}
|
||||
}
|
||||
|
||||
async migrate(knex: Knex): Promise<void> {
|
||||
await migrate(knex);
|
||||
}
|
||||
|
||||
private restartIfRunning(manager: SessionManager, session: string) {
|
||||
const isRunning = manager.isRunning(session);
|
||||
if (!isRunning) {
|
||||
return;
|
||||
}
|
||||
return manager.restart(session);
|
||||
}
|
||||
|
||||
private getAppService(app: App): IAppService {
|
||||
switch (app.app) {
|
||||
case AppName.chatwoot:
|
||||
return this.chatwootService;
|
||||
default:
|
||||
throw new Error(`App '${app.app}' not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
private async checkSessionExists(
|
||||
manager: SessionManager,
|
||||
sessionName: string,
|
||||
) {
|
||||
const session = await manager.exists(sessionName);
|
||||
if (session === null) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
|
||||
/**
|
||||
* Exact App service
|
||||
*/
|
||||
export interface IAppService {
|
||||
beforeCreated(app: App): Promise<void>;
|
||||
|
||||
beforeUpdated(savedApp: App, newApp: App): Promise<void>;
|
||||
|
||||
beforeDeleted(app: App): Promise<void>;
|
||||
|
||||
beforeSessionStart(app: App, session: WhatsappSession): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
import { DataStore } from '@waha/core/abc/DataStore';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export interface IAppsService {
|
||||
list(manager: SessionManager, session: string): Promise<App[]>;
|
||||
|
||||
create(manager: SessionManager, app: App): Promise<App>;
|
||||
|
||||
update(manager: SessionManager, app: App): Promise<void>;
|
||||
|
||||
delete(manager: SessionManager, appId: string): Promise<void>;
|
||||
|
||||
beforeSessionStart(session: WhatsappSession, store: DataStore): Promise<void>;
|
||||
|
||||
migrate(knex: Knex): Promise<void>;
|
||||
}
|
||||
|
||||
export const AppsService = Symbol('AppsService');
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
import { App } from '../dto/app.dto';
|
||||
import { AppDB } from './types';
|
||||
|
||||
export class AppRepository {
|
||||
static tableName = 'apps';
|
||||
|
||||
constructor(private readonly knex: Knex) {}
|
||||
|
||||
get tableName() {
|
||||
return AppRepository.tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an app to the database
|
||||
*/
|
||||
async save(app: Omit<App, 'pk'>): Promise<AppDB> {
|
||||
const appToSave = { ...app };
|
||||
if (appToSave.config && typeof appToSave.config === 'object') {
|
||||
appToSave.config = JSON.stringify(appToSave.config);
|
||||
}
|
||||
const [pk] = await this.knex(this.tableName)
|
||||
.insert(appToSave)
|
||||
.returning('pk');
|
||||
return { ...app, pk: pk };
|
||||
}
|
||||
|
||||
private deserialize(app: AppDB): AppDB {
|
||||
const parsedConfig =
|
||||
typeof app.config === 'string' ? JSON.parse(app.config) : app.config;
|
||||
return { ...app, config: parsedConfig };
|
||||
}
|
||||
|
||||
private serialize<T extends Partial<App>>(app: T): T {
|
||||
const appCopy = { ...app };
|
||||
if (appCopy.config && typeof appCopy.config === 'object') {
|
||||
appCopy.config = JSON.stringify(appCopy.config);
|
||||
}
|
||||
return appCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an app by its id
|
||||
*/
|
||||
async getById(id: string): Promise<AppDB | null> {
|
||||
const app = await this.knex(this.tableName).where('id', id).first();
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
return this.deserialize(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all apps
|
||||
*/
|
||||
async getAllBySession(session: string): Promise<AppDB[]> {
|
||||
return this.knex(this.tableName)
|
||||
.select('*')
|
||||
.where('session', session)
|
||||
.orderBy('id', 'asc')
|
||||
.then((apps) => apps.map((app) => this.deserialize(app)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an app
|
||||
*/
|
||||
async update(id: string, app: Partial<Omit<App, 'id'>>): Promise<void> {
|
||||
const appToUpdate = this.serialize(app);
|
||||
await this.knex(this.tableName).where('id', id).update(appToUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an app
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.knex(this.tableName).where('id', id).delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { App } from '@waha/apps/app_sdk/dto/app.dto';
|
||||
|
||||
export interface AppDB extends App {
|
||||
// Internal database number
|
||||
pk: number;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import * as process from 'node:process';
|
||||
|
||||
import { ExpressAdapter } from '@bull-board/express';
|
||||
import { BullBoardModule } from '@bull-board/nestjs';
|
||||
import { RedisModule } from '@liaoliaots/nestjs-redis';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { AppsController } from '@waha/apps/app_sdk/api/apps.controller';
|
||||
import { BullAuthMiddleware } from '@waha/apps/app_sdk/auth';
|
||||
import { AppsDisabledService } from '@waha/apps/app_sdk/services/AppsDisabledService';
|
||||
import { AppsEnabledService } from '@waha/apps/app_sdk/services/AppsEnabledService';
|
||||
import { AppsService } from '@waha/apps/app_sdk/services/IAppsService';
|
||||
import { ChatWootExports } from '@waha/apps/chatwoot/chatwoot.module';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import { RMutexModule } from '@waha/modules/rmutex';
|
||||
|
||||
const IMPORTS = [
|
||||
BullModule.forRoot({
|
||||
connection: {
|
||||
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
|
||||
maxRetriesPerRequest: null,
|
||||
},
|
||||
prefix: `waha-${process.env.WAHA_WORKER_ID}`,
|
||||
}),
|
||||
RedisModule.forRoot({
|
||||
closeClient: true,
|
||||
config: {
|
||||
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
|
||||
onClientCreated: async (client) => {
|
||||
try {
|
||||
await client.ping();
|
||||
} catch (err) {
|
||||
console.error('[Redis] Connection failed:', err);
|
||||
process.exit(1); // Stop the app if Redis is unavailable
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
RMutexModule,
|
||||
BullBoardModule.forRoot({
|
||||
route: '/jobs',
|
||||
adapter: ExpressAdapter,
|
||||
middleware: BullAuthMiddleware(),
|
||||
boardOptions: {
|
||||
uiConfig: {
|
||||
boardTitle: 'Jobs | WAHA',
|
||||
boardLogo: {
|
||||
path: '/dashboard/layout/images/logo-white.svg',
|
||||
width: 35,
|
||||
height: 35,
|
||||
},
|
||||
favIcon: {
|
||||
default: '/dashboard/favicon.ico',
|
||||
alternative: '/dashboard/favicon.ico',
|
||||
},
|
||||
miscLinks: [
|
||||
{
|
||||
text: '📊 Dashboard',
|
||||
url: '/dashboard',
|
||||
},
|
||||
{
|
||||
text: '📚 Swagger (OpenAPI)',
|
||||
url: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
...ChatWootExports.imports,
|
||||
];
|
||||
|
||||
const AppsEnabled = {
|
||||
imports: IMPORTS,
|
||||
controllers: [AppsController, ...ChatWootExports.controllers],
|
||||
providers: [
|
||||
{
|
||||
provide: AppsService,
|
||||
useClass: AppsEnabledService,
|
||||
},
|
||||
...ChatWootExports.providers,
|
||||
],
|
||||
};
|
||||
|
||||
const AppsDisabled = {
|
||||
providers: [
|
||||
{
|
||||
provide: AppsService,
|
||||
useClass: AppsDisabledService,
|
||||
},
|
||||
],
|
||||
imports: [],
|
||||
controllers: [AppsController],
|
||||
};
|
||||
|
||||
function checkApiKey() {
|
||||
const key = process.env.WHATSAPP_API_KEY || process.env.WAHA_API_KEY;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const plain = process.env.WAHA_API_KEY_PLAIN;
|
||||
if (!plain) {
|
||||
throw Error(
|
||||
'WAHA_API_KEY set, please provide WAHA_API_KEY_PLAIN when WAHA_APPS_ENABLED',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enabled = parseBool(process.env.WAHA_APPS_ENABLED);
|
||||
if (enabled) {
|
||||
checkApiKey();
|
||||
}
|
||||
export const AppsModuleExports = enabled ? AppsEnabled : AppsDisabled;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { WAHASessionStatus } from '@waha/structures/enums.dto';
|
||||
|
||||
export function SessionStatusEmoji(status: WAHASessionStatus): string {
|
||||
switch (status) {
|
||||
case WAHASessionStatus.STOPPED:
|
||||
return '⚠️';
|
||||
case WAHASessionStatus.STARTING:
|
||||
return '⏳';
|
||||
case WAHASessionStatus.SCAN_QR_CODE:
|
||||
return '⚠️';
|
||||
case WAHASessionStatus.WORKING:
|
||||
return '🟢';
|
||||
case WAHASessionStatus.FAILED:
|
||||
return '🛑';
|
||||
default:
|
||||
return '❓';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { LOCALE_NAMES } from '@waha/apps/chatwoot/locale';
|
||||
|
||||
interface LanguageResponse {
|
||||
name: string;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
@Controller('api/apps/chatwoot')
|
||||
@ApiSecurity('api_key')
|
||||
@ApiTags('🧩 Apps')
|
||||
export class ChatwootLocalesController {
|
||||
@Get('locales')
|
||||
@ApiOperation({
|
||||
summary: 'Get available languages for Chatwoot app',
|
||||
description: 'Get available languages for Chatwoot app',
|
||||
})
|
||||
getLanguages(): LanguageResponse[] {
|
||||
return Object.entries(LOCALE_NAMES).map(([locale, name]) => ({
|
||||
name: name,
|
||||
locale: locale,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Body, Controller, Param, Post } from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { GetChatID } from '@waha/apps/chatwoot/client/ids';
|
||||
import { EventName, MessageType } from '@waha/apps/chatwoot/client/types';
|
||||
import { INBOX_CONTACT_CHAT_ID } from '@waha/apps/chatwoot/const';
|
||||
import { InboxData } from '@waha/apps/chatwoot/consumers/types';
|
||||
import { ChatWootQueueService } from '@waha/apps/chatwoot/services/ChatWootQueueService';
|
||||
|
||||
@Controller('webhooks/chatwoot/')
|
||||
@ApiTags('🧩 Apps')
|
||||
export class ChatwootWebhookController {
|
||||
constructor(private readonly chatWootQueueService: ChatWootQueueService) {}
|
||||
|
||||
@Post(':session/:id')
|
||||
@ApiOperation({
|
||||
summary: 'Chatwoot Webhook',
|
||||
description: 'Chatwoot Webhook',
|
||||
})
|
||||
async webhook(
|
||||
@Param('session') session: string,
|
||||
@Param('id') id: string,
|
||||
@Body() body: any,
|
||||
) {
|
||||
if (!body || !body?.event) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Ignore all private notes
|
||||
if (body.private) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Ignore all incoming messages
|
||||
if (body.message_type == MessageType.INCOMING) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
const data: InboxData = {
|
||||
session: session,
|
||||
app: id,
|
||||
body: body,
|
||||
};
|
||||
|
||||
// Check if it's a command message (sent to the special inbox contact)
|
||||
const sender = body?.conversation?.meta?.sender;
|
||||
const chatId = GetChatID(sender);
|
||||
const isCommandsChat = chatId === INBOX_CONTACT_CHAT_ID;
|
||||
|
||||
// Check if it's a deleted message
|
||||
if (body.content_attributes?.deleted && !isCommandsChat) {
|
||||
await this.chatWootQueueService.addMessageDeletedJob(data);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// Route to specific queues based on an event type
|
||||
switch (body.event) {
|
||||
case EventName.MESSAGE_CREATED:
|
||||
if (!isCommandsChat) {
|
||||
await this.chatWootQueueService.addMessageCreatedJob(data);
|
||||
} else {
|
||||
await this.chatWootQueueService.addCommandsJob(body.event, data);
|
||||
}
|
||||
return { success: true };
|
||||
case EventName.MESSAGE_UPDATED:
|
||||
if (!isCommandsChat) {
|
||||
await this.chatWootQueueService.addMessageUpdatedJob(data);
|
||||
} else {
|
||||
await this.chatWootQueueService.addCommandsJob(body.event, data);
|
||||
}
|
||||
return { success: true };
|
||||
default:
|
||||
// Ignore other events
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
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';
|
||||
|
||||
const cache: NodeCache = new NodeCache({
|
||||
stdTTL: 24 * 60 * 60, // 1 day
|
||||
useClones: false,
|
||||
});
|
||||
|
||||
export function CacheForConfig(config: ChatWootAPIConfig): ConversationCache {
|
||||
return new ConversationCache(`${config.url}+${config.inboxId}`);
|
||||
}
|
||||
|
||||
class ConversationCache implements IConversationCache {
|
||||
constructor(private prefix: string) {}
|
||||
|
||||
fullKey(key: string) {
|
||||
return `${this.prefix}.${key}`;
|
||||
}
|
||||
|
||||
delete(key: string): void {
|
||||
const fullKey = this.fullKey(key);
|
||||
cache.del(fullKey);
|
||||
}
|
||||
|
||||
get(key: string): public_conversation | null {
|
||||
const fullKey = this.fullKey(key);
|
||||
return cache.get(fullKey) || null;
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
const fullKey = this.fullKey(key);
|
||||
return cache.has(fullKey);
|
||||
}
|
||||
|
||||
set(key: string, value: public_conversation): void {
|
||||
const fullKey = this.fullKey(key);
|
||||
cache.set(fullKey, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely clean the cache with the prefix
|
||||
*/
|
||||
clean() {
|
||||
cache.keys().forEach((key) => {
|
||||
if (key.startsWith(this.prefix)) {
|
||||
cache.del(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { public_conversation } from '@figuro/chatwoot-sdk';
|
||||
|
||||
export interface IConversationCache {
|
||||
get(key: string): public_conversation | null;
|
||||
set(key: string, value: public_conversation): void;
|
||||
has(key: string): boolean;
|
||||
delete(key: string): void;
|
||||
clean(): void;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RegisterAppQueue } from '@waha/apps/app_sdk/BullUtils';
|
||||
import {
|
||||
ExponentialRetriesJobOptions,
|
||||
JobRemoveOptions,
|
||||
merge,
|
||||
NoRetriesJobOptions,
|
||||
} from '@waha/apps/app_sdk/constants';
|
||||
import { MessageCleanupConsumer } from '@waha/apps/chatwoot/consumers/scheduled/message.cleanup';
|
||||
import { ChatWootAppService } from '@waha/apps/chatwoot/services/ChatWootAppService';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
import { ChatwootLocalesController } from './api/chatwoot.locales.controller';
|
||||
import { ChatwootWebhookController } from './api/chatwoot.webhook.controller';
|
||||
import { ChatWootInboxCommandsConsumer } from './consumers/inbox/commands';
|
||||
import { ChatWootInboxMessageCreatedConsumer } from './consumers/inbox/message_created';
|
||||
import { ChatWootInboxMessageDeletedConsumer } from './consumers/inbox/message_deleted';
|
||||
import { ChatWootInboxMessageUpdatedConsumer } from './consumers/inbox/message_updated';
|
||||
import { QueueName } from './consumers/QueueName';
|
||||
import { CheckVersionConsumer } from './consumers/scheduled/check.version';
|
||||
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 { WAHASessionStatusConsumer } from './consumers/waha/session.status';
|
||||
import { ChatWootQueueService } from './services/ChatWootQueueService';
|
||||
import { ChatWootScheduleService } from './services/ChatWootScheduleService';
|
||||
import { ChatWootWAHAQueueService } from './services/ChatWootWAHAQueueService';
|
||||
|
||||
const CONTROLLERS = [ChatwootWebhookController, ChatwootLocalesController];
|
||||
|
||||
const IMPORTS = lodash.flatten([
|
||||
RegisterAppQueue({
|
||||
name: QueueName.SCHEDULED_MESSAGE_CLEANUP,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.SCHEDULED_CHECK_VERSION,
|
||||
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.WAHA_MESSAGE_ANY,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.WAHA_MESSAGE_REACTION,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.WAHA_MESSAGE_EDITED,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.WAHA_MESSAGE_REVOKED,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.WAHA_SESSION_STATUS,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.INBOX_MESSAGE_CREATED,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.INBOX_MESSAGE_UPDATED,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.INBOX_MESSAGE_DELETED,
|
||||
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
RegisterAppQueue({
|
||||
name: QueueName.INBOX_COMMANDS,
|
||||
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
|
||||
}),
|
||||
]);
|
||||
|
||||
const PROVIDERS = [
|
||||
ChatWootInboxMessageCreatedConsumer,
|
||||
ChatWootInboxMessageUpdatedConsumer,
|
||||
ChatWootInboxMessageDeletedConsumer,
|
||||
ChatWootInboxCommandsConsumer,
|
||||
WAHASessionStatusConsumer,
|
||||
WAHAMessageAnyConsumer,
|
||||
WAHAMessageReactionConsumer,
|
||||
WAHAMessageEditedConsumer,
|
||||
WAHAMessageRevokedConsumer,
|
||||
MessageCleanupConsumer,
|
||||
CheckVersionConsumer,
|
||||
ChatWootWAHAQueueService,
|
||||
ChatWootQueueService,
|
||||
ChatWootScheduleService,
|
||||
ChatWootAppService,
|
||||
];
|
||||
|
||||
export const ChatWootExports = {
|
||||
providers: PROVIDERS,
|
||||
imports: IMPORTS,
|
||||
controllers: CONTROLLERS,
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
import ChatwootClient, {
|
||||
contact_update,
|
||||
public_contact_create_update_payload,
|
||||
} from '@figuro/chatwoot-sdk';
|
||||
import type { contact } from '@figuro/chatwoot-sdk/dist/models/contact';
|
||||
import type { generic_id } from '@figuro/chatwoot-sdk/dist/models/generic_id';
|
||||
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||
import {
|
||||
ChatWootAPIConfig,
|
||||
ChatWootInboxAPI,
|
||||
} from '@waha/apps/chatwoot/client/interfaces';
|
||||
import { isJidCusFormat } from '@waha/utils/wa';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
import { AttributeKey } from '../const';
|
||||
|
||||
export interface ContactResponse {
|
||||
data: generic_id & contact;
|
||||
sourceId: string;
|
||||
}
|
||||
|
||||
export class ContactAPI {
|
||||
constructor(
|
||||
private config: ChatWootAPIConfig,
|
||||
private accountAPI: ChatwootClient,
|
||||
protected inboxAPI: ChatWootInboxAPI,
|
||||
private logger: ILogger,
|
||||
) {}
|
||||
|
||||
async searchByAnyID(chatId: string): Promise<ContactResponse | null> {
|
||||
const payload: any[] = [
|
||||
{
|
||||
attribute_key: AttributeKey.WA_CHAT_ID,
|
||||
filter_operator: 'equal_to',
|
||||
values: [chatId],
|
||||
attribute_model: 'standard',
|
||||
custom_attribute_type: '',
|
||||
query_operator: 'OR',
|
||||
},
|
||||
{
|
||||
attribute_key: AttributeKey.WA_JID,
|
||||
filter_operator: 'equal_to',
|
||||
values: [chatId],
|
||||
attribute_model: 'standard',
|
||||
custom_attribute_type: '',
|
||||
query_operator: 'OR',
|
||||
},
|
||||
{
|
||||
attribute_key: AttributeKey.WA_LID,
|
||||
filter_operator: 'equal_to',
|
||||
values: [chatId],
|
||||
attribute_model: 'standard',
|
||||
custom_attribute_type: '',
|
||||
query_operator: 'OR',
|
||||
},
|
||||
{
|
||||
attribute_key: 'identifier',
|
||||
filter_operator: 'equal_to',
|
||||
values: [chatId],
|
||||
attribute_model: 'standard',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
|
||||
if (isJidCusFormat(chatId)) {
|
||||
// Search by phone
|
||||
let phone_number = chatId.split('@')[0];
|
||||
phone_number = phone_number.replace('+', '');
|
||||
payload[payload.length - 1].query_operator = 'OR';
|
||||
payload.push({
|
||||
attribute_key: 'phone_number',
|
||||
filter_operator: 'equal_to',
|
||||
values: [phone_number],
|
||||
});
|
||||
}
|
||||
|
||||
const response: any = await this.accountAPI.contacts.filter({
|
||||
accountId: this.config.accountId,
|
||||
payload: payload as any,
|
||||
});
|
||||
|
||||
const contacts = response.payload;
|
||||
if (contacts.length == 0) {
|
||||
return null;
|
||||
}
|
||||
const contact = contacts[0];
|
||||
const inboxes = lodash.filter(contact.contact_inboxes, {
|
||||
inbox: { id: this.config.inboxId },
|
||||
});
|
||||
if (inboxes.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
data: contact,
|
||||
sourceId: inboxes[0].source_id,
|
||||
};
|
||||
}
|
||||
|
||||
public updateCustomAttributes(
|
||||
contact: generic_id & contact,
|
||||
attributes: any,
|
||||
) {
|
||||
const update: contact_update = {
|
||||
custom_attributes: { ...contact.custom_attributes, ...attributes },
|
||||
};
|
||||
return this.accountAPI.contacts.update({
|
||||
id: contact.id,
|
||||
accountId: this.config.accountId,
|
||||
data: update,
|
||||
});
|
||||
}
|
||||
|
||||
public async create(
|
||||
chatId: string,
|
||||
payload: public_contact_create_update_payload,
|
||||
): Promise<ContactResponse> {
|
||||
const contact = await this.inboxAPI.contacts.create({
|
||||
inboxIdentifier: this.config.inboxIdentifier,
|
||||
data: payload,
|
||||
});
|
||||
this.logger.info(
|
||||
`Created contact for chat.id: ${chatId}, contact.id: ${contact.source_id}`,
|
||||
);
|
||||
const response: any = await this.accountAPI.contacts.get({
|
||||
accountId: this.config.accountId,
|
||||
id: contact.id,
|
||||
});
|
||||
return {
|
||||
data: response.payload,
|
||||
sourceId: contact.source_id,
|
||||
};
|
||||
}
|
||||
|
||||
public updateAvatarUrlSafe(contactId, avatarUrl: string) {
|
||||
this.accountAPI.contacts
|
||||
.update({
|
||||
accountId: this.config.accountId,
|
||||
id: contactId,
|
||||
data: {
|
||||
avatar_url: avatarUrl,
|
||||
},
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.warn(
|
||||
'Error updating avatar_url for contact.id: ' + contactId,
|
||||
);
|
||||
this.logger.warn(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import ChatwootClient, {
|
||||
ApiError,
|
||||
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 { Conversation } from '@waha/apps/chatwoot/client/Conversation';
|
||||
import { ConversationAPI } from '@waha/apps/chatwoot/client/ConversationAPI';
|
||||
import { ChatWootAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
|
||||
import { InboxContactInfo } from '@waha/apps/chatwoot/contacts/InboxContactInfo';
|
||||
import { Locale } from '@waha/apps/chatwoot/locale';
|
||||
|
||||
import { CacheForConfig } from '../cache/ConversationCache';
|
||||
import { IConversationCache } from '../cache/IConversationCache';
|
||||
|
||||
export interface ContactInfo {
|
||||
ChatId(): string;
|
||||
|
||||
AvatarUrl(): Promise<string | null>;
|
||||
|
||||
Attributes(): Promise<any>;
|
||||
|
||||
PublicContactCreate(): Promise<public_contact_create_update_payload>;
|
||||
}
|
||||
|
||||
export class ContactConversationService {
|
||||
private cache: IConversationCache;
|
||||
|
||||
constructor(
|
||||
private config: ChatWootAPIConfig,
|
||||
private contactAPI: ContactAPI,
|
||||
private conversationAPI: ConversationAPI,
|
||||
private accountAPI: ChatwootClient,
|
||||
private logger: ILogger,
|
||||
private l: Locale,
|
||||
) {
|
||||
this.cache = CacheForConfig(config);
|
||||
}
|
||||
|
||||
private async upsertByContactInfo(
|
||||
contactInfo: ContactInfo,
|
||||
): Promise<public_conversation> {
|
||||
const chatId = contactInfo.ChatId();
|
||||
|
||||
// Check cache for chat id
|
||||
if (this.cache.has(chatId)) {
|
||||
return this.cache.get(chatId);
|
||||
}
|
||||
|
||||
//
|
||||
// Find or create contact
|
||||
//
|
||||
let contact = await this.contactAPI.searchByAnyID(chatId);
|
||||
if (!contact) {
|
||||
const request = await contactInfo.PublicContactCreate();
|
||||
contact = await this.contactAPI.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}`,
|
||||
);
|
||||
await this.contactAPI.updateCustomAttributes(contact.data, attributes);
|
||||
|
||||
// Update Avatar if nothing, but keep the original one if any
|
||||
if (!contact.data.thumbnail) {
|
||||
const avatarUrl = await contactInfo.AvatarUrl();
|
||||
if (avatarUrl) {
|
||||
this.contactAPI.updateAvatarUrlSafe(contact.data.id, avatarUrl);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`Using contact for chat.id: ${chatId}, contact.id: ${contact.sourceId}`,
|
||||
);
|
||||
|
||||
//
|
||||
// Get or create a conversation for this inbox
|
||||
//
|
||||
const conversation = await this.conversationAPI.upsert(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;
|
||||
}
|
||||
|
||||
public async ConversationByContact(
|
||||
contactInfo: ContactInfo,
|
||||
): Promise<Conversation> {
|
||||
const chatId = contactInfo.ChatId();
|
||||
const publicConversation = await this.upsertByContactInfo(contactInfo);
|
||||
const conversation = new Conversation(
|
||||
this.accountAPI,
|
||||
this.config.accountId,
|
||||
publicConversation.id,
|
||||
);
|
||||
conversation.onError = (err) => {
|
||||
if (err instanceof ApiError) {
|
||||
// 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}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
return conversation;
|
||||
}
|
||||
|
||||
public ConversationById(conversationId: number): Conversation {
|
||||
return new Conversation(
|
||||
this.accountAPI,
|
||||
this.config.accountId,
|
||||
conversationId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build specific contact for inbox notifications
|
||||
* @constructor
|
||||
*/
|
||||
public async InboxNotifications() {
|
||||
return this.ConversationByContact(new InboxContactInfo(this.l));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import ChatwootClient from '@figuro/chatwoot-sdk';
|
||||
import type { conversation_message_create } from '@figuro/chatwoot-sdk/dist/models/conversation_message_create';
|
||||
import { MessageType } from '@waha/apps/chatwoot/client/types';
|
||||
|
||||
export class Conversation {
|
||||
public onError: (e: any) => void;
|
||||
|
||||
constructor(
|
||||
private accountAPI: ChatwootClient,
|
||||
private accountId: number,
|
||||
public conversationId: number,
|
||||
) {}
|
||||
|
||||
public async send(data: conversation_message_create) {
|
||||
try {
|
||||
const message = await this.accountAPI.messages.create({
|
||||
accountId: this.accountId,
|
||||
conversationId: this.conversationId,
|
||||
data: data,
|
||||
});
|
||||
return message;
|
||||
} catch (err) {
|
||||
if (this.onError) {
|
||||
this.onError(err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async incoming(text: string) {
|
||||
const data: conversation_message_create = {
|
||||
content: text,
|
||||
message_type: MessageType.INCOMING as any,
|
||||
};
|
||||
return this.send(data);
|
||||
}
|
||||
|
||||
public async activity(text: string) {
|
||||
const data: conversation_message_create = {
|
||||
content: text,
|
||||
message_type: MessageType.ACTIVITY as any,
|
||||
};
|
||||
return this.send(data);
|
||||
}
|
||||
|
||||
public async outgoing(text: string) {
|
||||
const data: conversation_message_create = {
|
||||
content: text,
|
||||
message_type: MessageType.OUTGOING as any,
|
||||
};
|
||||
return this.send(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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,53 @@
|
||||
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,34 @@
|
||||
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { AttributeKey } from '@waha/apps/chatwoot/const';
|
||||
import { WhatsAppMessage } from '@waha/apps/chatwoot/storage';
|
||||
import { buildMessageId } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
|
||||
export function GetJID(contact: any): string | null {
|
||||
return contact?.custom_attributes?.[AttributeKey.WA_JID];
|
||||
}
|
||||
|
||||
export function GetLID(contact: any): string | null {
|
||||
return contact?.custom_attributes?.[AttributeKey.WA_LID];
|
||||
}
|
||||
|
||||
export function GetChatID(contact: any): string | null {
|
||||
return contact?.custom_attributes?.[AttributeKey.WA_CHAT_ID];
|
||||
}
|
||||
|
||||
export function SerializeWhatsAppKey(message: WhatsAppMessage): string {
|
||||
const key = {
|
||||
id: message.message_id,
|
||||
remoteJid: message.chat_id,
|
||||
fromMe: Boolean(message.from_me),
|
||||
participant: message.participant,
|
||||
};
|
||||
return buildMessageId(key);
|
||||
}
|
||||
|
||||
export function ContactAttr(chatId: string): AttributeKey {
|
||||
if (isLidUser(chatId)) {
|
||||
return AttributeKey.WA_LID;
|
||||
} else {
|
||||
return AttributeKey.WA_JID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Original '@figuro/chatwoot-sdk'
|
||||
// Doesn't have the right types for client.client
|
||||
// So we defined it here
|
||||
import { ContactsApi } from '@figuro/chatwoot-sdk/dist/services/client/Contacts';
|
||||
import { ConversationsApi } from '@figuro/chatwoot-sdk/dist/services/client/Conversations';
|
||||
import { MessagesApi } from '@figuro/chatwoot-sdk/dist/services/client/Messages';
|
||||
|
||||
export interface ChatWootInboxAPI {
|
||||
contacts: ContactsApi;
|
||||
conversations: ConversationsApi;
|
||||
messages: MessagesApi;
|
||||
}
|
||||
|
||||
export interface ChatWootAccountAPIConfig {
|
||||
url: string;
|
||||
accountId: number;
|
||||
accountToken: string;
|
||||
}
|
||||
|
||||
export interface ChatWootInboxAPIConfig {
|
||||
url: string;
|
||||
inboxId: number;
|
||||
inboxIdentifier: string;
|
||||
}
|
||||
|
||||
export interface ChatWootAPIConfig
|
||||
extends ChatWootAccountAPIConfig,
|
||||
ChatWootInboxAPIConfig {}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { conversation_message_create } from '@figuro/chatwoot-sdk/dist/models/conversation_message_create';
|
||||
import { SendAttachment } from '@waha/apps/chatwoot/client/types';
|
||||
|
||||
export function AttachmentFromBuffer(
|
||||
buffer: Buffer,
|
||||
filename: string,
|
||||
): conversation_message_create {
|
||||
const content = buffer.toString('base64');
|
||||
const attachments: SendAttachment[] = [
|
||||
{
|
||||
content: content,
|
||||
filename: filename,
|
||||
encoding: 'base64',
|
||||
},
|
||||
];
|
||||
const message: conversation_message_create = {
|
||||
content: '',
|
||||
attachments: attachments as any,
|
||||
};
|
||||
return message;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
export enum EventName {
|
||||
CONVERSATION_CREATED = 'conversation_created',
|
||||
CONVERSATION_STATUS_CHANGED = 'conversation_status_changed',
|
||||
CONVERSATION_UPDATED = 'conversation_updated',
|
||||
CONTACT_CREATED = 'contact_created',
|
||||
CONTACT_UPDATED = 'contact_updated',
|
||||
MESSAGE_CREATED = 'message_created',
|
||||
MESSAGE_UPDATED = 'message_updated',
|
||||
WEBWIDGET_TRIGGERED = 'webwidget_triggered',
|
||||
CONVERSATION_TYPING_ON = 'conversation_typing_on',
|
||||
CONVERSATION_TYPING_OFF = 'conversation_typing_off',
|
||||
}
|
||||
|
||||
export interface MessageAttachment {
|
||||
id: number;
|
||||
account_id: number;
|
||||
message_id: number;
|
||||
file_type: 'image' | 'video' | 'audio' | string;
|
||||
file_size: number;
|
||||
data_url?: string;
|
||||
thumb_url?: string;
|
||||
extension?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export interface SendAttachment {
|
||||
content: string;
|
||||
filename?: string;
|
||||
encoding: 'base64';
|
||||
}
|
||||
|
||||
export enum MessageType {
|
||||
OUTGOING = 'outgoing',
|
||||
INCOMING = 'incoming',
|
||||
ACTIVITY = 'activity',
|
||||
}
|
||||
|
||||
export enum CustomAttributeType {
|
||||
TEXT = 0,
|
||||
NUMBER = 1,
|
||||
CURRENCY = 2,
|
||||
PERCENT = 3,
|
||||
LINK = 4,
|
||||
DATE = 5,
|
||||
LIST = 6,
|
||||
CHECKBOX = 7,
|
||||
}
|
||||
|
||||
export enum CustomAttributeModel {
|
||||
CONVERSATION = 0,
|
||||
CONTACT = 1,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { CustomAttribute } from '@waha/apps/chatwoot/client/CustomAttributesAPI';
|
||||
|
||||
import { CustomAttributeModel, CustomAttributeType } from './client/types';
|
||||
|
||||
export enum AttributeKey {
|
||||
WA_CHAT_ID = 'waha_whatsapp_chat_id',
|
||||
WA_JID = 'waha_whatsapp_jid',
|
||||
WA_LID = 'waha_whatsapp_lid',
|
||||
}
|
||||
|
||||
export const CHATWOOT_CUSTOM_ATTRIBUTES: CustomAttribute[] = [
|
||||
{
|
||||
key: AttributeKey.WA_CHAT_ID,
|
||||
name: 'WhatsApp Chat ID',
|
||||
description: 'WhatsApp Chat ID',
|
||||
type: CustomAttributeType.TEXT,
|
||||
model: CustomAttributeModel.CONTACT,
|
||||
},
|
||||
{
|
||||
key: AttributeKey.WA_JID,
|
||||
name: 'WhatsApp JID',
|
||||
description: 'WhatsApp Phone Number ID',
|
||||
type: CustomAttributeType.TEXT,
|
||||
model: CustomAttributeModel.CONTACT,
|
||||
},
|
||||
{
|
||||
key: AttributeKey.WA_LID,
|
||||
name: 'WhatsApp LID',
|
||||
description: 'WhatsApp Linked ID (Anonymous ID)',
|
||||
type: CustomAttributeType.TEXT,
|
||||
model: CustomAttributeModel.CONTACT,
|
||||
},
|
||||
];
|
||||
|
||||
export const INBOX_CONTACT_CHAT_ID = 'whatsapp.integration';
|
||||
@@ -0,0 +1,26 @@
|
||||
export enum QueueName {
|
||||
//
|
||||
// Scheduled
|
||||
//
|
||||
SCHEDULED_MESSAGE_CLEANUP = 'chatwoot.scheduled | message.cleanup',
|
||||
SCHEDULED_CHECK_VERSION = 'chatwoot.scheduled | check.version',
|
||||
|
||||
//
|
||||
// WAHA Events
|
||||
//
|
||||
WAHA_SESSION_STATUS = 'chatwoot.waha | session.status',
|
||||
WAHA_MESSAGE_ANY = 'chatwoot.waha | message.any',
|
||||
WAHA_MESSAGE_REACTION = 'chatwoot.waha | message.reaction',
|
||||
WAHA_MESSAGE_EDITED = 'chatwoot.waha | message.edited',
|
||||
WAHA_MESSAGE_REVOKED = 'chatwoot.waha | message.revoked',
|
||||
//
|
||||
// ChatWoot Events - Real
|
||||
//
|
||||
INBOX_MESSAGE_CREATED = 'chatwoot.inbox | message_created',
|
||||
INBOX_MESSAGE_UPDATED = 'chatwoot.inbox | message_updated',
|
||||
//
|
||||
// ChatWoot Events - Artificial
|
||||
//
|
||||
INBOX_MESSAGE_DELETED = 'chatwoot.inbox | message_deleted',
|
||||
INBOX_COMMANDS = 'chatwoot.inbox | commands',
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { AppConsumer } from '@waha/apps/app_sdk/AppConsumer';
|
||||
import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper';
|
||||
import { HasBeenRetried } from '@waha/apps/app_sdk/JobUtils';
|
||||
import { GetChatID } from '@waha/apps/chatwoot/client/ids';
|
||||
import { EventName } from '@waha/apps/chatwoot/client/types';
|
||||
import { ChatWootConversationKey } from '@waha/apps/chatwoot/consumers/mutex';
|
||||
import { InboxData } from '@waha/apps/chatwoot/consumers/types';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import {
|
||||
ChatIDNotFoundForContactError,
|
||||
PhoneNumberNotFoundInWhatsAppError,
|
||||
} from '@waha/apps/chatwoot/errors';
|
||||
import { TKey } from '@waha/apps/chatwoot/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 { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { AppRepository } from '../../storage';
|
||||
|
||||
/**
|
||||
* Base class for ChatWoot inbox consumers
|
||||
* Contains common logic for all inbox consumers
|
||||
*/
|
||||
export abstract class ChatWootInboxMessageConsumer extends AppConsumer {
|
||||
protected appRepository: AppRepository;
|
||||
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
protected readonly consumerName: string,
|
||||
) {
|
||||
super('ChatWoot', consumerName, log, rmutex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the DIContainer for the specified app.
|
||||
*/
|
||||
protected async DIContainer(job: Job, appId: string): Promise<DIContainer> {
|
||||
const knex = this.manager.store.getWAHADatabase();
|
||||
this.appRepository = new AppRepository(knex);
|
||||
const logger = new JobLoggerWrapper(job, this.logger);
|
||||
const app = await this.appRepository.getById(appId);
|
||||
return new DIContainer(app.pk, app.config, logger, knex);
|
||||
}
|
||||
|
||||
protected abstract ErrorHeaderKey(): TKey | null;
|
||||
|
||||
/**
|
||||
* Get the function that processes the message
|
||||
* This method must be implemented by subclasses
|
||||
*/
|
||||
protected abstract Process(
|
||||
container: DIContainer,
|
||||
body: any,
|
||||
job: Job,
|
||||
): Promise<any>;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return await this.withMutex(job, key, () =>
|
||||
this.ProcessAndReportStatus(job),
|
||||
);
|
||||
}
|
||||
|
||||
private async ProcessAndReportStatus(job) {
|
||||
const body = job.data.body;
|
||||
try {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const result = await this.Process(container, body, job);
|
||||
await this.ReportErrorRecovered(job, body);
|
||||
return result;
|
||||
} catch (err) {
|
||||
await this.ReportErrorForMessage(job, err, body);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report an error for a message
|
||||
*/
|
||||
protected async ReportErrorForMessage(job: Job, err: any, body: any) {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const header: string = this.ErrorHeaderKey()
|
||||
? container.Locale().key(this.ErrorHeaderKey()).render()
|
||||
: err.message || `${err}`;
|
||||
const conversation = container
|
||||
.ContactConversationService()
|
||||
.ConversationById(body.conversation.id);
|
||||
const reporter = container.ChatWootErrorReporter(job);
|
||||
await reporter.ReportError(
|
||||
conversation,
|
||||
header,
|
||||
body.message_type,
|
||||
err,
|
||||
body.id,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
protected async ReportErrorRecovered(job: Job, body: any) {
|
||||
if (!HasBeenRetried(job)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const conversation = container
|
||||
.ContactConversationService()
|
||||
.ConversationById(body.conversation.id);
|
||||
const reporter = container.ChatWootErrorReporter(job);
|
||||
await reporter.ReportSucceeded(conversation, body.message_type, body.id);
|
||||
}
|
||||
}
|
||||
|
||||
export async function LookupAndCheckChatId(
|
||||
session: WAHASessionAPI,
|
||||
body: any,
|
||||
): Promise<string> {
|
||||
const sender = body.conversation.meta.sender;
|
||||
let chatId = GetChatID(sender);
|
||||
if (!chatId && sender.phone_number) {
|
||||
const existResult = await session.contactCheckExists(sender.phone_number);
|
||||
if (!existResult.numberExists) {
|
||||
throw new PhoneNumberNotFoundInWhatsAppError(sender.phone_number);
|
||||
}
|
||||
chatId = existResult.chatId;
|
||||
}
|
||||
if (!chatId) {
|
||||
throw new ChatIDNotFoundForContactError(sender);
|
||||
}
|
||||
return chatId;
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
|
||||
import { AttachmentFromBuffer } from '@waha/apps/chatwoot/client/messages';
|
||||
import { MessageType } from '@waha/apps/chatwoot/client/types';
|
||||
import { ChatWootInboxMessageConsumer } from '@waha/apps/chatwoot/consumers/inbox/base';
|
||||
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { WAHASelf } from '@waha/apps/chatwoot/session/WAHASelf';
|
||||
import { SessionStatusEmoji } from '@waha/apps/chatwoot/SessionStatusEmoji';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
@Processor(QueueName.INBOX_COMMANDS, { concurrency: JOB_CONCURRENCY })
|
||||
export class ChatWootInboxCommandsConsumer extends ChatWootInboxMessageConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'ChatWootInboxCommandsConsumer');
|
||||
}
|
||||
|
||||
ErrorHeaderKey(): TKey | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async Process(container: DIContainer, body, job: Job) {
|
||||
const handler = new SessionCommandHandler(
|
||||
container.Logger(),
|
||||
container.ContactConversationService(),
|
||||
job.data.session,
|
||||
container.Locale(),
|
||||
container.WAHASelf(),
|
||||
);
|
||||
return await handler.handle(body);
|
||||
}
|
||||
}
|
||||
|
||||
enum Command {
|
||||
STATUS = 'status',
|
||||
RESTART = 'restart',
|
||||
START = 'start',
|
||||
STOP = 'stop',
|
||||
LOGOUT = 'logout',
|
||||
QR = 'qr',
|
||||
SCREENSHOT = 'screenshot',
|
||||
HELP = 'help',
|
||||
SERVER_STATUS = 'server status',
|
||||
SERVER_REBOOT = 'server reboot',
|
||||
SERVER_REBOOT_FORCE = 'server reboot force',
|
||||
}
|
||||
|
||||
const CommandMap: Record<string, Command> = {
|
||||
// Status
|
||||
[Command.STATUS]: Command.STATUS,
|
||||
['1']: Command.STATUS,
|
||||
// Restart
|
||||
[Command.RESTART]: Command.RESTART,
|
||||
['2']: Command.RESTART,
|
||||
// Start
|
||||
[Command.START]: Command.START,
|
||||
['3']: Command.START,
|
||||
// Stop
|
||||
[Command.STOP]: Command.STOP,
|
||||
['4']: Command.STOP,
|
||||
// Logout
|
||||
[Command.LOGOUT]: Command.LOGOUT,
|
||||
['5']: Command.LOGOUT,
|
||||
// QR
|
||||
[Command.QR]: Command.QR,
|
||||
['6']: Command.QR,
|
||||
// Screenshot
|
||||
[Command.SCREENSHOT]: Command.SCREENSHOT,
|
||||
['7']: Command.SCREENSHOT,
|
||||
// Help
|
||||
[Command.HELP]: Command.HELP,
|
||||
['8']: Command.HELP,
|
||||
// Server commands
|
||||
[Command.SERVER_STATUS]: Command.SERVER_STATUS,
|
||||
[Command.SERVER_REBOOT]: Command.SERVER_REBOOT,
|
||||
[Command.SERVER_REBOOT_FORCE]: Command.SERVER_REBOOT_FORCE,
|
||||
};
|
||||
|
||||
class CommandIsNotImplementedError extends Error {
|
||||
constructor(private command: string) {
|
||||
super(`Command is not implemented`);
|
||||
}
|
||||
}
|
||||
|
||||
class SessionCommandHandler {
|
||||
constructor(
|
||||
private logger: ILogger,
|
||||
private repo: ContactConversationService,
|
||||
private session: string,
|
||||
private l: Locale,
|
||||
private waha: WAHASelf,
|
||||
) {}
|
||||
|
||||
async handle(message: any) {
|
||||
const cmd = message.content?.toLowerCase();
|
||||
const command = this.findCommand(cmd) || Command.HELP;
|
||||
this.logger.info(
|
||||
`Executing command ${command} for session ${this.session}`,
|
||||
);
|
||||
switch (command) {
|
||||
case Command.RESTART:
|
||||
await this.waha.restart(this.session);
|
||||
break;
|
||||
case Command.START:
|
||||
await this.waha.restart(this.session);
|
||||
break;
|
||||
case Command.LOGOUT:
|
||||
this.logger.info(`Logging out session ${this.session}`);
|
||||
await this.waha.logout(this.session);
|
||||
const conversation = await this.repo.InboxNotifications();
|
||||
const text = this.l.key(TKey.APP_LOGOUT_SUCCESS).render();
|
||||
await conversation.incoming(text);
|
||||
break;
|
||||
case Command.STOP:
|
||||
this.logger.info(`Stopping session ${this.session}`);
|
||||
await this.waha.stop(this.session);
|
||||
break;
|
||||
case Command.STATUS: {
|
||||
this.logger.info(`Getting status for session ${this.session}`);
|
||||
const session = await this.waha.get(this.session);
|
||||
const conversation = await this.repo.InboxNotifications();
|
||||
const emoji = SessionStatusEmoji(session.status);
|
||||
const text = this.l.key(TKey.APP_SESSION_CURRENT_STATUS).render({
|
||||
emoji: emoji,
|
||||
session: session.name,
|
||||
status: session.status,
|
||||
name: session.me?.pushName || 'Unknown',
|
||||
id: session.me?.id || 'No phone number',
|
||||
});
|
||||
await conversation.incoming(text);
|
||||
break;
|
||||
}
|
||||
case Command.SERVER_STATUS: {
|
||||
this.logger.info('Getting server version and status');
|
||||
const version = await this.waha.serverVersion();
|
||||
const status = await this.waha.serverStatus();
|
||||
const conversation = await this.repo.InboxNotifications();
|
||||
const text = this.l.key(TKey.APP_SERVER_VERSION_AND_STATUS).render({
|
||||
version: JSON.stringify(version, null, 2),
|
||||
status: JSON.stringify(status, null, 2),
|
||||
});
|
||||
await conversation.incoming(text);
|
||||
break;
|
||||
}
|
||||
case Command.SERVER_REBOOT: {
|
||||
this.logger.info('Rebooting server (graceful)');
|
||||
const conversation = await this.repo.InboxNotifications();
|
||||
const text = this.l.key(TKey.APP_SERVER_REBOOT).render();
|
||||
await conversation.incoming(text);
|
||||
await this.waha.serverReboot(false);
|
||||
break;
|
||||
}
|
||||
case Command.SERVER_REBOOT_FORCE: {
|
||||
this.logger.info('Rebooting server (force)');
|
||||
const conversation = await this.repo.InboxNotifications();
|
||||
const text = this.l.key(TKey.APP_SERVER_REBOOT_FORCE).render();
|
||||
await conversation.incoming(text);
|
||||
await this.waha.serverReboot(true);
|
||||
break;
|
||||
}
|
||||
case Command.QR: {
|
||||
const conversation = await this.repo.InboxNotifications();
|
||||
const content = await this.waha.qr(this.session);
|
||||
const message = AttachmentFromBuffer(content, 'qr.jpg');
|
||||
message.message_type = MessageType.INCOMING;
|
||||
await conversation.send(message);
|
||||
break;
|
||||
}
|
||||
case Command.SCREENSHOT: {
|
||||
this.logger.info(`Getting screenshot for session ${this.session}`);
|
||||
const conversation = await this.repo.InboxNotifications();
|
||||
const content = await this.waha.screenshot(this.session);
|
||||
const message = AttachmentFromBuffer(content, 'screenshot.jpg');
|
||||
message.message_type = MessageType.INCOMING;
|
||||
await conversation.send(message);
|
||||
break;
|
||||
}
|
||||
case Command.HELP: {
|
||||
const conversation = await this.repo.InboxNotifications();
|
||||
const text = this.l.key(TKey.APP_COMMANDS_LIST).render();
|
||||
await conversation.incoming(text);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new CommandIsNotImplementedError(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
findCommand(text: string): Command {
|
||||
// Remove empty from end
|
||||
text = text.trim();
|
||||
// Remove empty from start
|
||||
text = text.trimStart();
|
||||
// Remove empty from end
|
||||
text = text.trimEnd();
|
||||
return CommandMap[text] || null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||
import { MessageAttachment } from '@waha/apps/chatwoot/client/types';
|
||||
import {
|
||||
ChatWootInboxMessageConsumer,
|
||||
LookupAndCheckChatId,
|
||||
} from '@waha/apps/chatwoot/consumers/inbox/base';
|
||||
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import { TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { EngineHelper } from '@waha/apps/chatwoot/session';
|
||||
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
|
||||
import {
|
||||
ChatwootMessage,
|
||||
MessageMappingService,
|
||||
} from '@waha/apps/chatwoot/storage';
|
||||
import { MarkdownToWhatsApp } from '@waha/apps/chatwoot/text';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import {
|
||||
MessageFileRequest,
|
||||
MessageImageRequest,
|
||||
MessageTextRequest,
|
||||
MessageVideoRequest,
|
||||
MessageVoiceRequest,
|
||||
} from '@waha/structures/chatting.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { SerializeWhatsAppKey } from '../../client/ids';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const mime = require('mime-types');
|
||||
|
||||
@Processor(QueueName.INBOX_MESSAGE_CREATED, { concurrency: JOB_CONCURRENCY })
|
||||
export class ChatWootInboxMessageCreatedConsumer extends ChatWootInboxMessageConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'ChatWootInboxMessageCreatedConsumer');
|
||||
}
|
||||
|
||||
ErrorHeaderKey(): TKey | null {
|
||||
return TKey.WHATSAPP_ERROR_SENDING_MESSAGE;
|
||||
}
|
||||
|
||||
protected async Process(container: DIContainer, body, job: Job) {
|
||||
const waha = container.WAHASelf();
|
||||
const session = new WAHASessionAPI(job.data.session, waha);
|
||||
const handler = new MessageHandler(
|
||||
container.MessageMappingService(),
|
||||
container.Logger(),
|
||||
session,
|
||||
);
|
||||
return await handler.handle(body);
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageHandler {
|
||||
constructor(
|
||||
private mappingService: MessageMappingService,
|
||||
private logger: ILogger,
|
||||
private session: WAHASessionAPI,
|
||||
) {}
|
||||
|
||||
async handle(body: any) {
|
||||
const chatId = await LookupAndCheckChatId(this.session, body);
|
||||
const message = body;
|
||||
if (message.content_type != 'text') {
|
||||
this.logger.info(
|
||||
`Message content type not supported. Content type: ${message.content_type}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = MarkdownToWhatsApp(message.content);
|
||||
const attachments = message.attachments || [];
|
||||
const results = [];
|
||||
const multipleAttachments = attachments.length > 1;
|
||||
let part = 1;
|
||||
const replyTo = await this.getReplyTo(message).catch((err) => {
|
||||
this.logger.error(`Error getting reply to message ID: ${err}`);
|
||||
return undefined;
|
||||
});
|
||||
|
||||
// Send text
|
||||
if (attachments.length == 0 || multipleAttachments) {
|
||||
const msg = await this.sendTextMessage(chatId, content, replyTo);
|
||||
results.push(msg);
|
||||
part = await this.saveMapping(message, msg, part);
|
||||
this.logger.info(`Text message sent: ${msg.id}`);
|
||||
}
|
||||
|
||||
// Send files
|
||||
const fileMessageContent = multipleAttachments ? '' : content;
|
||||
for (const file of attachments) {
|
||||
const msg = await this.sendFile(
|
||||
chatId,
|
||||
fileMessageContent,
|
||||
file,
|
||||
replyTo,
|
||||
);
|
||||
this.logger.info(
|
||||
`File message sent: ${msg.id} - ${file.data_url} - ${file.file_type}`,
|
||||
);
|
||||
results.push(msg);
|
||||
part = await this.saveMapping(message, msg, part);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private async saveMapping(
|
||||
chatwootMessage: any,
|
||||
whatsappMessage: any,
|
||||
part: number,
|
||||
): Promise<number> {
|
||||
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;
|
||||
}
|
||||
|
||||
async getReplyTo(message): Promise<string | undefined> {
|
||||
const message_id = message.content_attributes.in_reply_to;
|
||||
if (!message_id) {
|
||||
return;
|
||||
}
|
||||
const messages = await this.mappingService.getWhatsAppMessage({
|
||||
conversation_id: message.conversation.id,
|
||||
message_id: message_id,
|
||||
});
|
||||
if (!messages || messages.length == 0) {
|
||||
return;
|
||||
}
|
||||
const whatsapp = messages[0];
|
||||
return SerializeWhatsAppKey(whatsapp);
|
||||
}
|
||||
|
||||
private async sendTextMessage(
|
||||
chatId: string,
|
||||
content: string,
|
||||
replyTo: string,
|
||||
) {
|
||||
const request: MessageTextRequest = {
|
||||
session: '',
|
||||
chatId: chatId,
|
||||
text: content,
|
||||
reply_to: replyTo,
|
||||
};
|
||||
const session = this.session;
|
||||
await session.readMessages(chatId);
|
||||
await session.startTyping({ chatId: chatId, session: '' });
|
||||
await sleep(2000);
|
||||
await session.stopTyping({ chatId: chatId, session: '' });
|
||||
const msg = await session.sendText(request);
|
||||
return msg;
|
||||
}
|
||||
|
||||
private async sendFile(
|
||||
chatId: string,
|
||||
content: string,
|
||||
file: MessageAttachment,
|
||||
replyTo: string,
|
||||
) {
|
||||
const url = file.data_url;
|
||||
let filename = url.split('/').pop();
|
||||
if (filename) {
|
||||
filename = decodeURIComponent(filename);
|
||||
}
|
||||
const mimetype = mime.lookup(filename);
|
||||
const session = this.session;
|
||||
|
||||
switch (file.file_type) {
|
||||
case 'image':
|
||||
if (mimetype != 'image/jpeg' && mimetype != 'image/png') {
|
||||
// Send it as a file
|
||||
break;
|
||||
}
|
||||
const imageRequest: MessageImageRequest = {
|
||||
session: '',
|
||||
caption: content,
|
||||
chatId: chatId,
|
||||
reply_to: replyTo,
|
||||
file: {
|
||||
url: url,
|
||||
mimetype: mimetype,
|
||||
},
|
||||
};
|
||||
return session.sendImage(imageRequest);
|
||||
case 'video':
|
||||
if (mimetype != 'video/mp4') {
|
||||
break;
|
||||
}
|
||||
const videoRequest: MessageVideoRequest = {
|
||||
session: '',
|
||||
caption: content,
|
||||
chatId: chatId,
|
||||
reply_to: replyTo,
|
||||
file: {
|
||||
url: url,
|
||||
mimetype: mimetype,
|
||||
filename: filename,
|
||||
},
|
||||
convert: true,
|
||||
};
|
||||
return session.sendVideo(videoRequest);
|
||||
case 'audio':
|
||||
const voiceRequest: MessageVoiceRequest = {
|
||||
session: '',
|
||||
chatId: chatId,
|
||||
file: {
|
||||
url: url,
|
||||
mimetype: mimetype,
|
||||
filename: filename,
|
||||
},
|
||||
convert: true,
|
||||
};
|
||||
return session.sendVoice(voiceRequest);
|
||||
}
|
||||
// Fallback and send as file (attachment)
|
||||
const fileRequest: MessageFileRequest = {
|
||||
session: '',
|
||||
caption: content,
|
||||
chatId: chatId,
|
||||
reply_to: replyTo,
|
||||
file: {
|
||||
filename: filename,
|
||||
url: url,
|
||||
mimetype: mimetype,
|
||||
},
|
||||
};
|
||||
return session.sendFile(fileRequest);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||
import { SerializeWhatsAppKey } from '@waha/apps/chatwoot/client/ids';
|
||||
import {
|
||||
ChatWootInboxMessageConsumer,
|
||||
LookupAndCheckChatId,
|
||||
} from '@waha/apps/chatwoot/consumers/inbox/base';
|
||||
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import { TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
|
||||
import { MessageMappingService } from '@waha/apps/chatwoot/storage';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
@Processor(QueueName.INBOX_MESSAGE_DELETED, { concurrency: JOB_CONCURRENCY })
|
||||
export class ChatWootInboxMessageDeletedConsumer extends ChatWootInboxMessageConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'ChatWootInboxMessageDeletedConsumer');
|
||||
}
|
||||
|
||||
ErrorHeaderKey(): TKey | null {
|
||||
return TKey.WHATSAPP_ERROR_REMOVING_MESSAGE;
|
||||
}
|
||||
|
||||
protected async Process(container: DIContainer, body, job: Job) {
|
||||
const waha = container.WAHASelf();
|
||||
const session = new WAHASessionAPI(job.data.session, waha);
|
||||
const handler = new MessageDeletedHandler(
|
||||
container.MessageMappingService(),
|
||||
container.Logger(),
|
||||
session,
|
||||
);
|
||||
return await handler.handle(body);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageDeletedHandler {
|
||||
constructor(
|
||||
private mappingService: MessageMappingService,
|
||||
private logger: ILogger,
|
||||
private session: WAHASessionAPI,
|
||||
) {}
|
||||
|
||||
async handle(body: any) {
|
||||
const chatId = await LookupAndCheckChatId(this.session, body);
|
||||
const messages = await this.mappingService.getWhatsAppMessage({
|
||||
conversation_id: body.conversation.id,
|
||||
message_id: body.id,
|
||||
});
|
||||
if (!messages || messages.length == 0) {
|
||||
throw Error(
|
||||
`No WhatsApp message found for Chatwoot message '${body.id}'`,
|
||||
);
|
||||
}
|
||||
|
||||
const session = this.session;
|
||||
for (const whatsapp of messages) {
|
||||
this.logger.debug(
|
||||
`Deleting message '${whatsapp.message_id}' from '${whatsapp.chat_id}'`,
|
||||
);
|
||||
const messageId = SerializeWhatsAppKey(whatsapp);
|
||||
await session.deleteMessage(chatId, messageId);
|
||||
this.logger.info(
|
||||
`Message '${whatsapp.message_id}' deleted from '${whatsapp.chat_id}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { ChatWootInboxMessageConsumer } from '@waha/apps/chatwoot/consumers/inbox/base';
|
||||
import { MessageHandler } from '@waha/apps/chatwoot/consumers/inbox/message_created';
|
||||
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import { TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { WAHASessionAPI } from '../../session/WAHASelf';
|
||||
|
||||
@Processor(QueueName.INBOX_MESSAGE_UPDATED, { concurrency: JOB_CONCURRENCY })
|
||||
export class ChatWootInboxMessageUpdatedConsumer extends ChatWootInboxMessageConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'ChatWootInboxMessageUpdatedConsumer');
|
||||
}
|
||||
|
||||
ErrorHeaderKey(): TKey | null {
|
||||
return TKey.WHATSAPP_ERROR_SENDING_MESSAGE;
|
||||
}
|
||||
|
||||
protected async Process(container: DIContainer, body, job: Job) {
|
||||
const waha = container.WAHASelf();
|
||||
const session = new WAHASessionAPI(job.data.session, waha);
|
||||
const handler = new MessageHandler(
|
||||
container.MessageMappingService(),
|
||||
container.Logger(),
|
||||
session,
|
||||
);
|
||||
return await handler.handle(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function WhatsAppChatIdKey(app: string, chatId: string) {
|
||||
return `chatwoot.${app}.whatsapp.chat-id-${chatId}`;
|
||||
}
|
||||
|
||||
export function ChatWootConversationKey(app: string, conversationId: string) {
|
||||
return `chatwoot.${app}.chatwoot.conversation-id-${conversationId}`;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { AppConsumer } from '@waha/apps/app_sdk/AppConsumer';
|
||||
import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper';
|
||||
import { HasBeenRetried } from '@waha/apps/app_sdk/JobUtils';
|
||||
import { MessageType } from '@waha/apps/chatwoot/client/types';
|
||||
import { ScheduledData } from '@waha/apps/chatwoot/consumers/types';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
|
||||
import { TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { AppRepository } from '../../storage';
|
||||
|
||||
/**
|
||||
* Base class for ChatWoot scheduled consumers
|
||||
* Contains common logic for all scheduled consumers
|
||||
*/
|
||||
export abstract class ChatWootScheduledConsumer extends AppConsumer {
|
||||
protected appRepository: AppRepository;
|
||||
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
protected readonly consumerName: string,
|
||||
) {
|
||||
super('ChatWoot', consumerName, log, rmutex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the DIContainer for the specified app.
|
||||
*/
|
||||
protected async DIContainer(job: Job, appId: string): Promise<DIContainer> {
|
||||
const knex = this.manager.store.getWAHADatabase();
|
||||
this.appRepository = new AppRepository(knex);
|
||||
const logger = new JobLoggerWrapper(job, this.logger);
|
||||
const app = await this.appRepository.getById(appId);
|
||||
return new DIContainer(app.pk, app.config, logger, knex);
|
||||
}
|
||||
|
||||
protected abstract ErrorHeaderKey(): TKey | null;
|
||||
|
||||
/**
|
||||
* Get the function that processes the scheduled job
|
||||
* This method must be implemented by subclasses
|
||||
*/
|
||||
protected abstract Process(container: DIContainer, job: Job): Promise<any>;
|
||||
|
||||
/**
|
||||
* Process the job
|
||||
* This method is called by the queue processor
|
||||
*/
|
||||
async processJob(job: Job<ScheduledData, any, string>): Promise<any> {
|
||||
return this.ProcessAndReportStatus(job);
|
||||
}
|
||||
|
||||
private async ProcessAndReportStatus(job: Job) {
|
||||
try {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const result = await this.Process(container, job);
|
||||
await this.ReportErrorRecovered(job);
|
||||
return result;
|
||||
} catch (err) {
|
||||
await this.ReportErrorForJob(job, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Report an error for a scheduled job
|
||||
*/
|
||||
protected async ReportErrorForJob(job: Job, err: any) {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
let header = this.ErrorHeaderKey()
|
||||
? container.Locale().key(this.ErrorHeaderKey()).render()
|
||||
: err.message || `${err}`;
|
||||
header = `${job.queueName}: ${header}`;
|
||||
|
||||
const conversation = await container
|
||||
.ContactConversationService()
|
||||
.InboxNotifications();
|
||||
|
||||
const reporter = container.ChatWootErrorReporter(job);
|
||||
await reporter.ReportError(conversation, header, MessageType.INCOMING, err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
protected async ReportErrorRecovered(job: Job) {
|
||||
if (!HasBeenRetried(job)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const conversation = await container
|
||||
.ContactConversationService()
|
||||
.InboxNotifications();
|
||||
|
||||
const reporter = container.ChatWootErrorReporter(job);
|
||||
await reporter.ReportSucceeded(conversation, MessageType.INCOMING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import { TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { CHANGELOG_URL, SUPPORT_US_URL } from '@waha/core/constants';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { VERSION, WAHAVersion } from '@waha/version';
|
||||
import axios from 'axios';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { ChatWootScheduledConsumer } from './base';
|
||||
|
||||
/**
|
||||
* Scheduled consumer for version checking
|
||||
* Periodically checks for new versions and notifies if updates are available
|
||||
*/
|
||||
@Processor(QueueName.SCHEDULED_CHECK_VERSION, { concurrency: JOB_CONCURRENCY })
|
||||
export class CheckVersionConsumer extends ChatWootScheduledConsumer {
|
||||
constructor(manager: SessionManager, log: PinoLogger, rmutex: RMutexService) {
|
||||
super(manager, log, rmutex, CheckVersionConsumer.name);
|
||||
}
|
||||
|
||||
protected ErrorHeaderKey(): TKey {
|
||||
return TKey.APP_SCHEDULED_JOB_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the version check job
|
||||
* Checks for new versions and notifies if updates are available
|
||||
*/
|
||||
protected async Process(container: DIContainer, job: Job): Promise<any> {
|
||||
await this.CheckWAHACoreVersion(container);
|
||||
await this.CheckNewVersionAvailable(container);
|
||||
}
|
||||
|
||||
private async CheckWAHACoreVersion(container: DIContainer) {
|
||||
const logger = container.Logger();
|
||||
const locale = container.Locale();
|
||||
const conversation = await container
|
||||
.ContactConversationService()
|
||||
.InboxNotifications();
|
||||
// If using the CORE version, send a message encouraging support
|
||||
if (VERSION.tier !== WAHAVersion.CORE) {
|
||||
logger.info('WAHA is not using the CORE version');
|
||||
return;
|
||||
}
|
||||
logger.info('WAHA is using the CORE version');
|
||||
const supportMessage = locale.key(TKey.APP_CORE_VERSION_USED).render({
|
||||
supportUrl: SUPPORT_US_URL,
|
||||
});
|
||||
await conversation.incoming(supportMessage);
|
||||
}
|
||||
|
||||
private async CheckNewVersionAvailable(container: DIContainer) {
|
||||
const logger = container.Logger();
|
||||
logger.info('Processing version check job');
|
||||
const currentVersion = VERSION.version;
|
||||
logger.info(`Current WAHA version: ${currentVersion}`);
|
||||
const latestVersion = await this.fetchLatestVersion(logger);
|
||||
const isNewVersionAvailable = currentVersion !== latestVersion;
|
||||
if (!isNewVersionAvailable) {
|
||||
logger.info('WAHA is up to date');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`New version available: ${latestVersion} (current: ${currentVersion})`,
|
||||
);
|
||||
|
||||
// Send a message to Chatwoot conversation about a new version
|
||||
const locale = container.Locale();
|
||||
const message = locale.key(TKey.APP_NEW_VERSION_AVAILABLE).render({
|
||||
currentVersion: currentVersion,
|
||||
newVersion: latestVersion,
|
||||
changelogUrl: CHANGELOG_URL,
|
||||
});
|
||||
const conversation = await container
|
||||
.ContactConversationService()
|
||||
.InboxNotifications();
|
||||
await conversation.incoming(message);
|
||||
}
|
||||
|
||||
private async fetchLatestVersion(logger: ILogger): Promise<string> {
|
||||
const response = await axios.get(
|
||||
'https://api.github.com/repos/devlikeapro/waha/releases/latest',
|
||||
);
|
||||
const latestVersion = response.data.name;
|
||||
logger.info(`Latest WAHA version: ${latestVersion}`);
|
||||
return latestVersion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import { TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { ChatWootScheduledConsumer } from './base';
|
||||
|
||||
/**
|
||||
* Scheduled consumer for message cleanup tasks
|
||||
* Handles periodic cleanup of old messages and related data
|
||||
*/
|
||||
@Processor(QueueName.SCHEDULED_MESSAGE_CLEANUP, {
|
||||
concurrency: JOB_CONCURRENCY,
|
||||
})
|
||||
export class MessageCleanupConsumer extends ChatWootScheduledConsumer {
|
||||
REMOVE_AFTER_DAYS = 14;
|
||||
|
||||
constructor(manager: SessionManager, log: PinoLogger, rmutex: RMutexService) {
|
||||
super(manager, log, rmutex, MessageCleanupConsumer.name);
|
||||
}
|
||||
|
||||
protected ErrorHeaderKey(): TKey {
|
||||
return TKey.APP_SCHEDULED_JOB_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the message cleanup job
|
||||
* Performs cleanup of old messages and related data
|
||||
*/
|
||||
protected async Process(container: DIContainer, job: Job): Promise<any> {
|
||||
const logger = container.Logger();
|
||||
logger.info('Processing message cleanup job');
|
||||
const removeAfter = new Date(
|
||||
Date.now() - this.REMOVE_AFTER_DAYS * 24 * 60 * 60 * 1000,
|
||||
);
|
||||
logger.info(`Removing mapping for messages older than ${removeAfter}`);
|
||||
const removed = await container
|
||||
.MessageMappingService()
|
||||
.cleanup(removeAfter);
|
||||
logger.info(`Removed ${removed} mappings for messages`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
|
||||
|
||||
export interface InboxData {
|
||||
session: string;
|
||||
app: string;
|
||||
body: any;
|
||||
}
|
||||
|
||||
export interface EventData {
|
||||
app: string;
|
||||
event: WAHAWebhook;
|
||||
}
|
||||
|
||||
export interface ScheduledData {
|
||||
app: string;
|
||||
session: string;
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import { isJidGroup, isJidStatusBroadcast } from '@adiwajshing/baileys';
|
||||
import { conversation_message_create } from '@figuro/chatwoot-sdk';
|
||||
import type { generic_id } from '@figuro/chatwoot-sdk/dist/models/generic_id';
|
||||
import type { message } from '@figuro/chatwoot-sdk/dist/models/message';
|
||||
import { AppConsumer } from '@waha/apps/app_sdk/AppConsumer';
|
||||
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||
import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper';
|
||||
import { HasBeenRetried } from '@waha/apps/app_sdk/JobUtils';
|
||||
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
|
||||
import { Conversation } from '@waha/apps/chatwoot/client/Conversation';
|
||||
import { MessageType, SendAttachment } from '@waha/apps/chatwoot/client/types';
|
||||
import { WhatsAppChatIdKey } from '@waha/apps/chatwoot/consumers/mutex';
|
||||
import { EventData } from '@waha/apps/chatwoot/consumers/types';
|
||||
import { WhatsAppContactInfo } from '@waha/apps/chatwoot/contacts/WhatsAppContactInfo';
|
||||
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
|
||||
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { WAHASelf, WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
|
||||
import {
|
||||
AppRepository,
|
||||
ChatwootMessage,
|
||||
MessageMappingService,
|
||||
WhatsAppMessage,
|
||||
} from '@waha/apps/chatwoot/storage';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { MessageSource, WAMessageBase } from '@waha/structures/responses.dto';
|
||||
import {
|
||||
WAHAWebhook,
|
||||
WAHAWebhookMessageRevoked,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { ChatWootAppConfig } from '../../dto/config.dto';
|
||||
|
||||
export function ListenEventsForChatWoot() {
|
||||
return [
|
||||
WAHAEvents.MESSAGE_ANY,
|
||||
WAHAEvents.MESSAGE_REACTION,
|
||||
WAHAEvents.MESSAGE_EDITED,
|
||||
WAHAEvents.MESSAGE_REVOKED,
|
||||
WAHAEvents.SESSION_STATUS,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for ChatWoot WAHA consumers
|
||||
* Contains common logic for all WAHA consumers
|
||||
*/
|
||||
export abstract class ChatWootWAHABaseConsumer extends AppConsumer {
|
||||
protected appRepository: AppRepository;
|
||||
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
protected readonly consumerName: string,
|
||||
) {
|
||||
super('ChatWoot', consumerName, log, rmutex);
|
||||
}
|
||||
|
||||
protected ErrorHeaderKey(): TKey | null {
|
||||
return TKey.WHATSAPP_ERROR_RECEIVING_MESSAGE;
|
||||
}
|
||||
|
||||
private async ProcessAndReportErrors(job) {
|
||||
const errorReportInfo = new MessageReportInfo();
|
||||
try {
|
||||
const result = await this.Process(job, errorReportInfo);
|
||||
await this.ReportErrorRecovered(errorReportInfo, job);
|
||||
return result;
|
||||
} catch (err) {
|
||||
await this.ReportErrorForMessage(errorReportInfo, job, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the DIContainer for the specified app.
|
||||
*/
|
||||
protected async DIContainer(job: Job, appId: string): Promise<DIContainer> {
|
||||
const knex = this.manager.store.getWAHADatabase();
|
||||
this.appRepository = new AppRepository(knex);
|
||||
const logger = new JobLoggerWrapper(job, this.logger);
|
||||
const app = await this.appRepository.getById(appId);
|
||||
return new DIContainer(app.pk, app.config, logger, knex);
|
||||
}
|
||||
|
||||
abstract GetChatId(event: any): string;
|
||||
|
||||
abstract Process(
|
||||
job: Job<EventData, any, WAHAEvents>,
|
||||
messageInfo: IMessageInfo,
|
||||
): Promise<any>;
|
||||
|
||||
async processJob(job: Job<EventData, any, WAHAEvents>): Promise<any> {
|
||||
const event: WAHAWebhookMessageRevoked = job.data.event as any;
|
||||
const key = WhatsAppChatIdKey(job.data.app, this.GetChatId(event));
|
||||
return await this.withMutex(job, key, () =>
|
||||
this.ProcessAndReportErrors(job),
|
||||
);
|
||||
}
|
||||
|
||||
protected async ReportErrorForMessage(
|
||||
info: MessageReportInfo,
|
||||
job: Job,
|
||||
err: any,
|
||||
) {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
let conversation: Conversation;
|
||||
if (info.conversationId) {
|
||||
conversation = container
|
||||
.ContactConversationService()
|
||||
.ConversationById(info.conversationId);
|
||||
} else {
|
||||
conversation = await container
|
||||
.ContactConversationService()
|
||||
.InboxNotifications();
|
||||
}
|
||||
|
||||
const header: string = this.ErrorHeaderKey()
|
||||
? container.Locale().key(this.ErrorHeaderKey()).render()
|
||||
: err.message || `${err}`;
|
||||
const reporter = container.ChatWootErrorReporter(job);
|
||||
await reporter.ReportError(
|
||||
conversation,
|
||||
header,
|
||||
info.type || MessageType.INCOMING,
|
||||
err,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a job as recovered after retries
|
||||
* This method will only send a report if the job has been retried (not on its first attempt)
|
||||
*/
|
||||
protected async ReportErrorRecovered(info: MessageReportInfo, job: Job) {
|
||||
if (!HasBeenRetried(job)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
let conversation: Conversation;
|
||||
if (info.conversationId) {
|
||||
conversation = container
|
||||
.ContactConversationService()
|
||||
.ConversationById(info.conversationId);
|
||||
} else {
|
||||
const conversation = await container
|
||||
.ContactConversationService()
|
||||
.InboxNotifications();
|
||||
}
|
||||
|
||||
const reporter = container.ChatWootErrorReporter(job);
|
||||
await reporter.ReportSucceeded(
|
||||
conversation,
|
||||
info.type || MessageType.INCOMING,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IMessageInfo {
|
||||
onConversationId(id: number): void;
|
||||
|
||||
onMessageType(type: MessageType): void;
|
||||
}
|
||||
|
||||
class MessageReportInfo implements IMessageInfo {
|
||||
public conversationId: number | null = null;
|
||||
public type: MessageType | null = null;
|
||||
|
||||
onConversationId(id: number) {
|
||||
this.conversationId = id;
|
||||
}
|
||||
|
||||
onMessageType(type: MessageType) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class MessageBaseHandler<Payload extends WAMessageBase> {
|
||||
constructor(
|
||||
protected mappingService: MessageMappingService,
|
||||
protected repo: ContactConversationService,
|
||||
protected logger: ILogger,
|
||||
protected info: IMessageInfo,
|
||||
protected session: WAHASessionAPI,
|
||||
protected l: Locale,
|
||||
protected waha: WAHASelf,
|
||||
) {}
|
||||
|
||||
abstract getContent(payload: Payload): string;
|
||||
|
||||
abstract getReplyToWhatsAppID(payload: Payload): string | undefined;
|
||||
|
||||
abstract getAttachments(payload: Payload): Promise<SendAttachment[]>;
|
||||
|
||||
async handle(data: WAHAWebhook<Payload>) {
|
||||
const payload = data.payload;
|
||||
// Find the type as soon as possible for error reporting
|
||||
const type = payload.fromMe ? MessageType.OUTGOING : MessageType.INCOMING;
|
||||
this.info.onMessageType(type);
|
||||
|
||||
// Check if we have that message already in ChatWoot to avoid duplication
|
||||
// It also handles messages fromMe and ChatWoot (but does not if send via API)
|
||||
if (payload.fromMe && payload.source === MessageSource.API) {
|
||||
// Sleep a few seconds to save it to a database
|
||||
await sleep(3_000);
|
||||
}
|
||||
|
||||
const key = parseMessageIdSerialized(payload.id);
|
||||
const chatwoot = await this.mappingService.getChatWootMessage({
|
||||
chat_id: toCusFormat(key.remoteJid),
|
||||
message_id: key.id,
|
||||
});
|
||||
if (chatwoot) {
|
||||
this.logger.info(
|
||||
`Message '${payload.id}' already in ChatWoot: conversation.id=${chatwoot.conversation_id}, message.id=${chatwoot.message_id}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const contactInfo = WhatsAppContactInfo(this.session, payload.from, this.l);
|
||||
const conversation = await this.repo.ConversationByContact(contactInfo);
|
||||
this.info.onConversationId(conversation.conversationId);
|
||||
|
||||
const message = await this.buildChatWootMessage(payload);
|
||||
const response = await conversation.send(message);
|
||||
this.logger.info(
|
||||
`Created message as '${message.message_type}' from WhatsApp: ${response.id}`,
|
||||
);
|
||||
await this.saveMapping(response, payload);
|
||||
return message;
|
||||
}
|
||||
|
||||
private async saveMapping(
|
||||
chatwootMessage: generic_id & message,
|
||||
whatsappMessage: WAMessageBase,
|
||||
) {
|
||||
const chatwoot: Omit<ChatwootMessage, 'id'> = {
|
||||
timestamp: new Date(chatwootMessage.created_at * 1000),
|
||||
conversation_id: chatwootMessage.conversation_id,
|
||||
message_id: chatwootMessage.id,
|
||||
};
|
||||
const key = parseMessageIdSerialized(whatsappMessage.id);
|
||||
const whatsapp: WhatsAppMessage = {
|
||||
timestamp: new Date(whatsappMessage.timestamp * 1000),
|
||||
chat_id: toCusFormat(key.remoteJid),
|
||||
message_id: key.id,
|
||||
from_me: key.fromMe,
|
||||
participant: null,
|
||||
};
|
||||
await this.mappingService.map(chatwoot, whatsapp, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
private async buildChatWootMessage(
|
||||
payload: Payload,
|
||||
): Promise<conversation_message_create> {
|
||||
let content: string = this.getContent(payload);
|
||||
|
||||
// Format the content if the message from me
|
||||
if (payload.fromMe && payload.source === MessageSource.APP) {
|
||||
const key = TKey.MESSAGE_FROM_WHATSAPP;
|
||||
content = this.l.key(key).render({ text: content });
|
||||
} else if (payload.fromMe && payload.source === MessageSource.API) {
|
||||
const key = TKey.MESSAGE_FROM_API;
|
||||
content = this.l.key(key).render({ text: content });
|
||||
}
|
||||
|
||||
const chatId = payload.from;
|
||||
|
||||
// Add participant name to group messages
|
||||
const manyParticipants = isJidGroup(chatId) || isJidStatusBroadcast(chatId);
|
||||
if (!payload.fromMe && manyParticipants) {
|
||||
const key = parseMessageIdSerialized(payload.id, true);
|
||||
let participant = toCusFormat(key.participant);
|
||||
const contact: any = await this.session.getContact(key.participant);
|
||||
const name = contact?.name || contact?.pushName || contact?.pushname;
|
||||
if (name) {
|
||||
participant = `${name} (${participant})`;
|
||||
}
|
||||
content = this.l.key(TKey.WHATSAPP_GROUP_MESSAGE).render({
|
||||
text: content,
|
||||
participant: participant,
|
||||
});
|
||||
}
|
||||
const attachments: SendAttachment[] = await this.getAttachments(payload);
|
||||
if (!content && attachments.length > 0) {
|
||||
// With no content ChatWoot render the message as an ugly big attachment
|
||||
content = ' ';
|
||||
}
|
||||
const replyTo = await this.getReplyToChatWootMessageID(payload).catch(
|
||||
(err) => {
|
||||
this.logger.error(`Error getting reply to message ID: ${err}`);
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
const type = payload.fromMe ? MessageType.OUTGOING : MessageType.INCOMING;
|
||||
return {
|
||||
content: content,
|
||||
message_type: type,
|
||||
private: payload.fromMe,
|
||||
attachments: attachments as any,
|
||||
content_attributes: {
|
||||
in_reply_to: replyTo,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getReplyToChatWootMessageID(
|
||||
payload: Payload,
|
||||
): Promise<number | undefined> {
|
||||
const replyToWhatsAppID = this.getReplyToWhatsAppID(payload);
|
||||
if (!replyToWhatsAppID) {
|
||||
return;
|
||||
}
|
||||
const chatwoot = await this.mappingService.getChatWootMessage({
|
||||
chat_id: payload.from,
|
||||
message_id: replyToWhatsAppID,
|
||||
});
|
||||
return chatwoot.message_id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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 {
|
||||
ChatWootWAHABaseConsumer,
|
||||
IMessageInfo,
|
||||
} from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
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';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
import { WAHAWebhookMessageAny } from '@waha/structures/webhooks.dto';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const mime = require('mime-types');
|
||||
|
||||
@Processor(QueueName.WAHA_MESSAGE_ANY, { concurrency: JOB_CONCURRENCY })
|
||||
export class WAHAMessageAnyConsumer extends ChatWootWAHABaseConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'WAHAMessageAnyConsumer');
|
||||
}
|
||||
|
||||
GetChatId(event: WAHAWebhookMessageAny): string {
|
||||
return event.payload.from;
|
||||
}
|
||||
|
||||
async Process(
|
||||
job: Job<EventData, any, WAHAEvents>,
|
||||
info: IMessageInfo,
|
||||
): Promise<any> {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const event: WAHAWebhookMessageAny = job.data.event as any;
|
||||
const session = new WAHASessionAPI(event.session, container.WAHASelf());
|
||||
const handler = new MessageAnyHandler(
|
||||
container.MessageMappingService(),
|
||||
container.ContactConversationService(),
|
||||
container.Logger(),
|
||||
info,
|
||||
session,
|
||||
container.Locale(),
|
||||
container.WAHASelf(),
|
||||
);
|
||||
return await handler.handle(event);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageAnyHandler extends MessageBaseHandler<WAMessage> {
|
||||
getContent(payload: WAMessage): string {
|
||||
const body = payload.body;
|
||||
return WhatsappToMarkdown(body);
|
||||
}
|
||||
|
||||
getReplyToWhatsAppID(payload: WAMessage): string {
|
||||
const replyTo = payload.replyTo;
|
||||
if (!replyTo) {
|
||||
return undefined;
|
||||
}
|
||||
const key = parseMessageIdSerialized(replyTo.id, true);
|
||||
return key.id;
|
||||
}
|
||||
|
||||
async getAttachments(payload: WAMessage): Promise<SendAttachment[]> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
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 { MessageBaseHandler } from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
import { TKey } from '@waha/apps/chatwoot/locale';
|
||||
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';
|
||||
import {
|
||||
WAHAWebhookMessageEdited,
|
||||
WAMessageEditedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { SendAttachment } from '../../client/types';
|
||||
import { WAHASessionAPI } from '../../session/WAHASelf';
|
||||
|
||||
@Processor(QueueName.WAHA_MESSAGE_EDITED, { concurrency: JOB_CONCURRENCY })
|
||||
export class WAHAMessageEditedConsumer extends ChatWootWAHABaseConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'WAHAMessageEditedConsumer');
|
||||
}
|
||||
|
||||
GetChatId(event: WAHAWebhookMessageEdited): string {
|
||||
return event.payload.from;
|
||||
}
|
||||
|
||||
async Process(
|
||||
job: Job<EventData, any, WAHAEvents>,
|
||||
info: IMessageInfo,
|
||||
): Promise<any> {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const event: WAHAWebhookMessageEdited = job.data.event as any;
|
||||
const session = new WAHASessionAPI(event.session, container.WAHASelf());
|
||||
const handler = new MessageEditedHandler(
|
||||
container.MessageMappingService(),
|
||||
container.ContactConversationService(),
|
||||
container.Logger(),
|
||||
info,
|
||||
session,
|
||||
container.Locale(),
|
||||
container.WAHASelf(),
|
||||
);
|
||||
return await handler.handle(event);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageEditedHandler extends MessageBaseHandler<WAMessageEditedBody> {
|
||||
getContent(payload: WAMessageEditedBody): string {
|
||||
const body = payload.body;
|
||||
const formatted = WhatsappToMarkdown(body);
|
||||
return this.l
|
||||
.key(TKey.MESSAGE_EDITED_IN_WHATSAPP)
|
||||
.render({ text: formatted });
|
||||
}
|
||||
|
||||
getReplyToWhatsAppID(payload: WAMessageEditedBody): string {
|
||||
return payload.editedMessageId;
|
||||
}
|
||||
|
||||
async getAttachments(
|
||||
payload: WAMessageEditedBody,
|
||||
): Promise<SendAttachment[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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 {
|
||||
ChatWootWAHABaseConsumer,
|
||||
IMessageInfo,
|
||||
} from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
import { MessageBaseHandler } from '@waha/apps/chatwoot/consumers/waha/base';
|
||||
import { TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { WAMessageReaction, WAReaction } from '@waha/structures/responses.dto';
|
||||
import { WAHAWebhookMessageReaction } from '@waha/structures/webhooks.dto';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
@Processor(QueueName.WAHA_MESSAGE_REACTION, { concurrency: JOB_CONCURRENCY })
|
||||
export class WAHAMessageReactionConsumer extends ChatWootWAHABaseConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'WAHAMessageReactionConsumer');
|
||||
}
|
||||
|
||||
GetChatId(event: WAHAWebhookMessageReaction): string {
|
||||
return event.payload.from;
|
||||
}
|
||||
|
||||
async Process(
|
||||
job: Job<EventData, any, WAHAEvents>,
|
||||
info: IMessageInfo,
|
||||
): Promise<any> {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const event: WAHAWebhookMessageReaction = job.data.event as any;
|
||||
const session = new WAHASessionAPI(event.session, container.WAHASelf());
|
||||
const handler = new MessageReactionHandler(
|
||||
container.MessageMappingService(),
|
||||
container.ContactConversationService(),
|
||||
container.Logger(),
|
||||
info,
|
||||
session,
|
||||
container.Locale(),
|
||||
container.WAHASelf(),
|
||||
);
|
||||
return await handler.handle(event);
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageReactionHandler extends MessageBaseHandler<WAMessageReaction> {
|
||||
getContent(payload: WAMessageReaction) {
|
||||
const reaction = payload.reaction as WAReaction;
|
||||
const emoji = reaction.text;
|
||||
if (!emoji) {
|
||||
return this.l.key(TKey.WHATSAPP_REACTION_REMOVED).render();
|
||||
}
|
||||
return this.l.key(TKey.WHATSAPP_REACTION_ADDED).render({
|
||||
emoji: emoji,
|
||||
});
|
||||
}
|
||||
|
||||
getReplyToWhatsAppID(payload: WAMessageReaction) {
|
||||
const reaction = payload.reaction as WAReaction;
|
||||
const messageId = reaction.messageId;
|
||||
const key = parseMessageIdSerialized(messageId, false);
|
||||
return key.id;
|
||||
}
|
||||
|
||||
async getAttachments(payload: WAMessageReaction): Promise<SendAttachment[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { conversation_message_create } from '@figuro/chatwoot-sdk';
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
|
||||
import { MessageType } from '@waha/apps/chatwoot/client/types';
|
||||
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 { Locale, TKey } from '@waha/apps/chatwoot/locale';
|
||||
import {
|
||||
ChatwootMessage,
|
||||
MessageMappingService,
|
||||
} from '@waha/apps/chatwoot/storage';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { WAHAWebhookMessageRevoked } from '@waha/structures/webhooks.dto';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
@Processor(QueueName.WAHA_MESSAGE_REVOKED, { concurrency: JOB_CONCURRENCY })
|
||||
export class WAHAMessageRevokedConsumer extends ChatWootWAHABaseConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'WAHAMessageRevokedConsumer');
|
||||
}
|
||||
|
||||
GetChatId(event: WAHAWebhookMessageRevoked): string {
|
||||
return event.payload.after.from;
|
||||
}
|
||||
|
||||
async Process(
|
||||
job: Job<EventData, any, WAHAEvents>,
|
||||
info: IMessageInfo,
|
||||
): Promise<any> {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const event: WAHAWebhookMessageRevoked = job.data.event as any;
|
||||
const handler = new MessageRevokedHandler(
|
||||
container.MessageMappingService(),
|
||||
container.ContactConversationService(),
|
||||
container.Logger(),
|
||||
container.Locale(),
|
||||
info,
|
||||
);
|
||||
return await handler.handle(event);
|
||||
}
|
||||
}
|
||||
|
||||
export class MessageRevokedHandler {
|
||||
constructor(
|
||||
private mappingService: MessageMappingService,
|
||||
private repo: ContactConversationService,
|
||||
private logger: ILogger,
|
||||
private l: Locale,
|
||||
private info: IMessageInfo,
|
||||
) {}
|
||||
|
||||
async handle(event: WAHAWebhookMessageRevoked) {
|
||||
const payload = event.payload;
|
||||
const messageId = event.payload.revokedMessageId;
|
||||
const chatId = payload.after.from;
|
||||
const chatwoot = await this.mappingService.getChatWootMessage({
|
||||
chat_id: toCusFormat(chatId),
|
||||
message_id: messageId,
|
||||
});
|
||||
if (!chatwoot) {
|
||||
this.logger.warn('Message not found in mapping service');
|
||||
return;
|
||||
}
|
||||
const conversation = this.repo.ConversationById(chatwoot.conversation_id);
|
||||
const message = this.buildChatWootMessage(chatwoot, payload.after.fromMe);
|
||||
this.info.onConversationId(chatwoot.conversation_id);
|
||||
this.info.onMessageType(message.message_type as MessageType);
|
||||
await conversation.send(message);
|
||||
}
|
||||
|
||||
private buildChatWootMessage(
|
||||
chatwoot: ChatwootMessage,
|
||||
fromMe: boolean,
|
||||
): conversation_message_create {
|
||||
const content = this.l.key(TKey.MESSAGE_REMOVED_IN_WHATSAPP).render();
|
||||
const type = fromMe ? MessageType.OUTGOING : MessageType.INCOMING;
|
||||
return {
|
||||
content: content,
|
||||
message_type: type,
|
||||
private: true,
|
||||
content_attributes: {
|
||||
in_reply_to: chatwoot.message_id,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { conversation_message_create } from '@figuro/chatwoot-sdk/dist/models/conversation_message_create';
|
||||
import { Processor } from '@nestjs/bullmq';
|
||||
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
|
||||
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
|
||||
import { AttachmentFromBuffer } from '@waha/apps/chatwoot/client/messages';
|
||||
import { MessageType } from '@waha/apps/chatwoot/client/types';
|
||||
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 { Locale, TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { WAHASelf } from '@waha/apps/chatwoot/session/WAHASelf';
|
||||
import { SessionStatusEmoji } from '@waha/apps/chatwoot/SessionStatusEmoji';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
|
||||
import { WAHAEvents, WAHASessionStatus } from '@waha/structures/enums.dto';
|
||||
import { WAHAWebhookSessionStatus } from '@waha/structures/webhooks.dto';
|
||||
import { Job } from 'bullmq';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
@Processor(QueueName.WAHA_SESSION_STATUS, { concurrency: JOB_CONCURRENCY })
|
||||
export class WAHASessionStatusConsumer extends ChatWootWAHABaseConsumer {
|
||||
constructor(
|
||||
protected readonly manager: SessionManager,
|
||||
log: PinoLogger,
|
||||
rmutex: RMutexService,
|
||||
) {
|
||||
super(manager, log, rmutex, 'WAHASessionStatusConsumer');
|
||||
}
|
||||
|
||||
GetChatId(event: any): string {
|
||||
return 'session.status';
|
||||
}
|
||||
|
||||
async Process(
|
||||
job: Job<EventData, any, WAHAEvents>,
|
||||
info: IMessageInfo,
|
||||
): Promise<any> {
|
||||
const container = await this.DIContainer(job, job.data.app);
|
||||
const handler = new SessionStatusHandler(
|
||||
container.ContactConversationService(),
|
||||
container.Locale(),
|
||||
container.WAHASelf(),
|
||||
);
|
||||
return await handler.handle(job.data.event as any);
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionStatusHandler {
|
||||
constructor(
|
||||
private repo: ContactConversationService,
|
||||
private l: Locale,
|
||||
private waha: WAHASelf,
|
||||
) {}
|
||||
|
||||
async handle(data: WAHAWebhookSessionStatus) {
|
||||
const conversation = await this.repo.InboxNotifications();
|
||||
// Report session status
|
||||
const emoji = SessionStatusEmoji(data.payload.status);
|
||||
const activity = this.l.key(TKey.APP_SESSION_STATUS_CHANGE).r({
|
||||
emoji: emoji,
|
||||
session: data.session,
|
||||
status: data.payload.status,
|
||||
});
|
||||
await conversation.activity(activity);
|
||||
|
||||
const payload = data.payload;
|
||||
let text = '';
|
||||
switch (payload.status) {
|
||||
case WAHASessionStatus.STARTING:
|
||||
break;
|
||||
case WAHASessionStatus.WORKING:
|
||||
text = this.l.key(TKey.APP_SESSION_STATUS_WORKING).r({
|
||||
name: data.me?.pushName || 'Unknown',
|
||||
id: data.me?.id || 'No phone number',
|
||||
});
|
||||
break;
|
||||
case WAHASessionStatus.STOPPED:
|
||||
case WAHASessionStatus.FAILED:
|
||||
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.SCAN_QR_CODE:
|
||||
text = this.l.key(TKey.APP_SESSION_SCAN_QR_CODE).r();
|
||||
}
|
||||
if (text) {
|
||||
const message: conversation_message_create = {
|
||||
content: text,
|
||||
message_type: MessageType.INCOMING as any,
|
||||
};
|
||||
await conversation.send(message);
|
||||
}
|
||||
|
||||
// Send QR to the inbox
|
||||
if (payload.status === WAHASessionStatus.SCAN_QR_CODE) {
|
||||
const buffer = await this.waha.qr(data.session);
|
||||
const message = AttachmentFromBuffer(buffer, 'qr.jpg');
|
||||
message.message_type = MessageType.INCOMING;
|
||||
return await conversation.send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { public_contact_create_update_payload } from '@figuro/chatwoot-sdk';
|
||||
import { ContactInfo } from '@waha/apps/chatwoot/client/ContactConversationService';
|
||||
import { AttributeKey, INBOX_CONTACT_CHAT_ID } from '@waha/apps/chatwoot/const';
|
||||
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
|
||||
|
||||
/**
|
||||
* Inbox Notifications Contact
|
||||
*/
|
||||
export class InboxContactInfo implements ContactInfo {
|
||||
constructor(private l: Locale) {}
|
||||
|
||||
ChatId(): string {
|
||||
return INBOX_CONTACT_CHAT_ID;
|
||||
}
|
||||
|
||||
async AvatarUrl() {
|
||||
return this.l.key(TKey.APP_INBOX_CONTACT_AVATAR_URL).render();
|
||||
}
|
||||
|
||||
async Attributes(): Promise<any> {
|
||||
return {
|
||||
[AttributeKey.WA_CHAT_ID]: INBOX_CONTACT_CHAT_ID,
|
||||
};
|
||||
}
|
||||
|
||||
async PublicContactCreate(): Promise<public_contact_create_update_payload> {
|
||||
return {
|
||||
identifier: INBOX_CONTACT_CHAT_ID,
|
||||
name: this.l.key(TKey.APP_INBOX_CONTACT_NAME).render(),
|
||||
avatar_url: await this.AvatarUrl(),
|
||||
custom_attributes: await this.Attributes(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import {
|
||||
isJidGroup,
|
||||
isJidNewsletter,
|
||||
isJidStatusBroadcast,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { public_contact_create_update_payload as Contact } from '@figuro/chatwoot-sdk';
|
||||
import { ContactInfo } from '@waha/apps/chatwoot/client/ContactConversationService';
|
||||
import { AttributeKey } from '@waha/apps/chatwoot/const';
|
||||
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
|
||||
import { Channel } from '@waha/structures/channels.dto';
|
||||
import { CacheAsync } from '@waha/utils/Cache';
|
||||
|
||||
/**
|
||||
* Base WhatsApp contact info class
|
||||
*/
|
||||
abstract class ChatContactInfo implements ContactInfo {
|
||||
constructor(
|
||||
protected session: WAHASessionAPI,
|
||||
protected chatId: string,
|
||||
protected locale: Locale,
|
||||
) {}
|
||||
|
||||
ChatId(): string {
|
||||
return this.chatId;
|
||||
}
|
||||
|
||||
abstract AvatarUrl(): Promise<string | null>;
|
||||
|
||||
abstract Attributes(): Promise<any>;
|
||||
|
||||
abstract PublicContactCreate(): Promise<Contact>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regular JID contact info
|
||||
*/
|
||||
class JidContactInfo extends ChatContactInfo {
|
||||
@CacheAsync()
|
||||
async AvatarUrl(): Promise<string> {
|
||||
return await this.session.getChatPicture(this.chatId);
|
||||
}
|
||||
|
||||
@CacheAsync()
|
||||
async fetchLid() {
|
||||
return await this.session.findLIDByPN(this.chatId);
|
||||
}
|
||||
|
||||
@CacheAsync()
|
||||
async Attributes() {
|
||||
const attributes = {
|
||||
[AttributeKey.WA_CHAT_ID]: this.chatId,
|
||||
[AttributeKey.WA_JID]: this.chatId,
|
||||
};
|
||||
const lid = await this.fetchLid();
|
||||
if (lid) {
|
||||
attributes[AttributeKey.WA_LID] = lid;
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
async PublicContactCreate(): Promise<Contact> {
|
||||
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 result: Contact = {
|
||||
name: name,
|
||||
custom_attributes: {
|
||||
[AttributeKey.WA_CHAT_ID]: this.chatId,
|
||||
[AttributeKey.WA_JID]: this.chatId,
|
||||
},
|
||||
phone_number: phoneNumberE164,
|
||||
};
|
||||
result.custom_attributes = await this.Attributes();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LID contact info
|
||||
*/
|
||||
class LidContactInfo extends ChatContactInfo {
|
||||
@CacheAsync()
|
||||
async jid() {
|
||||
const pn = await this.session.findPNByLid(this.chatId);
|
||||
return new JidContactInfo(this.session, pn, this.locale);
|
||||
}
|
||||
|
||||
async AvatarUrl(): Promise<string | null> {
|
||||
const jid = await this.jid();
|
||||
if (jid) {
|
||||
return await jid.AvatarUrl();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@CacheAsync()
|
||||
async Attributes() {
|
||||
const jid = await this.jid();
|
||||
let attributes = {};
|
||||
if (jid) {
|
||||
attributes = await jid.Attributes();
|
||||
}
|
||||
attributes[AttributeKey.WA_CHAT_ID] = this.chatId;
|
||||
attributes[AttributeKey.WA_LID] = this.chatId;
|
||||
return attributes;
|
||||
}
|
||||
|
||||
async PublicContactCreate(): Promise<Contact> {
|
||||
const jid = await this.jid();
|
||||
let result;
|
||||
if (jid) {
|
||||
result = await jid.PublicContactCreate();
|
||||
} else {
|
||||
result = {
|
||||
inbox_id: 0,
|
||||
identifier: this.chatId,
|
||||
name: this.chatId,
|
||||
};
|
||||
}
|
||||
result.custom_attributes = await this.Attributes();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group contact info
|
||||
*/
|
||||
class GroupContactInfo extends ChatContactInfo {
|
||||
@CacheAsync()
|
||||
async AvatarUrl(): Promise<string> {
|
||||
return await this.session.getChatPicture(this.chatId);
|
||||
}
|
||||
|
||||
async Attributes() {
|
||||
return {
|
||||
[AttributeKey.WA_CHAT_ID]: this.chatId,
|
||||
};
|
||||
}
|
||||
|
||||
async PublicContactCreate(): Promise<Contact> {
|
||||
let name = this.chatId;
|
||||
const group: any = await this.session?.getGroup(this.chatId);
|
||||
if (group) {
|
||||
name = group.subject || group.name || group.topic || name;
|
||||
const suffix = this.locale
|
||||
.key(TKey.WHATSAPP_CONTACT_GROUP_SUFFIX)
|
||||
.render();
|
||||
name = `${name} (${suffix})`;
|
||||
}
|
||||
|
||||
return {
|
||||
identifier: this.chatId,
|
||||
name: name,
|
||||
custom_attributes: await this.Attributes(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel (newsletter) contact info
|
||||
*/
|
||||
class ChannelContactInfo extends ChatContactInfo {
|
||||
@CacheAsync()
|
||||
async AvatarUrl(): Promise<string> {
|
||||
return await this.session.getChatPicture(this.chatId);
|
||||
}
|
||||
|
||||
async Attributes() {
|
||||
return {
|
||||
[AttributeKey.WA_CHAT_ID]: this.chatId,
|
||||
};
|
||||
}
|
||||
|
||||
async PublicContactCreate(): Promise<Contact> {
|
||||
let name = this.chatId;
|
||||
const channel: Channel = await this.session.getChannel(this.chatId);
|
||||
if (channel) {
|
||||
name = channel.name || name;
|
||||
const suffix = this.locale
|
||||
.key(TKey.WHATSAPP_CONTACT_CHANNEL_SUFFIX)
|
||||
.render();
|
||||
name = `${name} (${suffix})`;
|
||||
}
|
||||
return {
|
||||
identifier: this.chatId,
|
||||
name: name,
|
||||
custom_attributes: await this.Attributes(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Status broadcast contact info
|
||||
*/
|
||||
class StatusContactInfo extends ChatContactInfo {
|
||||
@CacheAsync()
|
||||
async AvatarUrl(): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async Attributes() {
|
||||
return {
|
||||
[AttributeKey.WA_CHAT_ID]: this.chatId,
|
||||
};
|
||||
}
|
||||
|
||||
async PublicContactCreate(): Promise<Contact> {
|
||||
const name = this.locale.key(TKey.WHATSAPP_CONTACT_STATUS_NAME).render();
|
||||
|
||||
return {
|
||||
identifier: this.chatId,
|
||||
name: `🟢 ${name}`,
|
||||
custom_attributes: await this.Attributes(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to get the appropriate ContactInfo implementation
|
||||
* based on the chat ID type
|
||||
*/
|
||||
export function WhatsAppContactInfo(
|
||||
session: WAHASessionAPI,
|
||||
chatId: string,
|
||||
locale: Locale,
|
||||
): ContactInfo {
|
||||
if (isJidGroup(chatId)) {
|
||||
return new GroupContactInfo(session, chatId, locale);
|
||||
} else if (isJidNewsletter(chatId)) {
|
||||
return new ChannelContactInfo(session, chatId, locale);
|
||||
} else if (isJidStatusBroadcast(chatId)) {
|
||||
return new StatusContactInfo(session, chatId, locale);
|
||||
} else if (isLidUser(chatId)) {
|
||||
return new LidContactInfo(session, chatId, locale);
|
||||
}
|
||||
return new JidContactInfo(session, chatId, locale);
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import ChatwootClient from '@figuro/chatwoot-sdk';
|
||||
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 { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
|
||||
import { ConversationAPI } from '@waha/apps/chatwoot/client/ConversationAPI';
|
||||
import { CustomAttributesAPI } from '@waha/apps/chatwoot/client/CustomAttributesAPI';
|
||||
import { ChatWootInboxAPI } from '@waha/apps/chatwoot/client/interfaces';
|
||||
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
|
||||
import { ChatWootErrorReporter } from '@waha/apps/chatwoot/error/ChatWootErrorReporter';
|
||||
import { Locale } from '@waha/apps/chatwoot/locale';
|
||||
import { WAHASelf } from '@waha/apps/chatwoot/session/WAHASelf';
|
||||
import {
|
||||
ChatwootMessageRepository,
|
||||
MessageMappingRepository,
|
||||
MessageMappingService,
|
||||
WhatsAppMessageRepository,
|
||||
} from '@waha/apps/chatwoot/storage';
|
||||
import { Job } from 'bullmq';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
constructor(
|
||||
private readonly appPk: number,
|
||||
private readonly config: ChatWootAppConfig,
|
||||
private readonly logger: ILogger,
|
||||
private readonly knex: Knex,
|
||||
) {}
|
||||
|
||||
public Logger(): ILogger {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
private AppPk() {
|
||||
if (!this.appPk) {
|
||||
throw new Error('AppPk not set or 0');
|
||||
}
|
||||
return this.appPk;
|
||||
}
|
||||
|
||||
private Knex() {
|
||||
if (!this.knex) {
|
||||
throw new Error('Knex not set');
|
||||
}
|
||||
return this.knex;
|
||||
}
|
||||
|
||||
public Locale(): Locale {
|
||||
if (!this.locale) {
|
||||
this.locale = new Locale(this.config.locale);
|
||||
}
|
||||
return this.locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the AccountAPI client
|
||||
* @returns ChatwootClient instance
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the InboxAPI client
|
||||
* @returns ChatWootInboxAPI instance
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ContactAPI
|
||||
* @returns ContactAPI instance
|
||||
*/
|
||||
private ContactAPI(): ContactAPI {
|
||||
if (!this.contactAPI) {
|
||||
this.contactAPI = new ContactAPI(
|
||||
this.config,
|
||||
this.AccountAPI(),
|
||||
this.InboxAPI(),
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
return this.contactAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ConversationAPI
|
||||
* @returns ConversationAPI instance
|
||||
*/
|
||||
private ConversationAPI(): ConversationAPI {
|
||||
if (!this.conversationAPI) {
|
||||
this.conversationAPI = new ConversationAPI(
|
||||
this.config,
|
||||
this.InboxAPI(),
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
return this.conversationAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ContactConversationService
|
||||
* @returns ContactConversationService instance
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
private ChatwootMessageRepository(): ChatwootMessageRepository {
|
||||
return new ChatwootMessageRepository(this.Knex(), this.AppPk());
|
||||
}
|
||||
|
||||
private WhatsAppMessageRepository(): WhatsAppMessageRepository {
|
||||
return new WhatsAppMessageRepository(this.Knex(), this.AppPk());
|
||||
}
|
||||
|
||||
private MessageMappingRepository(): MessageMappingRepository {
|
||||
return new MessageMappingRepository(this.Knex(), this.AppPk());
|
||||
}
|
||||
|
||||
public MessageMappingService(): MessageMappingService {
|
||||
if (!this.messageMappingService) {
|
||||
this.messageMappingService = new MessageMappingService(
|
||||
this.Knex(),
|
||||
this.WhatsAppMessageRepository(),
|
||||
this.ChatwootMessageRepository(),
|
||||
this.MessageMappingRepository(),
|
||||
);
|
||||
}
|
||||
return this.messageMappingService;
|
||||
}
|
||||
|
||||
public ChatWootErrorReporter(job: Job): ChatWootErrorReporter {
|
||||
return new ChatWootErrorReporter(this.Logger(), job, this.Locale());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the WAHASelf instance
|
||||
* @returns WAHASelf instance
|
||||
*/
|
||||
public WAHASelf(): WAHASelf {
|
||||
if (!this.wahaSelf) {
|
||||
this.wahaSelf = new WAHASelf();
|
||||
const logging = new AxiosLogging(this.Logger());
|
||||
logging.applyTo(this.wahaSelf.client);
|
||||
}
|
||||
return this.wahaSelf;
|
||||
}
|
||||
|
||||
public CustomAttributesAPI() {
|
||||
return new CustomAttributesAPI(this.config, this.AccountAPI());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ChatWootAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
|
||||
import { DEFAULT_LOCALE, LOCALES } from '@waha/apps/chatwoot/locale';
|
||||
import { IsIn, IsNumber, IsString } from 'class-validator';
|
||||
|
||||
export class ChatWootAppConfig implements ChatWootAPIConfig {
|
||||
@IsString()
|
||||
url: string;
|
||||
|
||||
@IsNumber()
|
||||
accountId: number;
|
||||
|
||||
@IsString()
|
||||
accountToken: string;
|
||||
|
||||
@IsNumber()
|
||||
inboxId: number;
|
||||
|
||||
@IsString()
|
||||
inboxIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@IsIn(LOCALES)
|
||||
locale: string = DEFAULT_LOCALE;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { conversation_message_create } from '@figuro/chatwoot-sdk/dist/models/conversation_message_create';
|
||||
import { ILogger } from '@waha/apps/app_sdk/ILogger';
|
||||
import { NextAttemptDelayInWholeSeconds } from '@waha/apps/app_sdk/JobUtils';
|
||||
import { Conversation } from '@waha/apps/chatwoot/client/Conversation';
|
||||
import { MessageType } from '@waha/apps/chatwoot/client/types';
|
||||
import { ErrorRenderer } from '@waha/apps/chatwoot/error/ErrorRenderer';
|
||||
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
|
||||
import { Job } from 'bullmq';
|
||||
|
||||
export class ChatWootErrorReporter {
|
||||
private errorRenderer: ErrorRenderer = new ErrorRenderer();
|
||||
|
||||
constructor(
|
||||
private logger: ILogger,
|
||||
private job: Job,
|
||||
private l: Locale,
|
||||
) {}
|
||||
|
||||
async ReportError(
|
||||
conversation: Conversation,
|
||||
header: string,
|
||||
type: MessageType,
|
||||
error: any,
|
||||
replyTo?: number,
|
||||
) {
|
||||
const errorText = this.errorRenderer.renderError(error);
|
||||
this.logger.error(errorText);
|
||||
const errorUrl = `http://localhost:3000/jobs/queue/${encodeURIComponent(
|
||||
this.job.queueName,
|
||||
)}/${this.job.id}`;
|
||||
const template = this.l.key(TKey.JOB_ERROR_REPORT);
|
||||
const nextDelay = NextAttemptDelayInWholeSeconds(this.job);
|
||||
const attempts = {
|
||||
current: this.job.attemptsMade + 1,
|
||||
max: this.job.opts?.attempts || 1,
|
||||
nextDelay: nextDelay,
|
||||
};
|
||||
const content = template.render({
|
||||
header: header,
|
||||
error: nextDelay != null ? null : errorText,
|
||||
details: {
|
||||
text: `${this.job.queueName} => ${this.job.id}`,
|
||||
url: errorUrl,
|
||||
},
|
||||
attempts: attempts,
|
||||
});
|
||||
const request: conversation_message_create = {
|
||||
content: content,
|
||||
message_type: type as any,
|
||||
private: true, // Always private note
|
||||
};
|
||||
if (replyTo) {
|
||||
request.content_attributes = {
|
||||
in_reply_to: replyTo,
|
||||
};
|
||||
}
|
||||
await conversation.send(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a job as recovered after retries.
|
||||
* This method will only send a report if the job has been retried (not on its first attempt).
|
||||
*
|
||||
* @param conversation The conversation to send the report to
|
||||
* @param type The message type
|
||||
* @param replyTo Optional message ID to reply to
|
||||
* @returns Promise that resolves when the report is sent, or void if no report is sent
|
||||
*/
|
||||
async ReportSucceeded(
|
||||
conversation: Conversation,
|
||||
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_SUCCEEDED_REPORT);
|
||||
const attempts = {
|
||||
current: this.job.attemptsMade + 1,
|
||||
max: this.job.opts?.attempts || 1,
|
||||
};
|
||||
|
||||
const content = template.render({
|
||||
details: {
|
||||
text: `${this.job.queueName} => ${this.job.id}`,
|
||||
url: jobUrl,
|
||||
},
|
||||
attempts: attempts,
|
||||
});
|
||||
|
||||
const request: conversation_message_create = {
|
||||
content: content,
|
||||
message_type: type as any,
|
||||
private: true, // Always private note
|
||||
};
|
||||
|
||||
if (replyTo) {
|
||||
request.content_attributes = {
|
||||
in_reply_to: replyTo,
|
||||
};
|
||||
}
|
||||
|
||||
await conversation.send(request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { ApiError } from '@figuro/chatwoot-sdk/dist/core/ApiError';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
/**
|
||||
* Class responsible for rendering error information based on error type
|
||||
*/
|
||||
export class ErrorRenderer {
|
||||
/**
|
||||
* Renders error information based on error type
|
||||
* @param error The error to render
|
||||
* @returns Formatted error text
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders Axios error information
|
||||
* @param error The Axios error to render
|
||||
* @returns Formatted error text
|
||||
*/
|
||||
private renderAxiosError(error: AxiosError): string {
|
||||
let errorText = `Axios 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;
|
||||
if (Buffer.isBuffer(data)) {
|
||||
json = JSON.parse(data.toString());
|
||||
} else if (typeof data === 'object') {
|
||||
json = data;
|
||||
} else {
|
||||
json = JSON.parse(data as any);
|
||||
}
|
||||
errorText += `\nData: ${JSON.stringify(json, null, 2)}`;
|
||||
} catch (e) {
|
||||
errorText += 'Data: <unparsable>';
|
||||
}
|
||||
|
||||
return errorText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders Chatwoot SDK ApiError information
|
||||
* @param error The ApiError to render
|
||||
* @returns Formatted error text
|
||||
*/
|
||||
private renderApiError(error: ApiError): string {
|
||||
let errorText = `API Error: ${error.message}`;
|
||||
errorText += `\nStatus: ${error.status}`;
|
||||
|
||||
// Add body if it exists
|
||||
if (error.body) {
|
||||
try {
|
||||
const body =
|
||||
typeof error.body === 'object' ? error.body : JSON.parse(error.body);
|
||||
errorText += `\nBody: ${JSON.stringify(body, null, 2)}`;
|
||||
} catch (e) {
|
||||
errorText += `\nBody: ${error.body}`;
|
||||
}
|
||||
}
|
||||
|
||||
return errorText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders generic error information
|
||||
* @param error The error to render
|
||||
* @returns Formatted error text
|
||||
*/
|
||||
private renderGenericError(error: any): string {
|
||||
return error.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export class ChatIDNotFoundForContactError extends Error {
|
||||
constructor(public sender: any) {
|
||||
super('Chat ID not found');
|
||||
}
|
||||
}
|
||||
|
||||
export class PhoneNumberNotFoundInWhatsAppError extends Error {
|
||||
constructor(public phone: any) {
|
||||
super(`Phone number not found in WhatsApp: ${phone}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
# Arabic translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'تكامل واتساب (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 إعادة المحاولة في {{attempts.nextDelay}} ثانية...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ لم تبق محاولات أخرى. يرجى المحاولة لاحقًا.
|
||||
{{/attempts.nextDelay}}
|
||||
المحاولة: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
التفاصيل: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **نجاح**
|
||||
المحاولة: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
التفاصيل: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE: تعذر إرسال رسالة على واتساب.
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE: تعذر استلام رسالة من واتساب
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE: تعذر إزالة رسالة من واتساب
|
||||
APP_SCHEDULED_JOB_ERROR: فشل تنفيذ المهمة المجدولة
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *أرسلت من واتساب*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *أرسلت عبر API*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *تم حذف هذه الرسالة في واتساب*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *تم تعديلها في واتساب*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*تفاعل* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*تمت إزالة التفاعل*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'مجموعة'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'قناة'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'الحالة'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **متصل: جلسة واتساب إلى صندوق وارد ChatWoot**
|
||||
|
||||
✅ تم توصيل جلسة واتساب **{{{name}}}** بنجاح بصندوق وارد ChatWoot هذا.
|
||||
|
||||
صندوق الوارد هذا مرتبط الآن بـ **حساب واتساب** الخاص بك. يمكنك إرسال واستقبال الرسائل مباشرة من هنا — تمامًا مثل استخدام واتساب، ولكن داخل ChatWoot.
|
||||
|
||||
يمكنك أيضًا إدارة جلسة واتساب المرتبطة بصندوق الوارد هذا. لعرض الإجراءات المتاحة، أرسل **help** إلى هذه المحادثة.
|
||||
|
||||
📋 الإجراءات الشائعة التي يمكنك القيام بها:
|
||||
- التحقق مما إذا كانت جلسة واتساب نشطة
|
||||
- إعادة تشغيل أو إيقاف جلسة واتساب
|
||||
- تسجيل الخروج وإعادة الاتصال عن طريق مسح رمز QR جديد
|
||||
|
||||
إذا كان كل شيء يعمل، ستتمكن من الدردشة بحرية. إذا رأيت تحذيرات، فقد تحتاج إلى إعادة توصيل حساب واتساب الخاص بك.
|
||||
|
||||
لنبدأ! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **غير متصل: جلسة واتساب من صندوق وارد ChatWoot**
|
||||
|
||||
❌ تم قطع اتصال جلسة واتساب **{{{name}}}** من صندوق وارد ChatWoot هذا.
|
||||
|
||||
لن تتمكن بعد الآن من إرسال أو استقبال رسائل من واتساب.
|
||||
|
||||
لإعادة الاتصال، يرجى الاتصال بالمسؤول الخاص بك.
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **تم التحديث: إعدادات جلسة واتساب**
|
||||
|
||||
✅ تم تحديث إعدادات جلسة واتساب **{{{name}}}**.
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
يمكنك إدارة **تكامل واتساب** عن طريق إرسال أحد الأوامر التالية:
|
||||
|
||||
🖥️ الجلسات:
|
||||
1. **status** - الحصول على حالة الجلسة الحالية.
|
||||
2. **restart** - إعادة تشغيل الجلسة - سيحاول هذا إعادة توصيل حساب واتساب الخاص بك.
|
||||
3. **start** - بدء الجلسة.
|
||||
4. **stop** - إيقاف الجلسة.
|
||||
5. **logout** - تسجيل الخروج من الجلسة، حتى تتمكن من مسح رمز QR وإصلاح الجلسة.
|
||||
6. **qr** - الحصول على رمز QR للجلسة.
|
||||
7. **screenshot** - الحصول على لقطة شاشة للجلسة (متاح فقط في محرك **WEBJS**).
|
||||
8. **help** - الأوامر المتاحة.
|
||||
|
||||
⚙️ الخادم:
|
||||
- **server status** - الحصول على معلومات إصدار الخادم وحالته.
|
||||
- **server reboot** - إعادة تشغيل الخادم بسلاسة.
|
||||
- **server reboot force** - إعادة تشغيل الخادم بالقوة.
|
||||
|
||||
أرسل **أمرًا** (غير حساس لحالة الأحرف) أو **رقم أمر** إلى هذه المحادثة.
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
أرسل **help** للحصول على قائمة الأوامر المتاحة.
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} تم تغيير حالة جلسة "{{{session}}}" إلى {{{status}}}
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} حالة جلسة **{{{session}}}** هي **{{{status}}}**
|
||||
**حساب واتساب:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
قم بزيارة [لوحة تحكم WAHA]({{{url}}}) لمزيد من التفاصيل.
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ صندوق وارد ChatWoot هذا مرتبط بـ **حساب واتساب** وجاهز لإرسال واستقبال الرسائل!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **صندوق وارد ChatWoot هذا مرتبط بحساب واتساب ولكنه غير جاهز لإرسال واستقبال الرسائل!**
|
||||
|
||||
يرجى التحقق من حالة الجلسة ومحاولة إعادة تشغيلها أو تسجيل الخروج ومسح رمز QR مرة أخرى.
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **امسح رمز QR على هاتفك لربط حساب واتساب بصندوق وارد ChatWoot هذا!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**الإصدار:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**الحالة:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
يتم إعادة تشغيل الخادم بسلاسة...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
يتم إعادة تشغيل الخادم بالقوة...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ تم إلغاء تفويض الحساب بنجاح!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **إصدار جديد متاح!**
|
||||
|
||||
الإصدار الحالي: **{{{currentVersion}}}**
|
||||
الإصدار الجديد: **{{{newVersion}}}**
|
||||
|
||||
👉 تحقق مما هو جديد في [**🆕 سجل التغييرات**]({{{changelogUrl}}})
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **ادعم WAHA!**
|
||||
|
||||
أنت تستخدم إصدار **WAHA Core** المجاني. إذا وجدت هذا المشروع مفيدًا، فيرجى التفكير في دعم تطويره.
|
||||
|
||||
دعمك هو الطريقة الوحيدة التي يمكننا من خلالها الاستمرار في العمل على التحديثات والإصلاحات والميزات الجديدة.
|
||||
|
||||
[**🎁 ادعمنا**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,180 @@
|
||||
# Bengali translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'হোয়াটসঅ্যাপ ইন্টিগ্রেশন (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 {{attempts.nextDelay}} সেকেন্ডের মধ্যে পুনরায় চেষ্টা করা হচ্ছে...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ আর কোন চেষ্টা বাকি নেই। অনুগ্রহ করে পরে আবার চেষ্টা করুন।
|
||||
{{/attempts.nextDelay}}
|
||||
প্রচেষ্টা: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
বিবরণ: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **সফল হয়েছে**
|
||||
প্রচেষ্টা: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
বিবরণ: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE: হোয়াটসঅ্যাপে বার্তা পাঠাতে পারেনি।
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE: হোয়াটসঅ্যাপ থেকে বার্তা পেতে পারেনি
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE: হোয়াটসঅ্যাপ থেকে বার্তা সরাতে পারেনি
|
||||
APP_SCHEDULED_JOB_ERROR: নির্ধারিত কাজ সম্পাদন করতে ব্যর্থ হয়েছে
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *হোয়াটসঅ্যাপ থেকে পাঠানো হয়েছে*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *API এর মাধ্যমে পাঠানো হয়েছে*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *এই বার্তাটি হোয়াটসঅ্যাপে মুছে ফেলা হয়েছে*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *হোয়াটসঅ্যাপে সম্পাদিত*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*প্রতিক্রিয়া দিয়েছেন* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*প্রতিক্রিয়া সরানো হয়েছে*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'গ্রুপ'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'চ্যানেল'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'স্ট্যাটাস'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **সংযুক্ত: হোয়াটসঅ্যাপ সেশন থেকে ChatWoot ইনবক্স**
|
||||
|
||||
✅ হোয়াটসঅ্যাপ সেশন **{{{name}}}** সফলভাবে এই ChatWoot ইনবক্সের সাথে সংযুক্ত করা হয়েছে।
|
||||
|
||||
এই ইনবক্সটি এখন আপনার **হোয়াটসঅ্যাপ অ্যাকাউন্টের** সাথে সংযুক্ত। আপনি এখান থেকে সরাসরি বার্তা পাঠাতে এবং গ্রহণ করতে পারেন — ঠিক হোয়াটসঅ্যাপ ব্যবহার করার মতো, কিন্তু ChatWoot এর ভিতরে।
|
||||
|
||||
আপনি এই ইনবক্সের সাথে সংযুক্ত হোয়াটসঅ্যাপ সেশনও পরিচালনা করতে পারেন। উপলব্ধ কার্যগুলি দেখতে, এই চ্যাটে **help** পাঠান।
|
||||
|
||||
📋 আপনি যে সাধারণ কাজগুলি করতে পারেন:
|
||||
- হোয়াটসঅ্যাপ সেশন সক্রিয় কিনা তা পরীক্ষা করুন
|
||||
- হোয়াটসঅ্যাপ সেশন পুনরায় চালু করুন বা বন্ধ করুন
|
||||
- লগ আউট করুন এবং নতুন QR কোড স্ক্যান করে পুনরায় সংযোগ করুন
|
||||
|
||||
যদি সবকিছু কাজ করে, আপনি স্বাধীনভাবে চ্যাট করতে পারবেন। যদি আপনি সতর্কতা দেখেন, আপনাকে আপনার হোয়াটসঅ্যাপ অ্যাকাউন্ট পুনরায় সংযোগ করতে হতে পারে।
|
||||
|
||||
চলুন শুরু করা যাক! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **সংযোগ বিচ্ছিন্ন: হোয়াটসঅ্যাপ সেশন থেকে ChatWoot ইনবক্স**
|
||||
|
||||
❌ হোয়াটসঅ্যাপ সেশন **{{{name}}}** এই ChatWoot ইনবক্স থেকে সংযোগ বিচ্ছিন্ন করা হয়েছে।
|
||||
|
||||
আপনি আর হোয়াটসঅ্যাপ থেকে বার্তা পাঠাতে বা গ্রহণ করতে পারবেন না।
|
||||
|
||||
পুনরায় সংযোগ করতে, অনুগ্রহ করে আপনার প্রশাসকের সাথে যোগাযোগ করুন।
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **আপডেট করা হয়েছে: হোয়াটসঅ্যাপ সেশন সেটিংস**
|
||||
|
||||
✅ হোয়াটসঅ্যাপ সেশন **{{{name}}}** এর সেটিংস আপডেট করা হয়েছে।
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
আপনি নিম্নলিখিত কমান্ডগুলির একটি পাঠিয়ে **হোয়াটসঅ্যাপ ইন্টিগ্রেশন** পরিচালনা করতে পারেন:
|
||||
|
||||
🖥️ সেশন:
|
||||
1. **status** - বর্তমান সেশনের স্ট্যাটাস পান।
|
||||
2. **restart** - সেশন পুনরায় চালু করুন - এটি আপনার হোয়াটসঅ্যাপ অ্যাকাউন্ট পুনরায় সংযোগ করার চেষ্টা করবে।
|
||||
3. **start** - সেশন শুরু করুন।
|
||||
4. **stop** - সেশন বন্ধ করুন।
|
||||
5. **logout** - সেশন থেকে লগআউট করুন, যাতে আপনি QR কোড স্ক্যান করতে পারেন এবং সেশন মেরামত করতে পারেন।
|
||||
6. **qr** - সেশনের জন্য QR কোড পান।
|
||||
7. **screenshot** - সেশনের স্ক্রিনশট পান (শুধুমাত্র **WEBJS** ইঞ্জিনে উপলব্ধ)।
|
||||
8. **help** - উপলব্ধ কমান্ডগুলি।
|
||||
|
||||
⚙️ সার্ভার:
|
||||
- **server status** - সার্ভারের সংস্করণ এবং স্ট্যাটাস তথ্য পান।
|
||||
- **server reboot** - সার্ভার সুন্দরভাবে পুনরায় চালু করুন।
|
||||
- **server reboot force** - সার্ভার জোর করে পুনরায় চালু করুন।
|
||||
|
||||
এই কথোপকথনে একটি **কমান্ড** (কেস সংবেদনশীল নয়) বা **কমান্ড নম্বর** পাঠান।
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
উপলব্ধ কমান্ডগুলির তালিকা পেতে **help** পাঠান।
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} "{{{session}}}" সেশন {{{status}}} স্ট্যাটাসে পরিবর্তিত হয়েছে
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} **{{{session}}}** সেশনের স্ট্যাটাস **{{{status}}}**
|
||||
**হোয়াটসঅ্যাপ অ্যাকাউন্ট:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
আরও বিবরণের জন্য [WAHA ড্যাশবোর্ড]({{{url}}}) দেখুন।
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ এই ChatWoot ইনবক্স **হোয়াটসঅ্যাপ অ্যাকাউন্টের** সাথে সংযুক্ত এবং বার্তা পাঠাতে ও গ্রহণ করতে প্রস্তুত!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **এই ChatWoot ইনবক্স হোয়াটসঅ্যাপ অ্যাকাউন্টের সাথে সংযুক্ত কিন্তু বার্তা পাঠাতে ও গ্রহণ করতে প্রস্তুত নয়!**
|
||||
|
||||
অনুগ্রহ করে সেশনের স্ট্যাটাস পরীক্ষা করুন এবং এটি পুনরায় চালু করার চেষ্টা করুন অথবা লগআউট করুন এবং QR কোড আবার স্ক্যান করুন।
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **এই ChatWoot ইনবক্সের সাথে হোয়াটসঅ্যাপ অ্যাকাউন্ট লিঙ্ক করতে আপনার ফোনে QR কোড স্ক্যান করুন!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**সংস্করণ:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**স্ট্যাটাস:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
সার্ভার সুন্দরভাবে পুনরায় চালু হচ্ছে...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
সার্ভার জোর করে পুনরায় চালু হচ্ছে...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ অ্যাকাউন্ট সফলভাবে অননুমোদিত করা হয়েছে!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **নতুন সংস্করণ উপলব্ধ!**
|
||||
|
||||
বর্তমান সংস্করণ: **{{{currentVersion}}}**
|
||||
নতুন সংস্করণ: **{{{newVersion}}}**
|
||||
|
||||
👉 [**🆕 পরিবর্তন লগ**]({{{changelogUrl}}}) এ নতুন কি আছে তা দেখুন
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **WAHA সমর্থন করুন!**
|
||||
|
||||
আপনি বিনামূল্যের **WAHA Core** সংস্করণ ব্যবহার করছেন। যদি আপনি এই প্রকল্পটি উপযোগী মনে করেন, তবে এর বিকাশকে সমর্থন করার কথা বিবেচনা করুন।
|
||||
|
||||
আপনার সমর্থন হল একমাত্র উপায় যা দিয়ে আমরা আপডেট, ফিক্স এবং নতুন বৈশিষ্ট্যগুলিতে কাজ চালিয়ে যেতে পারি।
|
||||
|
||||
[**🎁 আমাদের সমর্থন করুন**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,183 @@
|
||||
# German translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'WhatsApp Integration (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 Wiederholung in {{attempts.nextDelay}} Sekunden...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ Keine weiteren Versuche übrig. Bitte versuchen Sie es später erneut.
|
||||
{{/attempts.nextDelay}}
|
||||
Versuch: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Details: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **Erfolgreich**
|
||||
Versuch: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Details: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE:
|
||||
Nachricht konnte nicht über WhatsApp gesendet werden.
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE:
|
||||
Nachricht konnte nicht von WhatsApp empfangen werden
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE:
|
||||
Nachricht konnte nicht von WhatsApp entfernt werden
|
||||
APP_SCHEDULED_JOB_ERROR: Geplante Aufgabe konnte nicht ausgeführt werden
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Von WhatsApp gesendet*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Über API gesendet*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *Diese Nachricht wurde in WhatsApp gelöscht*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *In WhatsApp bearbeitet*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*Reagiert mit* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*Reaktion entfernt*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'Gruppe'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'Kanal'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'Status'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **VERBUNDEN: WhatsApp-Sitzung mit ChatWoot Inbox**
|
||||
|
||||
✅ WhatsApp-Sitzung **{{{name}}}** wurde erfolgreich mit dieser ChatWoot Inbox verbunden.
|
||||
|
||||
Diese Inbox ist jetzt mit Ihrem **WhatsApp-Konto** verknüpft. Sie können Nachrichten direkt von hier senden und empfangen — genau wie bei WhatsApp, aber innerhalb von ChatWoot.
|
||||
|
||||
Sie können auch die mit dieser Inbox verknüpfte WhatsApp-Sitzung verwalten. Um verfügbare Aktionen anzuzeigen, senden Sie **help** an diesen Chat.
|
||||
|
||||
📋 Häufige Aktionen, die Sie durchführen können:
|
||||
- Überprüfen, ob die WhatsApp-Sitzung aktiv ist
|
||||
- Die WhatsApp-Sitzung neu starten oder stoppen
|
||||
- Abmelden und durch Scannen eines neuen QR-Codes wieder verbinden
|
||||
|
||||
Wenn alles funktioniert, können Sie frei chatten. Wenn Sie Warnungen sehen, müssen Sie möglicherweise Ihr WhatsApp-Konto neu verbinden.
|
||||
|
||||
Lassen Sie uns beginnen! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **GETRENNT: WhatsApp-Sitzung von ChatWoot Inbox**
|
||||
|
||||
❌ WhatsApp-Sitzung **{{{name}}}** wurde von dieser ChatWoot Inbox getrennt.
|
||||
|
||||
Sie können keine Nachrichten mehr von WhatsApp senden oder empfangen.
|
||||
|
||||
Um eine erneute Verbindung herzustellen, wenden Sie sich bitte an Ihren Administrator.
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **AKTUALISIERT: WhatsApp-Sitzungseinstellungen**
|
||||
|
||||
✅ Einstellungen für WhatsApp-Sitzung **{{{name}}}** wurden aktualisiert.
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
Sie können die **WhatsApp Integration** verwalten, indem Sie einen der folgenden Befehle senden:
|
||||
|
||||
🖥️ Sitzungen:
|
||||
1. **status** - den aktuellen Sitzungsstatus abrufen.
|
||||
2. **restart** - die Sitzung neu starten - dies versucht, Ihr WhatsApp-Konto neu zu verbinden.
|
||||
3. **start** - die Sitzung starten.
|
||||
4. **stop** - die Sitzung stoppen.
|
||||
5. **logout** - von der Sitzung abmelden, damit Sie den QR-Code scannen und die Sitzung reparieren können.
|
||||
6. **qr** - den QR-Code für die Sitzung abrufen.
|
||||
7. **screenshot** - einen Screenshot der Sitzung erhalten (nur in der **WEBJS**-Engine verfügbar).
|
||||
8. **help** - verfügbare Befehle.
|
||||
|
||||
⚙️ Server:
|
||||
- **server status** - Serverversion und Statusinformationen abrufen.
|
||||
- **server reboot** - Server ordnungsgemäß neu starten.
|
||||
- **server reboot force** - Server erzwungen neu starten.
|
||||
|
||||
Senden Sie einen **Befehl** (Groß-/Kleinschreibung wird nicht beachtet) oder eine **Befehlsnummer** an diese Konversation.
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
Senden Sie **help**, um die Liste der verfügbaren Befehle zu erhalten.
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} Sitzung "{{{session}}}" hat den Status zu {{{status}}} gewechselt
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} **{{{session}}}** Sitzungsstatus ist **{{{status}}}**
|
||||
**WhatsApp-Konto:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
Besuchen Sie das [WAHA Dashboard]({{{url}}}) für weitere Details.
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ Diese ChatWoot Inbox ist mit dem **WhatsApp-Konto** verknüpft und bereit zum Senden und Empfangen von Nachrichten!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **Diese ChatWoot Inbox ist mit dem WhatsApp-Konto verknüpft, aber NICHT bereit zum Senden und Empfangen von Nachrichten!**
|
||||
|
||||
Bitte überprüfen Sie den Sitzungsstatus und versuchen Sie, sie neu zu starten oder melden Sie sich ab und scannen Sie den QR-Code erneut.
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **Scannen Sie den QR-Code auf Ihrem Telefon, um das WhatsApp-Konto mit dieser ChatWoot Inbox zu verknüpfen!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**Version:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**Status:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
Server wird ordnungsgemäß neu gestartet...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
Server wird erzwungen neu gestartet...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ Konto erfolgreich abgemeldet!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **Neue Version verfügbar!**
|
||||
|
||||
Aktuelle Version: **{{{currentVersion}}}**
|
||||
Neue Version: **{{{newVersion}}}**
|
||||
|
||||
👉 Sehen Sie, was neu ist im [**🆕 Änderungsprotokoll**]({{{changelogUrl}}})
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **Unterstützen Sie WAHA!**
|
||||
|
||||
Sie verwenden die kostenlose **WAHA Core** Version. Wenn Sie dieses Projekt nützlich finden, erwägen Sie bitte, seine Entwicklung zu unterstützen.
|
||||
|
||||
Ihre Unterstützung ist der einzige Weg, wie wir weiterhin an Updates, Fehlerbehebungen und neuen Funktionen arbeiten können.
|
||||
|
||||
[**🎁 Unterstützen Sie uns**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,181 @@
|
||||
# English translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'WhatsApp Integration (WAHA)'
|
||||
APP_INBOX_CONTACT_AVATAR_URL: 'https://raw.githubusercontent.com/devlikeapro/waha/core/logo.png'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 Retrying in {{attempts.nextDelay}} seconds...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ No more retries left. Please try again later.
|
||||
{{/attempts.nextDelay}}
|
||||
Attempt: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Details: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **Succeeded**
|
||||
Attempt: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Details: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE: Couldn't send a message on WhatsApp.
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE: Couldn't receive a message from WhatsApp
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE: Couldn't remove a message from WhatsApp
|
||||
APP_SCHEDULED_JOB_ERROR: Scheduled job failed to execute
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Sent from WhatsApp*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Sent via API*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *This message was deleted in WhatsApp*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *Edited in WhatsApp*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*Reacted* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*Reaction removed*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'Group'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'Channel'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'Status'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **CONNECTED: WhatsApp Session to ChatWoot Inbox**
|
||||
|
||||
✅ WhatsApp session **{{{name}}}** has been successfully connected to this ChatWoot Inbox.
|
||||
|
||||
This inbox is now linked to your **WhatsApp Account**. You can send and receive messages directly from here — just like using WhatsApp, but inside ChatWoot.
|
||||
|
||||
You can also manage the WhatsApp session linked to this inbox. To view available actions, send **help** to this chat.
|
||||
|
||||
📋 Common actions you can do:
|
||||
- Check if the WhatsApp session is active
|
||||
- Restart or Stop the WhatsApp session
|
||||
- Log out and reconnect by scanning a new QR code
|
||||
|
||||
If everything is working, you'll be able to chat freely. If you see warnings, you might need to reconnect your WhatsApp account.
|
||||
|
||||
Let's get started! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **DISCONNECTED: WhatsApp Session from ChatWoot Inbox**
|
||||
|
||||
❌ WhatsApp session **{{{name}}}** has been disconnected from this ChatWoot Inbox.
|
||||
|
||||
You will no longer be able to send or receive messages from WhatsApp.
|
||||
|
||||
To reconnect, please contact your administrator.
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **UPDATED: WhatsApp Session Settings**
|
||||
|
||||
✅ Settings for WhatsApp session **{{{name}}}** have been updated.
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
You can manage **WhatsApp Integration** by sending one of the following commands:
|
||||
|
||||
🖥️ Sessions:
|
||||
1. **status** - get the current session status.
|
||||
2. **restart** - restart the session - this will try to reconnect your WhatsApp account.
|
||||
3. **start** - start the session.
|
||||
4. **stop** - stop the session.
|
||||
5. **logout** - logout from the session, so you can scan QR code and repair the session.
|
||||
6. **qr** - get the QR code for the session.
|
||||
7. **screenshot** - get a screenshot of the session (only available in **WEBJS** engine).
|
||||
8. **help** - available commands.
|
||||
|
||||
⚙️ Server:
|
||||
- **server status** - get the server version and status information.
|
||||
- **server reboot** - reboot the server gracefully.
|
||||
- **server reboot force** - force reboot the server.
|
||||
|
||||
Send a **command** (case insensitive) or a **command number** to this conversation.
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
Send **help** to get the list of available commands.
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} "{{{session}}}" session switched to {{{status}}} status
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} **{{{session}}}** session status is in **{{{status}}}**
|
||||
**WhatsApp Account:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
Visit [WAHA Dashboard]({{{url}}}) for more details.
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ This ChatWoot Inbox is linked to **WhatsApp Account** and ready to send and receive messages!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **This ChatWoot Inbox is linked to WhatsApp Account but NOT ready to send and receive messages!**
|
||||
|
||||
Please check the session status and try restarting it or logout and scan the QR code again.
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **Scan the QR code on your phone to link WhatsApp Account with this ChatWoot Inbox!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**Version:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**Status:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
Server is rebooting gracefully...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
Server is force rebooting...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ Account unauthorized successfully!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **New Version Available!**
|
||||
|
||||
Current version: **{{{currentVersion}}}**
|
||||
New version: **{{{newVersion}}}**
|
||||
|
||||
👉 Check out what's new in the [**🆕 Changelog**]({{{changelogUrl}}})
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **Support WAHA!**
|
||||
|
||||
You're using the free **WAHA Core** version. If you find this project useful, please consider supporting its development.
|
||||
|
||||
Your support is the only way we can continue working on updates, fixes, and new features.
|
||||
|
||||
[**🎁 Support Us**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,180 @@
|
||||
# Spanish translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'Integración de WhatsApp (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 Reintentando en {{attempts.nextDelay}} segundos...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ No quedan más intentos. Por favor, inténtelo de nuevo más tarde.
|
||||
{{/attempts.nextDelay}}
|
||||
Intento: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Detalles: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **Éxito**
|
||||
Intento: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Detalles: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE: No se pudo enviar un mensaje en WhatsApp.
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE: No se pudo recibir un mensaje de WhatsApp
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE: No se pudo eliminar un mensaje de WhatsApp
|
||||
APP_SCHEDULED_JOB_ERROR: La tarea programada no se pudo ejecutar
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Enviado desde WhatsApp*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Enviado a través de API*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *Este mensaje fue eliminado en WhatsApp*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *Editado en WhatsApp*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*Reaccionó con* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*Reacción eliminada*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'Grupo'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'Canal'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'Estado'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **CONECTADO: Sesión de WhatsApp a Bandeja de Entrada de ChatWoot**
|
||||
|
||||
✅ La sesión de WhatsApp **{{{name}}}** ha sido conectada exitosamente a esta Bandeja de Entrada de ChatWoot.
|
||||
|
||||
Esta bandeja de entrada ahora está vinculada a tu **Cuenta de WhatsApp**. Puedes enviar y recibir mensajes directamente desde aquí — justo como usar WhatsApp, pero dentro de ChatWoot.
|
||||
|
||||
También puedes gestionar la sesión de WhatsApp vinculada a esta bandeja de entrada. Para ver las acciones disponibles, envía **help** a este chat.
|
||||
|
||||
📋 Acciones comunes que puedes realizar:
|
||||
- Comprobar si la sesión de WhatsApp está activa
|
||||
- Reiniciar o Detener la sesión de WhatsApp
|
||||
- Cerrar sesión y reconectar escaneando un nuevo código QR
|
||||
|
||||
Si todo funciona, podrás chatear libremente. Si ves advertencias, es posible que necesites reconectar tu cuenta de WhatsApp.
|
||||
|
||||
¡Comencemos! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **DESCONECTADO: Sesión de WhatsApp de Bandeja de Entrada de ChatWoot**
|
||||
|
||||
❌ La sesión de WhatsApp **{{{name}}}** ha sido desconectada de esta Bandeja de Entrada de ChatWoot.
|
||||
|
||||
Ya no podrás enviar o recibir mensajes desde WhatsApp.
|
||||
|
||||
Para reconectar, por favor contacta a tu administrador.
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **ACTUALIZADO: Configuración de Sesión de WhatsApp**
|
||||
|
||||
✅ La configuración para la sesión de WhatsApp **{{{name}}}** ha sido actualizada.
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
Puedes gestionar la **Integración de WhatsApp** enviando uno de los siguientes comandos:
|
||||
|
||||
🖥️ Sesiones:
|
||||
1. **status** - obtener el estado actual de la sesión.
|
||||
2. **restart** - reiniciar la sesión - esto intentará reconectar tu cuenta de WhatsApp.
|
||||
3. **start** - iniciar la sesión.
|
||||
4. **stop** - detener la sesión.
|
||||
5. **logout** - cerrar sesión, para que puedas escanear el código QR y reparar la sesión.
|
||||
6. **qr** - obtener el código QR para la sesión.
|
||||
7. **screenshot** - obtener una captura de pantalla de la sesión (solo disponible en el motor **WEBJS**).
|
||||
8. **help** - comandos disponibles.
|
||||
|
||||
⚙️ Servidor:
|
||||
- **server status** - obtener información de versión y estado del servidor.
|
||||
- **server reboot** - reiniciar el servidor de forma segura.
|
||||
- **server reboot force** - forzar el reinicio del servidor.
|
||||
|
||||
Envía un **comando** (no sensible a mayúsculas/minúsculas) o un **número de comando** a esta conversación.
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
Envía **help** para obtener la lista de comandos disponibles.
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} La sesión "{{{session}}}" cambió al estado {{{status}}}
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} El estado de la sesión **{{{session}}}** es **{{{status}}}**
|
||||
**Cuenta de WhatsApp:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
Visita el [Panel de WAHA]({{{url}}}) para más detalles.
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ ¡Esta Bandeja de Entrada de ChatWoot está vinculada a la **Cuenta de WhatsApp** y lista para enviar y recibir mensajes!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **¡Esta Bandeja de Entrada de ChatWoot está vinculada a la Cuenta de WhatsApp pero NO está lista para enviar y recibir mensajes!**
|
||||
|
||||
Por favor, comprueba el estado de la sesión e intenta reiniciarla o cierra sesión y escanea el código QR de nuevo.
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **¡Escanea el código QR en tu teléfono para vincular la Cuenta de WhatsApp con esta Bandeja de Entrada de ChatWoot!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**Versión:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**Estado:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
El servidor se está reiniciando de forma segura...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
El servidor se está reiniciando forzadamente...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ ¡Cuenta desautorizada con éxito!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **¡Nueva Versión Disponible!**
|
||||
|
||||
Versión actual: **{{{currentVersion}}}**
|
||||
Nueva versión: **{{{newVersion}}}**
|
||||
|
||||
👉 Mira qué hay de nuevo en el [**🆕 Registro de Cambios**]({{{changelogUrl}}})
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **¡Apoya a WAHA!**
|
||||
|
||||
Estás usando la versión gratuita **WAHA Core**. Si encuentras útil este proyecto, por favor considera apoyar su desarrollo.
|
||||
|
||||
Tu apoyo es la única forma en que podemos seguir trabajando en actualizaciones, correcciones y nuevas funciones.
|
||||
|
||||
[**🎁 Apóyanos**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,180 @@
|
||||
# Persian translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'ادغام واتساپ (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 تلاش مجدد در {{attempts.nextDelay}} ثانیه...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ تلاش دیگری باقی نمانده است. لطفاً بعداً دوباره امتحان کنید.
|
||||
{{/attempts.nextDelay}}
|
||||
تلاش: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
جزئیات: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **موفقیتآمیز**
|
||||
تلاش: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
جزئیات: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE: ارسال پیام در واتساپ امکانپذیر نبود.
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE: دریافت پیام از واتساپ امکانپذیر نبود
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE: حذف پیام از واتساپ امکانپذیر نبود
|
||||
APP_SCHEDULED_JOB_ERROR: اجرای کار زمانبندی شده ناموفق بود
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *ارسال شده از واتساپ*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *ارسال شده از طریق API*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *این پیام در واتساپ حذف شده است*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *ویرایش شده در واتساپ*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*واکنش* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*واکنش حذف شد*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'گروه'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'کانال'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'وضعیت'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **متصل: جلسه واتساپ به صندوق ورودی ChatWoot**
|
||||
|
||||
✅ جلسه واتساپ **{{{name}}}** با موفقیت به این صندوق ورودی ChatWoot متصل شده است.
|
||||
|
||||
این صندوق ورودی اکنون به **حساب واتساپ** شما متصل است. شما میتوانید پیامها را مستقیماً از اینجا ارسال و دریافت کنید — درست مانند استفاده از واتساپ، اما در داخل ChatWoot.
|
||||
|
||||
همچنین میتوانید جلسه واتساپ متصل به این صندوق ورودی را مدیریت کنید. برای مشاهده اقدامات موجود، **help** را به این گفتگو ارسال کنید.
|
||||
|
||||
📋 اقدامات رایجی که میتوانید انجام دهید:
|
||||
- بررسی کنید که آیا جلسه واتساپ فعال است
|
||||
- راهاندازی مجدد یا توقف جلسه واتساپ
|
||||
- خروج و اتصال مجدد با اسکن کد QR جدید
|
||||
|
||||
اگر همه چیز کار میکند، میتوانید آزادانه گفتگو کنید. اگر هشدارهایی میبینید، ممکن است نیاز به اتصال مجدد حساب واتساپ خود داشته باشید.
|
||||
|
||||
بیایید شروع کنیم! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **قطع اتصال: جلسه واتساپ از صندوق ورودی ChatWoot**
|
||||
|
||||
❌ جلسه واتساپ **{{{name}}}** از این صندوق ورودی ChatWoot قطع شده است.
|
||||
|
||||
شما دیگر قادر به ارسال یا دریافت پیام از واتساپ نخواهید بود.
|
||||
|
||||
برای اتصال مجدد، لطفاً با مدیر خود تماس بگیرید.
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **بهروزرسانی: تنظیمات جلسه واتساپ**
|
||||
|
||||
✅ تنظیمات جلسه واتساپ **{{{name}}}** بهروزرسانی شده است.
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
شما میتوانید **ادغام واتساپ** را با ارسال یکی از دستورات زیر مدیریت کنید:
|
||||
|
||||
🖥️ جلسات:
|
||||
1. **status** - دریافت وضعیت فعلی جلسه.
|
||||
2. **restart** - راهاندازی مجدد جلسه - این تلاش خواهد کرد حساب واتساپ شما را مجدداً متصل کند.
|
||||
3. **start** - شروع جلسه.
|
||||
4. **stop** - توقف جلسه.
|
||||
5. **logout** - خروج از جلسه، تا بتوانید کد QR را اسکن کرده و جلسه را تعمیر کنید.
|
||||
6. **qr** - دریافت کد QR برای جلسه.
|
||||
7. **screenshot** - دریافت اسکرینشات از جلسه (فقط در موتور **WEBJS** در دسترس است).
|
||||
8. **help** - دستورات موجود.
|
||||
|
||||
⚙️ سرور:
|
||||
- **server status** - دریافت اطلاعات نسخه و وضعیت سرور.
|
||||
- **server reboot** - راهاندازی مجدد سرور به صورت نرم.
|
||||
- **server reboot force** - راهاندازی مجدد اجباری سرور.
|
||||
|
||||
یک **دستور** (بدون حساسیت به بزرگی و کوچکی حروف) یا **شماره دستور** را به این گفتگو ارسال کنید.
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
برای دریافت لیست دستورات موجود، **help** را ارسال کنید.
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} جلسه "{{{session}}}" به وضعیت {{{status}}} تغییر کرد
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} وضعیت جلسه **{{{session}}}** در **{{{status}}}** است
|
||||
**حساب واتساپ:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
برای جزئیات بیشتر به [داشبورد WAHA]({{{url}}}) مراجعه کنید.
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ این صندوق ورودی ChatWoot به **حساب واتساپ** متصل است و آماده ارسال و دریافت پیام است!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **این صندوق ورودی ChatWoot به حساب واتساپ متصل است اما آماده ارسال و دریافت پیام نیست!**
|
||||
|
||||
لطفاً وضعیت جلسه را بررسی کنید و سعی کنید آن را مجدداً راهاندازی کنید یا خارج شوید و کد QR را دوباره اسکن کنید.
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **کد QR را در تلفن خود اسکن کنید تا حساب واتساپ را با این صندوق ورودی ChatWoot پیوند دهید!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**نسخه:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**وضعیت:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
سرور در حال راهاندازی مجدد نرم است...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
سرور در حال راهاندازی مجدد اجباری است...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ حساب با موفقیت غیرمجاز شد!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **نسخه جدید در دسترس است!**
|
||||
|
||||
نسخه فعلی: **{{{currentVersion}}}**
|
||||
نسخه جدید: **{{{newVersion}}}**
|
||||
|
||||
👉 موارد جدید را در [**🆕 تغییرات**]({{{changelogUrl}}}) مشاهده کنید
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **از WAHA پشتیبانی کنید!**
|
||||
|
||||
شما از نسخه رایگان **WAHA Core** استفاده میکنید. اگر این پروژه را مفید میدانید، لطفاً حمایت از توسعه آن را در نظر بگیرید.
|
||||
|
||||
حمایت شما تنها راهی است که ما میتوانیم به کار بر روی بهروزرسانیها، رفع اشکالات و ویژگیهای جدید ادامه دهیم.
|
||||
|
||||
[**🎁 از ما حمایت کنید**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,180 @@
|
||||
# French translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'Intégration WhatsApp (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 Nouvelle tentative dans {{attempts.nextDelay}} secondes...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ Plus de tentatives restantes. Veuillez réessayer plus tard.
|
||||
{{/attempts.nextDelay}}
|
||||
Tentative : {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Détails : [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **Réussi**
|
||||
Tentative : {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Détails : [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE: Impossible d'envoyer un message sur WhatsApp.
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE: Impossible de recevoir un message de WhatsApp
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE: Impossible de supprimer un message de WhatsApp
|
||||
APP_SCHEDULED_JOB_ERROR: La tâche planifiée n'a pas pu être exécutée
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Envoyé depuis WhatsApp*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Envoyé via API*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *Ce message a été supprimé dans WhatsApp*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *Modifié dans WhatsApp*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*A réagi avec* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*Réaction supprimée*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'Groupe'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'Canal'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'Statut'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **CONNECTÉ : Session WhatsApp à la Boîte de Réception ChatWoot**
|
||||
|
||||
✅ La session WhatsApp **{{{name}}}** a été connectée avec succès à cette Boîte de Réception ChatWoot.
|
||||
|
||||
Cette boîte de réception est maintenant liée à votre **Compte WhatsApp**. Vous pouvez envoyer et recevoir des messages directement d'ici — tout comme utiliser WhatsApp, mais à l'intérieur de ChatWoot.
|
||||
|
||||
Vous pouvez également gérer la session WhatsApp liée à cette boîte de réception. Pour voir les actions disponibles, envoyez **help** à ce chat.
|
||||
|
||||
📋 Actions courantes que vous pouvez effectuer :
|
||||
- Vérifier si la session WhatsApp est active
|
||||
- Redémarrer ou Arrêter la session WhatsApp
|
||||
- Se déconnecter et se reconnecter en scannant un nouveau code QR
|
||||
|
||||
Si tout fonctionne, vous pourrez discuter librement. Si vous voyez des avertissements, vous devrez peut-être reconnecter votre compte WhatsApp.
|
||||
|
||||
Commençons ! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **DÉCONNECTÉ : Session WhatsApp de la Boîte de Réception ChatWoot**
|
||||
|
||||
❌ La session WhatsApp **{{{name}}}** a été déconnectée de cette Boîte de Réception ChatWoot.
|
||||
|
||||
Vous ne pourrez plus envoyer ou recevoir de messages depuis WhatsApp.
|
||||
|
||||
Pour vous reconnecter, veuillez contacter votre administrateur.
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **MIS À JOUR : Paramètres de Session WhatsApp**
|
||||
|
||||
✅ Les paramètres de la session WhatsApp **{{{name}}}** ont été mis à jour.
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
Vous pouvez gérer l'**Intégration WhatsApp** en envoyant l'une des commandes suivantes :
|
||||
|
||||
🖥️ Sessions :
|
||||
1. **status** - obtenir l'état actuel de la session.
|
||||
2. **restart** - redémarrer la session - cela tentera de reconnecter votre compte WhatsApp.
|
||||
3. **start** - démarrer la session.
|
||||
4. **stop** - arrêter la session.
|
||||
5. **logout** - se déconnecter de la session, pour pouvoir scanner le code QR et réparer la session.
|
||||
6. **qr** - obtenir le code QR pour la session.
|
||||
7. **screenshot** - obtenir une capture d'écran de la session (disponible uniquement dans le moteur **WEBJS**).
|
||||
8. **help** - commandes disponibles.
|
||||
|
||||
⚙️ Serveur :
|
||||
- **server status** - obtenir les informations de version et d'état du serveur.
|
||||
- **server reboot** - redémarrer le serveur en douceur.
|
||||
- **server reboot force** - forcer le redémarrage du serveur.
|
||||
|
||||
Envoyez une **commande** (insensible à la casse) ou un **numéro de commande** à cette conversation.
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
Envoyez **help** pour obtenir la liste des commandes disponibles.
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} La session "{{{session}}}" est passée au statut {{{status}}}
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} Le statut de la session **{{{session}}}** est **{{{status}}}**
|
||||
**Compte WhatsApp :**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
Visitez le [Tableau de bord WAHA]({{{url}}}) pour plus de détails.
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ Cette Boîte de Réception ChatWoot est liée au **Compte WhatsApp** et prête à envoyer et recevoir des messages !
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **Cette Boîte de Réception ChatWoot est liée au Compte WhatsApp mais N'EST PAS prête à envoyer et recevoir des messages !**
|
||||
|
||||
Veuillez vérifier l'état de la session et essayer de la redémarrer ou déconnectez-vous et scannez à nouveau le code QR.
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **Scannez le code QR sur votre téléphone pour lier le Compte WhatsApp à cette Boîte de Réception ChatWoot !**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**Version :**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**Statut :**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
Le serveur redémarre en douceur...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
Le serveur redémarre de force...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ Compte déconnecté avec succès !
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **Nouvelle Version Disponible !**
|
||||
|
||||
Version actuelle : **{{{currentVersion}}}**
|
||||
Nouvelle version : **{{{newVersion}}}**
|
||||
|
||||
👉 Découvrez les nouveautés dans le [**🆕 Journal des modifications**]({{{changelogUrl}}})
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **Soutenez WAHA !**
|
||||
|
||||
Vous utilisez la version gratuite **WAHA Core**. Si vous trouvez ce projet utile, veuillez envisager de soutenir son développement.
|
||||
|
||||
Votre soutien est le seul moyen pour nous de continuer à travailler sur les mises à jour, les corrections et les nouvelles fonctionnalités.
|
||||
|
||||
[**🎁 Soutenez-nous**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,180 @@
|
||||
# Hebrew translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'אינטגרציית וואטסאפ (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 מנסה שוב בעוד {{attempts.nextDelay}} שניות...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ לא נותרו ניסיונות נוספים. אנא נסה שוב מאוחר יותר.
|
||||
{{/attempts.nextDelay}}
|
||||
ניסיון: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
פרטים: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **הצליח**
|
||||
ניסיון: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
פרטים: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE: לא ניתן לשלוח הודעה בוואטסאפ.
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE: לא ניתן לקבל הודעה מוואטסאפ
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE: לא ניתן להסיר הודעה מוואטסאפ
|
||||
APP_SCHEDULED_JOB_ERROR: המשימה המתוזמנת נכשלה בביצוע
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *נשלח מוואטסאפ*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *נשלח דרך API*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *הודעה זו נמחקה בוואטסאפ*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *נערך בוואטסאפ*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*הגיב* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*התגובה הוסרה*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'קבוצה'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'ערוץ'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'סטטוס'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **מחובר: הפעלת וואטסאפ לתיבת הדואר הנכנס של ChatWoot**
|
||||
|
||||
✅ הפעלת וואטסאפ **{{{name}}}** חוברה בהצלחה לתיבת הדואר הנכנס של ChatWoot.
|
||||
|
||||
תיבת דואר נכנס זו מקושרת כעת ל**חשבון הוואטסאפ** שלך. אתה יכול לשלוח ולקבל הודעות ישירות מכאן — בדיוק כמו שימוש בוואטסאפ, אבל בתוך ChatWoot.
|
||||
|
||||
אתה יכול גם לנהל את הפעלת הוואטסאפ המקושרת לתיבת דואר נכנס זו. כדי לראות פעולות זמינות, שלח **help** לצ'אט זה.
|
||||
|
||||
📋 פעולות נפוצות שאתה יכול לעשות:
|
||||
- בדוק אם הפעלת הוואטסאפ פעילה
|
||||
- הפעל מחדש או עצור את הפעלת הוואטסאפ
|
||||
- התנתק והתחבר מחדש על ידי סריקת קוד QR חדש
|
||||
|
||||
אם הכל עובד, תוכל לשוחח בחופשיות. אם אתה רואה אזהרות, ייתכן שתצטרך לחבר מחדש את חשבון הוואטסאפ שלך.
|
||||
|
||||
בוא נתחיל! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **מנותק: הפעלת וואטסאפ מתיבת הדואר הנכנס של ChatWoot**
|
||||
|
||||
❌ הפעלת וואטסאפ **{{{name}}}** נותקה מתיבת הדואר הנכנס של ChatWoot.
|
||||
|
||||
לא תוכל יותר לשלוח או לקבל הודעות מוואטסאפ.
|
||||
|
||||
כדי להתחבר מחדש, אנא פנה למנהל המערכת שלך.
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **עודכן: הגדרות הפעלת וואטסאפ**
|
||||
|
||||
✅ ההגדרות עבור הפעלת וואטסאפ **{{{name}}}** עודכנו.
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
אתה יכול לנהל **אינטגרציית וואטסאפ** על ידי שליחת אחת מהפקודות הבאות:
|
||||
|
||||
🖥️ הפעלות:
|
||||
1. **status** - קבל את סטטוס ההפעלה הנוכחי.
|
||||
2. **restart** - הפעל מחדש את ההפעלה - זה ינסה לחבר מחדש את חשבון הוואטסאפ שלך.
|
||||
3. **start** - התחל את ההפעלה.
|
||||
4. **stop** - עצור את ההפעלה.
|
||||
5. **logout** - התנתק מההפעלה, כך שתוכל לסרוק קוד QR ולתקן את ההפעלה.
|
||||
6. **qr** - קבל את קוד ה-QR עבור ההפעלה.
|
||||
7. **screenshot** - קבל צילום מסך של ההפעלה (זמין רק במנוע **WEBJS**).
|
||||
8. **help** - פקודות זמינות.
|
||||
|
||||
⚙️ שרת:
|
||||
- **server status** - קבל מידע על גרסת השרת והסטטוס.
|
||||
- **server reboot** - הפעל מחדש את השרת בצורה חלקה.
|
||||
- **server reboot force** - אלץ הפעלה מחדש של השרת.
|
||||
|
||||
שלח **פקודה** (לא תלוי רישיות) או **מספר פקודה** לשיחה זו.
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
שלח **help** כדי לקבל את רשימת הפקודות הזמינות.
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} הפעלה "{{{session}}}" עברה לסטטוס {{{status}}}
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} סטטוס ההפעלה **{{{session}}}** הוא **{{{status}}}**
|
||||
**חשבון וואטסאפ:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
בקר ב[לוח הבקרה של WAHA]({{{url}}}) לפרטים נוספים.
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ תיבת הדואר הנכנס של ChatWoot מקושרת ל**חשבון וואטסאפ** ומוכנה לשלוח ולקבל הודעות!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **תיבת הדואר הנכנס של ChatWoot מקושרת לחשבון וואטסאפ אך אינה מוכנה לשלוח ולקבל הודעות!**
|
||||
|
||||
אנא בדוק את סטטוס ההפעלה ונסה להפעיל אותה מחדש או התנתק וסרוק את קוד ה-QR שוב.
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **סרוק את קוד ה-QR בטלפון שלך כדי לקשר את חשבון הוואטסאפ לתיבת הדואר הנכנס של ChatWoot!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**גרסה:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**סטטוס:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
השרת מופעל מחדש בצורה חלקה...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
השרת מופעל מחדש בכוח...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ החשבון בוטל בהצלחה!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **גרסה חדשה זמינה!**
|
||||
|
||||
גרסה נוכחית: **{{{currentVersion}}}**
|
||||
גרסה חדשה: **{{{newVersion}}}**
|
||||
|
||||
👉 בדוק מה חדש ב[**🆕 יומן שינויים**]({{{changelogUrl}}})
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **תמוך ב-WAHA!**
|
||||
|
||||
אתה משתמש בגרסת **WAHA Core** החינמית. אם אתה מוצא פרויקט זה שימושי, אנא שקול לתמוך בפיתוח שלו.
|
||||
|
||||
התמיכה שלך היא הדרך היחידה שבה אנחנו יכולים להמשיך לעבוד על עדכונים, תיקונים ותכונות חדשות.
|
||||
|
||||
[**🎁 תמוך בנו**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,180 @@
|
||||
# Hindi translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'व्हाट्सएप इंटीग्रेशन (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 {{attempts.nextDelay}} सेकंड में पुनः प्रयास कर रहा है...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ कोई और प्रयास नहीं बचा है। कृपया बाद में पुनः प्रयास करें।
|
||||
{{/attempts.nextDelay}}
|
||||
प्रयास: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
विवरण: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **सफल**
|
||||
प्रयास: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
विवरण: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE: व्हाट्सएप पर संदेश नहीं भेज सका।
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE: व्हाट्सएप से संदेश प्राप्त नहीं कर सका
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE: व्हाट्सएप से संदेश हटा नहीं सका
|
||||
APP_SCHEDULED_JOB_ERROR: निर्धारित कार्य निष्पादित करने में विफल रहा
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *व्हाट्सएप से भेजा गया*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *API के माध्यम से भेजा गया*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *यह संदेश व्हाट्सएप में हटा दिया गया था*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *व्हाट्सएप में संपादित*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*प्रतिक्रिया दी* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*प्रतिक्रिया हटा दी गई*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'समूह'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'चैनल'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'स्थिति'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **कनेक्टेड: व्हाट्सएप सत्र से ChatWoot इनबॉक्स**
|
||||
|
||||
✅ व्हाट्सएप सत्र **{{{name}}}** सफलतापूर्वक इस ChatWoot इनबॉक्स से जुड़ गया है।
|
||||
|
||||
यह इनबॉक्स अब आपके **व्हाट्सएप खाते** से जुड़ा हुआ है। आप यहां से सीधे संदेश भेज और प्राप्त कर सकते हैं — बिल्कुल व्हाट्सएप का उपयोग करने जैसा, लेकिन ChatWoot के अंदर।
|
||||
|
||||
आप इस इनबॉक्स से जुड़े व्हाट्सएप सत्र को भी प्रबंधित कर सकते हैं। उपलब्ध कार्यों को देखने के लिए, इस चैट पर **help** भेजें।
|
||||
|
||||
📋 आप जो सामान्य कार्य कर सकते हैं:
|
||||
- जांचें कि क्या व्हाट्सएप सत्र सक्रिय है
|
||||
- व्हाट्सएप सत्र को पुनरारंभ या बंद करें
|
||||
- लॉग आउट करें और नए QR कोड को स्कैन करके फिर से कनेक्ट करें
|
||||
|
||||
यदि सब कुछ काम कर रहा है, तो आप स्वतंत्र रूप से चैट कर पाएंगे। यदि आप चेतावनियां देखते हैं, तो आपको अपने व्हाट्सएप खाते को फिर से कनेक्ट करने की आवश्यकता हो सकती है।
|
||||
|
||||
चलिए शुरू करते हैं! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **डिस्कनेक्टेड: व्हाट्सएप सत्र से ChatWoot इनबॉक्स**
|
||||
|
||||
❌ व्हाट्सएप सत्र **{{{name}}}** इस ChatWoot इनबॉक्स से डिस्कनेक्ट हो गया है।
|
||||
|
||||
आप अब व्हाट्सएप से संदेश भेज या प्राप्त नहीं कर पाएंगे।
|
||||
|
||||
फिर से कनेक्ट करने के लिए, कृपया अपने प्रशासक से संपर्क करें।
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **अपडेटेड: व्हाट्सएप सत्र सेटिंग्स**
|
||||
|
||||
✅ व्हाट्सएप सत्र **{{{name}}}** की सेटिंग्स अपडेट की गई हैं।
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
आप निम्न कमांड में से एक भेजकर **व्हाट्सएप इंटीग्रेशन** का प्रबंधन कर सकते हैं:
|
||||
|
||||
🖥️ सत्र:
|
||||
1. **status** - वर्तमान सत्र स्थिति प्राप्त करें।
|
||||
2. **restart** - सत्र को पुनरारंभ करें - यह आपके व्हाट्सएप खाते को फिर से कनेक्ट करने का प्रयास करेगा।
|
||||
3. **start** - सत्र शुरू करें।
|
||||
4. **stop** - सत्र बंद करें।
|
||||
5. **logout** - सत्र से लॉग आउट करें, ताकि आप QR कोड स्कैन कर सकें और सत्र को मरम्मत कर सकें।
|
||||
6. **qr** - सत्र के लिए QR कोड प्राप्त करें।
|
||||
7. **screenshot** - सत्र का स्क्रीनशॉट प्राप्त करें (केवल **WEBJS** इंजन में उपलब्ध)।
|
||||
8. **help** - उपलब्ध कमांड।
|
||||
|
||||
⚙️ सर्वर:
|
||||
- **server status** - सर्वर संस्करण और स्थिति जानकारी प्राप्त करें।
|
||||
- **server reboot** - सर्वर को सुचारू रूप से पुनरारंभ करें।
|
||||
- **server reboot force** - सर्वर को जबरदस्ती पुनरारंभ करें।
|
||||
|
||||
इस वार्तालाप में **कमांड** (केस इनसेंसिटिव) या **कमांड नंबर** भेजें।
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
उपलब्ध कमांड की सूची प्राप्त करने के लिए **help** भेजें।
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} "{{{session}}}" सत्र {{{status}}} स्थिति में बदल गया
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} **{{{session}}}** सत्र की स्थिति **{{{status}}}** में है
|
||||
**व्हाट्सएप खाता:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
अधिक जानकारी के लिए [WAHA डैशबोर्ड]({{{url}}}) पर जाएं।
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ यह ChatWoot इनबॉक्स **व्हाट्सएप खाते** से जुड़ा हुआ है और संदेश भेजने और प्राप्त करने के लिए तैयार है!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **यह ChatWoot इनबॉक्स व्हाट्सएप खाते से जुड़ा हुआ है लेकिन संदेश भेजने और प्राप्त करने के लिए तैयार नहीं है!**
|
||||
|
||||
कृपया सत्र की स्थिति जांचें और इसे पुनरारंभ करने का प्रयास करें या लॉग आउट करें और QR कोड फिर से स्कैन करें।
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **इस ChatWoot इनबॉक्स के साथ व्हाट्सएप खाता जोड़ने के लिए अपने फोन पर QR कोड स्कैन करें!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**संस्करण:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**स्थिति:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
सर्वर सुचारू रूप से पुनरारंभ हो रहा है...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
सर्वर जबरदस्ती पुनरारंभ हो रहा है...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ खाता सफलतापूर्वक अनधिकृत किया गया!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **नया संस्करण उपलब्ध!**
|
||||
|
||||
वर्तमान संस्करण: **{{{currentVersion}}}**
|
||||
नया संस्करण: **{{{newVersion}}}**
|
||||
|
||||
👉 [**🆕 चेंजलॉग**]({{{changelogUrl}}}) में क्या नया है देखें
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **WAHA का समर्थन करें!**
|
||||
|
||||
आप मुफ्त **WAHA Core** संस्करण का उपयोग कर रहे हैं। यदि आपको यह प्रोजेक्ट उपयोगी लगता है, तो कृपया इसके विकास का समर्थन करने पर विचार करें।
|
||||
|
||||
आपका समर्थन ही एकमात्र तरीका है जिससे हम अपडेट, फिक्स और नई सुविधाओं पर काम करना जारी रख सकते हैं।
|
||||
|
||||
[**🎁 हमें समर्थन दें**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,180 @@
|
||||
# Indonesian translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'Integrasi WhatsApp (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 Mencoba lagi dalam {{attempts.nextDelay}} detik...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ Tidak ada percobaan lagi. Silakan coba lagi nanti.
|
||||
{{/attempts.nextDelay}}
|
||||
Percobaan: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Detail: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **Berhasil**
|
||||
Percobaan: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Detail: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE: Tidak dapat mengirim pesan di WhatsApp.
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE: Tidak dapat menerima pesan dari WhatsApp
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE: Tidak dapat menghapus pesan dari WhatsApp
|
||||
APP_SCHEDULED_JOB_ERROR: Tugas terjadwal gagal dijalankan
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Dikirim dari WhatsApp*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Dikirim melalui API*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *Pesan ini telah dihapus di WhatsApp*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *Diedit di WhatsApp*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*Bereaksi* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*Reaksi dihapus*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'Grup'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'Saluran'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'Status'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **TERHUBUNG: Sesi WhatsApp ke Kotak Masuk ChatWoot**
|
||||
|
||||
✅ Sesi WhatsApp **{{{name}}}** telah berhasil terhubung ke Kotak Masuk ChatWoot ini.
|
||||
|
||||
Kotak masuk ini sekarang terhubung ke **Akun WhatsApp** Anda. Anda dapat mengirim dan menerima pesan langsung dari sini — seperti menggunakan WhatsApp, tetapi di dalam ChatWoot.
|
||||
|
||||
Anda juga dapat mengelola sesi WhatsApp yang terhubung ke kotak masuk ini. Untuk melihat tindakan yang tersedia, kirim **help** ke obrolan ini.
|
||||
|
||||
📋 Tindakan umum yang dapat Anda lakukan:
|
||||
- Periksa apakah sesi WhatsApp aktif
|
||||
- Mulai ulang atau Hentikan sesi WhatsApp
|
||||
- Keluar dan hubungkan kembali dengan memindai kode QR baru
|
||||
|
||||
Jika semuanya berfungsi, Anda akan dapat mengobrol dengan bebas. Jika Anda melihat peringatan, Anda mungkin perlu menghubungkan kembali akun WhatsApp Anda.
|
||||
|
||||
Mari kita mulai! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **TERPUTUS: Sesi WhatsApp dari Kotak Masuk ChatWoot**
|
||||
|
||||
❌ Sesi WhatsApp **{{{name}}}** telah terputus dari Kotak Masuk ChatWoot ini.
|
||||
|
||||
Anda tidak akan dapat lagi mengirim atau menerima pesan dari WhatsApp.
|
||||
|
||||
Untuk menghubungkan kembali, silakan hubungi administrator Anda.
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **DIPERBARUI: Pengaturan Sesi WhatsApp**
|
||||
|
||||
✅ Pengaturan untuk sesi WhatsApp **{{{name}}}** telah diperbarui.
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
Anda dapat mengelola **Integrasi WhatsApp** dengan mengirimkan salah satu perintah berikut:
|
||||
|
||||
🖥️ Sesi:
|
||||
1. **status** - dapatkan status sesi saat ini.
|
||||
2. **restart** - mulai ulang sesi - ini akan mencoba menghubungkan kembali akun WhatsApp Anda.
|
||||
3. **start** - mulai sesi.
|
||||
4. **stop** - hentikan sesi.
|
||||
5. **logout** - keluar dari sesi, sehingga Anda dapat memindai kode QR dan memperbaiki sesi.
|
||||
6. **qr** - dapatkan kode QR untuk sesi.
|
||||
7. **screenshot** - dapatkan tangkapan layar sesi (hanya tersedia di mesin **WEBJS**).
|
||||
8. **help** - perintah yang tersedia.
|
||||
|
||||
⚙️ Server:
|
||||
- **server status** - dapatkan informasi versi dan status server.
|
||||
- **server reboot** - mulai ulang server dengan anggun.
|
||||
- **server reboot force** - paksa mulai ulang server.
|
||||
|
||||
Kirim **perintah** (tidak peka huruf besar/kecil) atau **nomor perintah** ke percakapan ini.
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
Kirim **help** untuk mendapatkan daftar perintah yang tersedia.
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} Sesi "{{{session}}}" beralih ke status {{{status}}}
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} Status sesi **{{{session}}}** adalah **{{{status}}}**
|
||||
**Akun WhatsApp:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
Kunjungi [Dasbor WAHA]({{{url}}}) untuk detail lebih lanjut.
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ Kotak Masuk ChatWoot ini terhubung ke **Akun WhatsApp** dan siap untuk mengirim dan menerima pesan!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **Kotak Masuk ChatWoot ini terhubung ke Akun WhatsApp tetapi TIDAK siap untuk mengirim dan menerima pesan!**
|
||||
|
||||
Silakan periksa status sesi dan coba mulai ulang atau keluar dan pindai kode QR lagi.
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **Pindai kode QR di ponsel Anda untuk menghubungkan Akun WhatsApp dengan Kotak Masuk ChatWoot ini!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**Versi:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**Status:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
Server sedang dimulai ulang dengan anggun...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
Server sedang dipaksa untuk dimulai ulang...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ Akun berhasil tidak diotorisasi!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **Versi Baru Tersedia!**
|
||||
|
||||
Versi saat ini: **{{{currentVersion}}}**
|
||||
Versi baru: **{{{newVersion}}}**
|
||||
|
||||
👉 Lihat apa yang baru di [**🆕 Changelog**]({{{changelogUrl}}})
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **Dukung WAHA!**
|
||||
|
||||
Anda menggunakan versi **WAHA Core** gratis. Jika Anda merasa proyek ini bermanfaat, harap pertimbangkan untuk mendukung pengembangannya.
|
||||
|
||||
Dukungan Anda adalah satu-satunya cara kami dapat terus bekerja pada pembaruan, perbaikan, dan fitur baru.
|
||||
|
||||
[**🎁 Dukung Kami**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,180 @@
|
||||
# Punjabi translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'ਵਟਸਐਪ ਇੰਟੀਗ੍ਰੇਸ਼ਨ (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 {{attempts.nextDelay}} ਸਕਿੰਟਾਂ ਵਿੱਚ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰ ਰਿਹਾ ਹੈ...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ ਕੋਈ ਹੋਰ ਕੋਸ਼ਿਸ਼ਾਂ ਨਹੀਂ ਬਚੀਆਂ। ਕਿਰਪਾ ਕਰਕੇ ਬਾਅਦ ਵਿੱਚ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।
|
||||
{{/attempts.nextDelay}}
|
||||
ਕੋਸ਼ਿਸ਼: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
ਵੇਰਵੇ: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **ਸਫਲ**
|
||||
ਕੋਸ਼ਿਸ਼: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
ਵੇਰਵੇ: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE: ਵਟਸਐਪ 'ਤੇ ਸੁਨੇਹਾ ਭੇਜ ਨਹੀਂ ਸਕਿਆ।
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE: ਵਟਸਐਪ ਤੋਂ ਸੁਨੇਹਾ ਪ੍ਰਾਪਤ ਨਹੀਂ ਕਰ ਸਕਿਆ
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE: ਵਟਸਐਪ ਤੋਂ ਸੁਨੇਹਾ ਹਟਾ ਨਹੀਂ ਸਕਿਆ
|
||||
APP_SCHEDULED_JOB_ERROR: ਨਿਰਧਾਰਿਤ ਕੰਮ ਚਲਾਉਣ ਵਿੱਚ ਅਸਫਲ ਰਿਹਾ
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *ਵਟਸਐਪ ਤੋਂ ਭੇਜਿਆ ਗਿਆ*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *API ਰਾਹੀਂ ਭੇਜਿਆ ਗਿਆ*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *ਇਹ ਸੁਨੇਹਾ ਵਟਸਐਪ ਵਿੱਚ ਮਿਟਾ ਦਿੱਤਾ ਗਿਆ ਸੀ*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *ਵਟਸਐਪ ਵਿੱਚ ਸੰਪਾਦਿਤ ਕੀਤਾ ਗਿਆ*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*ਪ੍ਰਤੀਕਿਰਿਆ ਕੀਤੀ* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*ਪ੍ਰਤੀਕਿਰਿਆ ਹਟਾਈ ਗਈ*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'ਗਰੁੱਪ'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'ਚੈਨਲ'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'ਸਥਿਤੀ'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **ਕਨੈਕਟ ਕੀਤਾ: ਵਟਸਐਪ ਸੈਸ਼ਨ ਤੋਂ ChatWoot ਇਨਬਾਕਸ**
|
||||
|
||||
✅ ਵਟਸਐਪ ਸੈਸ਼ਨ **{{{name}}}** ਨੂੰ ਸਫਲਤਾਪੂਰਵਕ ਇਸ ChatWoot ਇਨਬਾਕਸ ਨਾਲ ਜੋੜਿਆ ਗਿਆ ਹੈ।
|
||||
|
||||
ਇਹ ਇਨਬਾਕਸ ਹੁਣ ਤੁਹਾਡੇ **ਵਟਸਐਪ ਖਾਤੇ** ਨਾਲ ਜੁੜਿਆ ਹੋਇਆ ਹੈ। ਤੁਸੀਂ ਇੱਥੋਂ ਸਿੱਧੇ ਸੁਨੇਹੇ ਭੇਜ ਅਤੇ ਪ੍ਰਾਪਤ ਕਰ ਸਕਦੇ ਹੋ — ਬਿਲਕੁਲ ਵਟਸਐਪ ਦੀ ਵਰਤੋਂ ਵਾਂਗ, ਪਰ ChatWoot ਦੇ ਅੰਦਰ।
|
||||
|
||||
ਤੁਸੀਂ ਇਸ ਇਨਬਾਕਸ ਨਾਲ ਜੁੜੇ ਵਟਸਐਪ ਸੈਸ਼ਨ ਦਾ ਪ੍ਰਬੰਧਨ ਵੀ ਕਰ ਸਕਦੇ ਹੋ। ਉਪਲਬਧ ਕਾਰਵਾਈਆਂ ਦੇਖਣ ਲਈ, ਇਸ ਚੈਟ ਨੂੰ **help** ਭੇਜੋ।
|
||||
|
||||
📋 ਆਮ ਕਾਰਵਾਈਆਂ ਜੋ ਤੁਸੀਂ ਕਰ ਸਕਦੇ ਹੋ:
|
||||
- ਜਾਂਚ ਕਰੋ ਕਿ ਕੀ ਵਟਸਐਪ ਸੈਸ਼ਨ ਸਰਗਰਮ ਹੈ
|
||||
- ਵਟਸਐਪ ਸੈਸ਼ਨ ਨੂੰ ਮੁੜ ਚਾਲੂ ਜਾਂ ਬੰਦ ਕਰੋ
|
||||
- ਲੌਗ ਆਉਟ ਕਰੋ ਅਤੇ ਨਵਾਂ QR ਕੋਡ ਸਕੈਨ ਕਰਕੇ ਮੁੜ ਕਨੈਕਟ ਕਰੋ
|
||||
|
||||
ਜੇ ਸਭ ਕੁਝ ਕੰਮ ਕਰ ਰਿਹਾ ਹੈ, ਤਾਂ ਤੁਸੀਂ ਆਜ਼ਾਦੀ ਨਾਲ ਗੱਲਬਾਤ ਕਰ ਸਕੋਗੇ। ਜੇ ਤੁਸੀਂ ਚੇਤਾਵਨੀਆਂ ਦੇਖਦੇ ਹੋ, ਤਾਂ ਤੁਹਾਨੂੰ ਆਪਣੇ ਵਟਸਐਪ ਖਾਤੇ ਨੂੰ ਮੁੜ ਕਨੈਕਟ ਕਰਨ ਦੀ ਜ਼ਰੂਰਤ ਹੋ ਸਕਦੀ ਹੈ।
|
||||
|
||||
ਆਓ ਸ਼ੁਰੂ ਕਰੀਏ! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **ਡਿਸਕਨੈਕਟ ਕੀਤਾ: ਵਟਸਐਪ ਸੈਸ਼ਨ ਤੋਂ ChatWoot ਇਨਬਾਕਸ**
|
||||
|
||||
❌ ਵਟਸਐਪ ਸੈਸ਼ਨ **{{{name}}}** ਇਸ ChatWoot ਇਨਬਾਕਸ ਤੋਂ ਡਿਸਕਨੈਕਟ ਕੀਤਾ ਗਿਆ ਹੈ।
|
||||
|
||||
ਤੁਸੀਂ ਹੁਣ ਵਟਸਐਪ ਤੋਂ ਸੁਨੇਹੇ ਭੇਜ ਜਾਂ ਪ੍ਰਾਪਤ ਨਹੀਂ ਕਰ ਸਕੋਗੇ।
|
||||
|
||||
ਮੁੜ ਕਨੈਕਟ ਕਰਨ ਲਈ, ਕਿਰਪਾ ਕਰਕੇ ਆਪਣੇ ਪ੍ਰਸ਼ਾਸਕ ਨਾਲ ਸੰਪਰਕ ਕਰੋ।
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **ਅਪਡੇਟ ਕੀਤਾ: ਵਟਸਐਪ ਸੈਸ਼ਨ ਸੈਟਿੰਗਾਂ**
|
||||
|
||||
✅ ਵਟਸਐਪ ਸੈਸ਼ਨ **{{{name}}}** ਲਈ ਸੈਟਿੰਗਾਂ ਅਪਡੇਟ ਕੀਤੀਆਂ ਗਈਆਂ ਹਨ।
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
ਤੁਸੀਂ ਹੇਠਾਂ ਦਿੱਤੇ ਕਮਾਂਡਾਂ ਵਿੱਚੋਂ ਇੱਕ ਭੇਜ ਕੇ **ਵਟਸਐਪ ਇੰਟੀਗ੍ਰੇਸ਼ਨ** ਦਾ ਪ੍ਰਬੰਧਨ ਕਰ ਸਕਦੇ ਹੋ:
|
||||
|
||||
🖥️ ਸੈਸ਼ਨ:
|
||||
1. **status** - ਮੌਜੂਦਾ ਸੈਸ਼ਨ ਸਥਿਤੀ ਪ੍ਰਾਪਤ ਕਰੋ।
|
||||
2. **restart** - ਸੈਸ਼ਨ ਮੁੜ ਚਾਲੂ ਕਰੋ - ਇਹ ਤੁਹਾਡੇ ਵਟਸਐਪ ਖਾਤੇ ਨੂੰ ਮੁੜ ਕਨੈਕਟ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੇਗਾ।
|
||||
3. **start** - ਸੈਸ਼ਨ ਸ਼ੁਰੂ ਕਰੋ।
|
||||
4. **stop** - ਸੈਸ਼ਨ ਬੰਦ ਕਰੋ।
|
||||
5. **logout** - ਸੈਸ਼ਨ ਤੋਂ ਲੌਗਆਉਟ ਕਰੋ, ਤਾਂ ਜੋ ਤੁਸੀਂ QR ਕੋਡ ਸਕੈਨ ਕਰ ਸਕੋ ਅਤੇ ਸੈਸ਼ਨ ਨੂੰ ਮੁਰੰਮਤ ਕਰ ਸਕੋ।
|
||||
6. **qr** - ਸੈਸ਼ਨ ਲਈ QR ਕੋਡ ਪ੍ਰਾਪਤ ਕਰੋ।
|
||||
7. **screenshot** - ਸੈਸ਼ਨ ਦਾ ਸਕ੍ਰੀਨਸ਼ਾਟ ਪ੍ਰਾਪਤ ਕਰੋ (ਸਿਰਫ **WEBJS** ਇੰਜਣ ਵਿੱਚ ਉਪਲਬਧ)।
|
||||
8. **help** - ਉਪਲਬਧ ਕਮਾਂਡਾਂ।
|
||||
|
||||
⚙️ ਸਰਵਰ:
|
||||
- **server status** - ਸਰਵਰ ਵਰਜਨ ਅਤੇ ਸਥਿਤੀ ਜਾਣਕਾਰੀ ਪ੍ਰਾਪਤ ਕਰੋ।
|
||||
- **server reboot** - ਸਰਵਰ ਨੂੰ ਸੁਚਾਰੂ ਢੰਗ ਨਾਲ ਮੁੜ ਚਾਲੂ ਕਰੋ।
|
||||
- **server reboot force** - ਸਰਵਰ ਨੂੰ ਜ਼ਬਰਦਸਤੀ ਮੁੜ ਚਾਲੂ ਕਰੋ।
|
||||
|
||||
ਇਸ ਗੱਲਬਾਤ ਵਿੱਚ **ਕਮਾਂਡ** (ਕੇਸ ਇਨਸੈਂਸਿਟਿਵ) ਜਾਂ **ਕਮਾਂਡ ਨੰਬਰ** ਭੇਜੋ।
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
ਉਪਲਬਧ ਕਮਾਂਡਾਂ ਦੀ ਸੂਚੀ ਪ੍ਰਾਪਤ ਕਰਨ ਲਈ **help** ਭੇਜੋ।
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} "{{{session}}}" ਸੈਸ਼ਨ {{{status}}} ਸਥਿਤੀ 'ਤੇ ਬਦਲ ਗਿਆ
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} **{{{session}}}** ਸੈਸ਼ਨ ਸਥਿਤੀ **{{{status}}}** ਵਿੱਚ ਹੈ
|
||||
**ਵਟਸਐਪ ਖਾਤਾ:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
ਵਧੇਰੇ ਵੇਰਵਿਆਂ ਲਈ [WAHA ਡੈਸ਼ਬੋਰਡ]({{{url}}}) 'ਤੇ ਜਾਓ।
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ ਇਹ ChatWoot ਇਨਬਾਕਸ **ਵਟਸਐਪ ਖਾਤੇ** ਨਾਲ ਜੁੜਿਆ ਹੋਇਆ ਹੈ ਅਤੇ ਸੁਨੇਹੇ ਭੇਜਣ ਅਤੇ ਪ੍ਰਾਪਤ ਕਰਨ ਲਈ ਤਿਆਰ ਹੈ!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **ਇਹ ChatWoot ਇਨਬਾਕਸ ਵਟਸਐਪ ਖਾਤੇ ਨਾਲ ਜੁੜਿਆ ਹੋਇਆ ਹੈ ਪਰ ਸੁਨੇਹੇ ਭੇਜਣ ਅਤੇ ਪ੍ਰਾਪਤ ਕਰਨ ਲਈ ਤਿਆਰ ਨਹੀਂ ਹੈ!**
|
||||
|
||||
ਕਿਰਪਾ ਕਰਕੇ ਸੈਸ਼ਨ ਸਥਿਤੀ ਦੀ ਜਾਂਚ ਕਰੋ ਅਤੇ ਇਸਨੂੰ ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜਾਂ ਲੌਗਆਉਟ ਕਰੋ ਅਤੇ QR ਕੋਡ ਦੁਬਾਰਾ ਸਕੈਨ ਕਰੋ।
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **ਵਟਸਐਪ ਖਾਤੇ ਨੂੰ ਇਸ ChatWoot ਇਨਬਾਕਸ ਨਾਲ ਜੋੜਨ ਲਈ ਆਪਣੇ ਫੋਨ 'ਤੇ QR ਕੋਡ ਸਕੈਨ ਕਰੋ!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**ਵਰਜਨ:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**ਸਥਿਤੀ:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
ਸਰਵਰ ਸੁਚਾਰੂ ਢੰਗ ਨਾਲ ਮੁੜ ਚਾਲੂ ਹੋ ਰਿਹਾ ਹੈ...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
ਸਰਵਰ ਜ਼ਬਰਦਸਤੀ ਮੁੜ ਚਾਲੂ ਹੋ ਰਿਹਾ ਹੈ...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ ਖਾਤਾ ਸਫਲਤਾਪੂਰਵਕ ਅਣਅਧਿਕਾਰਤ ਕੀਤਾ ਗਿਆ!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **ਨਵਾਂ ਵਰਜਨ ਉਪਲਬਧ!**
|
||||
|
||||
ਮੌਜੂਦਾ ਵਰਜਨ: **{{{currentVersion}}}**
|
||||
ਨਵਾਂ ਵਰਜਨ: **{{{newVersion}}}**
|
||||
|
||||
👉 [**🆕 ਬਦਲਾਅ ਲੌਗ**]({{{changelogUrl}}}) ਵਿੱਚ ਨਵਾਂ ਕੀ ਹੈ ਦੇਖੋ
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **WAHA ਦਾ ਸਮਰਥਨ ਕਰੋ!**
|
||||
|
||||
ਤੁਸੀਂ ਮੁਫਤ **WAHA Core** ਵਰਜਨ ਦੀ ਵਰਤੋਂ ਕਰ ਰਹੇ ਹੋ। ਜੇ ਤੁਹਾਨੂੰ ਇਹ ਪ੍ਰੋਜੈਕਟ ਲਾਭਦਾਇਕ ਲੱਗਦਾ ਹੈ, ਤਾਂ ਕਿਰਪਾ ਕਰਕੇ ਇਸਦੇ ਵਿਕਾਸ ਦਾ ਸਮਰਥਨ ਕਰਨ 'ਤੇ ਵਿਚਾਰ ਕਰੋ।
|
||||
|
||||
ਤੁਹਾਡਾ ਸਮਰਥਨ ਹੀ ਇੱਕੋ ਇੱਕ ਤਰੀਕਾ ਹੈ ਜਿਸ ਨਾਲ ਅਸੀਂ ਅਪਡੇਟਾਂ, ਫਿਕਸਾਂ, ਅਤੇ ਨਵੀਆਂ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ 'ਤੇ ਕੰਮ ਕਰਨਾ ਜਾਰੀ ਰੱਖ ਸਕਦੇ ਹਾਂ।
|
||||
|
||||
[**🎁 ਸਾਡਾ ਸਮਰਥਨ ਕਰੋ**]({{{supportUrl}}})
|
||||
@@ -0,0 +1,183 @@
|
||||
# Portuguese translations for ChatWoot integration
|
||||
APP_INBOX_CONTACT_NAME: 'Integração WhatsApp (WAHA)'
|
||||
|
||||
JOB_ERROR_REPORT: |
|
||||
⚠️ **{{{header}}}**
|
||||
{{#attempts.nextDelay}}
|
||||
🔄 Tentando novamente em {{attempts.nextDelay}} segundos...
|
||||
{{/attempts.nextDelay}}
|
||||
{{^attempts.nextDelay}}
|
||||
❌ Não há mais tentativas restantes. Por favor, tente novamente mais tarde.
|
||||
{{/attempts.nextDelay}}
|
||||
Tentativa: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Detalhes: [{{{details.text}}}]({{{details.url}}})
|
||||
{{#error}}
|
||||
```
|
||||
|
||||
{{{error}}}
|
||||
```
|
||||
{{/error}}
|
||||
|
||||
JOB_SUCCEEDED_REPORT: |
|
||||
✅ **Sucesso**
|
||||
Tentativa: {{{attempts.current}}} / {{{attempts.max}}}
|
||||
Detalhes: [{{{details.text}}}]({{{details.url}}})
|
||||
|
||||
WHATSAPP_ERROR_SENDING_MESSAGE:
|
||||
Não foi possível enviar uma mensagem no WhatsApp.
|
||||
WHATSAPP_ERROR_RECEIVING_MESSAGE:
|
||||
Não foi possível receber uma mensagem do WhatsApp
|
||||
WHATSAPP_ERROR_REMOVING_MESSAGE:
|
||||
Não foi possível remover uma mensagem do WhatsApp
|
||||
APP_SCHEDULED_JOB_ERROR: Falha ao executar tarefa agendada
|
||||
|
||||
MESSAGE_FROM_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Enviado do WhatsApp*
|
||||
|
||||
MESSAGE_FROM_API: |
|
||||
{{{text}}}
|
||||
|
||||
📱 *Enviado via API*
|
||||
|
||||
MESSAGE_REMOVED_IN_WHATSAPP: |
|
||||
❌ *Esta mensagem foi excluída no WhatsApp*
|
||||
|
||||
MESSAGE_EDITED_IN_WHATSAPP: |
|
||||
{{{text}}}
|
||||
|
||||
✏️ *Editado no WhatsApp*
|
||||
|
||||
WHATSAPP_GROUP_MESSAGE: |
|
||||
{{{text}}}
|
||||
|
||||
👥 *{{{participant}}}*
|
||||
|
||||
WHATSAPP_REACTION_ADDED: |
|
||||
*Reagiu com* {{{emoji}}}
|
||||
|
||||
WHATSAPP_REACTION_REMOVED: |
|
||||
*Reação removida*
|
||||
|
||||
WHATSAPP_CONTACT_GROUP_SUFFIX: 'Grupo'
|
||||
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'Canal'
|
||||
# Aka Stories
|
||||
WHATSAPP_CONTACT_STATUS_NAME: 'Status'
|
||||
|
||||
APP_CONNECTED_MESSAGE: |
|
||||
🔗 **CONECTADO: Sessão do WhatsApp para Caixa de Entrada do ChatWoot**
|
||||
|
||||
✅ A sessão do WhatsApp **{{{name}}}** foi conectada com sucesso a esta Caixa de Entrada do ChatWoot.
|
||||
|
||||
Esta caixa de entrada agora está vinculada à sua **Conta do WhatsApp**. Você pode enviar e receber mensagens diretamente daqui — assim como usar o WhatsApp, mas dentro do ChatWoot.
|
||||
|
||||
Você também pode gerenciar a sessão do WhatsApp vinculada a esta caixa de entrada. Para ver as ações disponíveis, envie **help** para este chat.
|
||||
|
||||
📋 Ações comuns que você pode fazer:
|
||||
- Verificar se a sessão do WhatsApp está ativa
|
||||
- Reiniciar ou Parar a sessão do WhatsApp
|
||||
- Sair e reconectar escaneando um novo código QR
|
||||
|
||||
Se tudo estiver funcionando, você poderá conversar livremente. Se você vir avisos, pode precisar reconectar sua conta do WhatsApp.
|
||||
|
||||
Vamos começar! 🚀
|
||||
|
||||
APP_DISCONNECTED_MESSAGE: |
|
||||
🔌 **DESCONECTADO: Sessão do WhatsApp da Caixa de Entrada do ChatWoot**
|
||||
|
||||
❌ A sessão do WhatsApp **{{{name}}}** foi desconectada desta Caixa de Entrada do ChatWoot.
|
||||
|
||||
Você não poderá mais enviar ou receber mensagens do WhatsApp.
|
||||
|
||||
Para reconectar, entre em contato com seu administrador.
|
||||
|
||||
APP_UPDATED_MESSAGE: |
|
||||
🔄 **ATUALIZADO: Configurações da Sessão do WhatsApp**
|
||||
|
||||
✅ As configurações para a sessão do WhatsApp **{{{name}}}** foram atualizadas.
|
||||
|
||||
APP_COMMANDS_LIST: |
|
||||
Você pode gerenciar a **Integração WhatsApp** enviando um dos seguintes comandos:
|
||||
|
||||
🖥️ Sessões:
|
||||
1. **status** - obter o status atual da sessão.
|
||||
2. **restart** - reiniciar a sessão - isso tentará reconectar sua conta do WhatsApp.
|
||||
3. **start** - iniciar a sessão.
|
||||
4. **stop** - parar a sessão.
|
||||
5. **logout** - sair da sessão, para que você possa escanear o código QR e reparar a sessão.
|
||||
6. **qr** - obter o código QR para a sessão.
|
||||
7. **screenshot** - obter uma captura de tela da sessão (disponível apenas no motor **WEBJS**).
|
||||
8. **help** - comandos disponíveis.
|
||||
|
||||
⚙️ Servidor:
|
||||
- **server status** - obter informações sobre a versão e status do servidor.
|
||||
- **server reboot** - reiniciar o servidor suavemente.
|
||||
- **server reboot force** - forçar a reinicialização do servidor.
|
||||
|
||||
Envie um **comando** (não sensível a maiúsculas/minúsculas) ou um **número de comando** para esta conversa.
|
||||
|
||||
APP_HELP_REMINDER: |
|
||||
Envie **help** para obter a lista de comandos disponíveis.
|
||||
|
||||
APP_SESSION_STATUS_CHANGE: |
|
||||
{{{emoji}}} A sessão "{{{session}}}" mudou para o status {{{status}}}
|
||||
|
||||
APP_SESSION_CURRENT_STATUS: |
|
||||
{{{emoji}}} O status da sessão **{{{session}}}** é **{{{status}}}**
|
||||
**Conta do WhatsApp:**
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_DASHBOARD_LINK: |
|
||||
Visite o [Painel WAHA]({{{url}}}) para mais detalhes.
|
||||
|
||||
APP_SESSION_STATUS_WORKING: |
|
||||
✅ Esta Caixa de Entrada do ChatWoot está vinculada à **Conta do WhatsApp** e pronta para enviar e receber mensagens!
|
||||
|
||||
📱 **{{{name}}} ({{{id}}})**
|
||||
|
||||
APP_SESSION_STATUS_ERROR: |
|
||||
⚠️ **Esta Caixa de Entrada do ChatWoot está vinculada à Conta do WhatsApp, mas NÃO está pronta para enviar e receber mensagens!**
|
||||
|
||||
Por favor, verifique o status da sessão e tente reiniciá-la ou saia e escaneie o código QR novamente.
|
||||
|
||||
APP_SESSION_SCAN_QR_CODE: |
|
||||
⚠️ **Escaneie o código QR no seu telefone para vincular a Conta do WhatsApp a esta Caixa de Entrada do ChatWoot!**
|
||||
|
||||
APP_SERVER_VERSION_AND_STATUS: |
|
||||
**Versão:**
|
||||
```
|
||||
{{{version}}}
|
||||
```
|
||||
|
||||
**Status:**
|
||||
```
|
||||
{{{status}}}
|
||||
```
|
||||
|
||||
APP_SERVER_REBOOT: |
|
||||
O servidor está reiniciando suavemente...
|
||||
|
||||
APP_SERVER_REBOOT_FORCE: |
|
||||
O servidor está reiniciando forçadamente...
|
||||
|
||||
APP_LOGOUT_SUCCESS: |
|
||||
✅ Conta desautorizada com sucesso!
|
||||
|
||||
APP_NEW_VERSION_AVAILABLE: |
|
||||
🔄 **Nova Versão Disponível!**
|
||||
|
||||
Versão atual: **{{{currentVersion}}}**
|
||||
Nova versão: **{{{newVersion}}}**
|
||||
|
||||
👉 Confira o que há de novo no [**🆕 Registro de Alterações**]({{{changelogUrl}}})
|
||||
|
||||
APP_CORE_VERSION_USED: |
|
||||
❤️ **Apoie o WAHA!**
|
||||
|
||||
Você está usando a versão gratuita **WAHA Core**. Se você acha este projeto útil, considere apoiar seu desenvolvimento.
|
||||
|
||||
Seu apoio é a única maneira de continuarmos trabalhando em atualizações, correções e novos recursos.
|
||||
|
||||
[**🎁 Apoie-nos**]({{{supportUrl}}})
|
||||
Loaded 100 of 197 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user