Compare commits

...
24 Commits
Author SHA1 Message Date
devlikepro 35b8b7d01c [core] 2025.7.7
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-07-22 14:53:35 +07:00
devlikepro 4e2e2b2b3c [core] WEBJS - add 1GB cache back
fix #1131 fix #1140
2025-07-22 14:53:35 +07:00
devlikepro bfff0c8cf4 [core] 2025.7.6
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-07-20 14:06:59 +07:00
devlikepro e253f445b9 [core] WEBJS - fix sendSeen
fix #1117
2025-07-20 14:06:58 +07:00
devlikepro 73ed663c9c [core] Dashboard - show tier 2025-07-20 14:06:58 +07:00
devlikepro 2236150b83 [core] ChatWoot - adjust commands list text 2025-07-20 14:06:58 +07:00
devlikepro da24feb390 [core] Fix "Not implemented by engine" in status
fix #1115
2025-07-20 14:06:58 +07:00
devlikepro 44a5ada162 [core] Fix "You provided 'null' where a stream was expected" 2025-07-20 14:06:58 +07:00
devlikepro e57cc2587c [core] WEBJS - share media properly 2025-07-20 14:06:58 +07:00
Alan Frigo 38bfb9baa5 Added WHATSAPP_HOOK_CUSTOM_HEADERS to env example 2025-07-20 13:57:46 +07:00
devlikepro 540636d59b [core] Fix MongoDB
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
2025-07-13 18:40:19 +07:00
devlikepro 3cc784353e [core] 2025.7.5 2025-07-13 18:32:54 +07:00
devlikepro 44946ee784 [core] Add ChatWoot App
fix #441
2025-07-13 18:32:53 +07:00
devlikepro a9d77487c6 [core] Fix LegacyKeyValueFormat in Dockerfile 2025-07-13 18:32:53 +07:00
devlikepro 70d4f27abd [core] WEBJS - ignore old reactions
fixes #494
2025-07-13 18:32:53 +07:00
devlikepro 9fca09661d [core] WEBJS - render process 2 2025-07-13 18:32:53 +07:00
devlikepro 2f0efd0223 [core] 2025.7.4
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
2025-07-06 16:48:47 +07:00
devlikepro 5c31ed53df [core] WEBJS - fix error for message.ack
TypeError: You provided 'null' where a stream was expected.
2025-07-06 16:48:47 +07:00
devlikepro 87640ea238 [core] WEBJS - avoid STARTING => FAILED cycles
fix #1071
2025-07-06 16:48:47 +07:00
devlikepro eb8f771b06 [core] WEBJS - protocolTimeout to 300s (5m)
fix #1103
2025-07-06 16:48:46 +07:00
devlikepro b080b5862e [core] NOWEB - no lock on 'lids' updates 2025-07-06 16:48:46 +07:00
devlikepro df50e7527e [core] NOWEB - save @lid to @c.us from the first message
fix #1073
fix #1097
2025-07-06 16:48:46 +07:00
devlikepro e2b43575c0 [core] Up NOWEB - 2025-07-04
fix #531
fix #1098
2025-07-06 16:48:45 +07:00
devlikepro 2059793562 [core] Log update for unknown jid 2025-07-06 16:48:45 +07:00
134 changed files with 10055 additions and 344 deletions

No files matched your search

+1
View File
@@ -1,4 +1,5 @@
Dockerfile
docker-compose
src/core/engines/gows/proto
.*sessions
sessions
+13
View File
@@ -76,6 +76,18 @@ WHATSAPP_FILES_FOLDER=/app/.media
# 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
# ==================================
# ===== ADVANCED CONFIGURATION =====
@@ -90,6 +102,7 @@ WHATSAPP_FILES_FOLDER=/app/.media
# 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
+2 -2
View File
@@ -195,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 -1
View File
@@ -35,7 +35,7 @@ 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
+1 -1
View File
@@ -28,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
+260
View File
@@ -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
+115
View File
@@ -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: {}
+2 -2
View File
@@ -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:
+2 -2
View File
@@ -7,8 +7,8 @@ services:
ports:
- '3000:3000/tcp'
volumes:
- './.sessions:/app/.sessions'
- './.media:/app/.media'
- './sessions:/app/.sessions'
- './media:/app/.media'
environment:
- WAHA_API_KEY=321
- WAHA_DASHBOARD_USERNAME=admin
+6 -1
View File
@@ -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
}
}
+13 -2
View File
@@ -26,11 +26,17 @@
"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",
@@ -43,6 +49,7 @@
"@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",
@@ -52,6 +59,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",
@@ -62,6 +70,7 @@
"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",
@@ -89,7 +99,8 @@
"swagger-ui-express": "^4.1.4",
"ulid": "^2.3.0",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09",
"write-file-atomic": "^6.0.0"
"write-file-atomic": "^6.0.0",
"yaml": "^2.7.1"
},
"optionalDependencies": {
"bufferutil": "^4.0.8"
+1 -10
View File
@@ -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);
}
+60
View File
@@ -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();
}
}
}
+58
View File
@@ -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);
}
}
+26
View File
@@ -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,
}),
];
}
+8
View File
@@ -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;
}
+58
View File
@@ -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);
});
}
}
+52
View File
@@ -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;
}
+72
View File
@@ -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);
}
}
+22
View File
@@ -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,
},
});
}
+42
View File
@@ -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);
}
+42
View File
@@ -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;
+13
View File
@@ -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;
}
+29
View File
@@ -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');
}
}
}
+15
View File
@@ -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>;
}
+21
View File
@@ -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');
+79
View File
@@ -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();
}
}
+6
View File
@@ -0,0 +1,6 @@
import { App } from '@waha/apps/app_sdk/dto/app.dto';
export interface AppDB extends App {
// Internal database number
pk: number;
}
+111
View File
@@ -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;
+18
View File
@@ -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
View File
@@ -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);
}
});
}
}
+9
View File
@@ -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;
}
+101
View File
@@ -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,
};
+150
View File
@@ -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));
}
}
+53
View File
@@ -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,
},
});
}
}
}
+34
View File
@@ -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;
}
}
+28
View File
@@ -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 {}
+21
View File
@@ -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;
}
+53
View File
@@ -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,
}
+35
View File
@@ -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';
+26
View File
@@ -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',
}
+139
View File
@@ -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') {
// 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);
}
}
+7
View File
@@ -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`);
}
}
+17
View File
@@ -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;
}
+330
View File
@@ -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);
}
+201
View File
@@ -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());
}
}
+24
View File
@@ -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);
}
}
+89
View File
@@ -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();
}
}
+11
View File
@@ -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}`);
}
}
+180
View File
@@ -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}}})
+180
View File
@@ -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}}})
+183
View File
@@ -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}}})
+181
View File
@@ -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}}})
+180
View File
@@ -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}}})
+180
View File
@@ -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}}})
+180
View File
@@ -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}}})
+180
View File
@@ -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}}})
+180
View File
@@ -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}}})
+180
View File
@@ -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}}})
+180
View File
@@ -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}}})
+183
View File
@@ -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}}})
+180
View File
@@ -0,0 +1,180 @@
# Russian translations for ChatWoot integration
APP_INBOX_CONTACT_NAME: 'WhatsApp Интеграция (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.
WHATSAPP_ERROR_RECEIVING_MESSAGE: Не удалось получить сообщение из WhatsApp
WHATSAPP_ERROR_REMOVING_MESSAGE: Не удалось удалить сообщение из WhatsApp
APP_SCHEDULED_JOB_ERROR: Запланированная задача не выполнена
MESSAGE_FROM_WHATSAPP: |
{{{text}}}
📱 *Отправлено из WhatsApp*
MESSAGE_FROM_API: |
{{{text}}}
📱 *Отправлено через API*
MESSAGE_REMOVED_IN_WHATSAPP: |
❌ *Это сообщение было удалено в WhatsApp*
MESSAGE_EDITED_IN_WHATSAPP: |
{{{text}}}
✏️ *Отредактировано в WhatsApp*
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: |
🔗 **ПОДКЛЮЧЕНО: Сессия WhatsApp к ChatWoot Inbox**
✅ Сессия WhatsApp **{{{name}}}** была успешно подключена к этому ChatWoot Inbox.
Этот inbox теперь связан с вашим **аккаунтом WhatsApp**. Вы можете отправлять и получать сообщения прямо отсюда — как в WhatsApp, но внутри ChatWoot.
Вы также можете управлять сессией WhatsApp, связанной с этим inbox. Чтобы увидеть доступные действия, отправьте **help** в этот чат.
📋 Общие действия, которые вы можете выполнить:
- Проверить, активна ли сессия WhatsApp
- Перезапустить или остановить сессию WhatsApp
- Выйти и переподключиться, отсканировав новый QR-код
Если всё работает, вы сможете свободно общаться. Если вы видите предупреждения, возможно, вам потребуется переподключить ваш аккаунт WhatsApp.
Давайте начнем! 🚀
APP_DISCONNECTED_MESSAGE: |
🔌 **ОТКЛЮЧЕНО: Сессия WhatsApp от ChatWoot Inbox**
❌ Сессия WhatsApp **{{{name}}}** была отключена от этого ChatWoot Inbox.
Вы больше не сможете отправлять или получать сообщения из WhatsApp.
Для переподключения, пожалуйста, обратитесь к вашему администратору.
APP_UPDATED_MESSAGE: |
🔄 **ОБНОВЛЕНО: Настройки сессии WhatsApp**
✅ Настройки для сессии WhatsApp **{{{name}}}** были обновлены.
APP_COMMANDS_LIST: |
Вы можете управлять **WhatsApp Интеграцией**, отправив одну из следующих команд:
🖥️ Сессии:
1. **status** - получить текущий статус сессии.
2. **restart** - перезапустить сессию - это попытается переподключить ваш аккаунт WhatsApp.
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}}}**
**Аккаунт WhatsApp:**
📱 **{{{name}}} ({{{id}}})**
APP_DASHBOARD_LINK: |
Посетите [WAHA Dashboard]({{{url}}}) для получения дополнительной информации.
APP_SESSION_STATUS_WORKING: |
✅ Этот ChatWoot Inbox связан с **аккаунтом WhatsApp** и готов отправлять и получать сообщения!
📱 **{{{name}}} ({{{id}}})**
APP_SESSION_STATUS_ERROR: |
⚠️ **Этот ChatWoot Inbox связан с аккаунтом WhatsApp, но НЕ готов отправлять и получать сообщения!**
Пожалуйста, проверьте статус сессии и попробуйте перезапустить её или выйдите и отсканируйте QR-код снова.
APP_SESSION_SCAN_QR_CODE: |
⚠️ **Отсканируйте QR-код на вашем телефоне, чтобы связать аккаунт WhatsApp с этим ChatWoot Inbox!**
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}}})
+180
View File
@@ -0,0 +1,180 @@
# Turkish translations for ChatWoot integration
APP_INBOX_CONTACT_NAME: 'WhatsApp Entegrasyonu (WAHA)'
JOB_ERROR_REPORT: |
⚠️ **{{{header}}}**
{{#attempts.nextDelay}}
🔄 {{attempts.nextDelay}} saniye içinde tekrar deneniyor...
{{/attempts.nextDelay}}
{{^attempts.nextDelay}}
❌ Başka deneme hakkı kalmadı. Lütfen daha sonra tekrar deneyin.
{{/attempts.nextDelay}}
Deneme: {{{attempts.current}}} / {{{attempts.max}}}
Detaylar: [{{{details.text}}}]({{{details.url}}})
{{#error}}
```
{{{error}}}
```
{{/error}}
JOB_SUCCEEDED_REPORT: |
✅ **Başarılı**
Deneme: {{{attempts.current}}} / {{{attempts.max}}}
Detaylar: [{{{details.text}}}]({{{details.url}}})
WHATSAPP_ERROR_SENDING_MESSAGE: WhatsApp'ta mesaj gönderilemedi.
WHATSAPP_ERROR_RECEIVING_MESSAGE: WhatsApp'tan mesaj alınamadı
WHATSAPP_ERROR_REMOVING_MESSAGE: WhatsApp'tan mesaj kaldırılamadı
APP_SCHEDULED_JOB_ERROR: Zamanlanmış görev yürütülemedi
MESSAGE_FROM_WHATSAPP: |
{{{text}}}
📱 *WhatsApp'tan gönderildi*
MESSAGE_FROM_API: |
{{{text}}}
📱 *API aracılığıyla gönderildi*
MESSAGE_REMOVED_IN_WHATSAPP: |
❌ *Bu mesaj WhatsApp'ta silindi*
MESSAGE_EDITED_IN_WHATSAPP: |
{{{text}}}
✏️ *WhatsApp'ta düzenlendi*
WHATSAPP_GROUP_MESSAGE: |
{{{text}}}
👥 *{{{participant}}}*
WHATSAPP_REACTION_ADDED: |
*Tepki verdi* {{{emoji}}}
WHATSAPP_REACTION_REMOVED: |
*Tepki kaldırıldı*
WHATSAPP_CONTACT_GROUP_SUFFIX: 'Grup'
WHATSAPP_CONTACT_CHANNEL_SUFFIX: 'Kanal'
# Aka Stories
WHATSAPP_CONTACT_STATUS_NAME: 'Durum'
APP_CONNECTED_MESSAGE: |
🔗 **BAĞLANDI: WhatsApp Oturumu ChatWoot Gelen Kutusuna**
✅ WhatsApp oturumu **{{{name}}}** bu ChatWoot Gelen Kutusuna başarıyla bağlandı.
Bu gelen kutusu artık **WhatsApp Hesabınıza** bağlı. Mesajları doğrudan buradan gönderip alabilirsiniz — tıpkı WhatsApp kullanır gibi, ancak ChatWoot içinde.
Ayrıca bu gelen kutusuna bağlı WhatsApp oturumunu da yönetebilirsiniz. Kullanılabilir eylemleri görmek için, bu sohbete **help** gönderin.
📋 Yapabileceğiniz yaygın eylemler:
- WhatsApp oturumunun aktif olup olmadığını kontrol etme
- WhatsApp oturumunu yeniden başlatma veya durdurma
- Çıkış yapma ve yeni bir QR kod tarayarak yeniden bağlanma
Her şey çalışıyorsa, özgürce sohbet edebileceksiniz. Uyarılar görürseniz, WhatsApp hesabınızı yeniden bağlamanız gerekebilir.
Hadi başlayalım! 🚀
APP_DISCONNECTED_MESSAGE: |
🔌 **BAĞLANTI KESİLDİ: WhatsApp Oturumu ChatWoot Gelen Kutusundan**
❌ WhatsApp oturumu **{{{name}}}** bu ChatWoot Gelen Kutusundan bağlantısı kesildi.
Artık WhatsApp'tan mesaj gönderemez veya alamazsınız.
Yeniden bağlanmak için, lütfen yöneticinizle iletişime geçin.
APP_UPDATED_MESSAGE: |
🔄 **GÜNCELLENDİ: WhatsApp Oturum Ayarları**
✅ WhatsApp oturumu **{{{name}}}** için ayarlar güncellendi.
APP_COMMANDS_LIST: |
Aşağıdaki komutlardan birini göndererek **WhatsApp Entegrasyonu**nu yönetebilirsiniz:
🖥️ Oturumlar:
1. **status** - mevcut oturum durumunu alın.
2. **restart** - oturumu yeniden başlatın - bu, WhatsApp hesabınızı yeniden bağlamaya çalışacaktır.
3. **start** - oturumu başlatın.
4. **stop** - oturumu durdurun.
5. **logout** - oturumdan çıkış yapın, böylece QR kodu tarayabilir ve oturumu onarabilirsiniz.
6. **qr** - oturum için QR kodu alın.
7. **screenshot** - oturumun ekran görüntüsünü alın (yalnızca **WEBJS** motorunda kullanılabilir).
8. **help** - kullanılabilir komutlar.
⚙️ Sunucu:
- **server status** - sunucu sürümü ve durum bilgilerini alın.
- **server reboot** - sunucuyu nazikçe yeniden başlatın.
- **server reboot force** - sunucuyu zorla yeniden başlatın.
Bu konuşmaya bir **komut** (büyük/küçük harf duyarsız) veya **komut numarası** gönderin.
APP_HELP_REMINDER: |
Kullanılabilir komutların listesini almak için **help** gönderin.
APP_SESSION_STATUS_CHANGE: |
{{{emoji}}} "{{{session}}}" oturumu {{{status}}} durumuna geçti
APP_SESSION_CURRENT_STATUS: |
{{{emoji}}} **{{{session}}}** oturum durumu **{{{status}}}** içinde
**WhatsApp Hesabı:**
📱 **{{{name}}} ({{{id}}})**
APP_DASHBOARD_LINK: |
Daha fazla ayrıntı için [WAHA Kontrol Paneli]({{{url}}}) ziyaret edin.
APP_SESSION_STATUS_WORKING: |
✅ Bu ChatWoot Gelen Kutusu **WhatsApp Hesabına** bağlı ve mesaj gönderip almaya hazır!
📱 **{{{name}}} ({{{id}}})**
APP_SESSION_STATUS_ERROR: |
⚠️ **Bu ChatWoot Gelen Kutusu WhatsApp Hesabına bağlı ancak mesaj gönderip almaya hazır DEĞİL!**
Lütfen oturum durumunu kontrol edin ve yeniden başlatmayı deneyin veya çıkış yapın ve QR kodu tekrar tarayın.
APP_SESSION_SCAN_QR_CODE: |
⚠️ **WhatsApp Hesabını bu ChatWoot Gelen Kutusuna bağlamak için telefonunuzda QR kodu tarayın!**
APP_SERVER_VERSION_AND_STATUS: |
**Sürüm:**
```
{{{version}}}
```
**Durum:**
```
{{{status}}}
```
APP_SERVER_REBOOT: |
Sunucu nazikçe yeniden başlatılıyor...
APP_SERVER_REBOOT_FORCE: |
Sunucu zorla yeniden başlatılıyor...
APP_LOGOUT_SUCCESS: |
✅ Hesap başarıyla yetkilendirilmedi!
APP_NEW_VERSION_AVAILABLE: |
🔄 **Yeni Sürüm Mevcut!**
Mevcut sürüm: **{{{currentVersion}}}**
Yeni sürüm: **{{{newVersion}}}**
👉 [**🆕 Değişiklik Günlüğü**]({{{changelogUrl}}}) içinde yenilikleri kontrol edin
APP_CORE_VERSION_USED: |
❤️ **WAHA'yı Destekleyin!**
Ücretsiz **WAHA Core** sürümünü kullanıyorsunuz. Bu projeyi faydalı buluyorsanız, lütfen geliştirilmesini desteklemeyi düşünün.
Desteğiniz, güncellemeler, düzeltmeler ve yeni özellikler üzerinde çalışmaya devam edebilmemizin tek yoludur.
[**🎁 Bizi Destekleyin**]({{{supportUrl}}})
+180
View File
@@ -0,0 +1,180 @@
# Ukrainian translations for ChatWoot integration
APP_INBOX_CONTACT_NAME: 'Інтеграція WhatsApp (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.
WHATSAPP_ERROR_RECEIVING_MESSAGE: Не вдалося отримати повідомлення з WhatsApp
WHATSAPP_ERROR_REMOVING_MESSAGE: Не вдалося видалити повідомлення з WhatsApp
APP_SCHEDULED_JOB_ERROR: Заплановане завдання не вдалося виконати
MESSAGE_FROM_WHATSAPP: |
{{{text}}}
📱 *Надіслано з WhatsApp*
MESSAGE_FROM_API: |
{{{text}}}
📱 *Надіслано через API*
MESSAGE_REMOVED_IN_WHATSAPP: |
❌ *Це повідомлення було видалено у WhatsApp*
MESSAGE_EDITED_IN_WHATSAPP: |
{{{text}}}
✏️ *Відредаговано у WhatsApp*
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: |
🔗 **ПІДКЛЮЧЕНО: Сесія WhatsApp до Вхідних ChatWoot**
✅ Сесія WhatsApp **{{{name}}}** була успішно підключена до цих Вхідних ChatWoot.
Ці вхідні тепер пов'язані з вашим **Обліковим записом WhatsApp**. Ви можете надсилати та отримувати повідомлення безпосередньо звідси — так само, як використовуючи WhatsApp, але всередині ChatWoot.
Ви також можете керувати сесією WhatsApp, пов'язаною з цими вхідними. Щоб переглянути доступні дії, надішліть **help** у цей чат.
📋 Загальні дії, які ви можете виконати:
- Перевірити, чи активна сесія WhatsApp
- Перезапустити або Зупинити сесію WhatsApp
- Вийти та повторно підключитися, відсканувавши новий QR-код
Якщо все працює, ви зможете вільно спілкуватися. Якщо ви бачите попередження, можливо, вам потрібно повторно підключити свій обліковий запис WhatsApp.
Давайте почнемо! 🚀
APP_DISCONNECTED_MESSAGE: |
🔌 **ВІДКЛЮЧЕНО: Сесія WhatsApp від Вхідних ChatWoot**
❌ Сесія WhatsApp **{{{name}}}** була відключена від цих Вхідних ChatWoot.
Ви більше не зможете надсилати або отримувати повідомлення з WhatsApp.
Щоб повторно підключитися, зверніться до свого адміністратора.
APP_UPDATED_MESSAGE: |
🔄 **ОНОВЛЕНО: Налаштування сесії WhatsApp**
✅ Налаштування для сесії WhatsApp **{{{name}}}** були оновлені.
APP_COMMANDS_LIST: |
Ви можете керувати **Інтеграцією WhatsApp**, надіславши одну з наступних команд:
🖥️ Сесії:
1. **status** - отримати поточний статус сесії.
2. **restart** - перезапустити сесію - це спробує повторно підключити ваш обліковий запис WhatsApp.
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}}}**
**Обліковий запис WhatsApp:**
📱 **{{{name}}} ({{{id}}})**
APP_DASHBOARD_LINK: |
Відвідайте [Панель керування WAHA]({{{url}}}) для отримання додаткової інформації.
APP_SESSION_STATUS_WORKING: |
✅ Ці Вхідні ChatWoot пов'язані з **Обліковим записом WhatsApp** і готові надсилати та отримувати повідомлення!
📱 **{{{name}}} ({{{id}}})**
APP_SESSION_STATUS_ERROR: |
⚠️ **Ці Вхідні ChatWoot пов'язані з Обліковим записом WhatsApp, але НЕ готові надсилати та отримувати повідомлення!**
Будь ласка, перевірте статус сесії та спробуйте перезапустити її або вийдіть і відскануйте QR-код знову.
APP_SESSION_SCAN_QR_CODE: |
⚠️ **Відскануйте QR-код на вашому телефоні, щоб пов'язати Обліковий запис WhatsApp з цими Вхідними 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}}})
+233
View File
@@ -0,0 +1,233 @@
import * as fs from 'fs-extra';
import * as lodash from 'lodash';
import * as Mustache from 'mustache';
import * as path from 'path';
import * as yaml from 'yaml';
export enum LocaleCode {
EN_US = 'en-US',
AR_AE = 'ar-AE',
BN_BD = 'bn-BD',
PT_BR = 'pt-BR',
DE_DE = 'de-DE',
ES_ES = 'es-ES',
FR_FR = 'fr-FR',
ID_ID = 'id-ID',
HE_IL = 'he-IL',
HI_IN = 'hi-IN',
FA_IR = 'fa-IR',
PA_PK = 'pa-PK',
RU_RU = 'ru-RU',
TR_TR = 'tr-TR',
UK_UA = 'uk-UA',
}
export const LOCALES = [
LocaleCode.EN_US,
LocaleCode.AR_AE,
LocaleCode.BN_BD,
LocaleCode.PT_BR,
LocaleCode.DE_DE,
LocaleCode.ES_ES,
LocaleCode.FR_FR,
LocaleCode.ID_ID,
LocaleCode.HE_IL,
LocaleCode.HI_IN,
LocaleCode.FA_IR,
LocaleCode.PA_PK,
LocaleCode.RU_RU,
LocaleCode.TR_TR,
LocaleCode.UK_UA,
];
export const LOCALE_NAMES: Record<LocaleCode, string> = {
[LocaleCode.EN_US]: '🇺🇸 English (English)',
[LocaleCode.AR_AE]: '🇦🇪 العربية (Arabic)',
[LocaleCode.BN_BD]: '🇧🇩 বাংলা (Bengali)',
[LocaleCode.PT_BR]: '🇧🇷🇵🇹 Português (Portuguese)',
[LocaleCode.DE_DE]: '🇩🇪 Deutsch (German)',
[LocaleCode.ES_ES]: '🇪🇸 Español (Spanish)',
[LocaleCode.FR_FR]: '🇫🇷 Français (French)',
[LocaleCode.ID_ID]: '🇮🇩 Bahasa Indonesia (Indonesian)',
[LocaleCode.HE_IL]: '🇮🇱 עברית (Hebrew)',
[LocaleCode.HI_IN]: '🇮🇳 हिंदी (Hindi)',
[LocaleCode.FA_IR]: '🇮🇷 فارسی (Persian)',
[LocaleCode.PA_PK]: '🇵🇰 ਪੰਜਾਬੀ (Punjabi)',
[LocaleCode.RU_RU]: '🇷🇺 Русский (Russian)',
[LocaleCode.TR_TR]: '🇹🇷 Türkçe (Turkish)',
[LocaleCode.UK_UA]: '🇺🇦 Українська (Ukrainian)',
};
export const DEFAULT_LOCALE = 'en-US';
function loadTemplateStrings(locale: string) {
const filename = `${locale}.yaml`;
const yamlPath = path.join(__dirname, 'i18n', filename);
const content = fs.readFileSync(yamlPath, 'utf-8');
return yaml.parse(content);
}
export interface Link {
text: string;
url: string;
}
function loadLocales(locales: string[]) {
const defaults = loadTemplateStrings(DEFAULT_LOCALE);
const result: Record<string, Record<string, string>> = {};
for (const locale of locales) {
const strings = loadTemplateStrings(locale);
result[locale] = lodash.merge({}, defaults, strings);
}
return result;
}
let locales: Record<string, Record<string, string>> = {};
try {
locales = loadLocales(LOCALES);
} catch (error) {
console.error('Error loading locales:', error);
process.exit(1);
}
export enum TKey {
WHATSAPP_ERROR_SENDING_MESSAGE = 'WHATSAPP_ERROR_SENDING_MESSAGE',
WHATSAPP_ERROR_RECEIVING_MESSAGE = 'WHATSAPP_ERROR_RECEIVING_MESSAGE',
WHATSAPP_ERROR_REMOVING_MESSAGE = 'WHATSAPP_ERROR_REMOVING_MESSAGE',
JOB_ERROR_REPORT = 'JOB_ERROR_REPORT',
JOB_SUCCEEDED_REPORT = 'JOB_SUCCEEDED_REPORT',
MESSAGE_FROM_WHATSAPP = 'MESSAGE_FROM_WHATSAPP',
MESSAGE_FROM_API = 'MESSAGE_FROM_API',
MESSAGE_REMOVED_IN_WHATSAPP = 'MESSAGE_REMOVED_IN_WHATSAPP',
MESSAGE_EDITED_IN_WHATSAPP = 'MESSAGE_EDITED_IN_WHATSAPP',
WHATSAPP_GROUP_MESSAGE = 'WHATSAPP_GROUP_MESSAGE',
WHATSAPP_REACTION_ADDED = 'WHATSAPP_REACTION_ADDED',
WHATSAPP_REACTION_REMOVED = 'WHATSAPP_REACTION_REMOVED',
//
// Contact Suffixes
//
WHATSAPP_CONTACT_GROUP_SUFFIX = 'WHATSAPP_CONTACT_GROUP_SUFFIX',
WHATSAPP_CONTACT_CHANNEL_SUFFIX = 'WHATSAPP_CONTACT_CHANNEL_SUFFIX',
WHATSAPP_CONTACT_STATUS_NAME = 'WHATSAPP_CONTACT_STATUS_NAME',
//
// App Inbox
//
APP_INBOX_CONTACT_NAME = 'APP_INBOX_CONTACT_NAME',
APP_INBOX_CONTACT_AVATAR_URL = 'APP_INBOX_CONTACT_AVATAR_URL',
APP_CONNECTED_MESSAGE = 'APP_CONNECTED_MESSAGE',
APP_DISCONNECTED_MESSAGE = 'APP_DISCONNECTED_MESSAGE',
APP_UPDATED_MESSAGE = 'APP_UPDATED_MESSAGE',
APP_COMMANDS_LIST = 'APP_COMMANDS_LIST',
APP_HELP_REMINDER = 'APP_HELP_REMINDER',
APP_SESSION_STATUS_CHANGE = 'APP_SESSION_STATUS_CHANGE',
APP_SESSION_CURRENT_STATUS = 'APP_SESSION_CURRENT_STATUS',
APP_DASHBOARD_LINK = 'APP_DASHBOARD_LINK',
APP_SESSION_STATUS_WORKING = 'APP_SESSION_STATUS_WORKING',
APP_SESSION_STATUS_ERROR = 'APP_SESSION_STATUS_ERROR',
APP_SESSION_SCAN_QR_CODE = 'APP_SESSION_SCAN_QR_CODE',
APP_SERVER_VERSION_AND_STATUS = 'APP_SERVER_VERSION_AND_STATUS',
APP_SERVER_REBOOT = 'APP_SERVER_REBOOT',
APP_SERVER_REBOOT_FORCE = 'APP_SERVER_REBOOT_FORCE',
APP_LOGOUT_SUCCESS = 'APP_LOGOUT_SUCCESS',
APP_NEW_VERSION_AVAILABLE = 'APP_NEW_VERSION_AVAILABLE',
APP_CORE_VERSION_USED = 'APP_CORE_VERSION_USED',
APP_SCHEDULED_JOB_ERROR = 'APP_SCHEDULED_JOB_ERROR',
}
// Define payload types
type TemplatePayloads = {
[TKey.APP_INBOX_CONTACT_NAME]: void;
[TKey.APP_INBOX_CONTACT_AVATAR_URL]: void;
[TKey.MESSAGE_FROM_WHATSAPP]: { text: string };
[TKey.MESSAGE_FROM_API]: { text: string };
[TKey.MESSAGE_REMOVED_IN_WHATSAPP]: void;
[TKey.MESSAGE_EDITED_IN_WHATSAPP]: { text: string };
[TKey.WHATSAPP_GROUP_MESSAGE]: { text: string; participant: string };
[TKey.WHATSAPP_REACTION_ADDED]: { emoji: string };
[TKey.WHATSAPP_REACTION_REMOVED]: void;
[TKey.WHATSAPP_ERROR_SENDING_MESSAGE]: void;
[TKey.WHATSAPP_ERROR_RECEIVING_MESSAGE]: void;
[TKey.WHATSAPP_ERROR_REMOVING_MESSAGE]: void;
[TKey.WHATSAPP_CONTACT_GROUP_SUFFIX]: void;
[TKey.WHATSAPP_CONTACT_CHANNEL_SUFFIX]: void;
[TKey.WHATSAPP_CONTACT_STATUS_NAME]: void;
[TKey.APP_SCHEDULED_JOB_ERROR]: void;
[TKey.JOB_ERROR_REPORT]: {
header: string;
error: string;
details: Link;
attempts: {
current: number;
max: number;
nextDelay?: number;
};
};
[TKey.JOB_SUCCEEDED_REPORT]: {
details: Link;
attempts: {
current: number;
max: number;
};
};
[TKey.APP_CONNECTED_MESSAGE]: { name: string };
[TKey.APP_DISCONNECTED_MESSAGE]: { name: string };
[TKey.APP_UPDATED_MESSAGE]: { name: string };
[TKey.APP_SESSION_STATUS_CHANGE]: {
emoji: string;
session: string;
status: string;
};
[TKey.APP_SESSION_CURRENT_STATUS]: {
emoji: string;
session: string;
status: string;
name: string;
id: string;
};
[TKey.APP_DASHBOARD_LINK]: { url: string };
[TKey.APP_SESSION_STATUS_WORKING]: { name: string; id: string };
[TKey.APP_SESSION_STATUS_ERROR]: void;
[TKey.APP_SESSION_SCAN_QR_CODE]: void;
[TKey.APP_HELP_REMINDER]: void;
[TKey.APP_COMMANDS_LIST]: void;
[TKey.APP_SERVER_VERSION_AND_STATUS]: { version: string; status: string };
[TKey.APP_SERVER_REBOOT]: void;
[TKey.APP_SERVER_REBOOT_FORCE]: void;
[TKey.APP_LOGOUT_SUCCESS]: void;
[TKey.APP_NEW_VERSION_AVAILABLE]: {
currentVersion: string;
newVersion: string;
changelogUrl: string;
};
[TKey.APP_CORE_VERSION_USED]: {
supportUrl: string;
};
};
export class Locale {
private readonly strings: Record<string, string>;
constructor(locale: string = DEFAULT_LOCALE) {
locale = locale || DEFAULT_LOCALE;
if (!locales[locale]) {
throw new Error(`Locale '${locale}' not found`);
}
this.strings = locales[locale];
}
key<K extends TKey>(key: K): Template<K> {
return new Template(this.strings[key] || key);
}
}
export class Template<K extends TKey> {
constructor(private readonly template: string) {}
render(data: TemplatePayloads[K]): string {
return Mustache.render(this.template, data);
}
r(data: TemplatePayloads[K]): string {
return this.render(data);
}
}
@@ -0,0 +1,103 @@
import { Knex } from 'knex';
exports.up = function (knex: Knex) {
return (
knex.schema
// ChatWoot Messages
.createTable('app_chatwoot_chatwoot_messages', function (table) {
table.increments('id');
table.integer('app_pk');
table
.foreign('app_pk')
.references('pk')
.inTable('apps')
.onDelete('CASCADE');
table.datetime('timestamp');
table.integer('conversation_id');
table.integer('message_id');
})
.table('app_chatwoot_chatwoot_messages', function (table) {
table.unique(['id'], { indexName: 'cwt_msg_id_unique' });
table.unique(['app_pk', 'conversation_id', 'message_id'], {
indexName: 'cwt_msg_app_conv_msg_unique',
});
table.index(['app_pk'], 'cwt_msg_app_idx');
table.index(['app_pk', 'conversation_id'], 'cwt_msg_app_conv_idx');
table.index(
['app_pk', 'conversation_id', 'message_id'],
'cwt_msg_app_conv_msg_idx',
);
table.index(['app_pk', 'timestamp'], 'cwt_msg_app_ts_idx');
})
// WhatsApp Messages
.createTable('app_chatwoot_whatsapp_messages', function (table) {
table.increments('id');
table.integer('app_pk');
table
.foreign('app_pk')
.references('pk')
.inTable('apps')
.onDelete('CASCADE');
table.string('chat_id', 64);
table.string('message_id', 64);
table.boolean('from_me');
table.string('participant', 64).nullable();
table.datetime('timestamp');
})
.table('app_chatwoot_whatsapp_messages', function (table) {
table.unique(['id'], { indexName: 'wa_msg_id_unique' });
table.unique(['app_pk', 'chat_id', 'message_id'], {
indexName: 'wa_msg_app_chat_msg_unique',
});
table.index(['app_pk'], 'wa_msg_app_idx');
table.index(['app_pk', 'chat_id'], 'wa_msg_app_chat_idx');
table.index(['app_pk', 'message_id'], 'wa_msg_app_msg_idx');
table.index(
['app_pk', 'chat_id', 'timestamp'],
'wa_msg_app_chat_ts_idx',
);
table.index(['app_pk', 'timestamp'], 'wa_msg_app_ts_idx');
})
// Message Mapping (Chatwoot <-> WhatsApp)
.createTable('app_chatwoot_message_mappings', function (table) {
table.increments('id');
table.integer('app_pk');
table
.foreign('app_pk')
.references('pk')
.inTable('apps')
.onDelete('CASCADE');
table.integer('chatwoot_message_id');
table
.foreign('chatwoot_message_id')
.references('id')
.inTable('app_chatwoot_chatwoot_messages')
.onDelete('CASCADE');
table.integer('whatsapp_message_id');
table
.foreign('whatsapp_message_id')
.references('id')
.inTable('app_chatwoot_whatsapp_messages')
.onDelete('CASCADE');
table.integer('part');
})
.table('app_chatwoot_message_mappings', function (table) {
table.unique(['id'], { indexName: 'map_id_unique' });
table.unique(
['app_pk', 'chatwoot_message_id', 'whatsapp_message_id', 'part'],
{ indexName: 'map_app_cwt_wa_part_unique' },
);
table.index(['app_pk'], 'map_app_idx');
table.index(['app_pk', 'chatwoot_message_id'], 'map_app_cwt_idx');
table.index(['app_pk', 'whatsapp_message_id'], 'map_app_wa_idx');
})
);
};
exports.down = function (knex: Knex) {
return knex.schema
.dropTable('app_chatwoot_chatwoot_messages')
.dropTable('app_chatwoot_whatsapp_messages')
.dropTable('app_chatwoot_message_mappings');
};
@@ -0,0 +1,113 @@
import { Injectable } from '@nestjs/common';
import { IAppService } from '@waha/apps/app_sdk/services/IAppService';
import { CacheForConfig } from '@waha/apps/chatwoot/cache/ConversationCache';
import { CHATWOOT_CUSTOM_ATTRIBUTES } from '@waha/apps/chatwoot/const';
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
import { ChatWootScheduleService } from '@waha/apps/chatwoot/services/ChatWootScheduleService';
import { ChatWootWAHAQueueService } from '@waha/apps/chatwoot/services/ChatWootWAHAQueueService';
import { App } from '@waha/apps/chatwoot/storage';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
import { DIContainer } from '../di/DIContainer';
import { TKey } from '../locale';
@Injectable()
export class ChatWootAppService implements IAppService {
constructor(
private chatWootWAHAQueueService: ChatWootWAHAQueueService,
private chatWootScheduleService: ChatWootScheduleService,
@InjectPinoLogger('ChatWootAppService')
protected logger: PinoLogger,
) {}
async beforeCreated(app: App<ChatWootAppConfig>) {
await this.setupCustomAttributes(app);
await this.sendConnectedMessage(app);
}
async beforeUpdated(
savedApp: App<ChatWootAppConfig>,
newApp: App<ChatWootAppConfig>,
) {
const isTheSameUrl = savedApp.config.url === newApp.config.url;
const isTheSameInboxId = savedApp.config.inboxId === newApp.config.inboxId;
const isTheSameInbox = isTheSameUrl && isTheSameInboxId;
if (isTheSameInbox) {
await this.setupCustomAttributes(newApp);
await this.sendUpdatedMessage(newApp);
} else {
this.sendDisconnectedMessage(savedApp).catch((err) => {
this.logger.error(
'Error sending disconnected message to ChatWoot - ' + err,
);
});
await this.setupCustomAttributes(newApp);
await this.sendConnectedMessage(newApp);
}
}
async beforeDeleted(app: App<ChatWootAppConfig>): Promise<void> {
await this.chatWootScheduleService.unschedule(app.id, app.session);
this.cleanCache(app);
this.sendDisconnectedMessage(app).catch((err) => {
this.logger.error(
'Error sending disconnected message to ChatWoot - ' + err,
);
});
}
private async sendConnectedMessage(app: App<ChatWootAppConfig>) {
const di = new DIContainer(0, app.config, this.logger, null);
const repo = di.ContactConversationService();
const conversation = await repo.InboxNotifications();
const welcome = di
.Locale()
.key(TKey.APP_CONNECTED_MESSAGE)
.r({ name: app.session });
await conversation.incoming(welcome);
}
private async sendDisconnectedMessage(app: App<ChatWootAppConfig>) {
const di = new DIContainer(0, app.config, this.logger, null);
const repo = di.ContactConversationService();
const conversation = await repo.InboxNotifications();
const disconnected = di
.Locale()
.key(TKey.APP_DISCONNECTED_MESSAGE)
.r({ name: app.session });
await conversation.incoming(disconnected);
}
private async sendUpdatedMessage(app: App<ChatWootAppConfig>) {
const di = new DIContainer(0, app.config, this.logger, null);
const repo = di.ContactConversationService();
const conversation = await repo.InboxNotifications();
const updated = di
.Locale()
.key(TKey.APP_UPDATED_MESSAGE)
.r({ name: app.session });
await conversation.incoming(updated);
}
async beforeSessionStart(
app: App<ChatWootAppConfig>,
session: WhatsappSession,
) {
this.chatWootWAHAQueueService.listenEvents(app.id, session);
await this.chatWootScheduleService.schedule(app.id, session.name);
}
private async setupCustomAttributes(app: App<ChatWootAppConfig>) {
const di = new DIContainer(0, app.config, this.logger, null);
const api = di.CustomAttributesAPI();
for (const attr of CHATWOOT_CUSTOM_ATTRIBUTES) {
await api.upsert(attr);
}
}
private cleanCache(app: App<ChatWootAppConfig>) {
const cache = CacheForConfig(app.config);
cache.clean();
}
}
@@ -0,0 +1,103 @@
import { InjectQueue } from '@nestjs/bullmq';
import { Injectable } from '@nestjs/common';
import { EventName } from '@waha/apps/chatwoot/client/types';
import { InboxData } from '@waha/apps/chatwoot/consumers/types';
import { Queue } from 'bullmq';
import { QueueName } from '../consumers/QueueName';
/**
* Service for managing ChatWoot queues for inbox events
* This service is used to centralize queue configuration and job options
*/
@Injectable()
export class ChatWootQueueService {
constructor(
@InjectQueue(QueueName.INBOX_MESSAGE_CREATED)
private readonly messageCreatedQueue: Queue,
@InjectQueue(QueueName.INBOX_MESSAGE_UPDATED)
private readonly messageUpdatedQueue: Queue,
@InjectQueue(QueueName.INBOX_MESSAGE_DELETED)
private readonly messageDeletedQueue: Queue,
@InjectQueue(QueueName.INBOX_COMMANDS)
private readonly commandsQueue: Queue,
) {}
/**
* Generic method to add a job to a queue
* @param queue The queue to add the job to
* @param name The name of the job
* @param data The data for the job
* @returns The result of adding the job to the queue
*/
async add(queue: Queue, name: string, data: any): Promise<any> {
return await queue.add(name, data);
}
/**
* Get the specific queue for an event
* @param event The event to get the queue for
* @returns The queue for the event, or null if there is no specific queue
*/
private getQueueForEvent(event: string): Queue | null {
switch (event) {
case EventName.MESSAGE_CREATED:
return this.messageCreatedQueue;
case EventName.MESSAGE_UPDATED:
return this.messageUpdatedQueue;
case 'message_deleted':
return this.messageDeletedQueue;
case 'commands':
return this.commandsQueue;
default:
return null;
}
}
/**
* Add a job to the message created queue
*/
async addMessageCreatedJob(data: InboxData): Promise<any> {
return await this.add(
this.messageCreatedQueue,
EventName.MESSAGE_CREATED,
data,
);
}
/**
* Add a job to the message updated queue
*/
async addMessageUpdatedJob(data: InboxData): Promise<any> {
return await this.add(
this.messageUpdatedQueue,
EventName.MESSAGE_UPDATED,
data,
);
}
/**
* Add a job to the message deleted queue
*/
async addMessageDeletedJob(data: InboxData): Promise<any> {
return await this.add(this.messageDeletedQueue, 'message_deleted', data);
}
/**
* Add a job to the commands queue
*/
async addCommandsJob(event: string, data: InboxData): Promise<any> {
return await this.add(this.commandsQueue, event, data);
}
/**
* Add a job to the appropriate queue based on the event
*/
async addJobToQueue(event: string, data: InboxData): Promise<any> {
const queue = this.getQueueForEvent(event);
if (queue) {
return await this.add(queue, event, data);
}
return { ignored: true, event };
}
}
@@ -0,0 +1,66 @@
import { InjectQueue } from '@nestjs/bullmq';
import { Injectable } from '@nestjs/common';
import { Queue } from 'bullmq';
import { QueueName } from '../consumers/QueueName';
/**
* Service for scheduling ChatWoot tasks
* This service is used to schedule periodic tasks for ChatWoot
*/
@Injectable()
export class ChatWootScheduleService {
constructor(
@InjectQueue(QueueName.SCHEDULED_MESSAGE_CLEANUP)
private readonly messageCleanupQueue: Queue,
@InjectQueue(QueueName.SCHEDULED_CHECK_VERSION)
private readonly checkVersionQueue: Queue,
) {}
/**
* Schedule periodic tasks for a ChatWoot app
* @param appId The ID of the app
* @param sessionName The name of the session
*/
async schedule(appId: string, sessionName: string): Promise<void> {
// Message Cleanup
await this.messageCleanupQueue.upsertJobScheduler(
this.JobId(QueueName.SCHEDULED_MESSAGE_CLEANUP, appId),
// Every day at 17:00
{ pattern: '0 0 17 * * *' },
{
data: {
app: appId,
session: sessionName,
},
},
);
// Check the version
await this.checkVersionQueue.upsertJobScheduler(
this.JobId(QueueName.SCHEDULED_CHECK_VERSION, appId),
// Every Wednesday (3) at 18:00
{ pattern: '0 0 18 * * 3' },
{
data: {
app: appId,
session: sessionName,
},
},
);
}
async unschedule(appId: string, sessionName: string): Promise<void> {
// Message Cleanup
await this.messageCleanupQueue.removeJobScheduler(
this.JobId(QueueName.SCHEDULED_MESSAGE_CLEANUP, appId),
);
// Check the version
await this.checkVersionQueue.removeJobScheduler(
this.JobId(QueueName.SCHEDULED_CHECK_VERSION, appId),
);
}
private JobId(queue: QueueName, appId: string) {
return `${queue} | ${appId}`;
}
}
@@ -0,0 +1,79 @@
import { InjectQueue } from '@nestjs/bullmq';
import { Injectable } from '@nestjs/common';
import { ListenEventsForChatWoot } from '@waha/apps/chatwoot/consumers/waha/base';
import { populateSessionInfo } from '@waha/core/abc/manager.abc';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { Queue } from 'bullmq';
import { QueueName } from '../consumers/QueueName';
/**
* Service for managing ChatWoot queues for WAHA events
* This service is used to avoid cycle dependency between ChatWoot module and SessionManager
*/
@Injectable()
export class ChatWootWAHAQueueService {
constructor(
@InjectQueue(QueueName.WAHA_MESSAGE_ANY)
private readonly queueMessageAny: Queue,
@InjectQueue(QueueName.WAHA_MESSAGE_REACTION)
private readonly queueMessageReaction: Queue,
@InjectQueue(QueueName.WAHA_MESSAGE_EDITED)
private readonly queueMessageEdited: Queue,
@InjectQueue(QueueName.WAHA_MESSAGE_REVOKED)
private readonly queueMessageRevoked: Queue,
@InjectQueue(QueueName.WAHA_SESSION_STATUS)
private readonly queueSessionStatus: Queue,
) {}
/**
* Get the specific queue for an event
* @param event The event to get the queue for
* @returns The queue for the event, or null if there is no specific queue
*/
private getQueueForEvent(event: WAHAEvents): Queue | null {
switch (event) {
case WAHAEvents.MESSAGE_ANY:
return this.queueMessageAny;
case WAHAEvents.MESSAGE_REACTION:
return this.queueMessageReaction;
case WAHAEvents.MESSAGE_EDITED:
return this.queueMessageEdited;
case WAHAEvents.MESSAGE_REVOKED:
return this.queueMessageRevoked;
case WAHAEvents.SESSION_STATUS:
return this.queueSessionStatus;
default:
return null;
}
}
/**
* Add a job to the queue for an event
*/
private async addJobToQueue(
event: WAHAEvents,
data: any,
appId: string,
): Promise<void> {
const queue = this.getQueueForEvent(event);
if (queue) {
await queue.add(data.event, { app: appId, event: data });
}
}
/**
* Configure ChatWoot event handling for a session
*/
listenEvents(appId: string, session: WhatsappSession): void {
const events = ListenEventsForChatWoot();
for (const event of events) {
const obs$ = session.getEventObservable(event);
obs$.subscribe(async (payload) => {
const data = populateSessionInfo(event, session)(payload);
await this.addJobToQueue(event, data, appId);
});
}
}
}
+280
View File
@@ -0,0 +1,280 @@
import { Channel } from '@waha/structures/channels.dto';
import { ChatPictureResponse } from '@waha/structures/chats.dto';
import {
ChatRequest,
MessageFileRequest,
MessageImageRequest,
MessageTextRequest,
MessageVideoRequest,
MessageVoiceRequest,
WANumberExistResult,
} from '@waha/structures/chatting.dto';
import { SessionInfo } from '@waha/structures/sessions.dto';
import axios, { AxiosInstance } from 'axios';
export class WAHASelf {
public client: AxiosInstance;
constructor() {
// Set 'X-Api-Key'
const key = process.env.WAHA_API_KEY_PLAIN;
const port =
parseInt(process.env.PORT) ||
parseInt(process.env.WHATSAPP_API_PORT) ||
3000;
const url = `http://localhost:${port}`;
this.client = axios.create({
baseURL: url,
headers: {
'X-Api-Key': key,
'Content-Type': 'application/json',
},
});
}
async fetch(url: string): Promise<Buffer> {
const response = await this.client.get(url, {
responseType: 'arraybuffer',
});
return Buffer.from(response.data);
}
async qr(session: string): Promise<Buffer> {
const url = `/api/${session}/auth/qr`;
return await this.fetch(url);
}
async screenshot(session: string): Promise<Buffer> {
const url = `/api/screenshot?session=${session}`;
return await this.fetch(url);
}
async restart(session: string): Promise<any> {
const url = `/api/sessions/${session}/restart`;
return await this.client.post(url);
}
async logout(session: string): Promise<any> {
const url = `/api/sessions/${session}/logout`;
return await this.client.post(url);
}
async stop(session: string): Promise<any> {
const url = `/api/sessions/${session}/stop`;
return await this.client.post(url);
}
async get(session: string): Promise<SessionInfo> {
const url = `/api/sessions/${session}/`;
return await this.client.get(url).then((response) => response.data);
}
async getContact(session: string, contactId: string) {
const url = `/api/contacts`;
const params = {
session: session,
contactId: contactId,
};
return await this.client
.get(url, { params: params })
.then((response) => response.data);
}
async contactCheckExists(
session: string,
phone: string,
): Promise<WANumberExistResult> {
const url = `/api/contacts/check-exists`;
const params = {
phone: phone,
session: session,
};
return await this.client
.get(url, { params: params })
.then((response) => response.data);
}
async getGroup(session: string, groupId: string) {
const url = `/api/${session}/groups/${groupId}`;
return await this.client.get(url).then((response) => response.data);
}
async getChannel(session: string, channelId: string): Promise<Channel> {
const url = `/api/${session}/channels/${channelId}`;
return await this.client.get(url).then((response) => response.data);
}
async getChatPicture(
session: string,
chatId: string,
): Promise<string | null> {
const url = `/api/${session}/chats/${chatId}/picture`;
return await this.client.get(url).then((response) => response.data?.url);
}
async sendText(body: MessageTextRequest): Promise<any> {
const url = `/api/sendText`;
return await this.client.post(url, body).then((response) => response.data);
}
async sendImage(body: MessageImageRequest): Promise<any> {
const url = `/api/sendImage`;
return await this.client.post(url, body).then((response) => response.data);
}
async sendVideo(body: MessageVideoRequest): Promise<any> {
const url = `/api/sendVideo`;
return await this.client.post(url, body).then((response) => response.data);
}
async sendVoice(body: MessageVoiceRequest): Promise<any> {
const url = `/api/sendVoice`;
return await this.client.post(url, body).then((response) => response.data);
}
async sendFile(body: MessageFileRequest): Promise<any> {
const url = `/api/sendFile`;
return await this.client.post(url, body).then((response) => response.data);
}
async deleteMessage(session: string, chatId: string, messageId: string) {
const url = `/api/${session}/chats/${chatId}/messages/${messageId}`;
return await this.client.delete(url).then((response) => response.data);
}
async startTyping(body: ChatRequest) {
const url = `/api/startTyping`;
return await this.client.post(url, body).then((response) => response.data);
}
async stopTyping(body: ChatRequest) {
const url = `/api/stopTyping`;
return await this.client.post(url, body);
}
async readMessages(session: string, chatId: string) {
const url = `/api/${session}/chats/${chatId}/messages/read`;
return await this.client.post(url).then((response) => response.data);
}
async findPNByLid(session: string, lid: string): Promise<string | null> {
const url = `/api/${session}/lids/${lid}`;
return await this.client.get(url).then((response) => response.data.pn);
}
async findLIDByPN(session: string, pn: string): Promise<string | null> {
const url = `/api/${session}/lids/pn/${pn}`;
return await this.client.get(url).then((response) => response.data.lid);
}
/**
* Get the server version
* @returns Server version information
*/
async serverVersion(): Promise<any> {
const url = `/api/server/version`;
return await this.client.get(url).then((response) => response.data);
}
/**
* Get the server status
* @returns Server status information
*/
async serverStatus(): Promise<any> {
const url = `/api/server/status`;
return await this.client.get(url).then((response) => response.data);
}
/**
* Reboot the server
* @param force Whether to force reboot (true) or gracefully reboot (false)
* @returns Server stop response
*/
async serverReboot(force: boolean = false): Promise<any> {
const url = `/api/server/stop`;
return await this.client
.post(url, { force })
.then((response) => response.data);
}
}
export class WAHASessionAPI {
constructor(
private session: string,
private api: WAHASelf,
) {}
getContact(contactId: string): Promise<any> {
return this.api.getContact(this.session, contactId);
}
contactCheckExists(phone: string): Promise<WANumberExistResult> {
return this.api.contactCheckExists(this.session, phone);
}
getGroup(groupId: string): Promise<any> {
return this.api.getGroup(this.session, groupId);
}
getChannel(channelId: string): Promise<Channel> {
return this.api.getChannel(this.session, channelId);
}
getChatPicture(chatId: string): Promise<string | null> {
return this.api.getChatPicture(this.session, chatId);
}
sendText(body: MessageTextRequest): Promise<any> {
body.session = this.session;
return this.api.sendText(body);
}
sendImage(body: MessageImageRequest): Promise<any> {
body.session = this.session;
return this.api.sendImage(body);
}
sendVideo(body: MessageVideoRequest): Promise<any> {
body.session = this.session;
return this.api.sendVideo(body);
}
sendVoice(body: MessageVoiceRequest): Promise<any> {
body.session = this.session;
return this.api.sendVoice(body);
}
sendFile(body: MessageFileRequest): Promise<any> {
body.session = this.session;
return this.api.sendFile(body);
}
deleteMessage(chatId: string, messageId: string) {
return this.api.deleteMessage(this.session, chatId, messageId);
}
startTyping(body: ChatRequest) {
body.session = this.session;
return this.api.startTyping(body);
}
stopTyping(body: ChatRequest) {
body.session = this.session;
return this.api.stopTyping(body);
}
readMessages(chatId: string) {
return this.api.readMessages(this.session, chatId);
}
//
// Lids
//
findPNByLid(lid: string) {
return this.api.findPNByLid(this.session, lid);
}
findLIDByPN(pn: string) {
return this.api.findLIDByPN(this.session, pn);
}
}
+74
View File
@@ -0,0 +1,74 @@
import { WhatsAppMessage } from '@waha/apps/chatwoot/storage';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { WAHAEngine } from '@waha/structures/enums.dto';
import { getEngineName } from '@waha/version';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
interface IEngineHelper {
WhatsAppMessageKeys(message: any): WhatsAppMessage;
}
class NOWEBHelper implements IEngineHelper {
WhatsAppMessageKeys(message: any): WhatsAppMessage {
const timestamp = parseInt(message.messageTimestamp) * 1000;
return {
timestamp: new Date(timestamp),
from_me: message.key.fromMe,
chat_id: toCusFormat(message.key.remoteJid),
message_id: message.key.id,
participant: message.key.participant,
};
}
}
class GOWSHelper implements IEngineHelper {
/**
* Parse API response and get the data
* API Response depends on engine right now
*/
WhatsAppMessageKeys(message: any): WhatsAppMessage {
const Info = message._data.Info;
const timestamp = new Date(Info.Timestamp).getTime();
return {
timestamp: new Date(timestamp),
from_me: Info.IsFromMe,
chat_id: toCusFormat(Info.Chat),
message_id: Info.ID,
participant: Info.Sender ? toCusFormat(Info.Sender) : null,
};
}
}
class WEBJSHelper implements IEngineHelper {
/**
* Parse API response and get the data for WEBJS engine
*/
WhatsAppMessageKeys(message: MessageInstance): WhatsAppMessage {
return {
timestamp: new Date(message.timestamp * 1000),
from_me: message.fromMe,
chat_id: message.from,
message_id: message.id.id,
participant: message.author || null,
};
}
}
// Choose the right EngineHelper based on getEngineName() function
let engineHelper: IEngineHelper;
switch (getEngineName()) {
case WAHAEngine.NOWEB:
engineHelper = new NOWEBHelper();
break;
case WAHAEngine.GOWS:
engineHelper = new GOWSHelper();
break;
case WAHAEngine.WEBJS:
engineHelper = new WEBJSHelper();
break;
default:
engineHelper = new WEBJSHelper(); // Default to WEBJS as it's the default engine
}
export const EngineHelper = engineHelper;
+3
View File
@@ -0,0 +1,3 @@
import { EngineHelper } from './engines';
export { EngineHelper };
@@ -0,0 +1,71 @@
import { Knex } from 'knex';
import { ChatWootCombinedKey, ChatwootMessage } from './types';
export class ChatwootMessageRepository {
static tableName = 'app_chatwoot_chatwoot_messages';
constructor(
private readonly knex: Knex,
private readonly appPk: number,
) {}
get tableName() {
return ChatwootMessageRepository.tableName;
}
async upsertWithTrx(
trx: Knex.Transaction,
message: Omit<ChatwootMessage, 'id'>,
): Promise<ChatwootMessage> {
const [id] = await trx(this.tableName)
.insert({
...message,
app_pk: this.appPk,
})
.onConflict(['app_pk', 'conversation_id', 'message_id'])
.merge()
.returning('id');
return { ...message, ...id };
}
/**
* Gets a message by its id
*/
async getById(id: number): Promise<ChatwootMessage | null> {
return this.knex(this.tableName)
.where({
app_pk: this.appPk,
id: id,
})
.first();
}
async getByCombinedKey(key: ChatWootCombinedKey): Promise<ChatwootMessage[]> {
return this.knex(this.tableName)
.where({
app_pk: this.appPk,
conversation_id: key.conversation_id,
message_id: key.message_id,
})
.select('*');
}
/**
* Deletes messages older than the specified date
* @param date Date before which messages should be removed
* @param trx Optional transaction object
* @returns Number of deleted messages
*/
async deleteMessagesOlderThan(
trx: Knex.Transaction,
date: Date,
): Promise<number> {
const result = await trx(this.tableName)
.where('app_pk', this.appPk)
.andWhere('timestamp', '<', date)
.del();
return result;
}
}
@@ -0,0 +1,77 @@
import { Knex } from 'knex';
import { ChatwootMessage, MessageMapping, WhatsAppMessage } from './types';
export class MessageMappingRepository {
static tableName = 'app_chatwoot_message_mappings';
constructor(
private readonly knex: Knex,
private readonly appPk: number,
) {}
get tableName() {
return MessageMappingRepository.tableName;
}
/**
* Creates a mapping between a Chatwoot message and a WhatsApp message
*/
upsertMapping(
chatwoot: Pick<ChatwootMessage, 'id'>,
whatsapp: Pick<WhatsAppMessage, 'id'>,
part: number,
): Promise<MessageMapping> {
return this.knex.transaction((trx) =>
this.upsertMappingWithTrx(trx, chatwoot, whatsapp, part),
);
}
async upsertMappingWithTrx(
trx: Knex.Transaction,
chatwoot: Pick<ChatwootMessage, 'id'>,
whatsapp: Pick<WhatsAppMessage, 'id'>,
part: number,
): Promise<MessageMapping> {
const [id] = await trx(this.tableName)
.insert({
app_pk: this.appPk,
chatwoot_message_id: chatwoot.id,
whatsapp_message_id: whatsapp.id,
part,
})
.onConflict([
'app_pk',
'chatwoot_message_id',
'whatsapp_message_id',
'part',
])
.merge()
.returning('id');
return {
id,
chatwoot_message_id: chatwoot.id,
whatsapp_message_id: whatsapp.id,
part: part,
};
}
async getByWhatsAppMessageId(id: number): Promise<MessageMapping | null> {
return this.knex(this.tableName)
.where({
app_pk: this.appPk,
whatsapp_message_id: id,
})
.first();
}
async getByChatwootMessageId(id: number): Promise<MessageMapping | null> {
return this.knex(this.tableName)
.where({
app_pk: this.appPk,
chatwoot_message_id: id,
})
.first();
}
}
@@ -0,0 +1,126 @@
import { Knex } from 'knex';
import { ChatwootMessageRepository } from './ChatwootMessageRepository';
import { MessageMappingRepository } from './MessageMappingRepository';
import {
ChatWootCombinedKey,
ChatwootMessage,
MessageMapping,
WhatsAppMessage,
} from './types';
import { WhatsAppMessageRepository } from './WhatsAppMessageRepository';
export class MessageMappingService {
constructor(
private readonly knex: Knex,
private whatsAppMessageRepository: WhatsAppMessageRepository,
private chatwootMessageRepository: ChatwootMessageRepository,
private messageMappingRepository: MessageMappingRepository,
) {}
/**
* Cleans up messages older than the specified date
* @param removeAfter Date before which messages should be removed
* @returns Object containing the number of deleted WhatsApp and Chatwoot messages
*/
async cleanup(removeAfter: Date): Promise<number> {
const trx = await this.knex.transaction();
try {
// Delete WhatsApp messages older than removeAfter
const whatsapp =
await this.whatsAppMessageRepository.deleteMessagesOlderThan(
trx,
removeAfter,
);
const chatwoot =
await this.chatwootMessageRepository.deleteMessagesOlderThan(
trx,
removeAfter,
);
await trx.commit();
return whatsapp + chatwoot;
} finally {
await trx.commit();
}
}
async map(
chatwoot: ChatwootMessage,
whatsapp: WhatsAppMessage,
part?: number,
): Promise<MessageMapping> {
const trx = await this.knex.transaction();
try {
const chatwootMessage =
await this.chatwootMessageRepository.upsertWithTrx(trx, chatwoot);
chatwoot.id = chatwootMessage.id;
const whatsappMessage =
await this.whatsAppMessageRepository.upsertWithTrx(trx, whatsapp);
whatsapp.id = whatsappMessage.id;
const mapping = await this.messageMappingRepository.upsertMappingWithTrx(
trx,
chatwootMessage,
whatsappMessage,
part ?? 1,
);
return mapping;
} catch (e) {
await trx.rollback();
throw e;
} finally {
await trx.commit();
}
}
async getChatWootMessage(
whatsapp: Pick<WhatsAppMessage, 'chat_id' | 'message_id'>,
): Promise<ChatwootMessage | null> {
const message = await this.whatsAppMessageRepository.getByMessageId(
whatsapp.message_id,
);
if (!message) {
return null;
}
const mapping = await this.messageMappingRepository.getByWhatsAppMessageId(
message.id,
);
if (!mapping) {
return null;
}
const chatwoot = await this.chatwootMessageRepository.getById(
mapping.chatwoot_message_id,
);
if (!chatwoot) {
return null;
}
return chatwoot;
}
async getWhatsAppMessage(
chatwoot: ChatWootCombinedKey,
): Promise<WhatsAppMessage[]> {
const messages =
await this.chatwootMessageRepository.getByCombinedKey(chatwoot);
if (!messages) {
return [];
}
const mappings = [];
for (const message of messages) {
const mapping =
await this.messageMappingRepository.getByChatwootMessageId(message.id);
if (mapping) {
mappings.push(mapping);
}
}
const whatsapp = [];
for (const mapping of mappings) {
const message = await this.whatsAppMessageRepository.getById(
mapping.whatsapp_message_id,
);
if (message) {
whatsapp.push(message);
}
}
return whatsapp;
}
}
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more